Compare commits

..

54 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
80f83bf84b Merge branch 'edge' 2022-10-05 08:57:04 -04:00
FoxxMD
7933f77764 Merge branch 'edge' 2022-10-05 08:55:39 -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
31 changed files with 675 additions and 314 deletions

2
.nvmrc
View File

@@ -1 +1 @@
16.18.0
16.14.2

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

@@ -46,5 +46,5 @@ Example:
### 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)

274
package-lock.json generated
View File

@@ -29,7 +29,7 @@
"autolinker": "^3.14.3",
"body-parser": "^1.19.0",
"cache-manager": "^3.4.4",
"cache-manager-redis-store": "^3.0.1",
"cache-manager-redis-store": "^2.0.0",
"commander": "^8.0.0",
"comment-json": "^4.1.1",
"connect-typeorm": "^2.0.0",
@@ -95,6 +95,7 @@
"@tsconfig/node14": "^1.0.0",
"@types/async": "^3.2.7",
"@types/cache-manager": "^3.4.2",
"@types/cache-manager-redis-store": "^2.0.0",
"@types/chai": "^4.3.0",
"@types/chai-as-promised": "^7.1.5",
"@types/cookie-parser": "^1.4.2",
@@ -137,7 +138,7 @@
"typescript-json-schema": "~0.53"
},
"engines": {
"node": ">=16.18.0"
"node": ">=16"
},
"optionalDependencies": {
"better-sqlite3": "^7.5.0",
@@ -978,59 +979,6 @@
"resolved": "https://registry.npmjs.org/@nlpjs/slot/-/slot-4.22.17.tgz",
"integrity": "sha512-cNYcxf9DKB+fnRa2NxT5wbWq5j57R1WCTXLWI/1Cyycr227IP7GN7qaD4RbkzotBFFB8wm63UHod9frzmuiXxg=="
},
"node_modules/@redis/bloom": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.0.2.tgz",
"integrity": "sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/client": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.3.0.tgz",
"integrity": "sha512-XCFV60nloXAefDsPnYMjHGtvbtHR8fV5Om8cQ0JYqTNbWcQo/4AryzJ2luRj4blveWazRK/j40gES8M7Cp6cfQ==",
"dependencies": {
"cluster-key-slot": "1.1.0",
"generic-pool": "3.8.2",
"yallist": "4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@redis/graph": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.0.1.tgz",
"integrity": "sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/json": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz",
"integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/search": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.0.tgz",
"integrity": "sha512-NyFZEVnxIJEybpy+YskjgOJRNsfTYqaPbK/Buv6W2kmFNaRk85JiqjJZA5QkRmWvGbyQYwoO5QfDi2wHskKrQQ==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/time-series": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.3.tgz",
"integrity": "sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA==",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
@@ -1215,6 +1163,16 @@
"integrity": "sha512-71aBXoFYXZW4TnDHHH8gExw2lS28BZaWeKefgsiJI7QYZeJfUEbMKw6CQtzGjlYQcGIWwB76hcCrkVA3YHSvsw==",
"dev": true
},
"node_modules/@types/cache-manager-redis-store": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/cache-manager-redis-store/-/cache-manager-redis-store-2.0.1.tgz",
"integrity": "sha512-8QuccvcPieh1xM/5kReE76SfdcIdEB0ePc+54ah/NBuK2eG+6O50SX4WKoJX81UxGdW3sh/WlDaDNqjnqxWNsA==",
"dev": true,
"dependencies": {
"@types/cache-manager": "*",
"@types/redis": "^2.8.0"
}
},
"node_modules/@types/cacheable-request": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
@@ -1498,6 +1456,15 @@
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
},
"node_modules/@types/redis": {
"version": "2.8.32",
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz",
"integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/responselike": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
@@ -2383,14 +2350,14 @@
}
},
"node_modules/cache-manager-redis-store": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/cache-manager-redis-store/-/cache-manager-redis-store-3.0.1.tgz",
"integrity": "sha512-o560kw+dFqusC9lQJhcm6L2F2fMKobJ5af+FoR2PdnMVdpQ3f3Bz6qzvObTGyvoazQJxjQNWgMQeChP4vRTuXQ==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/cache-manager-redis-store/-/cache-manager-redis-store-2.0.0.tgz",
"integrity": "sha512-bWLWlUg6nCYHiJLCCYxY2MgvwvKnvlWwrbuynrzpjEIhfArD2GC9LtutIHFEPeyGVQN6C+WEw+P3r+BFBwhswg==",
"dependencies": {
"redis": "^4.3.1"
"redis": "^3.0.2"
},
"engines": {
"node": ">= 16.18.0"
"node": ">= 8.3"
}
},
"node_modules/cacheable-lookup": {
@@ -2674,14 +2641,6 @@
"mimic-response": "^1.0.0"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
@@ -3251,6 +3210,14 @@
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
"optional": true
},
"node_modules/denque": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -4199,14 +4166,6 @@
"node": ">=10"
}
},
"node_modules/generic-pool": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz",
"integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg==",
"engines": {
"node": ">= 4"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -7839,16 +7798,45 @@
}
},
"node_modules/redis": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.3.1.tgz",
"integrity": "sha512-cM7yFU5CA6zyCF7N/+SSTcSJQSRMEKN0k0Whhu6J7n9mmXRoXugfWDBo5iOzGwABmsWKSwGPTU5J4Bxbl+0mrA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
"dependencies": {
"@redis/bloom": "1.0.2",
"@redis/client": "1.3.0",
"@redis/graph": "1.0.1",
"@redis/json": "1.0.4",
"@redis/search": "1.1.0",
"@redis/time-series": "1.0.3"
"denque": "^1.5.0",
"redis-commands": "^1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-redis"
}
},
"node_modules/redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
},
"node_modules/redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=",
"engines": {
"node": ">=4"
}
},
"node_modules/redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
"dependencies": {
"redis-errors": "^1.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/reflect-metadata": {
@@ -11127,46 +11115,6 @@
"resolved": "https://registry.npmjs.org/@nlpjs/slot/-/slot-4.22.17.tgz",
"integrity": "sha512-cNYcxf9DKB+fnRa2NxT5wbWq5j57R1WCTXLWI/1Cyycr227IP7GN7qaD4RbkzotBFFB8wm63UHod9frzmuiXxg=="
},
"@redis/bloom": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.0.2.tgz",
"integrity": "sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw==",
"requires": {}
},
"@redis/client": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.3.0.tgz",
"integrity": "sha512-XCFV60nloXAefDsPnYMjHGtvbtHR8fV5Om8cQ0JYqTNbWcQo/4AryzJ2luRj4blveWazRK/j40gES8M7Cp6cfQ==",
"requires": {
"cluster-key-slot": "1.1.0",
"generic-pool": "3.8.2",
"yallist": "4.0.0"
}
},
"@redis/graph": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.0.1.tgz",
"integrity": "sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ==",
"requires": {}
},
"@redis/json": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz",
"integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==",
"requires": {}
},
"@redis/search": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.0.tgz",
"integrity": "sha512-NyFZEVnxIJEybpy+YskjgOJRNsfTYqaPbK/Buv6W2kmFNaRk85JiqjJZA5QkRmWvGbyQYwoO5QfDi2wHskKrQQ==",
"requires": {}
},
"@redis/time-series": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.3.tgz",
"integrity": "sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA==",
"requires": {}
},
"@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
@@ -11266,6 +11214,16 @@
"integrity": "sha512-71aBXoFYXZW4TnDHHH8gExw2lS28BZaWeKefgsiJI7QYZeJfUEbMKw6CQtzGjlYQcGIWwB76hcCrkVA3YHSvsw==",
"dev": true
},
"@types/cache-manager-redis-store": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@types/cache-manager-redis-store/-/cache-manager-redis-store-2.0.1.tgz",
"integrity": "sha512-8QuccvcPieh1xM/5kReE76SfdcIdEB0ePc+54ah/NBuK2eG+6O50SX4WKoJX81UxGdW3sh/WlDaDNqjnqxWNsA==",
"dev": true,
"requires": {
"@types/cache-manager": "*",
"@types/redis": "^2.8.0"
}
},
"@types/cacheable-request": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
@@ -11549,6 +11507,15 @@
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
},
"@types/redis": {
"version": "2.8.32",
"resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.32.tgz",
"integrity": "sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/responselike": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
@@ -12286,11 +12253,11 @@
}
},
"cache-manager-redis-store": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/cache-manager-redis-store/-/cache-manager-redis-store-3.0.1.tgz",
"integrity": "sha512-o560kw+dFqusC9lQJhcm6L2F2fMKobJ5af+FoR2PdnMVdpQ3f3Bz6qzvObTGyvoazQJxjQNWgMQeChP4vRTuXQ==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/cache-manager-redis-store/-/cache-manager-redis-store-2.0.0.tgz",
"integrity": "sha512-bWLWlUg6nCYHiJLCCYxY2MgvwvKnvlWwrbuynrzpjEIhfArD2GC9LtutIHFEPeyGVQN6C+WEw+P3r+BFBwhswg==",
"requires": {
"redis": "^4.3.1"
"redis": "^3.0.2"
}
},
"cacheable-lookup": {
@@ -12503,11 +12470,6 @@
"mimic-response": "^1.0.0"
}
},
"cluster-key-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz",
"integrity": "sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw=="
},
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
@@ -12971,6 +12933,11 @@
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
"optional": true
},
"denque": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw=="
},
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -13708,11 +13675,6 @@
"json-bigint": "^1.0.0"
}
},
"generic-pool": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.8.2.tgz",
"integrity": "sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg=="
},
"gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -16493,16 +16455,32 @@
}
},
"redis": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.3.1.tgz",
"integrity": "sha512-cM7yFU5CA6zyCF7N/+SSTcSJQSRMEKN0k0Whhu6J7n9mmXRoXugfWDBo5iOzGwABmsWKSwGPTU5J4Bxbl+0mrA==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
"requires": {
"@redis/bloom": "1.0.2",
"@redis/client": "1.3.0",
"@redis/graph": "1.0.1",
"@redis/json": "1.0.4",
"@redis/search": "1.1.0",
"@redis/time-series": "1.0.3"
"denque": "^1.5.0",
"redis-commands": "^1.7.0",
"redis-errors": "^1.2.0",
"redis-parser": "^3.0.0"
}
},
"redis-commands": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
},
"redis-errors": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60="
},
"redis-parser": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
"integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=",
"requires": {
"redis-errors": "^1.0.0"
}
},
"reflect-metadata": {

View File

@@ -24,7 +24,7 @@
"initMigration": "npm run typeorm -- migration:generate -t 1642180264563 -d ormconfig.js \"src/Common/Migrations/Database/init\""
},
"engines": {
"node": ">=16.18.0"
"node": ">=16"
},
"keywords": [],
"author": "",
@@ -49,7 +49,7 @@
"autolinker": "^3.14.3",
"body-parser": "^1.19.0",
"cache-manager": "^3.4.4",
"cache-manager-redis-store": "^3.0.1",
"cache-manager-redis-store": "^2.0.0",
"commander": "^8.0.0",
"comment-json": "^4.1.1",
"connect-typeorm": "^2.0.0",
@@ -115,6 +115,7 @@
"@tsconfig/node14": "^1.0.0",
"@types/async": "^3.2.7",
"@types/cache-manager": "^3.4.2",
"@types/cache-manager-redis-store": "^2.0.0",
"@types/chai": "^4.3.0",
"@types/chai-as-promised": "^7.1.5",
"@types/cookie-parser": "^1.4.2",

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

@@ -1,47 +0,0 @@
import {CacheOptions} from "../interfaces";
import cacheManager, {Cache} from "cache-manager";
import {redisStore} from "cache-manager-redis-store";
import {create as createMemoryStore} from "../../Utils/memoryStore";
import {CacheProvider} from "../Infrastructure/Atomic";
import {cacheOptDefaults} from "../defaults";
export const buildCacheOptionsFromProvider = (provider: CacheProvider | any): CacheOptions => {
if (typeof provider === 'string') {
return {
store: provider as CacheProvider,
...cacheOptDefaults
}
}
return {
store: 'memory',
...cacheOptDefaults,
...provider,
}
}
export const createCacheManager = async (options: CacheOptions): Promise<Cache> => {
const {store, max, ttl = 60, host = 'localhost', port, auth_pass, db, ...rest} = options;
switch (store) {
case 'none':
return cacheManager.caching({store: 'none', max, ttl});
case 'redis':
const rStore = await redisStore(
{
socket: {
host,
port
},
password: auth_pass,
database: db,
}
);
return cacheManager.caching({
store: rStore,
ttl,
...rest,
});
case 'memory':
default:
//return cacheManager.caching({store: 'memory', max, ttl});
return cacheManager.caching({store: {create: createMemoryStore}, max, ttl, shouldCloneBeforeSet: false});
}
}

View File

@@ -119,6 +119,34 @@ 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 {
@@ -130,6 +158,9 @@ export interface ModActionCriteria extends UserSubredditHistoryCriteria {
export interface FullModActionCriteria extends Omit<ModActionCriteria, 'count'> {
type?: ModActionType[]
count?: GenericComparison
/*
* Does this action/note link to the currently processing Activity?
* */
activityType?: MaybeActivityType[]
}
@@ -140,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[]
}

View File

@@ -163,9 +163,3 @@ declare module 'wink-sentiment' {
export default sentiment;
}
declare module 'cache-manager-redis-store' {
import {RedisClientOptions} from "@redis/client";
import {Cache, CachingConfig} from "cache-manager";
export async function redisStore(config: RedisClientOptions & Partial<CachingConfig>): Cache;
}

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.13.1';
export const VERSION = '0.13.2';

View File

@@ -2972,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
],
@@ -3028,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": [
{
@@ -3095,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

@@ -6854,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
],
@@ -6910,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": [
{
@@ -6977,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

@@ -6270,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
],
@@ -6326,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": [
{
@@ -6393,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

@@ -968,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": [
@@ -2283,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",
@@ -2342,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

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

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

@@ -6467,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
],
@@ -6523,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": [
{
@@ -6590,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

@@ -14,8 +14,10 @@ import {
asActivity,
asSubmission,
asUserNoteCriteria,
buildCacheOptionsFromProvider,
buildCachePrefix,
cacheStats,
createCacheManager,
escapeRegex,
FAIL,
fetchExternalResult,
@@ -121,7 +123,7 @@ import {
SubmissionState,
SubredditCriteria,
toFullModLogCriteria,
toFullModNoteCriteria,
toFullModNoteCriteria, toFullUserNoteCriteria,
TypedActivityState,
TypedActivityStates,
UserNoteCriteria
@@ -171,7 +173,6 @@ import ConfigParseError from "../Utils/ConfigParseError";
import {ActivityReport} from "../Common/Entities/ActivityReport";
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
import {ActivitySource} from "../Common/ActivitySource";
import {buildCacheOptionsFromProvider, createCacheManager} from "../Common/Cache";
export const DEFAULT_FOOTER = '\r\n*****\r\nThis action was performed by [a bot.]({{botLink}}) Mention a moderator or [send a modmail]({{modmailLink}}) if you have any ideas, questions, or concerns about this action.';
@@ -3234,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,
@@ -3246,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();
@@ -3273,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 {
@@ -3285,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;
@@ -3296,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;
}
}
@@ -3544,7 +3536,7 @@ export class BotResourcesManager {
authorTTL: number = 10000;
enabled: boolean = true;
modStreams: Map<string, SPoll<Snoowrap.Submission | Snoowrap.Comment>> = new Map();
defaultCache: Promise<Cache>;
defaultCache: Cache;
defaultCacheConfig: StrongCache
defaultCacheMigrated: boolean = false;
cacheType: string = 'none';
@@ -3648,7 +3640,7 @@ export class BotResourcesManager {
// });
let opts: SubredditResourceOptions = {
cache: await this.defaultCache,
cache: this.defaultCache,
cacheType: this.cacheType,
cacheSettingsHash: hash,
ttl: this.ttlDefaults,
@@ -3679,7 +3671,7 @@ export class BotResourcesManager {
trueProvider.prefix = subPrefix;
const eventsMax = this.actionedEventsMaxDefault !== undefined ? Math.min(actionedEventsMax, this.actionedEventsMaxDefault) : actionedEventsMax;
opts = {
cache: await createCacheManager(trueProvider),
cache: createCacheManager(trueProvider),
actionedEventsMax: eventsMax,
cacheType: trueProvider.store,
cacheSettingsHash: hash,
@@ -3694,7 +3686,7 @@ export class BotResourcesManager {
await runMigrations(opts.cache, opts.logger, trueProvider.prefix);
}
} else if(!this.defaultCacheMigrated) {
await runMigrations(await this.defaultCache, this.logger, opts.prefix);
await runMigrations(this.defaultCache, this.logger, opts.prefix);
this.defaultCacheMigrated = true;
}
@@ -3729,7 +3721,7 @@ export class BotResourcesManager {
}
async getPendingSubredditInvites(): Promise<(string[])> {
const subredditNames = await (await this.defaultCache).get(`modInvites`);
const subredditNames = await this.defaultCache.get(`modInvites`);
if (subredditNames !== undefined && subredditNames !== null) {
return subredditNames as string[];
}
@@ -3740,7 +3732,7 @@ export class BotResourcesManager {
if(subreddit === null || subreddit === undefined || subreddit == '') {
throw new CMError('Subreddit name cannot be empty');
}
let subredditNames = await (await this.defaultCache).get(`modInvites`) as (string[] | undefined | null);
let subredditNames = await this.defaultCache.get(`modInvites`) as (string[] | undefined | null);
if (subredditNames === undefined || subredditNames === null) {
subredditNames = [];
}
@@ -3750,22 +3742,22 @@ export class BotResourcesManager {
throw new CMError(`An invite for the Subreddit '${subreddit}' already exists`);
}
subredditNames.push(cleanName);
await (await this.defaultCache).set(`modInvites`, subredditNames, {ttl: 0});
await this.defaultCache.set(`modInvites`, subredditNames, {ttl: 0});
return;
}
async deletePendingSubredditInvite(subreddit: string): Promise<void> {
let subredditNames = await (await this.defaultCache).get(`modInvites`) as (string[] | undefined | null);
let subredditNames = await this.defaultCache.get(`modInvites`) as (string[] | undefined | null);
if (subredditNames === undefined || subredditNames === null) {
subredditNames = [];
}
subredditNames = subredditNames.filter(x => x.toLowerCase() !== subreddit.trim().toLowerCase());
await (await this.defaultCache).set(`modInvites`, subredditNames, {ttl: 0});
await this.defaultCache.set(`modInvites`, subredditNames, {ttl: 0});
return;
}
async clearPendingSubredditInvites(): Promise<void> {
await (await this.defaultCache).del(`modInvites`);
await this.defaultCache.del(`modInvites`);
return;
}
}

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

@@ -1,7 +1,7 @@
import {SessionOptions, Store} from "express-session";
import {TypeormStore} from "connect-typeorm";
import {InviteData} from "../Common/interfaces";
import {buildCachePrefix, mergeArr} from "../../util";
import {buildCachePrefix, createCacheManager, mergeArr} from "../../util";
import {Cache} from "cache-manager";
// @ts-ignore
import CacheManagerStore from 'express-session-cache-manager'
@@ -11,7 +11,6 @@ import {ClientSession} from "../../Common/WebEntities/ClientSession";
import {Logger} from "winston";
import {WebSetting} from "../../Common/WebEntities/WebSetting";
import {ErrorWithCause} from "pony-cause";
import {createCacheManager} from "../../Common/Cache";
export interface CacheManagerStoreOptions {
prefix?: string
@@ -25,7 +24,7 @@ export type TypeormStoreOptions = Partial<SessionOptions & {
}>;
interface IWebStorageProvider {
createSessionStore(options?: CacheManagerStoreOptions | TypeormStoreOptions): Promise<Store>
createSessionStore(options?: CacheManagerStoreOptions | TypeormStoreOptions): Store
getSessionSecret(): Promise<string | undefined>
@@ -49,7 +48,7 @@ abstract class StorageProvider implements IWebStorageProvider {
this.logger = logger.child({labels: ['Web', 'Storage', ...loggerLabels]}, mergeArr);
}
abstract createSessionStore(options?: CacheManagerStoreOptions | TypeormStoreOptions): Promise<Store>;
abstract createSessionStore(options?: CacheManagerStoreOptions | TypeormStoreOptions): Store;
abstract getSessionSecret(): Promise<string | undefined>;
@@ -58,24 +57,24 @@ abstract class StorageProvider implements IWebStorageProvider {
export class CacheStorageProvider extends StorageProvider {
protected cache: Promise<Cache>;
protected cache: Cache;
constructor(caching: CacheOptions & StorageProviderOptions) {
super(caching);
const {logger, invitesMaxAge, loggerLabels, ...restCache } = caching;
this.cache = createCacheManager({...restCache, prefix: buildCachePrefix(['web'])}) as Promise<Cache>;
this.cache = createCacheManager({...restCache, prefix: buildCachePrefix(['web'])}) as Cache;
this.logger.debug('Using CACHE');
if (caching.store === 'none') {
this.logger.warn(`Using 'none' as cache provider means no one will be able to access the interface since sessions will never be persisted!`);
}
}
async createSessionStore(options?: CacheManagerStoreOptions): Promise<Store> {
return new CacheManagerStore((await this.cache), {prefix: 'sess:'});
createSessionStore(options?: CacheManagerStoreOptions): Store {
return new CacheManagerStore(this.cache, {prefix: 'sess:'});
}
async getSessionSecret() {
const val = await (await this.cache).get(`sessionSecret`);
const val = await this.cache.get(`sessionSecret`);
if (val === null || val === undefined) {
return undefined;
}
@@ -83,7 +82,7 @@ export class CacheStorageProvider extends StorageProvider {
}
async setSessionSecret(secret: string) {
await (await this.cache).set('sessionSecret', secret, {ttl: 0});
await this.cache.set('sessionSecret', secret, {ttl: 0});
}
}
@@ -102,7 +101,7 @@ export class DatabaseStorageProvider extends StorageProvider {
this.logger.debug('Using DATABASE');
}
async createSessionStore(options?: TypeormStoreOptions): Promise<Store> {
createSessionStore(options?: TypeormStoreOptions): Store {
return new TypeormStore(options).connect(this.clientSessionRepo)
}

View File

@@ -17,7 +17,7 @@ import {
} from "../../Common/interfaces";
import {
buildCachePrefix,
defaultFormat, filterLogBySubreddit, filterCriteriaSummary, formatFilterData,
createCacheManager, defaultFormat, filterLogBySubreddit, filterCriteriaSummary, formatFilterData,
formatLogLineToHtml, filterLogs, getUserAgent,
intersect, isLogLineMinLevel,
LogEntry, parseInstanceLogInfoName, parseInstanceLogName, parseRedditEntity,
@@ -64,7 +64,6 @@ import {
InviteData, SubredditInviteDataPersisted
} from "../Common/interfaces";
import {open} from "fs/promises";
import {createCacheManager} from "../../Common/Cache";
const emitter = new EventEmitter();
@@ -324,7 +323,7 @@ const webClient = async (options: OperatorConfigWithFileContext) => {
cookie: {
maxAge: sessionMaxAge * 1000,
},
store: await sessionStoreProvider.createSessionStore(sessionStorage === 'database' ? {
store: sessionStoreProvider.createSessionStore(sessionStorage === 'database' ? {
cleanupLimit: 2,
ttl: sessionMaxAge
} : {}),

View File

@@ -14,6 +14,7 @@ import {
ActionResult,
ActivityDispatch,
ActivityDispatchConfig,
CacheOptions,
CheckSummary,
ImageComparisonResult,
ItemCritPropHelper,
@@ -34,9 +35,11 @@ import {
} from "./Common/interfaces";
import InvalidRegexError from "./Utils/InvalidRegexError";
import {accessSync, constants, promises} from "fs";
import {VERSION} from "./Common/defaults";
import cacheManager from "cache-manager";
import {cacheOptDefaults, VERSION} from "./Common/defaults";
import cacheManager, {Cache} from "cache-manager";
import redisStore from "cache-manager-redis-store";
import Autolinker from 'autolinker';
import {create as createMemoryStore} from './Utils/memoryStore';
import {LEVEL, MESSAGE} from "triple-beam";
import {Comment, PrivateMessage, RedditUser, Submission, Subreddit} from "snoowrap/dist/objects";
import reRegExp from '@stdlib/regexp-regexp';
@@ -68,9 +71,9 @@ import {
UserNoteCriteria
} from "./Common/Infrastructure/Filters/FilterCriteria";
import {
ActivitySourceData,
ActivitySourceTypes,
ActivitySourceValue,
ActivitySourceTypes,
CacheProvider,
ConfigFormat,
DurationVal,
ExternalUrlContext,
@@ -78,13 +81,13 @@ import {
ModUserNoteLabel,
modUserNoteLabels,
RedditEntity,
RedditEntityType,
RelativeDateTimeMatch,
RedditEntityType, RelativeDateTimeMatch,
statFrequencies,
StatisticFrequency,
StatisticFrequencyOption,
UrlContext,
WikiContext
WikiContext,
ActivitySourceData
} from "./Common/Infrastructure/Atomic";
import {
AuthorOptions,
@@ -1762,6 +1765,42 @@ export const cacheStats = (): ResourceStats => {
};
}
export const buildCacheOptionsFromProvider = (provider: CacheProvider | any): CacheOptions => {
if(typeof provider === 'string') {
return {
store: provider as CacheProvider,
...cacheOptDefaults
}
}
return {
store: 'memory',
...cacheOptDefaults,
...provider,
}
}
export const createCacheManager = (options: CacheOptions): Cache => {
const {store, max, ttl = 60, host = 'localhost', port, auth_pass, db, ...rest} = options;
switch (store) {
case 'none':
return cacheManager.caching({store: 'none', max, ttl});
case 'redis':
return cacheManager.caching({
store: redisStore,
host,
port,
auth_pass,
db,
ttl,
...rest,
});
case 'memory':
default:
//return cacheManager.caching({store: 'memory', max, ttl});
return cacheManager.caching({store: {create: createMemoryStore}, max, ttl, shouldCloneBeforeSet: false});
}
}
export const randomId = () => crypto.randomBytes(20).toString('hex');
export const intersect = (a: Array<any>, b: Array<any>) => {