Add dryrun setting to help with testing

Can be configured at action, check, subreddit, or app level
This commit is contained in:
FoxxMD
2021-06-11 12:38:01 -04:00
parent 1279975a8a
commit 1897d96a8f
15 changed files with 142 additions and 34 deletions

View File

@@ -235,6 +235,7 @@ Options:
-n, --snooDebug Set Snoowrap to debug (default: process.env.SNOO_DEBUG || false)
--authorTTL <ms> Set the TTL (ms) for the Author Activities shared cache (default: process.env.AUTHOR_TTL || 10000)
--disableCache Disable caching for all subreddits
--dryRun Override dryRun=true for all checks/actions on all subreddits (default: process.env.DRYRUN)
-h, --help display help for command
Commands:

View File

@@ -8,21 +8,17 @@ import {Logger} from "winston";
export function actionFactory
(config: ActionJson, logger: Logger, subredditName: string): Action {
let cfg;
switch (config.kind) {
case 'comment':
cfg = config as CommentActionJson;
return new CommentAction({...cfg, logger, subredditName});
return new CommentAction({...config as CommentActionJson, logger, subredditName});
case 'lock':
return new LockAction({logger, subredditName});
return new LockAction({...config, logger, subredditName});
case 'remove':
return new RemoveAction({logger, subredditName});
return new RemoveAction({...config, logger, subredditName});
case 'report':
cfg = config as ReportActionJson;
return new ReportAction({...cfg, logger, subredditName});
return new ReportAction({...config as ReportActionJson, logger, subredditName});
case 'flair':
cfg = config as FlairActionJson;
return new FlairAction({...cfg, logger, subredditName});
return new FlairAction({...config as FlairActionJson, logger, subredditName});
default:
throw new Error('rule "kind" was not recognized.');
}

View File

@@ -37,13 +37,15 @@ export class CommentAction extends Action {
const reply: Comment = await item.reply(renderedContent);
if (this.lock) {
if(item instanceof Submission) {
// @ts-ignore
await item.lock();
if(!this.dryRun) {
// @ts-ignore
await item.lock();
}
} else {
this.logger.warn('Snoowrap does not support locking Comments');
}
}
if (this.distinguish) {
if (this.distinguish && !this.dryRun) {
// @ts-ignore
await reply.distinguish({sticky: this.sticky});
}

View File

@@ -10,8 +10,10 @@ export class LockAction extends Action {
async process(item: Comment|Submission, ruleResults: RuleResult[]): Promise<void> {
if (item instanceof Submission) {
// @ts-ignore
await item.lock();
if(!this.dryRun) {
// @ts-ignore
await item.lock();
}
} else {
this.logger.warn('Snoowrap does not support locking Comments');
}

View File

@@ -9,8 +9,10 @@ export class RemoveAction extends Action {
}
async process(item: Comment|Submission, ruleResults: RuleResult[]): Promise<void> {
// @ts-ignore
await item.remove();
if(!this.dryRun) {
// @ts-ignore
await item.remove();
}
}
}

View File

@@ -27,8 +27,10 @@ export class ReportAction extends Action {
const renderedContent = await renderContent(content, item, ruleResults);
this.logger.verbose(`Contents:\r\n${renderedContent}`);
const truncatedContent = reportTrunc(renderedContent);
// @ts-ignore
await item.report({reason: truncatedContent});
if(!this.dryRun) {
// @ts-ignore
await item.report({reason: truncatedContent});
}
}
}

View File

@@ -22,8 +22,10 @@ export class FlairAction extends Action {
async process(item: Comment | Submission, ruleResults: RuleResult[]): Promise<void> {
if (item instanceof Submission) {
// @ts-ignore
await item.assignFlair({text: this.text, cssClass: this.css})
if(!this.dryRun) {
// @ts-ignore
await item.assignFlair({text: this.text, cssClass: this.css})
}
} else {
this.logger.warn('Cannot flair Comment');
}

View File

@@ -7,15 +7,18 @@ export abstract class Action {
name?: string;
logger: Logger;
cache: SubredditCache;
dryRun: boolean;
constructor(options: ActionOptions) {
const {
name = this.getKind(),
logger,
subredditName
subredditName,
dryRun = false,
} = options;
this.name = name;
this.dryRun = dryRun;
this.cache = CacheManager.get(subredditName);
const uniqueName = this.name === this.getKind() ? this.getKind() : `${this.getKind()} - ${this.name}`;
this.logger = logger.child({labels: ['Action', uniqueName]});
@@ -25,15 +28,14 @@ export abstract class Action {
async handle(item: Comment | Submission, ruleResults: RuleResult[]): Promise<void> {
await this.process(item, ruleResults);
this.logger.debug('Done');
this.logger.debug(`${this.dryRun ? 'DRYRUN - ' : ''}Done`);
}
abstract process(item: Comment | Submission, ruleResults: RuleResult[]): Promise<void>;
}
export interface ActionOptions {
name?: string;
logger: Logger,
export interface ActionOptions extends ActionConfig {
logger: Logger;
subredditName: string;
}
@@ -46,6 +48,12 @@ export interface ActionConfig {
* @pattern ^[a-zA-Z]([\w -]*[\w])?$
* */
name?: string;
/**
* If `true` the Action will not make the API request to Reddit to perform its action.
*
* @default false
* */
dryRun?: boolean;
}
export interface ActionJson extends ActionConfig {

View File

@@ -24,6 +24,7 @@ export class App {
subManagers: Manager[] = [];
logger: Logger;
wikiLocation: string;
dryRun?: true | undefined;
constructor(options: any = {}) {
const {
@@ -36,6 +37,7 @@ export class App {
logLevel,
wikiConfig,
snooDebug = process.env.SNOO_DEBUG,
dryRun = process.env.DRYRUN,
version,
authorTTL,
disableCache = false,
@@ -44,6 +46,7 @@ export class App {
CacheManager.authorTTL = authorTTL;
CacheManager.enabled = !disableCache;
this.dryRun = dryRun === true || dryRun === 'true' ? true : undefined;
this.wikiLocation = wikiConfig;
const consoleTransport = new transports.Console();
@@ -89,6 +92,10 @@ export class App {
this.logger = winston.loggers.get('default');
if(this.dryRun) {
this.logger.info('Running in DRYRUN mode');
}
let subredditsArg = [];
if (subreddits !== undefined) {
if (Array.isArray(subreddits)) {
@@ -175,7 +182,7 @@ export class App {
continue;
}
try {
subSchedule.push(new Manager(sub, this.client, this.logger, json));
subSchedule.push(new Manager(sub, this.client, this.logger, json, {dryRun: this.dryRun}));
} catch (err) {
this.logger.error(`[${sub.display_name_prefixed}] Config was not valid`, undefined, err);
}

View File

@@ -20,6 +20,7 @@ export class Check implements ICheck {
condition: JoinOperands;
rules: Array<RuleSet | Rule> = [];
logger: Logger;
dryRun?: boolean;
constructor(options: CheckOptions) {
const {
@@ -28,7 +29,8 @@ export class Check implements ICheck {
condition = 'AND',
rules = [],
actions = [],
subredditName
subredditName,
dryRun,
} = options;
this.logger = options.logger.child({labels: [`Check ${name}`]}, mergeArr);
@@ -38,6 +40,7 @@ export class Check implements ICheck {
this.name = name;
this.description = description;
this.condition = condition;
this.dryRun = dryRun;
for (const r of rules) {
if (r instanceof Rule || r instanceof RuleSet) {
this.rules.push(r);
@@ -73,7 +76,7 @@ export class Check implements ICheck {
let valid = ajv.validate(ActionSchema, a);
if (valid) {
const aj = a as ActionJson;
this.actions.push(actionFactory(aj, this.logger, subredditName));
this.actions.push(actionFactory({...aj, dryRun: this.dryRun || aj.dryRun}, this.logger, subredditName));
// @ts-ignore
a.logger = this.logger;
} else {
@@ -115,11 +118,11 @@ export class Check implements ICheck {
}
async runActions(item: Submission | Comment, ruleResults: RuleResult[]): Promise<void> {
this.logger.debug('Running Actions');
this.logger.debug(`${this.dryRun ? 'DRYRUN - ' : ''}Running Actions`);
for (const a of this.actions) {
await a.handle(item, ruleResults);
}
this.logger.info('Ran Actions');
this.logger.info(`${this.dryRun ? 'DRYRUN - ' : ''}Ran Actions`);
}
}
@@ -133,6 +136,13 @@ export interface ICheck extends JoinCondition {
* */
name: string,
description?: string,
/**
* Use this option to override the `dryRun` setting for all of its `Actions`
*
* @default undefined
* */
dryRun?: boolean;
}
export interface CheckOptions extends ICheck {

View File

@@ -192,6 +192,13 @@ export interface ManagerOptions {
apiLimitWarning?: number
caching?: false | SubredditCacheConfig
/**
* Use this option to override the `dryRun` setting for all `Checks`
*
* @default undefined
* */
dryRun?: boolean;
}
export interface ThresholdCriteria {

View File

@@ -1,6 +1,11 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"dryRun": {
"default": false,
"description": "If `true` the Action will not make the API request to Reddit to perform its action.",
"type": "boolean"
},
"kind": {
"description": "The type of action that will be performed",
"enum": [

View File

@@ -291,6 +291,11 @@
"description": {
"type": "string"
},
"dryRun": {
"default": "undefined",
"description": "Use this option to override the `dryRun` setting for all of its `Actions`",
"type": "boolean"
},
"kind": {
"description": "The type of event (new submission or new comment) this check should be run against",
"enum": [
@@ -359,6 +364,11 @@
"description": "Distinguish the comment after creation?",
"type": "boolean"
},
"dryRun": {
"default": false,
"description": "If `true` the Action will not make the API request to Reddit to perform its action.",
"type": "boolean"
},
"kind": {
"description": "The type of action that will be performed",
"enum": [
@@ -456,6 +466,11 @@
"description": "The text of the css class of the flair to apply",
"type": "string"
},
"dryRun": {
"default": false,
"description": "If `true` the Action will not make the API request to Reddit to perform its action.",
"type": "boolean"
},
"kind": {
"description": "The type of action that will be performed",
"enum": [
@@ -593,6 +608,11 @@
"LockActionJson": {
"description": "Lock the Activity",
"properties": {
"dryRun": {
"default": false,
"description": "If `true` the Action will not make the API request to Reddit to perform its action.",
"type": "boolean"
},
"kind": {
"description": "The type of action that will be performed",
"enum": [
@@ -741,6 +761,11 @@
"RemoveActionJson": {
"description": "Remove the Activity",
"properties": {
"dryRun": {
"default": false,
"description": "If `true` the Action will not make the API request to Reddit to perform its action.",
"type": "boolean"
},
"kind": {
"description": "The type of action that will be performed",
"enum": [
@@ -880,6 +905,11 @@
"description": "The text of the report. If longer than 100 characters will be truncated to \"[content]...\"",
"type": "string"
},
"dryRun": {
"default": false,
"description": "If `true` the Action will not make the API request to Reddit to perform its action.",
"type": "boolean"
},
"kind": {
"description": "The type of action that will be performed",
"enum": [
@@ -974,6 +1004,18 @@
],
"type": "object"
},
"SubredditCacheConfig": {
"properties": {
"authorTTL": {
"description": "Amount of time, in milliseconds, author activities (Comments/Submission) should be cached",
"type": "number"
},
"wikiTTL": {
"type": "number"
}
},
"type": "object"
},
"ThresholdCriteria": {
"properties": {
"condition": {
@@ -1007,6 +1049,19 @@
"description": "When Reddit API limit remaining reaches this number context bot will start warning on every poll interval",
"type": "number"
},
"caching": {
"anyOf": [
{
"$ref": "#/definitions/SubredditCacheConfig"
},
{
"enum": [
false
],
"type": "boolean"
}
]
},
"checks": {
"description": "A list of all the checks that should be run for a subreddit.\n\nChecks are split into two lists -- submission or comment -- based on kind and run independently.\n\nChecks in each list are run in the order found in the configuration.\n\nWhen a check \"passes\", and actions are performed, then all subsequent checks are skipped.",
"items": {
@@ -1015,6 +1070,11 @@
"minItems": 1,
"type": "array"
},
"dryRun": {
"default": "undefined",
"description": "Use this option to override the `dryRun` setting for all `Checks`",
"type": "boolean"
},
"heartbeatInterval": {
"description": "If present, time in milliseconds between HEARTBEAT log statements with current api limit count. Nice to have to know things are still ticking if there is low activity",
"type": "number"

View File

@@ -32,6 +32,7 @@ export class Manager {
heartbeatInterval?: number;
lastHeartbeat = dayjs();
apiLimitWarning: number;
dryRun?: boolean;
displayLabel: string;
currentLabels?: string[];
@@ -55,13 +56,14 @@ export class Manager {
const configBuilder = new ConfigBuilder({logger: this.logger});
const validJson = configBuilder.validateJson(sourceData);
const {checks, ...managerOpts} = validJson;
const {polling = {}, heartbeatInterval, apiLimitWarning = 250, caching} = managerOpts || {};
const {checks, ...configManagerOpts} = validJson;
const {polling = {}, heartbeatInterval, apiLimitWarning = 250, caching, dryRun} = configManagerOpts || {};
this.pollOptions = {...polling, ...opts.polling};
this.heartbeatInterval = heartbeatInterval;
this.apiLimitWarning = apiLimitWarning;
this.subreddit = sub;
this.client = client;
this.dryRun = opts.dryRun || dryRun;
const cacheConfig = caching === false ? {enabled: false, logger: this.logger} : {
...caching,
@@ -74,10 +76,11 @@ export class Manager {
const subChecks: Array<SubmissionCheck> = [];
const structuredChecks = configBuilder.parseToStructured(validJson);
for (const jCheck of structuredChecks) {
const checkConfig = {...jCheck, dryRun: this.dryRun || jCheck.dryRun, logger: this.logger, subredditName: sub.display_name};
if (jCheck.kind === 'comment') {
commentChecks.push(new CommentCheck({...jCheck, logger: this.logger, subredditName: sub.display_name}));
commentChecks.push(new CommentCheck(checkConfig));
} else if (jCheck.kind === 'submission') {
subChecks.push(new SubmissionCheck({...jCheck, logger: this.logger, subredditName: sub.display_name}));
subChecks.push(new SubmissionCheck(checkConfig));
}
}

View File

@@ -24,6 +24,7 @@ export const getOptions = () => {
options.push(new Option('--snooDebug', 'Set Snoowrap to debug').default(undefined, 'process.env.SNOO_DEBUG'));
options.push(new Option('--authorTTL <ms>', 'Set the TTL (ms) for the Author Activities shared cache').default(process.env.AUTHOR_TTL || 10000, 'process.env.AUTHOR_TTL || 10000'));
options.push(new Option('--disableCache', 'Disable caching for all subreddits'));
options.push(new Option('--dryRun', 'Set dryRun=true for all checks/actions on all subreddits (overrides any existing)').default(undefined, 'process.env.DRYRUN'));
return options;
}