diff --git a/src/Bot/index.ts b/src/Bot/index.ts index 6f47d7c..f430506 100644 --- a/src/Bot/index.ts +++ b/src/Bot/index.ts @@ -15,11 +15,11 @@ import { USER } from "../Common/interfaces"; import { - createRetryHandler, + createRetryHandler, difference, formatNumber, getExceptionMessage, getUserAgent, mergeArr, parseBool, - parseDuration, parseMatchMessage, + parseDuration, parseMatchMessage, parseRedditEntity, parseSubredditName, RetryOptions, sleep, snooLogWrapper @@ -79,6 +79,8 @@ class Bot { cacheManager: BotResourcesManager; + config: BotInstanceConfig; + getBotName = () => { return this.botName; } @@ -131,6 +133,7 @@ class Bot { } } = config; + this.config = config; this.dryRun = parseBool(dryRun) === true ? true : undefined; this.softLimit = softLimit; this.hardLimit = hardLimit; @@ -350,6 +353,31 @@ class Bot { } } + const { + subreddits: { + overrides = [], + } = {} + } = this.config; + if(overrides.length > 0) { + // check for overrides that don't match subs to run and warn operator + const subsToRunNames = subsToRun.map(x => x.display_name.toLowerCase()); + + const normalizedOverrideNames = overrides.reduce((acc: string[], curr) => { + try { + const ent = parseRedditEntity(curr.name); + return acc.concat(ent.name.toLowerCase()); + } catch (e) { + this.logger.warn(new ErrorWithCause(`Could not use subreddit override because name was not valid: ${curr.name}`, {cause: e})); + return acc; + } + }, []); + const notMatched = difference(normalizedOverrideNames, subsToRunNames); + if(notMatched.length > 0) { + this.logger.warn(`There are overrides defined for subreddits the bot is not running. Check your spelling! Overrides not matched: ${notMatched.join(', ')}`); + } + } + + // get configs for subs we want to run on and build/validate them for (const sub of subsToRun) { try { @@ -487,6 +515,29 @@ class Bot { } createManager(sub: Subreddit): Manager { + const { + flowControlDefaults: { + maxGotoDepth: botMaxDefault + } = {}, + subreddits: { + overrides = [], + } = {} + } = this.config; + + const override = overrides.find(x => { + const configName = parseRedditEntity(x.name).name; + if(configName !== undefined) { + return configName.toLowerCase() === sub.display_name.toLowerCase(); + } + return false; + }); + + const { + flowControlDefaults: { + maxGotoDepth: subMax = undefined, + } = {} + } = override || {}; + const manager = new Manager(sub, this.client, this.logger, this.cacheManager, { dryRun: this.dryRun, sharedStreams: this.sharedStreams, @@ -494,6 +545,7 @@ class Bot { botName: this.botName as string, maxWorkers: this.maxWorkers, filterCriteriaDefaults: this.filterCriteriaDefaults, + maxGotoDepth: subMax ?? botMaxDefault }); // all errors from managers will count towards bot-level retry count manager.on('error', async (err) => await this.panicOnRetries(err)); diff --git a/src/Common/interfaces.ts b/src/Common/interfaces.ts index 4081ea4..ad1d535 100644 --- a/src/Common/interfaces.ts +++ b/src/Common/interfaces.ts @@ -1467,6 +1467,13 @@ export interface FilterCriteriaDefaults { authorIsBehavior?: FilterCriteriaDefaultBehavior } +export interface SubredditOverrides { + name: string + flowControlDefaults?: { + maxGotoDepth?: number + } +} + /** * The configuration for an **individual reddit account** ContextMod will run as a bot. * @@ -1504,6 +1511,10 @@ export interface BotInstanceJsonConfig { postCheckBehaviorDefaults?: PostBehavior + flowControlDefaults?: { + maxGotoDepth?: number + } + /** * Settings related to bot behavior for subreddits it is managing * */ @@ -1564,6 +1575,8 @@ export interface BotInstanceJsonConfig { * @examples [300] * */ heartbeatInterval?: number, + + overrides?: SubredditOverrides[] } /** @@ -1600,22 +1613,23 @@ export interface BotInstanceJsonConfig { * Useful when running many subreddits and rules are potentially cpu/memory/traffic heavy -- allows spreading out load * */ stagger?: number, - }, + } + /** * Settings related to default configurations for queue behavior for subreddits * */ queue?: { - /** - * Set the number of maximum concurrent workers any subreddit can use. - * - * Subreddits may define their own number of max workers in their config but the application will never allow any subreddit's max workers to be larger than the operator - * - * NOTE: Do not increase this unless you are certain you know what you are doing! The default is suitable for the majority of use cases. - * - * @default 1 - * @examples [1] - * */ - maxWorkers?: number, + /** + * Set the number of maximum concurrent workers any subreddit can use. + * + * Subreddits may define their own number of max workers in their config but the application will never allow any subreddit's max workers to be larger than the operator + * + * NOTE: Do not increase this unless you are certain you know what you are doing! The default is suitable for the majority of use cases. + * + * @default 1 + * @examples [1] + * */ + maxWorkers?: number, } /** @@ -1885,6 +1899,7 @@ export interface BotInstanceConfig extends BotInstanceJsonConfig { dryRun?: boolean, wikiConfig: string, heartbeatInterval: number, + overrides?: SubredditOverrides[] }, polling: { shared: PollOn[], diff --git a/src/ConfigBuilder.ts b/src/ConfigBuilder.ts index 0720286..5f67505 100644 --- a/src/ConfigBuilder.ts +++ b/src/ConfigBuilder.ts @@ -880,8 +880,10 @@ export const buildBotConfig = (data: BotInstanceJsonConfig, opConfig: OperatorCo hardLimit = 50 } = {}, snoowrap = snoowrapOp, + flowControlDefaults, credentials = {}, subreddits: { + overrides = [], names = [], exclude = [], wikiConfig = 'botconfig/contextbot', @@ -990,6 +992,7 @@ export const buildBotConfig = (data: BotInstanceJsonConfig, opConfig: OperatorCo return { name: botName, snoowrap: snoowrap || {}, + flowControlDefaults, filterCriteriaDefaults, postCheckBehaviorDefaults, subreddits: { @@ -998,6 +1001,7 @@ export const buildBotConfig = (data: BotInstanceJsonConfig, opConfig: OperatorCo wikiConfig, heartbeatInterval, dryRun, + overrides, }, credentials: botCreds, caching: botCache, diff --git a/src/Run/index.ts b/src/Run/index.ts index 886246e..5a25db0 100644 --- a/src/Run/index.ts +++ b/src/Run/index.ts @@ -108,6 +108,10 @@ export class Run { triggered: false, checkResults: [], } + const { + maxGotoDepth = 1, + gotoContext: optGotoContext = '', + } = options || {}; if(!this.enabled) { runResult.error = 'Not enabled'; @@ -126,7 +130,7 @@ export class Run { return [{...runResult, reason: msg}, postBehavior]; } - let gotoContext = options?.gotoContext ?? ''; + let gotoContext = optGotoContext; const checks = isSubmission(activity) ? this.submissionChecks : this.commentChecks; let continueCheckIteration = true; let checkIndex = 0; @@ -163,10 +167,11 @@ export class Run { let check: Check; if (gotoContext !== '') { const [runName, checkName] = gotoContext.split('.'); - if(hitGotos.includes(checkName)) { - throw new Error(`The check specified in goto "${gotoContext}" has already been hit once. This indicates a possible endless loop may occur so CM will terminate processing this activity to save you from yourself!`); - } hitGotos.push(checkName); + if(hitGotos.filter(x => x === gotoContext).length > maxGotoDepth) { + throw new Error(`The check specified in goto "${gotoContext}" has been triggered ${hitGotos.filter(x => x === gotoContext).length} times which is more than the max allowed for any single goto (${maxGotoDepth}). + This indicates a possible endless loop may occur so CM will terminate processing this activity to save you from yourself! The max triggered depth can be configured by the operator.`); + } const gotoIndex = checks.findIndex(x => normalizeName(x.name) === normalizeName(checkName)); if (gotoIndex !== -1) { if (gotoIndex > checkIndex) { diff --git a/src/Schema/Action.json b/src/Schema/Action.json index 6bb0cb9..8afbfe1 100644 --- a/src/Schema/Action.json +++ b/src/Schema/Action.json @@ -438,21 +438,18 @@ "type": "boolean" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run." + } + ] + }, + "type": "array" }, "kind": { "description": "The type of action that will be performed", diff --git a/src/Schema/App.json b/src/Schema/App.json index 879b264..a404dcd 100644 --- a/src/Schema/App.json +++ b/src/Schema/App.json @@ -192,21 +192,18 @@ "type": "boolean" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run." + } + ] + }, + "type": "array" }, "kind": { "description": "The type of action that will be performed", @@ -428,21 +425,18 @@ "type": "string" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -684,21 +678,18 @@ "type": "array" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -784,21 +775,18 @@ "description": "Customize the footer for Actions that send replies (Comment/Ban)\n\nIf `false` no footer is appended\n\nIf `string` the value is rendered as markdown or will use `wiki:` parser the same way `content` properties on Actions are rendered with [templating](https://github.com/FoxxMD/context-mod#action-templating).\n\nIf footer is `undefined` (not set) the default footer will be used:\n\n> *****\n> This action was performed by [a bot.] Mention a moderator or [send a modmail] if you any ideas, questions, or concerns about this action.\n\n*****\n\nThe following properties are available for [templating](https://github.com/FoxxMD/context-mod#action-templating):\n```\nsubName => name of subreddit Action was performed in (EX 'mealtimevideos')\npermaLink => The permalink for the Activity the Action was performed on EX https://reddit.com/r/yourSub/comments/o1h0i0/title_name/1v3b7x\nmodmaiLink => An encoded URL that will open a new message to your subreddit with the Action permalink appended to the body\nbotLink => A permalink to the FAQ for this bot.\n```\nIf you use your own footer or no footer **please link back to the bot FAQ** using the `{{botLink}}` property in your content :)" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run." + } + ] + }, + "type": "array" }, "kind": { "description": "The type of action that will be performed", @@ -1088,21 +1076,18 @@ "description": "Customize the footer for Actions that send replies (Comment/Ban)\n\nIf `false` no footer is appended\n\nIf `string` the value is rendered as markdown or will use `wiki:` parser the same way `content` properties on Actions are rendered with [templating](https://github.com/FoxxMD/context-mod#action-templating).\n\nIf footer is `undefined` (not set) the default footer will be used:\n\n> *****\n> This action was performed by [a bot.] Mention a moderator or [send a modmail] if you any ideas, questions, or concerns about this action.\n\n*****\n\nThe following properties are available for [templating](https://github.com/FoxxMD/context-mod#action-templating):\n```\nsubName => name of subreddit Action was performed in (EX 'mealtimevideos')\npermaLink => The permalink for the Activity the Action was performed on EX https://reddit.com/r/yourSub/comments/o1h0i0/title_name/1v3b7x\nmodmaiLink => An encoded URL that will open a new message to your subreddit with the Action permalink appended to the body\nbotLink => A permalink to the FAQ for this bot.\n```\nIf you use your own footer or no footer **please link back to the bot FAQ** using the `{{botLink}}` property in your content :)" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run." + } + ] + }, + "type": "array" }, "kind": { "description": "The type of action that will be performed", @@ -1501,20 +1486,17 @@ "type": "string" }, "itemIs": { - "anyOf": [ - { - "items": { + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ] + } + ] + }, + "type": "array" }, "itemIsBehavior": { "description": "Determine how itemIs defaults behave when itemIs is present on the check\n\n* merge => adds defaults to check's itemIs\n* replace => check itemIs will replace defaults (no defaults used)", @@ -1575,21 +1557,18 @@ "type": "string" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run." + } + ] + }, + "type": "array" }, "kind": { "description": "The type of action that will be performed", @@ -1755,21 +1734,18 @@ "type": "array" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -1904,21 +1880,18 @@ "type": "boolean" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run." + } + ] + }, + "type": "array" }, "kind": { "description": "The type of action that will be performed", @@ -2003,21 +1976,18 @@ "description": "Customize the footer for Actions that send replies (Comment/Ban)\n\nIf `false` no footer is appended\n\nIf `string` the value is rendered as markdown or will use `wiki:` parser the same way `content` properties on Actions are rendered with [templating](https://github.com/FoxxMD/context-mod#action-templating).\n\nIf footer is `undefined` (not set) the default footer will be used:\n\n> *****\n> This action was performed by [a bot.] Mention a moderator or [send a modmail] if you any ideas, questions, or concerns about this action.\n\n*****\n\nThe following properties are available for [templating](https://github.com/FoxxMD/context-mod#action-templating):\n```\nsubName => name of subreddit Action was performed in (EX 'mealtimevideos')\npermaLink => The permalink for the Activity the Action was performed on EX https://reddit.com/r/yourSub/comments/o1h0i0/title_name/1v3b7x\nmodmaiLink => An encoded URL that will open a new message to your subreddit with the Action permalink appended to the body\nbotLink => A permalink to the FAQ for this bot.\n```\nIf you use your own footer or no footer **please link back to the bot FAQ** using the `{{botLink}}` property in your content :)" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run." + } + ] + }, + "type": "array" }, "kind": { "description": "The type of action that will be performed", @@ -2279,21 +2249,18 @@ "description": "When comparing submissions detect if the reference submission is an image and do a pixel-comparison to other detected image submissions.\n\n**Note:** This is an **experimental feature**" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -2554,21 +2521,18 @@ "type": "array" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -2635,21 +2599,18 @@ "type": "boolean" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run." + } + ] + }, + "type": "array" }, "kind": { "description": "The type of action that will be performed", @@ -2755,21 +2716,18 @@ "type": "array" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "keepRemoved": { "default": false, @@ -2902,21 +2860,18 @@ "type": "boolean" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run." + } + ] + }, + "type": "array" }, "kind": { "description": "The type of action that will be performed", @@ -3117,21 +3072,18 @@ "type": "array" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -3266,21 +3218,18 @@ "description": "Set the default filter criteria for all checks. If this property is specified it will override any defaults passed from the bot's config\n\nDefault behavior is to exclude all mods and automoderator from checks" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]" + } + ] + }, + "type": "array" }, "name": { "description": "Friendly name for this Run EX \"flairsRun\"\n\nCan only contain letters, numbers, underscore, spaces, and dashes", @@ -3830,21 +3779,18 @@ "type": "string" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run." + } + ] + }, + "type": "array" }, "kind": { "description": "The type of action that will be performed", @@ -3928,21 +3874,18 @@ "type": "boolean" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Action.\n\nIf any set of criteria passes the Action will be run." + } + ] + }, + "type": "array" }, "kind": { "description": "The type of action that will be performed", diff --git a/src/Schema/OperatorConfig.json b/src/Schema/OperatorConfig.json index 5969e32..ea789ed 100644 --- a/src/Schema/OperatorConfig.json +++ b/src/Schema/OperatorConfig.json @@ -245,6 +245,14 @@ "$ref": "#/definitions/FilterCriteriaDefaults", "description": "Define the default behavior for all filter criteria on all checks in all subreddits\n\nDefaults to exclude mods and automoderator from checks" }, + "flowControlDefaults": { + "properties": { + "maxGotoDepth": { + "type": "number" + } + }, + "type": "object" + }, "name": { "type": "string" }, @@ -385,6 +393,12 @@ }, "type": "array" }, + "overrides": { + "items": { + "$ref": "#/definitions/SubredditOverrides" + }, + "type": "array" + }, "wikiConfig": { "default": "botconfig/contextbot", "description": "The default relative url to the ContextMod wiki page EX `https://reddit.com/r/subreddit/wiki/`\n\n* ENV => `WIKI_CONFIG`\n* ARG => `--wikiConfig `", @@ -583,20 +597,17 @@ "type": "string" }, "itemIs": { - "anyOf": [ - { - "items": { + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ] + } + ] + }, + "type": "array" }, "itemIsBehavior": { "description": "Determine how itemIs defaults behave when itemIs is present on the check\n\n* merge => adds defaults to check's itemIs\n* replace => check itemIs will replace defaults (no defaults used)", @@ -1221,6 +1232,25 @@ }, "type": "object" }, + "SubredditOverrides": { + "properties": { + "flowControlDefaults": { + "properties": { + "maxGotoDepth": { + "type": "number" + } + }, + "type": "object" + }, + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, "ThirdPartyCredentialsJsonConfig": { "additionalProperties": {}, "properties": { diff --git a/src/Schema/Rule.json b/src/Schema/Rule.json index 73bb708..1572e4b 100644 --- a/src/Schema/Rule.json +++ b/src/Schema/Rule.json @@ -366,21 +366,18 @@ "type": "string" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -622,21 +619,18 @@ "type": "array" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -921,21 +915,18 @@ "type": "array" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -1127,21 +1118,18 @@ "description": "When comparing submissions detect if the reference submission is an image and do a pixel-comparison to other detected image submissions.\n\n**Note:** This is an **experimental feature**" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -1402,21 +1390,18 @@ "type": "array" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -1523,21 +1508,18 @@ "type": "array" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "keepRemoved": { "default": false, @@ -1799,21 +1781,18 @@ "type": "array" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", diff --git a/src/Schema/RuleSet.json b/src/Schema/RuleSet.json index 69e9931..d8064e9 100644 --- a/src/Schema/RuleSet.json +++ b/src/Schema/RuleSet.json @@ -340,21 +340,18 @@ "type": "string" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -596,21 +593,18 @@ "type": "array" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -895,21 +889,18 @@ "type": "array" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -1101,21 +1092,18 @@ "description": "When comparing submissions detect if the reference submission is an image and do a pixel-comparison to other detected image submissions.\n\n**Note:** This is an **experimental feature**" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -1376,21 +1364,18 @@ "type": "array" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", @@ -1497,21 +1482,18 @@ "type": "array" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "keepRemoved": { "default": false, @@ -1773,21 +1755,18 @@ "type": "array" }, "itemIs": { - "anyOf": [ - { - "items": { + "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped.", + "items": { + "anyOf": [ + { "$ref": "#/definitions/SubmissionState" }, - "type": "array" - }, - { - "items": { + { "$ref": "#/definitions/CommentState" - }, - "type": "array" - } - ], - "description": "A list of criteria to test the state of the `Activity` against before running the Rule.\n\nIf any set of criteria passes the Rule will be run. If the criteria fails then the Rule is skipped." + } + ] + }, + "type": "array" }, "kind": { "description": "The kind of rule to run", diff --git a/src/Subreddit/Manager.ts b/src/Subreddit/Manager.ts index 6d5ef8d..3bc4797 100644 --- a/src/Subreddit/Manager.ts +++ b/src/Subreddit/Manager.ts @@ -66,6 +66,7 @@ export interface runCheckOptions { refresh?: boolean, force?: boolean, gotoContext?: string + maxGotoDepth?: number } export interface CheckTask { @@ -76,8 +77,9 @@ export interface CheckTask { export interface RuntimeManagerOptions extends ManagerOptions { sharedStreams?: PollOn[]; wikiLocation?: string; - botName: string; - maxWorkers: number; + botName?: string; + maxWorkers?: number; + maxGotoDepth?: number } interface QueuedIdentifier { @@ -128,6 +130,7 @@ export class Manager extends EventEmitter { queuedItemsMeta: QueuedIdentifier[] = []; globalMaxWorkers: number; subMaxWorkers?: number; + maxGotoDepth: number; displayLabel: string; currentLabels: string[] = []; @@ -206,10 +209,19 @@ export class Manager extends EventEmitter { return this.displayLabel; } - constructor(sub: Subreddit, client: ExtendedSnoowrap, logger: Logger, cacheManager: BotResourcesManager, opts: RuntimeManagerOptions = {botName: 'ContextMod', maxWorkers: 1}) { + constructor(sub: Subreddit, client: ExtendedSnoowrap, logger: Logger, cacheManager: BotResourcesManager, opts: RuntimeManagerOptions) { super(); - const {dryRun, sharedStreams = [], wikiLocation = 'botconfig/contextbot', botName, maxWorkers, filterCriteriaDefaults, postCheckBehaviorDefaults} = opts; + const { + dryRun, + sharedStreams = [], + wikiLocation = 'botconfig/contextbot', + botName = 'ContextMod', + maxWorkers = 1, + maxGotoDepth = 1, + filterCriteriaDefaults, + postCheckBehaviorDefaults + } = opts || {}; this.displayLabel = opts.nickname || `${sub.display_name_prefixed}`; const getLabels = this.getCurrentLabels; const getDisplay = this.getDisplay; @@ -237,6 +249,7 @@ export class Manager extends EventEmitter { this.subreddit = sub; this.client = client; this.botName = botName; + this.maxGotoDepth = maxGotoDepth; this.globalMaxWorkers = maxWorkers; this.notificationManager = new NotificationManager(this.logger, this.subreddit, this.displayLabel, botName); this.cacheManager = cacheManager; @@ -245,6 +258,8 @@ export class Manager extends EventEmitter { this.queue.pause(); this.firehose = this.generateFirehose(); + this.logger.info(`Max GOTO Depth: ${this.maxGotoDepth}`); + this.eventsSampleInterval = setInterval((function(self) { return function() { const et = self.resources !== undefined ? self.resources.stats.historical.allTime.eventsCheckedTotal : 0; @@ -734,10 +749,11 @@ export class Manager extends EventEmitter { while(continueRunIteration && (runIndex < this.runs.length || gotoContext !== '')) { let currRun: Run; if(gotoContext !== '') { - if(hitGotos.includes(gotoContext)) { - throw new Error(`The goto "${gotoContext}" has already been hit once. This indicates a possible endless loop may occur so CM will terminate processing this activity to save you from yourself!`); - } hitGotos.push(gotoContext); + if(hitGotos.filter(x => x === gotoContext).length > this.maxGotoDepth) { + throw new Error(`The goto "${gotoContext}" has been triggered ${hitGotos.filter(x => x === gotoContext).length} times which is more than the max allowed for any single goto (${this.maxGotoDepth}). + This indicates a possible endless loop may occur so CM will terminate processing this activity to save you from yourself! The max triggered depth can be configured by the operator.`); + } const [runName] = gotoContext.split('.'); const gotoIndex = this.runs.findIndex(x => normalizeName(x.name) === normalizeName(runName)); if(gotoIndex !== -1) { @@ -761,7 +777,7 @@ export class Manager extends EventEmitter { currRun = this.runs[runIndex]; } - const [runResult, postBehavior] = await currRun.handle(item,allRuleResults, runResults.filter(x => x.name === currRun.name), {...options, gotoContext}); + const [runResult, postBehavior] = await currRun.handle(item,allRuleResults, runResults.filter(x => x.name === currRun.name), {...options, gotoContext, maxGotoDepth: this.maxGotoDepth}); runResults.push(runResult); allRuleResults = allRuleResults.concat(determineNewResults(allRuleResults, (runResult.checkResults ?? []).map(x => x.ruleResults).flat())); diff --git a/src/util.ts b/src/util.ts index 43a2476..56e821d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1610,6 +1610,28 @@ export const intersect = (a: Array, b: Array) => { const intersection = new Set([...setA].filter(x => setB.has(x))); return Array.from(intersection); } +/** + * @see https://stackoverflow.com/a/64245521/1469797 + * */ +function *setMinus(A: Array, B: Array) { + const setA = new Set(A); + const setB = new Set(B); + + for (const v of setB.values()) { + if (!setA.delete(v)) { + yield v; + } + } + + for (const v of setA.values()) { + yield v; + } +} + + +export const difference = (a: Array, b: Array) => { + return Array.from(setMinus(a, b)); +} export const snooLogWrapper = (logger: Logger) => { return {