Compare commits
21 Commits
modlog
...
redisCache
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e364c3e28 | ||
|
|
acd4b54d8e | ||
|
|
19f4ff0b53 | ||
|
|
98a8568eb6 | ||
|
|
457f947603 | ||
|
|
7fb69ae67a | ||
|
|
2241d40e49 | ||
|
|
a3ca3f17ec | ||
|
|
f527a17fa2 | ||
|
|
e98364eae9 | ||
|
|
8b125d7433 | ||
|
|
6ee060c5ce | ||
|
|
9b12d0b2b3 | ||
|
|
b174c7928a | ||
|
|
74dfe9258a | ||
|
|
1cf8855a24 | ||
|
|
adc69894fc | ||
|
|
3435c683c8 | ||
|
|
f0032cd433 | ||
|
|
ade0b7948e | ||
|
|
542aa26c62 |
1
.github/FUNDING.yml
vendored
@@ -1,2 +1,3 @@
|
|||||||
github: [FoxxMD]
|
github: [FoxxMD]
|
||||||
|
patreon: FoxxMD
|
||||||
custom: ["bitcoincash:qqmpsh365r8n9jhp4p8ks7f7qdr7203cws4kmkmr8q"]
|
custom: ["bitcoincash:qqmpsh365r8n9jhp4p8ks7f7qdr7203cws4kmkmr8q"]
|
||||||
|
|||||||
@@ -5,9 +5,45 @@ The **History** rule can check an Author's submission/comment statistics over a
|
|||||||
* Submission total or percentage of All Activity
|
* Submission total or percentage of All Activity
|
||||||
* Comment 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
|
* 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.
|
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
|
### 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.
|
* 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.
|
||||||
|
|||||||
302
package-lock.json
generated
@@ -13,8 +13,8 @@
|
|||||||
"@awaitjs/express": "^0.8.0",
|
"@awaitjs/express": "^0.8.0",
|
||||||
"@datasert/cronjs-matcher": "^1.2.0",
|
"@datasert/cronjs-matcher": "^1.2.0",
|
||||||
"@googleapis/youtube": "^2.0.0",
|
"@googleapis/youtube": "^2.0.0",
|
||||||
"@influxdata/influxdb-client": "^1.27.0",
|
"@influxdata/influxdb-client": "^1.31.0",
|
||||||
"@influxdata/influxdb-client-apis": "^1.27.0",
|
"@influxdata/influxdb-client-apis": "^1.31.0",
|
||||||
"@nlpjs/core": "^4.23.4",
|
"@nlpjs/core": "^4.23.4",
|
||||||
"@nlpjs/lang-de": "^4.23.4",
|
"@nlpjs/lang-de": "^4.23.4",
|
||||||
"@nlpjs/lang-en": "^4.23.4",
|
"@nlpjs/lang-en": "^4.23.4",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"autolinker": "^3.14.3",
|
"autolinker": "^3.14.3",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"cache-manager": "^3.4.4",
|
"cache-manager": "^3.4.4",
|
||||||
"cache-manager-redis-store": "^2.0.0",
|
"cache-manager-redis-store": "^3.0.1",
|
||||||
"commander": "^8.0.0",
|
"commander": "^8.0.0",
|
||||||
"comment-json": "^4.1.1",
|
"comment-json": "^4.1.1",
|
||||||
"connect-typeorm": "^2.0.0",
|
"connect-typeorm": "^2.0.0",
|
||||||
@@ -95,7 +95,6 @@
|
|||||||
"@tsconfig/node14": "^1.0.0",
|
"@tsconfig/node14": "^1.0.0",
|
||||||
"@types/async": "^3.2.7",
|
"@types/async": "^3.2.7",
|
||||||
"@types/cache-manager": "^3.4.2",
|
"@types/cache-manager": "^3.4.2",
|
||||||
"@types/cache-manager-redis-store": "^2.0.0",
|
|
||||||
"@types/chai": "^4.3.0",
|
"@types/chai": "^4.3.0",
|
||||||
"@types/chai-as-promised": "^7.1.5",
|
"@types/chai-as-promised": "^7.1.5",
|
||||||
"@types/cookie-parser": "^1.4.2",
|
"@types/cookie-parser": "^1.4.2",
|
||||||
@@ -138,7 +137,7 @@
|
|||||||
"typescript-json-schema": "~0.53"
|
"typescript-json-schema": "~0.53"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16.18.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"better-sqlite3": "^7.5.0",
|
"better-sqlite3": "^7.5.0",
|
||||||
@@ -683,14 +682,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@influxdata/influxdb-client": {
|
"node_modules/@influxdata/influxdb-client": {
|
||||||
"version": "1.27.0",
|
"version": "1.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.31.0.tgz",
|
||||||
"integrity": "sha512-hOBi+ApIurDd8jFWo+eYjMWWsDRp3wih/U/NOVRoHaTOE8ihSQthi9wfMD4YeVqt4pCN6ygIwo7lEKFXwNuwcA=="
|
"integrity": "sha512-8DVT3ZB/VeCK5Nn+BxhgMrAMSTseQAEgV20AK+ZMO5Fcup9XWsA9L2zE+3eBFl0Y+lF3UeKiASkiKMQvws35GA=="
|
||||||
},
|
},
|
||||||
"node_modules/@influxdata/influxdb-client-apis": {
|
"node_modules/@influxdata/influxdb-client-apis": {
|
||||||
"version": "1.27.0",
|
"version": "1.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client-apis/-/influxdb-client-apis-1.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client-apis/-/influxdb-client-apis-1.31.0.tgz",
|
||||||
"integrity": "sha512-a4gd7CwNRXSsSVt9tm8GzGxuPXngEmQucMdoTZ0YYeWSbKUXz3B/3u9/EqMGEbtq5MdbbB2OKA611hu205UiNg==",
|
"integrity": "sha512-6ALGNLxtfffhICobOdj13Z6vj6gdQVOzVXPoPNd+w7V60zrbGhTqzXHV1KMZ/lzOb6YkRTRODbxz4W/b/7N5hg==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@influxdata/influxdb-client": "*"
|
"@influxdata/influxdb-client": "*"
|
||||||
}
|
}
|
||||||
@@ -979,6 +978,59 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@nlpjs/slot/-/slot-4.22.17.tgz",
|
"resolved": "https://registry.npmjs.org/@nlpjs/slot/-/slot-4.22.17.tgz",
|
||||||
"integrity": "sha512-cNYcxf9DKB+fnRa2NxT5wbWq5j57R1WCTXLWI/1Cyycr227IP7GN7qaD4RbkzotBFFB8wm63UHod9frzmuiXxg=="
|
"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": {
|
"node_modules/@sindresorhus/is": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||||
@@ -1163,16 +1215,6 @@
|
|||||||
"integrity": "sha512-71aBXoFYXZW4TnDHHH8gExw2lS28BZaWeKefgsiJI7QYZeJfUEbMKw6CQtzGjlYQcGIWwB76hcCrkVA3YHSvsw==",
|
"integrity": "sha512-71aBXoFYXZW4TnDHHH8gExw2lS28BZaWeKefgsiJI7QYZeJfUEbMKw6CQtzGjlYQcGIWwB76hcCrkVA3YHSvsw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/cacheable-request": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
|
||||||
@@ -1456,15 +1498,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
|
||||||
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
|
"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": {
|
"node_modules/@types/responselike": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
|
||||||
@@ -2350,14 +2383,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cache-manager-redis-store": {
|
"node_modules/cache-manager-redis-store": {
|
||||||
"version": "2.0.0",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cache-manager-redis-store/-/cache-manager-redis-store-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cache-manager-redis-store/-/cache-manager-redis-store-3.0.1.tgz",
|
||||||
"integrity": "sha512-bWLWlUg6nCYHiJLCCYxY2MgvwvKnvlWwrbuynrzpjEIhfArD2GC9LtutIHFEPeyGVQN6C+WEw+P3r+BFBwhswg==",
|
"integrity": "sha512-o560kw+dFqusC9lQJhcm6L2F2fMKobJ5af+FoR2PdnMVdpQ3f3Bz6qzvObTGyvoazQJxjQNWgMQeChP4vRTuXQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"redis": "^3.0.2"
|
"redis": "^4.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8.3"
|
"node": ">= 16.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cacheable-lookup": {
|
"node_modules/cacheable-lookup": {
|
||||||
@@ -2641,6 +2674,14 @@
|
|||||||
"mimic-response": "^1.0.0"
|
"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": {
|
"node_modules/code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||||
@@ -3210,14 +3251,6 @@
|
|||||||
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
|
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
|
||||||
"optional": true
|
"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": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -4166,6 +4199,14 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@@ -7798,45 +7839,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/redis": {
|
"node_modules/redis": {
|
||||||
"version": "3.1.2",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/redis/-/redis-4.3.1.tgz",
|
||||||
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
|
"integrity": "sha512-cM7yFU5CA6zyCF7N/+SSTcSJQSRMEKN0k0Whhu6J7n9mmXRoXugfWDBo5iOzGwABmsWKSwGPTU5J4Bxbl+0mrA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"denque": "^1.5.0",
|
"@redis/bloom": "1.0.2",
|
||||||
"redis-commands": "^1.7.0",
|
"@redis/client": "1.3.0",
|
||||||
"redis-errors": "^1.2.0",
|
"@redis/graph": "1.0.1",
|
||||||
"redis-parser": "^3.0.0"
|
"@redis/json": "1.0.4",
|
||||||
},
|
"@redis/search": "1.1.0",
|
||||||
"engines": {
|
"@redis/time-series": "1.0.3"
|
||||||
"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": {
|
"node_modules/reflect-metadata": {
|
||||||
@@ -10858,14 +10870,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@influxdata/influxdb-client": {
|
"@influxdata/influxdb-client": {
|
||||||
"version": "1.27.0",
|
"version": "1.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.31.0.tgz",
|
||||||
"integrity": "sha512-hOBi+ApIurDd8jFWo+eYjMWWsDRp3wih/U/NOVRoHaTOE8ihSQthi9wfMD4YeVqt4pCN6ygIwo7lEKFXwNuwcA=="
|
"integrity": "sha512-8DVT3ZB/VeCK5Nn+BxhgMrAMSTseQAEgV20AK+ZMO5Fcup9XWsA9L2zE+3eBFl0Y+lF3UeKiASkiKMQvws35GA=="
|
||||||
},
|
},
|
||||||
"@influxdata/influxdb-client-apis": {
|
"@influxdata/influxdb-client-apis": {
|
||||||
"version": "1.27.0",
|
"version": "1.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client-apis/-/influxdb-client-apis-1.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client-apis/-/influxdb-client-apis-1.31.0.tgz",
|
||||||
"integrity": "sha512-a4gd7CwNRXSsSVt9tm8GzGxuPXngEmQucMdoTZ0YYeWSbKUXz3B/3u9/EqMGEbtq5MdbbB2OKA611hu205UiNg==",
|
"integrity": "sha512-6ALGNLxtfffhICobOdj13Z6vj6gdQVOzVXPoPNd+w7V60zrbGhTqzXHV1KMZ/lzOb6YkRTRODbxz4W/b/7N5hg==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@istanbuljs/load-nyc-config": {
|
"@istanbuljs/load-nyc-config": {
|
||||||
@@ -11115,6 +11127,46 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@nlpjs/slot/-/slot-4.22.17.tgz",
|
"resolved": "https://registry.npmjs.org/@nlpjs/slot/-/slot-4.22.17.tgz",
|
||||||
"integrity": "sha512-cNYcxf9DKB+fnRa2NxT5wbWq5j57R1WCTXLWI/1Cyycr227IP7GN7qaD4RbkzotBFFB8wm63UHod9frzmuiXxg=="
|
"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": {
|
"@sindresorhus/is": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
|
||||||
@@ -11214,16 +11266,6 @@
|
|||||||
"integrity": "sha512-71aBXoFYXZW4TnDHHH8gExw2lS28BZaWeKefgsiJI7QYZeJfUEbMKw6CQtzGjlYQcGIWwB76hcCrkVA3YHSvsw==",
|
"integrity": "sha512-71aBXoFYXZW4TnDHHH8gExw2lS28BZaWeKefgsiJI7QYZeJfUEbMKw6CQtzGjlYQcGIWwB76hcCrkVA3YHSvsw==",
|
||||||
"dev": true
|
"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": {
|
"@types/cacheable-request": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.2.tgz",
|
||||||
@@ -11507,15 +11549,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz",
|
||||||
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw=="
|
"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": {
|
"@types/responselike": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz",
|
||||||
@@ -12253,11 +12286,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cache-manager-redis-store": {
|
"cache-manager-redis-store": {
|
||||||
"version": "2.0.0",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cache-manager-redis-store/-/cache-manager-redis-store-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cache-manager-redis-store/-/cache-manager-redis-store-3.0.1.tgz",
|
||||||
"integrity": "sha512-bWLWlUg6nCYHiJLCCYxY2MgvwvKnvlWwrbuynrzpjEIhfArD2GC9LtutIHFEPeyGVQN6C+WEw+P3r+BFBwhswg==",
|
"integrity": "sha512-o560kw+dFqusC9lQJhcm6L2F2fMKobJ5af+FoR2PdnMVdpQ3f3Bz6qzvObTGyvoazQJxjQNWgMQeChP4vRTuXQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"redis": "^3.0.2"
|
"redis": "^4.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cacheable-lookup": {
|
"cacheable-lookup": {
|
||||||
@@ -12470,6 +12503,11 @@
|
|||||||
"mimic-response": "^1.0.0"
|
"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": {
|
"code-point-at": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
|
||||||
@@ -12933,11 +12971,6 @@
|
|||||||
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
|
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
|
||||||
"optional": true
|
"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": {
|
"depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -13675,6 +13708,11 @@
|
|||||||
"json-bigint": "^1.0.0"
|
"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": {
|
"gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@@ -16455,32 +16493,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"redis": {
|
"redis": {
|
||||||
"version": "3.1.2",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/redis/-/redis-4.3.1.tgz",
|
||||||
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
|
"integrity": "sha512-cM7yFU5CA6zyCF7N/+SSTcSJQSRMEKN0k0Whhu6J7n9mmXRoXugfWDBo5iOzGwABmsWKSwGPTU5J4Bxbl+0mrA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"denque": "^1.5.0",
|
"@redis/bloom": "1.0.2",
|
||||||
"redis-commands": "^1.7.0",
|
"@redis/client": "1.3.0",
|
||||||
"redis-errors": "^1.2.0",
|
"@redis/graph": "1.0.1",
|
||||||
"redis-parser": "^3.0.0"
|
"@redis/json": "1.0.4",
|
||||||
}
|
"@redis/search": "1.1.0",
|
||||||
},
|
"@redis/time-series": "1.0.3"
|
||||||
"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": {
|
"reflect-metadata": {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"initMigration": "npm run typeorm -- migration:generate -t 1642180264563 -d ormconfig.js \"src/Common/Migrations/Database/init\""
|
"initMigration": "npm run typeorm -- migration:generate -t 1642180264563 -d ormconfig.js \"src/Common/Migrations/Database/init\""
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16.18.0"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
@@ -33,8 +33,8 @@
|
|||||||
"@awaitjs/express": "^0.8.0",
|
"@awaitjs/express": "^0.8.0",
|
||||||
"@datasert/cronjs-matcher": "^1.2.0",
|
"@datasert/cronjs-matcher": "^1.2.0",
|
||||||
"@googleapis/youtube": "^2.0.0",
|
"@googleapis/youtube": "^2.0.0",
|
||||||
"@influxdata/influxdb-client": "^1.27.0",
|
"@influxdata/influxdb-client": "^1.31.0",
|
||||||
"@influxdata/influxdb-client-apis": "^1.27.0",
|
"@influxdata/influxdb-client-apis": "^1.31.0",
|
||||||
"@nlpjs/core": "^4.23.4",
|
"@nlpjs/core": "^4.23.4",
|
||||||
"@nlpjs/lang-de": "^4.23.4",
|
"@nlpjs/lang-de": "^4.23.4",
|
||||||
"@nlpjs/lang-en": "^4.23.4",
|
"@nlpjs/lang-en": "^4.23.4",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"autolinker": "^3.14.3",
|
"autolinker": "^3.14.3",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"cache-manager": "^3.4.4",
|
"cache-manager": "^3.4.4",
|
||||||
"cache-manager-redis-store": "^2.0.0",
|
"cache-manager-redis-store": "^3.0.1",
|
||||||
"commander": "^8.0.0",
|
"commander": "^8.0.0",
|
||||||
"comment-json": "^4.1.1",
|
"comment-json": "^4.1.1",
|
||||||
"connect-typeorm": "^2.0.0",
|
"connect-typeorm": "^2.0.0",
|
||||||
@@ -115,7 +115,6 @@
|
|||||||
"@tsconfig/node14": "^1.0.0",
|
"@tsconfig/node14": "^1.0.0",
|
||||||
"@types/async": "^3.2.7",
|
"@types/async": "^3.2.7",
|
||||||
"@types/cache-manager": "^3.4.2",
|
"@types/cache-manager": "^3.4.2",
|
||||||
"@types/cache-manager-redis-store": "^2.0.0",
|
|
||||||
"@types/chai": "^4.3.0",
|
"@types/chai": "^4.3.0",
|
||||||
"@types/chai-as-promised": "^7.1.5",
|
"@types/chai-as-promised": "^7.1.5",
|
||||||
"@types/cookie-parser": "^1.4.2",
|
"@types/cookie-parser": "^1.4.2",
|
||||||
|
|||||||
@@ -1,30 +1,32 @@
|
|||||||
import {ActionJson, ActionConfig, ActionOptions} from "./index";
|
import {ActionJson, ActionConfig, ActionOptions} from "./index";
|
||||||
import Action from "./index";
|
import Action from "./index";
|
||||||
import {Comment} from "snoowrap";
|
import {Comment} from "snoowrap";
|
||||||
import {renderContent} from "../Utils/SnoowrapUtils";
|
|
||||||
import Submission from "snoowrap/dist/objects/Submission";
|
import Submission from "snoowrap/dist/objects/Submission";
|
||||||
import {ActionProcessResult, RichContent} from "../Common/interfaces";
|
import {ActionProcessResult, RichContent} from "../Common/interfaces";
|
||||||
import {toModNoteLabel} from "../util";
|
import {buildFilterCriteriaSummary, normalizeModActionCriteria, toModNoteLabel} from "../util";
|
||||||
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
import {RuleResultEntity} from "../Common/Entities/RuleResultEntity";
|
||||||
import {runCheckOptions} from "../Subreddit/Manager";
|
import {runCheckOptions} from "../Subreddit/Manager";
|
||||||
import {ActionTypes, ModUserNoteLabel} from "../Common/Infrastructure/Atomic";
|
import {
|
||||||
import {ModNote} from "../Subreddit/ModNotes/ModNote";
|
ActionTypes,
|
||||||
|
ModUserNoteLabel,
|
||||||
|
} from "../Common/Infrastructure/Atomic";
|
||||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||||
|
import {ModNoteCriteria} from "../Common/Infrastructure/Filters/FilterCriteria";
|
||||||
|
|
||||||
|
|
||||||
export class ModNoteAction extends Action {
|
export class ModNoteAction extends Action {
|
||||||
content: string;
|
content: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
allowDuplicate: boolean;
|
existingNoteCheck?: ModNoteCriteria
|
||||||
referenceActivity: boolean
|
referenceActivity: boolean
|
||||||
|
|
||||||
constructor(options: ModNoteActionOptions) {
|
constructor(options: ModNoteActionOptions) {
|
||||||
super(options);
|
super(options);
|
||||||
const {type, content = '', allowDuplicate = false, referenceActivity = true} = options;
|
const {type, content = '', existingNoteCheck = true, referenceActivity = true} = options;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.allowDuplicate = allowDuplicate;
|
|
||||||
this.referenceActivity = referenceActivity;
|
this.referenceActivity = referenceActivity;
|
||||||
|
this.existingNoteCheck = typeof existingNoteCheck === 'boolean' ? this.generateModLogCriteriaFromDuplicateConvenience(existingNoteCheck) : normalizeModActionCriteria(existingNoteCheck);
|
||||||
}
|
}
|
||||||
|
|
||||||
getKind(): ActionTypes {
|
getKind(): ActionTypes {
|
||||||
@@ -35,7 +37,7 @@ export class ModNoteAction extends Action {
|
|||||||
return {
|
return {
|
||||||
content: this.content,
|
content: this.content,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
allowDuplicate: this.allowDuplicate,
|
existingNoteCheck: this.existingNoteCheck,
|
||||||
referenceActivity: this.referenceActivity,
|
referenceActivity: this.referenceActivity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,27 +50,30 @@ export class ModNoteAction extends Action {
|
|||||||
const renderedContent = await this.renderContent(this.content, item, ruleResults, actionResults);
|
const renderedContent = await this.renderContent(this.content, item, ruleResults, actionResults);
|
||||||
this.logger.verbose(`Note:\r\n(${this.type}) ${renderedContent}`);
|
this.logger.verbose(`Note:\r\n(${this.type}) ${renderedContent}`);
|
||||||
|
|
||||||
// TODO see what changes are made for bulk fetch of notes before implementing this
|
let noteCheckPassed: boolean = true;
|
||||||
// https://www.reddit.com/r/redditdev/comments/t8w861/new_mod_notes_api/
|
let noteCheckResult: undefined | string;
|
||||||
// if (!this.allowDuplicate) {
|
|
||||||
// const notes = await this.resources.userNotes.getUserNotes(item.author);
|
if(this.existingNoteCheck === undefined) {
|
||||||
// let existingNote = notes.find((x) => x.link !== null && x.link.includes(item.id));
|
// nothing to do!
|
||||||
// if(existingNote === undefined && notes.length > 0) {
|
noteCheckResult = 'existingNoteCheck=false so no existing note checks were performed.';
|
||||||
// const lastNote = notes[notes.length - 1];
|
} else {
|
||||||
// // possibly notes don't have a reference link so check if last one has same text
|
const noteCheckCriteriaResult = await this.resources.isAuthor(item, {
|
||||||
// if(lastNote.link === null && lastNote.text === renderedContent) {
|
modActions: [this.existingNoteCheck]
|
||||||
// existingNote = lastNote;
|
});
|
||||||
// }
|
noteCheckPassed = noteCheckCriteriaResult.passed;
|
||||||
// }
|
const {details} = buildFilterCriteriaSummary(noteCheckCriteriaResult);
|
||||||
// if (existingNote !== undefined && existingNote.noteType === this.type) {
|
noteCheckResult = `${noteCheckPassed ? 'Existing note check condition succeeded' : 'Will not add note because existing note check condition failed'} -- ${details.join(' ')}`;
|
||||||
// this.logger.info(`Will not add note because one already exists for this Activity (${existingNote.time.local().format()}) and allowDuplicate=false`);
|
}
|
||||||
// return {
|
|
||||||
// dryRun,
|
this.logger.info(noteCheckResult);
|
||||||
// success: false,
|
if (!noteCheckPassed) {
|
||||||
// result: `Will not add note because one already exists for this Activity (${existingNote.time.local().format()}) and allowDuplicate=false`
|
return {
|
||||||
// };
|
dryRun,
|
||||||
// }
|
success: false,
|
||||||
// }
|
result: noteCheckResult
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!dryRun) {
|
if (!dryRun) {
|
||||||
await this.resources.addModNote({
|
await this.resources.addModNote({
|
||||||
label: modLabel,
|
label: modLabel,
|
||||||
@@ -84,15 +89,36 @@ export class ModNoteAction extends Action {
|
|||||||
result: `${modLabel !== undefined ? `(${modLabel})` : ''} ${renderedContent}`
|
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 {
|
export interface ModNoteActionConfig extends ActionConfig, RichContent {
|
||||||
/**
|
/**
|
||||||
* Add Note even if a Note already exists for this Activity
|
* Check if there is an existing Note matching some criteria before adding the Note.
|
||||||
* @examples [false]
|
*
|
||||||
* @default false
|
* 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
|
type?: ModUserNoteLabel
|
||||||
referenceActivity?: boolean
|
referenceActivity?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -529,7 +529,7 @@ class Bot implements BotInstanceFunctions {
|
|||||||
for (const sub of subsToRun) {
|
for (const sub of subsToRun) {
|
||||||
if(!this.subManagers.some(x => x.subreddit.display_name === sub.display_name)) {
|
if(!this.subManagers.some(x => x.subreddit.display_name === sub.display_name)) {
|
||||||
subManagersChanged = true;
|
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);
|
subsToInit.push(sub.display_name);
|
||||||
try {
|
try {
|
||||||
this.subManagers.push(await this.createManager(sub));
|
this.subManagers.push(await this.createManager(sub));
|
||||||
@@ -743,6 +743,9 @@ class Bot implements BotInstanceFunctions {
|
|||||||
eventsState: new EventsRunState({invokee, runType}),
|
eventsState: new EventsRunState({invokee, runType}),
|
||||||
managerState: new ManagerRunState({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, {
|
const manager = new Manager(sub, this.client, this.logger, this.cacheManager, {
|
||||||
|
|||||||
47
src/Common/Cache/index.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,40 @@
|
|||||||
import {InfluxConfig} from "./interfaces";
|
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 {HealthAPI} from "@influxdata/influxdb-client-apis";
|
||||||
import dayjs, {Dayjs} from "dayjs";
|
import dayjs, {Dayjs} from "dayjs";
|
||||||
import {Logger} from "winston";
|
import {Logger} from "winston";
|
||||||
import {mergeArr} from "../../util";
|
import {mergeArr} from "../../util";
|
||||||
import {CMError} from "../../Utils/Errors";
|
import {CMError} from "../../Utils/Errors";
|
||||||
|
import {Agent} from 'http';
|
||||||
|
import {WriteOptions} from "@influxdata/influxdb-client/dist";
|
||||||
|
|
||||||
export interface InfluxClientConfig extends InfluxConfig {
|
export interface InfluxClientConfig extends InfluxConfig {
|
||||||
client?: InfluxDB
|
client?: InfluxDB
|
||||||
ready?: boolean
|
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 {
|
export class InfluxClient {
|
||||||
config: InfluxConfig;
|
config: InfluxConfig;
|
||||||
client: InfluxDB;
|
client: InfluxDB;
|
||||||
@@ -38,9 +62,10 @@ export class InfluxClient {
|
|||||||
this.client = client;
|
this.client = client;
|
||||||
} else {
|
} else {
|
||||||
this.client = InfluxClient.createClient(this.config);
|
this.client = InfluxClient.createClient(this.config);
|
||||||
setLogger(this.logger);
|
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.tags = tags;
|
||||||
this.write.useDefaultTags(tags);
|
this.write.useDefaultTags(tags);
|
||||||
this.health = new HealthAPI(this.client);
|
this.health = new HealthAPI(this.client);
|
||||||
@@ -96,13 +121,62 @@ export class InfluxClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static createClient(config: InfluxConfig): InfluxDB {
|
static createClient(config: InfluxConfig): InfluxDB {
|
||||||
return new InfluxDB({
|
const {
|
||||||
url: config.credentials.url,
|
credentials,
|
||||||
token: config.credentials.token,
|
useKeepAliveAgent = true,
|
||||||
writeOptions: {
|
} = config;
|
||||||
defaultTags: config.defaultTags
|
|
||||||
|
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: 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> = {}) {
|
childClient(logger: Logger, tags: Record<string, string> = {}) {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import {InfluxDB, WriteApi} from "@influxdata/influxdb-client/dist";
|
import {InfluxDB, WriteApi, WriteOptions} from "@influxdata/influxdb-client/dist";
|
||||||
|
|
||||||
export interface InfluxConfig {
|
export interface InfluxConfig {
|
||||||
credentials: InfluxCredentials
|
credentials: InfluxCredentials
|
||||||
defaultTags?: Record<string, string>
|
defaultTags?: Record<string, string>
|
||||||
|
writeOptions?: WriteOptions
|
||||||
|
useKeepAliveAgent?: boolean
|
||||||
|
debug?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InfluxCredentials {
|
export interface InfluxCredentials {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export type RelativeDateTimeMatch = string;
|
|||||||
* * EX `> 100` => greater than 100
|
* * EX `> 100` => greater than 100
|
||||||
* * EX `<= 75%` => less than or equal to 75%
|
* * 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 CompareValueOrPercent = string;
|
||||||
export type StringOperator = '>' | '>=' | '<' | '<=';
|
export type StringOperator = '>' | '>=' | '<' | '<=';
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ export const asGenericComparison = (val: any): val is GenericComparison => {
|
|||||||
return typeof val === 'object' && 'value' in val;
|
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 = /^\s*(?<opStr>>|>=|<|<=)\s*(?<value>-?(?:\d+)(?:(?:(?:.|,)\d+)+)?)(?<extra>\s+.*)*$/
|
||||||
export const GENERIC_VALUE_COMPARISON_URL = 'https://regexr.com/60dq4';
|
export const GENERIC_VALUE_COMPARISON_URL = 'https://regexr.com/6vama';
|
||||||
export const parseGenericValueComparison = (val: string, options?: {
|
export const parseGenericValueComparison = (val: string, options?: {
|
||||||
requireDuration?: boolean,
|
requireDuration?: boolean,
|
||||||
reg?: RegExp
|
reg?: RegExp
|
||||||
@@ -107,8 +107,8 @@ export const parseGenericValueComparison = (val: string, options?: {
|
|||||||
durationText,
|
durationText,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const GENERIC_VALUE_PERCENT_COMPARISON = /^\s*(?<opStr>>|>=|<|<=)\s*(?<value>\d+)\s*(?<percent>%)?(?<extra>.*)$/
|
const GENERIC_VALUE_PERCENT_COMPARISON = /^\s*(?<opStr>>|>=|<|<=)\s*(?<value>(?:\d+)(?:(?:(?:.|,)\d+)+)?)\s*(?<percent>%)?(?<extra>.*)$/
|
||||||
const GENERIC_VALUE_PERCENT_COMPARISON_URL = 'https://regexr.com/60a16';
|
const GENERIC_VALUE_PERCENT_COMPARISON_URL = 'https://regexr.com/6valr';
|
||||||
export const parseGenericValueOrPercentComparison = (val: string, options?: {requireDuration: boolean}): GenericComparison => {
|
export const parseGenericValueOrPercentComparison = (val: string, options?: {requireDuration: boolean}): GenericComparison => {
|
||||||
return parseGenericValueComparison(val, {...(options ?? {}), reg: GENERIC_VALUE_PERCENT_COMPARISON});
|
return parseGenericValueComparison(val, {...(options ?? {}), reg: GENERIC_VALUE_PERCENT_COMPARISON});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import {
|
|||||||
ModeratorNames, ModActionType,
|
ModeratorNames, ModActionType,
|
||||||
ModUserNoteLabel, RelativeDateTimeMatch
|
ModUserNoteLabel, RelativeDateTimeMatch
|
||||||
} from "../Atomic";
|
} from "../Atomic";
|
||||||
import {ActivityType} from "../Reddit";
|
import {ActivityType, MaybeActivityType} from "../Reddit";
|
||||||
import {GenericComparison, parseGenericValueComparison} from "../Comparisons";
|
import {GenericComparison, parseGenericValueComparison} from "../Comparisons";
|
||||||
import {parseStringToRegexOrLiteralSearch} from "../../../util";
|
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.
|
* Different attributes a `Subreddit` can be in. Only include a property if you want to check it.
|
||||||
@@ -122,13 +123,14 @@ export interface UserNoteCriteria extends UserSubredditHistoryCriteria {
|
|||||||
|
|
||||||
export interface ModActionCriteria extends UserSubredditHistoryCriteria {
|
export interface ModActionCriteria extends UserSubredditHistoryCriteria {
|
||||||
type?: ModActionType | ModActionType[]
|
type?: ModActionType | ModActionType[]
|
||||||
activityType?: ActivityType | ActivityType[]
|
activityType?: MaybeActivityType | MaybeActivityType[]
|
||||||
|
referencesCurrentActivity?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FullModActionCriteria extends Omit<ModActionCriteria, 'count'> {
|
export interface FullModActionCriteria extends Omit<ModActionCriteria, 'count'> {
|
||||||
type?: ModActionType[]
|
type?: ModActionType[]
|
||||||
count?: GenericComparison
|
count?: GenericComparison
|
||||||
activityType?: ActivityType[]
|
activityType?: MaybeActivityType[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModNoteCriteria extends ModActionCriteria {
|
export interface ModNoteCriteria extends ModActionCriteria {
|
||||||
@@ -167,6 +169,7 @@ export const toFullModNoteCriteria = (val: ModNoteCriteria): FullModNoteCriteria
|
|||||||
break;
|
break;
|
||||||
case 'activityType':
|
case 'activityType':
|
||||||
case 'noteType':
|
case 'noteType':
|
||||||
|
case 'referencesCurrentActivity':
|
||||||
acc[k] = rawVal;
|
acc[k] = rawVal;
|
||||||
break;
|
break;
|
||||||
case 'note':
|
case 'note':
|
||||||
@@ -219,6 +222,7 @@ export const toFullModLogCriteria = (val: ModLogCriteria): FullModLogCriteria =>
|
|||||||
break;
|
break;
|
||||||
case 'activityType':
|
case 'activityType':
|
||||||
case 'type':
|
case 'type':
|
||||||
|
case 'referencesCurrentActivity':
|
||||||
acc[k as keyof FullModLogCriteria] = rawVal;
|
acc[k as keyof FullModLogCriteria] = rawVal;
|
||||||
break;
|
break;
|
||||||
case 'action':
|
case 'action':
|
||||||
@@ -485,6 +489,33 @@ export interface ActivityState {
|
|||||||
*
|
*
|
||||||
* */
|
* */
|
||||||
source?: string | string[]
|
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 +538,22 @@ export interface SubmissionState extends ActivityState {
|
|||||||
/**
|
/**
|
||||||
* * If `true` then passes if flair has ANY text
|
* * If `true` then passes if flair has ANY text
|
||||||
* * If `false` then passes if flair has NO 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[]
|
link_flair_text?: boolean | string | string[]
|
||||||
/**
|
/**
|
||||||
* * If `true` then passes if flair has ANY css
|
* * If `true` then passes if flair has ANY css
|
||||||
* * If `false` then passes if flair has NO 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[]
|
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 `true` then passes if there is ANY flair template id
|
||||||
* * If `false` then passes if there is NO flair template id
|
* * If `false` then passes if there is NO flair template id
|
||||||
@@ -537,6 +577,16 @@ export interface SubmissionState extends ActivityState {
|
|||||||
upvoteRatio?: number | CompareValue
|
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'];
|
export const cmActivityProperties = ['submissionState', 'score', 'reports', 'removed', 'deleted', 'filtered', 'age', 'title'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {Comment, Submission} from "snoowrap/dist/objects";
|
import {Comment, Submission} from "snoowrap/dist/objects";
|
||||||
|
|
||||||
export type ActivityType = 'submission' | 'comment';
|
export type ActivityType = 'submission' | 'comment';
|
||||||
|
export type MaybeActivityType = ActivityType | false;
|
||||||
export type FullNameTypes = ActivityType | 'user' | 'subreddit' | 'message';
|
export type FullNameTypes = ActivityType | 'user' | 'subreddit' | 'message';
|
||||||
|
|
||||||
export interface RedditThing {
|
export interface RedditThing {
|
||||||
|
|||||||
6
src/Common/Typings/support.d.ts
vendored
@@ -163,3 +163,9 @@ declare module 'wink-sentiment' {
|
|||||||
|
|
||||||
export default 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,4 +45,4 @@ export const filterCriteriaDefault: FilterCriteriaDefaults = {
|
|||||||
export const defaultDataDir = path.resolve(__dirname, '../..');
|
export const defaultDataDir = path.resolve(__dirname, '../..');
|
||||||
export const defaultConfigFilenames = ['config.json', 'config.yaml'];
|
export const defaultConfigFilenames = ['config.json', 'config.yaml'];
|
||||||
|
|
||||||
export const VERSION = '0.12.2';
|
export const VERSION = '0.13.1';
|
||||||
|
|||||||
@@ -81,6 +81,23 @@ export interface HistoryCriteria {
|
|||||||
|
|
||||||
window: ActivityWindowConfig
|
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
|
* The minimum number of **filtered** activities that must exist from the `window` results for this criteria to run
|
||||||
* @default 5
|
* @default 5
|
||||||
@@ -170,7 +187,7 @@ export class HistoryRule extends Rule {
|
|||||||
|
|
||||||
for (const criteria of this.criteria) {
|
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);
|
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 firstActivity = activities[0];
|
||||||
const lastActivity = activities[activities.length - 1];
|
const lastActivity = activities[activities.length - 1];
|
||||||
|
|
||||||
@@ -263,11 +298,13 @@ export class HistoryRule extends Rule {
|
|||||||
submissionTotal: fSubmissionTotal,
|
submissionTotal: fSubmissionTotal,
|
||||||
commentTotal: fCommentTotal,
|
commentTotal: fCommentTotal,
|
||||||
opTotal: fOpTotal,
|
opTotal: fOpTotal,
|
||||||
|
foundRatio,
|
||||||
filteredTotal: filteredActivities.length,
|
filteredTotal: filteredActivities.length,
|
||||||
submissionTrigger,
|
submissionTrigger,
|
||||||
commentTrigger,
|
commentTrigger,
|
||||||
totalTrigger,
|
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))
|
subredditBreakdown: getSubredditBreakdownByActivityType(!asOp ? filteredActivities : filteredActivities.filter(x => asSubmission(x) || x.is_submitter))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -318,11 +355,13 @@ export class HistoryRule extends Rule {
|
|||||||
submissionTotal,
|
submissionTotal,
|
||||||
commentTotal,
|
commentTotal,
|
||||||
filteredTotal,
|
filteredTotal,
|
||||||
|
foundRatio,
|
||||||
opTotal,
|
opTotal,
|
||||||
criteria: {
|
criteria: {
|
||||||
comment,
|
comment,
|
||||||
submission,
|
submission,
|
||||||
total,
|
total,
|
||||||
|
ratio,
|
||||||
window,
|
window,
|
||||||
},
|
},
|
||||||
criteria,
|
criteria,
|
||||||
@@ -330,6 +369,7 @@ export class HistoryRule extends Rule {
|
|||||||
submissionTrigger,
|
submissionTrigger,
|
||||||
commentTrigger,
|
commentTrigger,
|
||||||
totalTrigger,
|
totalTrigger,
|
||||||
|
ratioTrigger,
|
||||||
subredditBreakdown,
|
subredditBreakdown,
|
||||||
} = results;
|
} = results;
|
||||||
|
|
||||||
@@ -338,6 +378,7 @@ export class HistoryRule extends Rule {
|
|||||||
submissionTotal,
|
submissionTotal,
|
||||||
commentTotal,
|
commentTotal,
|
||||||
filteredTotal,
|
filteredTotal,
|
||||||
|
foundRatio,
|
||||||
opTotal,
|
opTotal,
|
||||||
commentPercent: formatNumber((commentTotal/activityTotal)*100),
|
commentPercent: formatNumber((commentTotal/activityTotal)*100),
|
||||||
submissionPercent: formatNumber((submissionTotal/activityTotal)*100),
|
submissionPercent: formatNumber((submissionTotal/activityTotal)*100),
|
||||||
@@ -349,6 +390,7 @@ export class HistoryRule extends Rule {
|
|||||||
submissionTrigger,
|
submissionTrigger,
|
||||||
commentTrigger,
|
commentTrigger,
|
||||||
totalTrigger,
|
totalTrigger,
|
||||||
|
ratioTrigger,
|
||||||
subredditBreakdown
|
subredditBreakdown
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -356,6 +398,7 @@ export class HistoryRule extends Rule {
|
|||||||
let totalSummary;
|
let totalSummary;
|
||||||
let submissionSummary;
|
let submissionSummary;
|
||||||
let commentSummary;
|
let commentSummary;
|
||||||
|
let ratioSummary;
|
||||||
if(total !== undefined) {
|
if(total !== undefined) {
|
||||||
const {operator, value, isPercent, displayText} = parseGenericValueOrPercentComparison(total);
|
const {operator, value, isPercent, displayText} = parseGenericValueOrPercentComparison(total);
|
||||||
const suffix = !isPercent ? 'Items' : `(${formatNumber((filteredTotal/activityTotal)*100)}%) of ${activityTotal} Total`;
|
const suffix = !isPercent ? 'Items' : `(${formatNumber((filteredTotal/activityTotal)*100)}%) of ${activityTotal} Total`;
|
||||||
@@ -380,6 +423,13 @@ export class HistoryRule extends Rule {
|
|||||||
data.commentSummary = commentSummary;
|
data.commentSummary = commentSummary;
|
||||||
thresholdSummary.push(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 ');
|
data.thresholdSummary = thresholdSummary.join(' and ');
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ import {ActivityWindow, ActivityWindowConfig} from "../Common/Infrastructure/Act
|
|||||||
import {comparisonTextOp, parseGenericValueOrPercentComparison} from "../Common/Infrastructure/Comparisons";
|
import {comparisonTextOp, parseGenericValueOrPercentComparison} from "../Common/Infrastructure/Comparisons";
|
||||||
import {ImageHashCacheData} from "../Common/Infrastructure/Atomic";
|
import {ImageHashCacheData} from "../Common/Infrastructure/Atomic";
|
||||||
import {getSubredditBreakdownByActivityType} from "../Utils/SnoowrapUtils";
|
import {getSubredditBreakdownByActivityType} from "../Utils/SnoowrapUtils";
|
||||||
|
import {CMError} from "../Utils/Errors";
|
||||||
|
|
||||||
const parseLink = parseUsableLinkIdentifier();
|
const parseLink = parseUsableLinkIdentifier();
|
||||||
|
|
||||||
@@ -315,7 +316,7 @@ export class RecentActivityRule extends Rule {
|
|||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if(!err.message.includes('did not end with a valid image extension')) {
|
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}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -1717,18 +1785,18 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1767,6 +1835,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"referencesCurrentActivity": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"default": "current",
|
"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",
|
"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": {
|
"ModNoteActionJson": {
|
||||||
"description": "Add a Toolbox User Note to the Author of this Activity",
|
"description": "Add a Toolbox User Note to the Author of this Activity",
|
||||||
"properties": {
|
"properties": {
|
||||||
"allowDuplicate": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Add Note even if a Note already exists for this Activity",
|
|
||||||
"examples": [
|
|
||||||
false
|
|
||||||
],
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"authorIs": {
|
"authorIs": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -1871,6 +1934,21 @@
|
|||||||
],
|
],
|
||||||
"type": "boolean"
|
"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": {
|
"itemIs": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -1943,18 +2021,18 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -2013,6 +2091,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"referencesCurrentActivity": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"default": "current",
|
"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",
|
"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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -2586,6 +2735,23 @@
|
|||||||
"is_self": {
|
"is_self": {
|
||||||
"type": "boolean"
|
"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": {
|
"link_flair_css_class": {
|
||||||
"anyOf": [
|
"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": {
|
"link_flair_text": {
|
||||||
"anyOf": [
|
"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": {
|
"locked": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
|||||||
@@ -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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -2904,6 +3040,40 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"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": {
|
"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",
|
"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*(%?)(.*)$",
|
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
|
||||||
@@ -3574,18 +3744,18 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -3624,6 +3794,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"referencesCurrentActivity": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"default": "current",
|
"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",
|
"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": {
|
"ModNoteActionJson": {
|
||||||
"description": "Add a Toolbox User Note to the Author of this Activity",
|
"description": "Add a Toolbox User Note to the Author of this Activity",
|
||||||
"properties": {
|
"properties": {
|
||||||
"allowDuplicate": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Add Note even if a Note already exists for this Activity",
|
|
||||||
"examples": [
|
|
||||||
false
|
|
||||||
],
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"authorIs": {
|
"authorIs": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -3728,6 +3893,21 @@
|
|||||||
],
|
],
|
||||||
"type": "boolean"
|
"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": {
|
"itemIs": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -3800,18 +3980,18 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -3870,6 +4050,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"referencesCurrentActivity": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"default": "current",
|
"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",
|
"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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -6282,6 +6533,23 @@
|
|||||||
"is_self": {
|
"is_self": {
|
||||||
"type": "boolean"
|
"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": {
|
"link_flair_css_class": {
|
||||||
"anyOf": [
|
"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": {
|
"link_flair_text": {
|
||||||
"anyOf": [
|
"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": {
|
"locked": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
|||||||
@@ -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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -2618,6 +2754,40 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"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": {
|
"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",
|
"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*(%?)(.*)$",
|
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
|
||||||
@@ -3288,18 +3458,18 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -3338,6 +3508,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"referencesCurrentActivity": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"default": "current",
|
"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",
|
"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": {
|
"ModNoteActionJson": {
|
||||||
"description": "Add a Toolbox User Note to the Author of this Activity",
|
"description": "Add a Toolbox User Note to the Author of this Activity",
|
||||||
"properties": {
|
"properties": {
|
||||||
"allowDuplicate": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Add Note even if a Note already exists for this Activity",
|
|
||||||
"examples": [
|
|
||||||
false
|
|
||||||
],
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"authorIs": {
|
"authorIs": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -3442,6 +3607,21 @@
|
|||||||
],
|
],
|
||||||
"type": "boolean"
|
"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": {
|
"itemIs": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -3514,18 +3694,18 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -3584,6 +3764,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"referencesCurrentActivity": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"default": "current",
|
"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",
|
"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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -5726,6 +5977,23 @@
|
|||||||
"is_self": {
|
"is_self": {
|
||||||
"type": "boolean"
|
"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": {
|
"link_flair_css_class": {
|
||||||
"anyOf": [
|
"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": {
|
"link_flair_text": {
|
||||||
"anyOf": [
|
"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": {
|
"locked": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
|||||||
@@ -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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -1065,18 +1133,18 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1115,6 +1183,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"referencesCurrentActivity": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"default": "current",
|
"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",
|
"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 +1237,18 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1236,6 +1307,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"referencesCurrentActivity": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"default": "current",
|
"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",
|
"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 +1934,74 @@
|
|||||||
],
|
],
|
||||||
"description": "* true/false => test whether Activity is approved or not\n* string or list of strings => test which moderator approved this Activity"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -1924,6 +2066,23 @@
|
|||||||
"is_self": {
|
"is_self": {
|
||||||
"type": "boolean"
|
"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": {
|
"link_flair_css_class": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -1939,7 +2098,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": {
|
"link_flair_text": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
@@ -1956,7 +2115,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": {
|
"locked": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
|||||||
@@ -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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -1467,6 +1603,40 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"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": {
|
"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",
|
"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*(%?)(.*)$",
|
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
|
||||||
@@ -1915,18 +2085,18 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1965,6 +2135,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"referencesCurrentActivity": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"default": "current",
|
"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",
|
"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": {
|
"items": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -2086,6 +2259,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"referencesCurrentActivity": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"default": "current",
|
"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",
|
"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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -3506,6 +3750,23 @@
|
|||||||
"is_self": {
|
"is_self": {
|
||||||
"type": "boolean"
|
"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": {
|
"link_flair_css_class": {
|
||||||
"anyOf": [
|
"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": {
|
"link_flair_text": {
|
||||||
"anyOf": [
|
"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": {
|
"locked": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
|||||||
@@ -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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -1432,6 +1568,40 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"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": {
|
"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",
|
"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*(%?)(.*)$",
|
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
|
||||||
@@ -1880,18 +2050,18 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1930,6 +2100,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"referencesCurrentActivity": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"default": "current",
|
"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",
|
"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": {
|
"items": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -2051,6 +2224,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"referencesCurrentActivity": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"default": "current",
|
"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",
|
"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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -3471,6 +3715,23 @@
|
|||||||
"is_self": {
|
"is_self": {
|
||||||
"type": "boolean"
|
"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": {
|
"link_flair_css_class": {
|
||||||
"anyOf": [
|
"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": {
|
"link_flair_text": {
|
||||||
"anyOf": [
|
"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": {
|
"locked": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
|||||||
@@ -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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -2685,6 +2821,40 @@
|
|||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"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": {
|
"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",
|
"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*(%?)(.*)$",
|
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
|
||||||
@@ -3355,18 +3525,18 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -3405,6 +3575,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"referencesCurrentActivity": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"default": "current",
|
"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",
|
"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": {
|
"ModNoteActionJson": {
|
||||||
"description": "Add a Toolbox User Note to the Author of this Activity",
|
"description": "Add a Toolbox User Note to the Author of this Activity",
|
||||||
"properties": {
|
"properties": {
|
||||||
"allowDuplicate": {
|
|
||||||
"default": false,
|
|
||||||
"description": "Add Note even if a Note already exists for this Activity",
|
|
||||||
"examples": [
|
|
||||||
false
|
|
||||||
],
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"authorIs": {
|
"authorIs": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -3509,6 +3674,21 @@
|
|||||||
],
|
],
|
||||||
"type": "boolean"
|
"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": {
|
"itemIs": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -3581,18 +3761,18 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
},
|
},
|
||||||
"type": "array"
|
"type": "array"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enum": [
|
"enum": [
|
||||||
"comment",
|
"comment",
|
||||||
|
false,
|
||||||
"submission"
|
"submission"
|
||||||
],
|
]
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -3651,6 +3831,9 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"referencesCurrentActivity": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"default": "current",
|
"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",
|
"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"
|
"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": {
|
"createdOn": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
@@ -5923,6 +6174,23 @@
|
|||||||
"is_self": {
|
"is_self": {
|
||||||
"type": "boolean"
|
"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": {
|
"link_flair_css_class": {
|
||||||
"anyOf": [
|
"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": {
|
"link_flair_text": {
|
||||||
"anyOf": [
|
"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": {
|
"locked": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
|||||||
@@ -654,10 +654,6 @@ export class Manager extends EventEmitter implements RunningStates {
|
|||||||
|
|
||||||
this.displayLabel = nickname || `${this.subreddit.display_name_prefixed}`;
|
this.displayLabel = nickname || `${this.subreddit.display_name_prefixed}`;
|
||||||
|
|
||||||
if (footer !== undefined && this.resources !== undefined) {
|
|
||||||
this.resources.footer = footer;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.subMaxWorkers = maxWorkers;
|
this.subMaxWorkers = maxWorkers;
|
||||||
const realMax = this.getMaxWorkers(this.subMaxWorkers);
|
const realMax = this.getMaxWorkers(this.subMaxWorkers);
|
||||||
if(realMax !== this.queue.concurrency) {
|
if(realMax !== this.queue.concurrency) {
|
||||||
@@ -697,6 +693,10 @@ export class Manager extends EventEmitter implements RunningStates {
|
|||||||
await this.setResourceManager(resourceConfig);
|
await this.setResourceManager(resourceConfig);
|
||||||
this.resources.setLogger(this.logger);
|
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('Subreddit-specific options updated');
|
||||||
this.logger.info('Building Runs and Checks...');
|
this.logger.info('Building Runs and Checks...');
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,8 @@ import {
|
|||||||
asActivity,
|
asActivity,
|
||||||
asSubmission,
|
asSubmission,
|
||||||
asUserNoteCriteria,
|
asUserNoteCriteria,
|
||||||
buildCacheOptionsFromProvider,
|
|
||||||
buildCachePrefix,
|
buildCachePrefix,
|
||||||
cacheStats,
|
cacheStats,
|
||||||
createCacheManager,
|
|
||||||
escapeRegex,
|
escapeRegex,
|
||||||
FAIL,
|
FAIL,
|
||||||
fetchExternalResult,
|
fetchExternalResult,
|
||||||
@@ -112,9 +110,20 @@ import cloneDeep from "lodash/cloneDeep";
|
|||||||
import {
|
import {
|
||||||
asModLogCriteria,
|
asModLogCriteria,
|
||||||
asModNoteCriteria,
|
asModNoteCriteria,
|
||||||
AuthorCriteria, CommentState, ModLogCriteria, ModNoteCriteria, orderedAuthorCriteriaProps, RequiredAuthorCrit,
|
AuthorCriteria,
|
||||||
StrongSubredditCriteria, SubmissionState,
|
cmToSnoowrapActivityMap,
|
||||||
SubredditCriteria, toFullModLogCriteria, toFullModNoteCriteria, TypedActivityState, TypedActivityStates,
|
CommentState,
|
||||||
|
ModLogCriteria,
|
||||||
|
ModNoteCriteria,
|
||||||
|
orderedAuthorCriteriaProps,
|
||||||
|
RequiredAuthorCrit,
|
||||||
|
StrongSubredditCriteria,
|
||||||
|
SubmissionState,
|
||||||
|
SubredditCriteria,
|
||||||
|
toFullModLogCriteria,
|
||||||
|
toFullModNoteCriteria,
|
||||||
|
TypedActivityState,
|
||||||
|
TypedActivityStates,
|
||||||
UserNoteCriteria
|
UserNoteCriteria
|
||||||
} from "../Common/Infrastructure/Filters/FilterCriteria";
|
} from "../Common/Infrastructure/Filters/FilterCriteria";
|
||||||
import {
|
import {
|
||||||
@@ -144,7 +153,7 @@ import {
|
|||||||
|
|
||||||
ActivityType,
|
ActivityType,
|
||||||
AuthorHistorySort,
|
AuthorHistorySort,
|
||||||
CachedFetchedActivitiesResult, FetchedActivitiesResult,
|
CachedFetchedActivitiesResult, FetchedActivitiesResult, MaybeActivityType,
|
||||||
SnoowrapActivity, SubredditRemovalReason
|
SnoowrapActivity, SubredditRemovalReason
|
||||||
} from "../Common/Infrastructure/Reddit";
|
} from "../Common/Infrastructure/Reddit";
|
||||||
import {AuthorCritPropHelper} from "../Common/Infrastructure/Filters/AuthorCritPropHelper";
|
import {AuthorCritPropHelper} from "../Common/Infrastructure/Filters/AuthorCritPropHelper";
|
||||||
@@ -162,6 +171,7 @@ import ConfigParseError from "../Utils/ConfigParseError";
|
|||||||
import {ActivityReport} from "../Common/Entities/ActivityReport";
|
import {ActivityReport} from "../Common/Entities/ActivityReport";
|
||||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||||
import {ActivitySource} from "../Common/ActivitySource";
|
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.';
|
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.';
|
||||||
|
|
||||||
@@ -1221,6 +1231,156 @@ export class SubredditResources {
|
|||||||
return false;
|
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) {
|
async getAuthorModNotesByActivityAuthor(activity: Comment | Submission) {
|
||||||
const author = activity.author instanceof RedditUser ? activity.author : getActivityAuthorName(activity.author);
|
const author = activity.author instanceof RedditUser ? activity.author : getActivityAuthorName(activity.author);
|
||||||
if (activity.subreddit.display_name !== this.subreddit.display_name) {
|
if (activity.subreddit.display_name !== this.subreddit.display_name) {
|
||||||
@@ -2654,13 +2814,22 @@ export class SubredditResources {
|
|||||||
case 'flairTemplate':
|
case 'flairTemplate':
|
||||||
case 'link_flair_text':
|
case 'link_flair_text':
|
||||||
case 'link_flair_css_class':
|
case 'link_flair_css_class':
|
||||||
if(asSubmission(item)) {
|
case 'link_flair_background_color':
|
||||||
let propertyValue: string | null;
|
case 'authorFlairText':
|
||||||
if(k === 'flairTemplate') {
|
case 'authorFlairCssClass':
|
||||||
propertyValue = await item.link_flair_template_id;
|
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 {
|
} else {
|
||||||
propertyValue = await item[k];
|
// @ts-ignore
|
||||||
}
|
let propertyValue: string | null = await item[actualPropName];
|
||||||
|
|
||||||
propResultsMap[k]!.found = propertyValue;
|
propResultsMap[k]!.found = propertyValue;
|
||||||
|
|
||||||
@@ -2674,14 +2843,37 @@ export class SubredditResources {
|
|||||||
// if crit is not a boolean but property is "empty" then it'll never pass anyway
|
// if crit is not a boolean but property is "empty" then it'll never pass anyway
|
||||||
propResultsMap[k]!.passed = !include;
|
propResultsMap[k]!.passed = !include;
|
||||||
} else {
|
} else {
|
||||||
const expectedValues = typeof itemOptVal === 'string' ? [itemOptVal] : (itemOptVal as string[]);
|
// remove # if comparing hex values
|
||||||
propResultsMap[k]!.passed = criteriaPassWithIncludeBehavior(expectedValues.some(x => x.trim().toLowerCase() === propertyValue?.trim().toLowerCase()), include);
|
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;
|
||||||
}
|
}
|
||||||
break;
|
} 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 {
|
} else {
|
||||||
propResultsMap[k]!.passed = true;
|
errorReasons.push(err.message);
|
||||||
propResultsMap[k]!.reason = `Cannot test for ${k} on Comment`;
|
}
|
||||||
log.warn(`Cannot test for ${k} on Comment`);
|
}
|
||||||
|
if(anyPassed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(errorReasons.length > 0) {
|
||||||
|
propResultsMap[k]!.reason = `Some errors occurred while testing: ${errorReasons.join(' | ')}`;
|
||||||
|
}
|
||||||
|
propResultsMap[k]!.passed = criteriaPassWithIncludeBehavior(anyPassed, include);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -3132,7 +3324,6 @@ export class SubredditResources {
|
|||||||
|
|
||||||
const {search = 'current', count = '>= 1'} = actionCriteria;
|
const {search = 'current', count = '>= 1'} = actionCriteria;
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
value,
|
value,
|
||||||
operator,
|
operator,
|
||||||
@@ -3140,146 +3331,10 @@ export class SubredditResources {
|
|||||||
duration,
|
duration,
|
||||||
extra = ''
|
extra = ''
|
||||||
} = parseGenericValueOrPercentComparison(count);
|
} = parseGenericValueOrPercentComparison(count);
|
||||||
const cutoffDate = duration === undefined ? undefined : dayjs().subtract(duration);
|
|
||||||
|
|
||||||
let actionsToUse: ModNote[] = [];
|
const [validActions, actionsToUse] = this.filterAuthorModActions(modActions, actionCriteria, item);
|
||||||
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)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (search) {
|
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':
|
case 'consecutive':
|
||||||
if (isPercent) {
|
if (isPercent) {
|
||||||
throw new SimpleError(`When comparing Mod Actions with 'search: consecutive' the 'count' value cannot be a percentage. Given: ${count}`);
|
throw new SimpleError(`When comparing Mod Actions with 'search: consecutive' the 'count' value cannot be a percentage. Given: ${count}`);
|
||||||
@@ -3306,10 +3361,11 @@ export class SubredditResources {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'current':
|
||||||
case 'total':
|
case 'total':
|
||||||
if (isPercent) {
|
if (isPercent) {
|
||||||
// avoid divide by zero
|
// 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`);
|
actionResult.push(`${formatNumber(percent)}% of ${actionsToUse.length} matched criteria`);
|
||||||
if (comparisonTextOp(percent, operator, value / 100)) {
|
if (comparisonTextOp(percent, operator, value / 100)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -3488,7 +3544,7 @@ export class BotResourcesManager {
|
|||||||
authorTTL: number = 10000;
|
authorTTL: number = 10000;
|
||||||
enabled: boolean = true;
|
enabled: boolean = true;
|
||||||
modStreams: Map<string, SPoll<Snoowrap.Submission | Snoowrap.Comment>> = new Map();
|
modStreams: Map<string, SPoll<Snoowrap.Submission | Snoowrap.Comment>> = new Map();
|
||||||
defaultCache: Cache;
|
defaultCache: Promise<Cache>;
|
||||||
defaultCacheConfig: StrongCache
|
defaultCacheConfig: StrongCache
|
||||||
defaultCacheMigrated: boolean = false;
|
defaultCacheMigrated: boolean = false;
|
||||||
cacheType: string = 'none';
|
cacheType: string = 'none';
|
||||||
@@ -3592,7 +3648,7 @@ export class BotResourcesManager {
|
|||||||
// });
|
// });
|
||||||
|
|
||||||
let opts: SubredditResourceOptions = {
|
let opts: SubredditResourceOptions = {
|
||||||
cache: this.defaultCache,
|
cache: await this.defaultCache,
|
||||||
cacheType: this.cacheType,
|
cacheType: this.cacheType,
|
||||||
cacheSettingsHash: hash,
|
cacheSettingsHash: hash,
|
||||||
ttl: this.ttlDefaults,
|
ttl: this.ttlDefaults,
|
||||||
@@ -3623,7 +3679,7 @@ export class BotResourcesManager {
|
|||||||
trueProvider.prefix = subPrefix;
|
trueProvider.prefix = subPrefix;
|
||||||
const eventsMax = this.actionedEventsMaxDefault !== undefined ? Math.min(actionedEventsMax, this.actionedEventsMaxDefault) : actionedEventsMax;
|
const eventsMax = this.actionedEventsMaxDefault !== undefined ? Math.min(actionedEventsMax, this.actionedEventsMaxDefault) : actionedEventsMax;
|
||||||
opts = {
|
opts = {
|
||||||
cache: createCacheManager(trueProvider),
|
cache: await createCacheManager(trueProvider),
|
||||||
actionedEventsMax: eventsMax,
|
actionedEventsMax: eventsMax,
|
||||||
cacheType: trueProvider.store,
|
cacheType: trueProvider.store,
|
||||||
cacheSettingsHash: hash,
|
cacheSettingsHash: hash,
|
||||||
@@ -3638,7 +3694,7 @@ export class BotResourcesManager {
|
|||||||
await runMigrations(opts.cache, opts.logger, trueProvider.prefix);
|
await runMigrations(opts.cache, opts.logger, trueProvider.prefix);
|
||||||
}
|
}
|
||||||
} else if(!this.defaultCacheMigrated) {
|
} else if(!this.defaultCacheMigrated) {
|
||||||
await runMigrations(this.defaultCache, this.logger, opts.prefix);
|
await runMigrations(await this.defaultCache, this.logger, opts.prefix);
|
||||||
this.defaultCacheMigrated = true;
|
this.defaultCacheMigrated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3673,7 +3729,7 @@ export class BotResourcesManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPendingSubredditInvites(): Promise<(string[])> {
|
async getPendingSubredditInvites(): Promise<(string[])> {
|
||||||
const subredditNames = await this.defaultCache.get(`modInvites`);
|
const subredditNames = await (await this.defaultCache).get(`modInvites`);
|
||||||
if (subredditNames !== undefined && subredditNames !== null) {
|
if (subredditNames !== undefined && subredditNames !== null) {
|
||||||
return subredditNames as string[];
|
return subredditNames as string[];
|
||||||
}
|
}
|
||||||
@@ -3684,7 +3740,7 @@ export class BotResourcesManager {
|
|||||||
if(subreddit === null || subreddit === undefined || subreddit == '') {
|
if(subreddit === null || subreddit === undefined || subreddit == '') {
|
||||||
throw new CMError('Subreddit name cannot be empty');
|
throw new CMError('Subreddit name cannot be empty');
|
||||||
}
|
}
|
||||||
let subredditNames = await this.defaultCache.get(`modInvites`) as (string[] | undefined | null);
|
let subredditNames = await (await this.defaultCache).get(`modInvites`) as (string[] | undefined | null);
|
||||||
if (subredditNames === undefined || subredditNames === null) {
|
if (subredditNames === undefined || subredditNames === null) {
|
||||||
subredditNames = [];
|
subredditNames = [];
|
||||||
}
|
}
|
||||||
@@ -3694,22 +3750,22 @@ export class BotResourcesManager {
|
|||||||
throw new CMError(`An invite for the Subreddit '${subreddit}' already exists`);
|
throw new CMError(`An invite for the Subreddit '${subreddit}' already exists`);
|
||||||
}
|
}
|
||||||
subredditNames.push(cleanName);
|
subredditNames.push(cleanName);
|
||||||
await this.defaultCache.set(`modInvites`, subredditNames, {ttl: 0});
|
await (await this.defaultCache).set(`modInvites`, subredditNames, {ttl: 0});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deletePendingSubredditInvite(subreddit: string): Promise<void> {
|
async deletePendingSubredditInvite(subreddit: string): Promise<void> {
|
||||||
let subredditNames = await this.defaultCache.get(`modInvites`) as (string[] | undefined | null);
|
let subredditNames = await (await this.defaultCache).get(`modInvites`) as (string[] | undefined | null);
|
||||||
if (subredditNames === undefined || subredditNames === null) {
|
if (subredditNames === undefined || subredditNames === null) {
|
||||||
subredditNames = [];
|
subredditNames = [];
|
||||||
}
|
}
|
||||||
subredditNames = subredditNames.filter(x => x.toLowerCase() !== subreddit.trim().toLowerCase());
|
subredditNames = subredditNames.filter(x => x.toLowerCase() !== subreddit.trim().toLowerCase());
|
||||||
await this.defaultCache.set(`modInvites`, subredditNames, {ttl: 0});
|
await (await this.defaultCache).set(`modInvites`, subredditNames, {ttl: 0});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearPendingSubredditInvites(): Promise<void> {
|
async clearPendingSubredditInvites(): Promise<void> {
|
||||||
await this.defaultCache.del(`modInvites`);
|
await (await this.defaultCache).del(`modInvites`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {SessionOptions, Store} from "express-session";
|
import {SessionOptions, Store} from "express-session";
|
||||||
import {TypeormStore} from "connect-typeorm";
|
import {TypeormStore} from "connect-typeorm";
|
||||||
import {InviteData} from "../Common/interfaces";
|
import {InviteData} from "../Common/interfaces";
|
||||||
import {buildCachePrefix, createCacheManager, mergeArr} from "../../util";
|
import {buildCachePrefix, mergeArr} from "../../util";
|
||||||
import {Cache} from "cache-manager";
|
import {Cache} from "cache-manager";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import CacheManagerStore from 'express-session-cache-manager'
|
import CacheManagerStore from 'express-session-cache-manager'
|
||||||
@@ -11,6 +11,7 @@ import {ClientSession} from "../../Common/WebEntities/ClientSession";
|
|||||||
import {Logger} from "winston";
|
import {Logger} from "winston";
|
||||||
import {WebSetting} from "../../Common/WebEntities/WebSetting";
|
import {WebSetting} from "../../Common/WebEntities/WebSetting";
|
||||||
import {ErrorWithCause} from "pony-cause";
|
import {ErrorWithCause} from "pony-cause";
|
||||||
|
import {createCacheManager} from "../../Common/Cache";
|
||||||
|
|
||||||
export interface CacheManagerStoreOptions {
|
export interface CacheManagerStoreOptions {
|
||||||
prefix?: string
|
prefix?: string
|
||||||
@@ -24,7 +25,7 @@ export type TypeormStoreOptions = Partial<SessionOptions & {
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
interface IWebStorageProvider {
|
interface IWebStorageProvider {
|
||||||
createSessionStore(options?: CacheManagerStoreOptions | TypeormStoreOptions): Store
|
createSessionStore(options?: CacheManagerStoreOptions | TypeormStoreOptions): Promise<Store>
|
||||||
|
|
||||||
getSessionSecret(): Promise<string | undefined>
|
getSessionSecret(): Promise<string | undefined>
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ abstract class StorageProvider implements IWebStorageProvider {
|
|||||||
this.logger = logger.child({labels: ['Web', 'Storage', ...loggerLabels]}, mergeArr);
|
this.logger = logger.child({labels: ['Web', 'Storage', ...loggerLabels]}, mergeArr);
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract createSessionStore(options?: CacheManagerStoreOptions | TypeormStoreOptions): Store;
|
abstract createSessionStore(options?: CacheManagerStoreOptions | TypeormStoreOptions): Promise<Store>;
|
||||||
|
|
||||||
abstract getSessionSecret(): Promise<string | undefined>;
|
abstract getSessionSecret(): Promise<string | undefined>;
|
||||||
|
|
||||||
@@ -57,24 +58,24 @@ abstract class StorageProvider implements IWebStorageProvider {
|
|||||||
|
|
||||||
export class CacheStorageProvider extends StorageProvider {
|
export class CacheStorageProvider extends StorageProvider {
|
||||||
|
|
||||||
protected cache: Cache;
|
protected cache: Promise<Cache>;
|
||||||
|
|
||||||
constructor(caching: CacheOptions & StorageProviderOptions) {
|
constructor(caching: CacheOptions & StorageProviderOptions) {
|
||||||
super(caching);
|
super(caching);
|
||||||
const {logger, invitesMaxAge, loggerLabels, ...restCache } = caching;
|
const {logger, invitesMaxAge, loggerLabels, ...restCache } = caching;
|
||||||
this.cache = createCacheManager({...restCache, prefix: buildCachePrefix(['web'])}) as Cache;
|
this.cache = createCacheManager({...restCache, prefix: buildCachePrefix(['web'])}) as Promise<Cache>;
|
||||||
this.logger.debug('Using CACHE');
|
this.logger.debug('Using CACHE');
|
||||||
if (caching.store === 'none') {
|
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!`);
|
this.logger.warn(`Using 'none' as cache provider means no one will be able to access the interface since sessions will never be persisted!`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createSessionStore(options?: CacheManagerStoreOptions): Store {
|
async createSessionStore(options?: CacheManagerStoreOptions): Promise<Store> {
|
||||||
return new CacheManagerStore(this.cache, {prefix: 'sess:'});
|
return new CacheManagerStore((await this.cache), {prefix: 'sess:'});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSessionSecret() {
|
async getSessionSecret() {
|
||||||
const val = await this.cache.get(`sessionSecret`);
|
const val = await (await this.cache).get(`sessionSecret`);
|
||||||
if (val === null || val === undefined) {
|
if (val === null || val === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -82,7 +83,7 @@ export class CacheStorageProvider extends StorageProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setSessionSecret(secret: string) {
|
async setSessionSecret(secret: string) {
|
||||||
await this.cache.set('sessionSecret', secret, {ttl: 0});
|
await (await this.cache).set('sessionSecret', secret, {ttl: 0});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -101,7 +102,7 @@ export class DatabaseStorageProvider extends StorageProvider {
|
|||||||
this.logger.debug('Using DATABASE');
|
this.logger.debug('Using DATABASE');
|
||||||
}
|
}
|
||||||
|
|
||||||
createSessionStore(options?: TypeormStoreOptions): Store {
|
async createSessionStore(options?: TypeormStoreOptions): Promise<Store> {
|
||||||
return new TypeormStore(options).connect(this.clientSessionRepo)
|
return new TypeormStore(options).connect(this.clientSessionRepo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
} from "../../Common/interfaces";
|
} from "../../Common/interfaces";
|
||||||
import {
|
import {
|
||||||
buildCachePrefix,
|
buildCachePrefix,
|
||||||
createCacheManager, defaultFormat, filterLogBySubreddit, filterCriteriaSummary, formatFilterData,
|
defaultFormat, filterLogBySubreddit, filterCriteriaSummary, formatFilterData,
|
||||||
formatLogLineToHtml, filterLogs, getUserAgent,
|
formatLogLineToHtml, filterLogs, getUserAgent,
|
||||||
intersect, isLogLineMinLevel,
|
intersect, isLogLineMinLevel,
|
||||||
LogEntry, parseInstanceLogInfoName, parseInstanceLogName, parseRedditEntity,
|
LogEntry, parseInstanceLogInfoName, parseInstanceLogName, parseRedditEntity,
|
||||||
@@ -64,6 +64,7 @@ import {
|
|||||||
InviteData, SubredditInviteDataPersisted
|
InviteData, SubredditInviteDataPersisted
|
||||||
} from "../Common/interfaces";
|
} from "../Common/interfaces";
|
||||||
import {open} from "fs/promises";
|
import {open} from "fs/promises";
|
||||||
|
import {createCacheManager} from "../../Common/Cache";
|
||||||
|
|
||||||
const emitter = new EventEmitter();
|
const emitter = new EventEmitter();
|
||||||
|
|
||||||
@@ -323,7 +324,7 @@ const webClient = async (options: OperatorConfigWithFileContext) => {
|
|||||||
cookie: {
|
cookie: {
|
||||||
maxAge: sessionMaxAge * 1000,
|
maxAge: sessionMaxAge * 1000,
|
||||||
},
|
},
|
||||||
store: sessionStoreProvider.createSessionStore(sessionStorage === 'database' ? {
|
store: await sessionStoreProvider.createSessionStore(sessionStorage === 'database' ? {
|
||||||
cleanupLimit: 2,
|
cleanupLimit: 2,
|
||||||
ttl: sessionMaxAge
|
ttl: sessionMaxAge
|
||||||
} : {}),
|
} : {}),
|
||||||
|
|||||||
@@ -73,8 +73,10 @@ const logs = () => {
|
|||||||
const requestedBots = bots.map(x => x.botName);
|
const requestedBots = bots.map(x => x.botName);
|
||||||
|
|
||||||
const origin = req.header('X-Forwarded-For') ?? req.header('host');
|
const origin = req.header('X-Forwarded-For') ?? req.header('host');
|
||||||
|
const stream = logger.stream();
|
||||||
try {
|
try {
|
||||||
logger.stream().on('log', (log: LogInfo) => {
|
|
||||||
|
stream.on('log', (log: LogInfo) => {
|
||||||
if (isLogLineMinLevel(log, level as string)) {
|
if (isLogLineMinLevel(log, level as string)) {
|
||||||
const {subreddit: subName, bot, user} = log;
|
const {subreddit: subName, bot, user} = log;
|
||||||
let canAccess = false;
|
let canAccess = false;
|
||||||
@@ -105,13 +107,13 @@ const logs = () => {
|
|||||||
logger.info(`${userName} from ${origin} => CONNECTED`);
|
logger.info(`${userName} from ${origin} => CONNECTED`);
|
||||||
await pEvent(req, 'close');
|
await pEvent(req, 'close');
|
||||||
//logger.debug('Request closed detected with "close" listener');
|
//logger.debug('Request closed detected with "close" listener');
|
||||||
res.destroy();
|
|
||||||
return;
|
return;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.code !== 'ECONNRESET') {
|
if (e.code !== 'ECONNRESET') {
|
||||||
logger.error(e);
|
logger.error(e);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
stream.removeAllListeners();
|
||||||
logger.info(`${userName} from ${origin} => DISCONNECTED`);
|
logger.info(`${userName} from ${origin} => DISCONNECTED`);
|
||||||
res.destroy();
|
res.destroy();
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/Web/assets/public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
src/Web/assets/public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
@@ -169,6 +169,14 @@ a {
|
|||||||
display: inherit;
|
display: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.show {
|
||||||
|
display: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invisible {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.triggeredStateToggle {
|
.triggeredStateToggle {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -183,7 +191,7 @@ li > ul {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.smallLi:before {
|
.smallLi:before {
|
||||||
margin-left: -10px;
|
margin-left: -5px;
|
||||||
content: ""
|
content: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
src/Web/assets/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
BIN
src/Web/assets/public/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 620 B |
BIN
src/Web/assets/public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/Web/assets/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
1
src/Web/assets/public/site.webmanifest
Normal 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"}
|
||||||
@@ -13,4 +13,9 @@
|
|||||||
<meta name="robots" content="noindex">
|
<meta name="robots" content="noindex">
|
||||||
<!--icons from https://heroicons.com -->
|
<!--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="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>
|
</head>
|
||||||
|
|||||||
@@ -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"
|
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
viewBox="0 0 100 100" xml:space="preserve">
|
viewBox="0 0 100 100" xml:space="preserve">
|
||||||
<path
|
<path
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 596 B After Width: | Height: | Size: 606 B |
@@ -689,7 +689,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<%- include('partials/logSettings') %>
|
<%- include('partials/logSettings') %>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="liveLogIndicator">
|
||||||
<%- include('partials/loadingIcon') %>
|
<%- 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">
|
<div data-subreddit="<%= data.name %>" class="logs font-mono text-sm">
|
||||||
<% data.logs.forEach(function (logEntry){ %>
|
<% data.logs.forEach(function (logEntry){ %>
|
||||||
<%- 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 => {
|
document.querySelectorAll(".checkUrl").forEach(el => {
|
||||||
const toggleButtons = (e) => {
|
const toggleButtons = (e) => {
|
||||||
const subFilter = `.sub[data-subreddit="${e.target.dataset.subreddit}"]`;
|
const subFilter = `.sub[data-subreddit="${e.target.dataset.subreddit}"]`;
|
||||||
@@ -962,7 +979,7 @@
|
|||||||
}).observe(element);
|
}).observe(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStreamingLogs(sub, bot) {
|
function getStreamingLogs(sub, bot, restarts = 0) {
|
||||||
|
|
||||||
console.debug(`Getting stream for ${bot} ${sub}`);
|
console.debug(`Getting stream for ${bot} ${sub}`);
|
||||||
|
|
||||||
@@ -1018,10 +1035,13 @@
|
|||||||
bufferedLogs = [];
|
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})
|
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(response => {
|
||||||
.then(rs =>
|
return response.body;
|
||||||
rs.pipeThrough(new TextDecoderStream())
|
})
|
||||||
|
.then(rs => {
|
||||||
|
return rs.pipeThrough(new TextDecoderStream())
|
||||||
.pipeThrough(new TransformStream({
|
.pipeThrough(new TransformStream({
|
||||||
transform(chunk, controller) {
|
transform(chunk, controller) {
|
||||||
textBuffer += chunk;
|
textBuffer += chunk;
|
||||||
@@ -1047,9 +1067,17 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}))
|
}));
|
||||||
).catch((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);
|
console.warn(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchPromise.then(async res => {
|
fetchPromise.then(async res => {
|
||||||
@@ -1064,6 +1092,11 @@
|
|||||||
if(done) {
|
if(done) {
|
||||||
keepReading = false;
|
keepReading = false;
|
||||||
console.debug(`${bot}.${sub} log stream reader signalled it is done`);
|
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) {
|
if(value) {
|
||||||
//console.log(`((Logged For ${bot} ${sub})) ${value.message}`);
|
//console.log(`((Logged For ${bot} ${sub})) ${value.message}`);
|
||||||
@@ -1098,9 +1131,11 @@
|
|||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
if(e.name !== 'AbortError') {
|
if(e.name !== 'AbortError') {
|
||||||
console.debug(`Non-abort error occurred while streaming logs for ${bot} ${sub}`);
|
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 {
|
} else {
|
||||||
console.debug(`Log streaming for ${bot} ${sub} aborted`);
|
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()});
|
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();
|
const delayedItemsMap = new Map();
|
||||||
let lastSeenIdentifier = null;
|
let lastSeenIdentifier = null;
|
||||||
const subIndicators = ['red', 'green', 'yellow'];
|
const subIndicators = ['red', 'green', 'yellow'];
|
||||||
|
|||||||
81
src/util.ts
@@ -14,7 +14,6 @@ import {
|
|||||||
ActionResult,
|
ActionResult,
|
||||||
ActivityDispatch,
|
ActivityDispatch,
|
||||||
ActivityDispatchConfig,
|
ActivityDispatchConfig,
|
||||||
CacheOptions,
|
|
||||||
CheckSummary,
|
CheckSummary,
|
||||||
ImageComparisonResult,
|
ImageComparisonResult,
|
||||||
ItemCritPropHelper,
|
ItemCritPropHelper,
|
||||||
@@ -35,11 +34,9 @@ import {
|
|||||||
} from "./Common/interfaces";
|
} from "./Common/interfaces";
|
||||||
import InvalidRegexError from "./Utils/InvalidRegexError";
|
import InvalidRegexError from "./Utils/InvalidRegexError";
|
||||||
import {accessSync, constants, promises} from "fs";
|
import {accessSync, constants, promises} from "fs";
|
||||||
import {cacheOptDefaults, VERSION} from "./Common/defaults";
|
import {VERSION} from "./Common/defaults";
|
||||||
import cacheManager, {Cache} from "cache-manager";
|
import cacheManager from "cache-manager";
|
||||||
import redisStore from "cache-manager-redis-store";
|
|
||||||
import Autolinker from 'autolinker';
|
import Autolinker from 'autolinker';
|
||||||
import {create as createMemoryStore} from './Utils/memoryStore';
|
|
||||||
import {LEVEL, MESSAGE} from "triple-beam";
|
import {LEVEL, MESSAGE} from "triple-beam";
|
||||||
import {Comment, PrivateMessage, RedditUser, Submission, Subreddit} from "snoowrap/dist/objects";
|
import {Comment, PrivateMessage, RedditUser, Submission, Subreddit} from "snoowrap/dist/objects";
|
||||||
import reRegExp from '@stdlib/regexp-regexp';
|
import reRegExp from '@stdlib/regexp-regexp';
|
||||||
@@ -71,9 +68,9 @@ import {
|
|||||||
UserNoteCriteria
|
UserNoteCriteria
|
||||||
} from "./Common/Infrastructure/Filters/FilterCriteria";
|
} from "./Common/Infrastructure/Filters/FilterCriteria";
|
||||||
import {
|
import {
|
||||||
ActivitySourceValue,
|
ActivitySourceData,
|
||||||
ActivitySourceTypes,
|
ActivitySourceTypes,
|
||||||
CacheProvider,
|
ActivitySourceValue,
|
||||||
ConfigFormat,
|
ConfigFormat,
|
||||||
DurationVal,
|
DurationVal,
|
||||||
ExternalUrlContext,
|
ExternalUrlContext,
|
||||||
@@ -81,13 +78,13 @@ import {
|
|||||||
ModUserNoteLabel,
|
ModUserNoteLabel,
|
||||||
modUserNoteLabels,
|
modUserNoteLabels,
|
||||||
RedditEntity,
|
RedditEntity,
|
||||||
RedditEntityType, RelativeDateTimeMatch,
|
RedditEntityType,
|
||||||
|
RelativeDateTimeMatch,
|
||||||
statFrequencies,
|
statFrequencies,
|
||||||
StatisticFrequency,
|
StatisticFrequency,
|
||||||
StatisticFrequencyOption,
|
StatisticFrequencyOption,
|
||||||
UrlContext,
|
UrlContext,
|
||||||
WikiContext,
|
WikiContext
|
||||||
ActivitySourceData
|
|
||||||
} from "./Common/Infrastructure/Atomic";
|
} from "./Common/Infrastructure/Atomic";
|
||||||
import {
|
import {
|
||||||
AuthorOptions,
|
AuthorOptions,
|
||||||
@@ -1765,42 +1762,6 @@ 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 randomId = () => crypto.randomBytes(20).toString('hex');
|
||||||
|
|
||||||
export const intersect = (a: Array<any>, b: Array<any>) => {
|
export const intersect = (a: Array<any>, b: Array<any>) => {
|
||||||
@@ -2489,20 +2450,24 @@ export const mergeFilters = (objectConfig: RunnableBaseJson, filterDefs: FilterC
|
|||||||
let derivedAuthorIs: AuthorOptions = buildFilter(authorIsDefault);
|
let derivedAuthorIs: AuthorOptions = buildFilter(authorIsDefault);
|
||||||
if (authorIsBehavior === 'merge') {
|
if (authorIsBehavior === 'merge') {
|
||||||
derivedAuthorIs = merge.all([authorIs, authorIsDefault], {arrayMerge: removeFromSourceIfKeysExistsInDestination});
|
derivedAuthorIs = merge.all([authorIs, authorIsDefault], {arrayMerge: removeFromSourceIfKeysExistsInDestination});
|
||||||
} else if (Object.keys(authorIs).length > 0) {
|
} else if (!filterIsEmpty(authorIs)) {
|
||||||
derivedAuthorIs = authorIs;
|
derivedAuthorIs = authorIs;
|
||||||
}
|
}
|
||||||
|
|
||||||
let derivedItemIs: ItemOptions = buildFilter(itemIsDefault);
|
let derivedItemIs: ItemOptions = buildFilter(itemIsDefault);
|
||||||
if (itemIsBehavior === 'merge') {
|
if (itemIsBehavior === 'merge') {
|
||||||
derivedItemIs = merge.all([itemIs, itemIsDefault], {arrayMerge: removeFromSourceIfKeysExistsInDestination});
|
derivedItemIs = merge.all([itemIs, itemIsDefault], {arrayMerge: removeFromSourceIfKeysExistsInDestination});
|
||||||
} else if (Object.keys(itemIs).length > 0) {
|
} else if (!filterIsEmpty(itemIs)) {
|
||||||
derivedItemIs = itemIs;
|
derivedItemIs = itemIs;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [derivedAuthorIs, derivedItemIs];
|
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> => {
|
export const buildFilter = (filterVal: MinimalOrFullMaybeAnonymousFilter<AuthorCriteria | TypedActivityState | ActivityState>): FilterOptions<AuthorCriteria | TypedActivityState | ActivityState> => {
|
||||||
if(Array.isArray(filterVal)) {
|
if(Array.isArray(filterVal)) {
|
||||||
const named = filterVal.map(x => normalizeCriteria(x));
|
const named = filterVal.map(x => normalizeCriteria(x));
|
||||||
@@ -2606,7 +2571,17 @@ export const normalizeCriteria = <T extends AuthorCriteria | TypedActivityState
|
|||||||
criteria.description = Array.isArray(criteria.description) ? criteria.description : [criteria.description];
|
criteria.description = Array.isArray(criteria.description) ? criteria.description : [criteria.description];
|
||||||
}
|
}
|
||||||
if(criteria.modActions !== undefined) {
|
if(criteria.modActions !== undefined) {
|
||||||
criteria.modActions.map((x, index) => {
|
criteria.modActions.map((x, index) => normalizeModActionCriteria(x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
criteria
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const normalizeModActionCriteria = (x: (ModNoteCriteria | ModLogCriteria)): (ModNoteCriteria | ModLogCriteria) => {
|
||||||
const common = {
|
const common = {
|
||||||
...x,
|
...x,
|
||||||
type: x.type === undefined ? undefined : (Array.isArray(x.type) ? x.type : [x.type])
|
type: x.type === undefined ? undefined : (Array.isArray(x.type) ? x.type : [x.type])
|
||||||
@@ -2627,14 +2602,6 @@ export const normalizeCriteria = <T extends AuthorCriteria | TypedActivityState
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return common;
|
return common;
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
criteria
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const asNamedCriteria = <T>(val: MaybeAnonymousCriteria<T> | undefined): val is NamedCriteria<T> => {
|
export const asNamedCriteria = <T>(val: MaybeAnonymousCriteria<T> | undefined): val is NamedCriteria<T> => {
|
||||||
|
|||||||
193
tests/authorCriteria.test.ts
Normal 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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ import Snoowrap from "snoowrap";
|
|||||||
import {getResource, getSnoowrap, getSubreddit, sampleActivity} from "./testFactory";
|
import {getResource, getSnoowrap, getSubreddit, sampleActivity} from "./testFactory";
|
||||||
import {Subreddit as SubredditEntity} from "../src/Common/Entities/Subreddit";
|
import {Subreddit as SubredditEntity} from "../src/Common/Entities/Subreddit";
|
||||||
import {Activity} from '../src/Common/Entities/Activity';
|
import {Activity} from '../src/Common/Entities/Activity';
|
||||||
|
import {cmToSnoowrapActivityMap} from "../src/Common/Infrastructure/Filters/FilterCriteria";
|
||||||
|
|
||||||
dayjs.extend(dduration);
|
dayjs.extend(dduration);
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
@@ -229,49 +230,98 @@ describe('Item Criteria', function () {
|
|||||||
}, snoowrap, false), {upvoteRatio: '> 33'}, NoopLogger, true)).passed);
|
}, snoowrap, false), {upvoteRatio: '> 33'}, NoopLogger, true)).passed);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should detect specific link flair template', async function () {
|
for(const prop of ['link_flair_text', 'link_flair_css_class', 'authorFlairCssClass', 'authorFlairTemplateId', 'authorFlairText', 'flairTemplate']) {
|
||||||
assert.isTrue((await resource.isItem(new Submission({
|
const activityPropName = cmToSnoowrapActivityMap[prop] ?? prop;
|
||||||
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']) {
|
it(`Should detect specific ${prop} as single string`, async function () {
|
||||||
it(`Should detect specific ${prop}`, async function () {
|
|
||||||
assert.isTrue((await resource.isItem(new Submission({
|
assert.isTrue((await resource.isItem(new Submission({
|
||||||
[prop]: 'test',
|
[activityPropName]: 'test',
|
||||||
}, snoowrap, false), {[prop]: 'test'}, NoopLogger, true)).passed);
|
}, 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({
|
assert.isTrue((await resource.isItem(new Submission({
|
||||||
[prop]: 'test',
|
[activityPropName]: 'test',
|
||||||
}, snoowrap, false), {[prop]: ['foo','test']}, NoopLogger, true)).passed);
|
}, 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({
|
assert.isFalse((await resource.isItem(new Submission({
|
||||||
[prop]: 'test',
|
[activityPropName]: 'test',
|
||||||
}, snoowrap, false), {[prop]: ['foo']}, NoopLogger, true)).passed);
|
}, snoowrap, false), {[prop]: ['foo']}, NoopLogger, true)).passed);
|
||||||
});
|
});
|
||||||
it(`Should detect any ${prop}`, async function () {
|
it(`Should detect any ${prop}`, async function () {
|
||||||
assert.isTrue((await resource.isItem(new Submission({
|
assert.isTrue((await resource.isItem(new Submission({
|
||||||
[prop]: 'test',
|
[activityPropName]: 'test',
|
||||||
}, snoowrap, false), {[prop]: true}, NoopLogger, true)).passed);
|
}, snoowrap, false), {[prop]: true}, NoopLogger, true)).passed);
|
||||||
});
|
});
|
||||||
it(`Should detect no ${prop}`, async function () {
|
it(`Should detect no ${prop}`, async function () {
|
||||||
assert.isTrue((await resource.isItem(new Submission({
|
assert.isTrue((await resource.isItem(new Submission({
|
||||||
[prop]: null
|
[activityPropName]: null
|
||||||
}, snoowrap, false), {[prop]: false}, NoopLogger, true)).passed);
|
}, 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`)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,12 @@ export const getBot = async () => {
|
|||||||
bot = new Bot(config.bots[0], NoopLogger);
|
bot = new Bot(config.bots[0], NoopLogger);
|
||||||
await bot.cacheManager.set('test', {
|
await bot.cacheManager.set('test', {
|
||||||
logger: NoopLogger,
|
logger: NoopLogger,
|
||||||
|
caching: {
|
||||||
|
authorTTL: false,
|
||||||
|
submissionTTL: false,
|
||||||
|
commentTTL: false,
|
||||||
|
provider: 'memory'
|
||||||
|
},
|
||||||
subreddit: bot.client.getSubreddit('test'),
|
subreddit: bot.client.getSubreddit('test'),
|
||||||
client: bot.client,
|
client: bot.client,
|
||||||
statFrequency: 'minute',
|
statFrequency: 'minute',
|
||||||
|
|||||||