Refactor json parsing to use ajv for all validation instead of guards

* Generate individual schemas for root objects from config (Rule, RuleSet, Action)
* Using ajv validation means we can also report all validation errors
* Remove noisy generated guard files
This commit is contained in:
FoxxMD
2021-06-03 15:19:47 -04:00
parent c632efbbb2
commit bf70a23a25
48 changed files with 665 additions and 517 deletions

View File

@@ -8,7 +8,11 @@
"build": "tsc",
"start": "node server.js",
"guard": "ts-auto-guard src/JsonConfig.ts",
"schema": "typescript-json-schema tsconfig.json JSONConfig --out src/Schema/schema.json --required --tsNodeRegister --refs --propOrder",
"schema": "npm run -s schema-app & npm run -s schema-ruleset & npm run -s schema-rule & npm run -s schema-action",
"schema-app": "typescript-json-schema tsconfig.json JSONConfig --out src/Schema/App.json --required --tsNodeRegister --refs --propOrder",
"schema-ruleset": "typescript-json-schema tsconfig.json RuleSetJSONConfig --out src/Schema/RuleSet.json --required --tsNodeRegister --refs --propOrder",
"schema-rule": "typescript-json-schema tsconfig.json RuleJSONConfig --out src/Schema/Rule.json --required --tsNodeRegister --refs --propOrder",
"schema-action": "typescript-json-schema tsconfig.json ActionJSONConfig --out src/Schema/Action.json --required --tsNodeRegister --refs --propOrder",
"schemaNotWorking": "./node_modules/.bin/ts-json-schema-generator -f tsconfig.json -p src/JsonConfig.ts -t JSONConfig --out src/Schema/vegaSchema.json"
},
"keywords": [],

View File

@@ -1,2 +0,0 @@
"use strict";
//# sourceMappingURL=Action.guard.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"Action.guard.js","sourceRoot":"","sources":["Action.guard.ts"],"names":[],"mappings":""}

View File

@@ -1,2 +0,0 @@
"use strict";
//# sourceMappingURL=CommentAction.guard.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"CommentAction.guard.js","sourceRoot":"","sources":["CommentAction.guard.ts"],"names":[],"mappings":""}

View File

@@ -1,2 +0,0 @@
"use strict";
//# sourceMappingURL=LockAction.guard.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"LockAction.guard.js","sourceRoot":"","sources":["LockAction.guard.ts"],"names":[],"mappings":""}

View File

@@ -1,2 +0,0 @@
"use strict";
//# sourceMappingURL=RemoveAction.guard.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"RemoveAction.guard.js","sourceRoot":"","sources":["RemoveAction.guard.ts"],"names":[],"mappings":""}

View File

@@ -1,2 +0,0 @@
"use strict";
//# sourceMappingURL=ReportAction.guard.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"ReportAction.guard.js","sourceRoot":"","sources":["ReportAction.guard.ts"],"names":[],"mappings":""}

View File

@@ -1,2 +0,0 @@
"use strict";
//# sourceMappingURL=FlairAction.guard.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"FlairAction.guard.js","sourceRoot":"","sources":["FlairAction.guard.ts"],"names":[],"mappings":""}

View File

@@ -1,2 +0,0 @@
"use strict";
//# sourceMappingURL=index.guard.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.guard.js","sourceRoot":"","sources":["index.guard.ts"],"names":[],"mappings":""}

View File

@@ -1,20 +0,0 @@
/*
* Generated type guards for "index.ts".
* WARNING: Do not manually change this file.
*/
import { ActionJSONConfig } from "./index";
export function isActionConfig(obj: any, _argumentName?: string): obj is ActionJSONConfig {
return (
(obj !== null &&
typeof obj === "object" ||
typeof obj === "function") &&
(typeof obj.name === "undefined" ||
typeof obj.name === "string") &&
(obj.kind === "comment" ||
obj.kind === "lock" ||
obj.kind === "remove" ||
obj.kind === "report" ||
obj.kind === "flair")
)
}

View File

@@ -1,6 +1,6 @@
import Snoowrap, {Comment, Submission} from "snoowrap";
import {Logger} from "winston";
import {createLabelledLogger} from "../util";
import {createLabelledLogger, loggerMetaShuffle} from "../util";
export abstract class Action {
name?: string;
@@ -19,7 +19,7 @@ export abstract class Action {
const prefix = `${loggerPrefix}|${this.name}`;
this.logger = createLabelledLogger(prefix, prefix);
} else {
this.logger = logger;
this.logger = logger.child(loggerMetaShuffle(logger, name || 'Action', undefined, {truncateLength: 100}));
}
}

View File

@@ -1,2 +0,0 @@
"use strict";
//# sourceMappingURL=CommentCheck.guard.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"CommentCheck.guard.js","sourceRoot":"","sources":["CommentCheck.guard.ts"],"names":[],"mappings":""}

View File

@@ -1,2 +0,0 @@
"use strict";
//# sourceMappingURL=SubmissionCheck.guard.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"SubmissionCheck.guard.js","sourceRoot":"","sources":["SubmissionCheck.guard.ts"],"names":[],"mappings":""}

View File

@@ -1,80 +0,0 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isCheckConfig = void 0;
/*
* Generated type guards for "index.ts".
* WARNING: Do not manually change this file.
*/
const index_guard_1 = require("../Rule/index.guard");
function isCheckConfig(obj, _argumentName) {
return ((obj !== null &&
typeof obj === "object" ||
typeof obj === "function") &&
typeof obj.name === "string" &&
(typeof obj.description === "undefined" ||
typeof obj.description === "string") &&
(typeof obj.ruleJoin === "undefined" ||
obj.ruleJoin === "OR" ||
obj.ruleJoin === "AND") &&
Array.isArray(obj.rules) &&
obj.rules.every((e) => (index_guard_1.isRuleConfig(e) ||
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(e.condition === "OR" ||
e.condition === "AND") &&
Array.isArray(e.rules) &&
e.rules.every((e) => (e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(e.authors !== null &&
typeof e.authors === "object" ||
typeof e.authors === "function") &&
(typeof e.authors.exclude === "undefined" ||
Array.isArray(e.authors.exclude) &&
e.authors.exclude.every((e) => (e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(typeof e.name === "undefined" ||
Array.isArray(e.name) &&
e.name.every((e) => typeof e === "string")) &&
(typeof e.flairCssClass === "undefined" ||
Array.isArray(e.flairCssClass) &&
e.flairCssClass.every((e) => typeof e === "string")) &&
(typeof e.flairText === "undefined" ||
Array.isArray(e.flairText) &&
e.flairText.every((e) => typeof e === "string")) &&
(typeof e.isMod === "undefined" ||
e.isMod === false ||
e.isMod === true))) &&
(typeof e.authors.include === "undefined" ||
Array.isArray(e.authors.include) &&
e.authors.include.every((e) => (e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(typeof e.name === "undefined" ||
Array.isArray(e.name) &&
e.name.every((e) => typeof e === "string")) &&
(typeof e.flairCssClass === "undefined" ||
Array.isArray(e.flairCssClass) &&
e.flairCssClass.every((e) => typeof e === "string")) &&
(typeof e.flairText === "undefined" ||
Array.isArray(e.flairText) &&
e.flairText.every((e) => typeof e === "string")) &&
(typeof e.isMod === "undefined" ||
e.isMod === false ||
e.isMod === true)))) &&
Array.isArray(e.rules) &&
e.rules.every((e) => index_guard_1.isRuleConfig(e)))) &&
Array.isArray(obj.actions) &&
obj.actions.every((e) => (e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(e.kind === "comment" ||
e.kind === "lock" ||
e.kind === "remove" ||
e.kind === "report" ||
e.kind === "flair")));
}
exports.isCheckConfig = isCheckConfig;
//# sourceMappingURL=index.guard.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.guard.js","sourceRoot":"","sources":["index.guard.ts"],"names":[],"mappings":"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"}

View File

@@ -1,103 +0,0 @@
/*
* Generated type guards for "index.ts".
* WARNING: Do not manually change this file.
*/
import { isRuleConfig } from "../Rule/index.guard";
import { CheckJSONConfig } from "./index";
export function isCheckConfig(obj: any, _argumentName?: string): obj is CheckJSONConfig {
return (
(obj !== null &&
typeof obj === "object" ||
typeof obj === "function") &&
typeof obj.name === "string" &&
(typeof obj.description === "undefined" ||
typeof obj.description === "string") &&
(typeof obj.ruleJoin === "undefined" ||
obj.ruleJoin === "OR" ||
obj.ruleJoin === "AND") &&
Array.isArray(obj.rules) &&
obj.rules.every((e: any) =>
(isRuleConfig(e) as boolean ||
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(e.condition === "OR" ||
e.condition === "AND") &&
Array.isArray(e.rules) &&
e.rules.every((e: any) =>
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(e.authors !== null &&
typeof e.authors === "object" ||
typeof e.authors === "function") &&
(typeof e.authors.exclude === "undefined" ||
Array.isArray(e.authors.exclude) &&
e.authors.exclude.every((e: any) =>
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(typeof e.name === "undefined" ||
Array.isArray(e.name) &&
e.name.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairCssClass === "undefined" ||
Array.isArray(e.flairCssClass) &&
e.flairCssClass.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairText === "undefined" ||
Array.isArray(e.flairText) &&
e.flairText.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.isMod === "undefined" ||
e.isMod === false ||
e.isMod === true)
)) &&
(typeof e.authors.include === "undefined" ||
Array.isArray(e.authors.include) &&
e.authors.include.every((e: any) =>
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(typeof e.name === "undefined" ||
Array.isArray(e.name) &&
e.name.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairCssClass === "undefined" ||
Array.isArray(e.flairCssClass) &&
e.flairCssClass.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairText === "undefined" ||
Array.isArray(e.flairText) &&
e.flairText.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.isMod === "undefined" ||
e.isMod === false ||
e.isMod === true)
))
) &&
Array.isArray(e.rules) &&
e.rules.every((e: any) =>
isRuleConfig(e) as boolean
))
) &&
Array.isArray(obj.actions) &&
obj.actions.every((e: any) =>
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(e.kind === "comment" ||
e.kind === "lock" ||
e.kind === "remove" ||
e.kind === "report" ||
e.kind === "flair")
)
)
}

View File

@@ -18,6 +18,12 @@ import {ReportActionJSONConfig} from "../Action/ReportAction";
import {LockActionJSONConfig} from "../Action/LockAction";
import {RemoveActionJSONConfig} from "../Action/RemoveAction";
import {JoinCondition, JoinOperands} from "../Common/interfaces";
import * as RuleSchema from '../Schema/Rule.json';
import * as RuleSetSchema from '../Schema/RuleSet.json';
import * as ActionSchema from '../Schema/Action.json';
import Ajv from 'ajv';
const ajv = new Ajv();
export class Check implements ICheck {
actions: Action[] = [];
@@ -49,20 +55,46 @@ export class Check implements ICheck {
for (const r of rules) {
if (r instanceof Rule || r instanceof RuleSet) {
this.rules.push(r);
} else if (isRuleSetConfig(r)) {
this.rules.push(new RuleSet(r));
} else if (isRuleConfig(r)) {
// @ts-ignore
r.logger = this.logger;
this.rules.push(ruleFactory(r));
} else {
let valid = ajv.validate(RuleSetSchema, r);
let setErrors: any = [];
let ruleErrors: any = [];
if (valid) {
// @ts-ignore
r.logger = this.logger;
this.rules.push(new RuleSet(r as RuleSetJSONConfig));
} else {
setErrors = ajv.errors;
valid = ajv.validate(RuleSchema, r);
if (valid) {
// @ts-ignore
r.logger = this.logger;
this.rules.push(ruleFactory(r as RuleJSONConfig));
} else {
ruleErrors = ajv.errors;
const leastErrorType = setErrors.length < ruleErrors ? 'RuleSet' : 'Rule';
const errors = setErrors.length < ruleErrors ? setErrors : ruleErrors;
this.logger.warn(`Could not parse object as RuleSet or Rule json. ${leastErrorType} validation had least errors`, {}, {
errors,
obj: r
});
}
}
}
}
for (const a of actions) {
if (a instanceof Action) {
this.actions.push(a);
} else if (isActionConfig(a)) {
this.actions.push(actionFactory(a));
} else {
let valid = ajv.validate(ActionSchema, a);
if (valid) {
this.actions.push(actionFactory(a as ActionJSONConfig));
// @ts-ignore
a.logger = this.logger;
} else {
this.logger.warn('Could not parse object as Action', {}, {error: ajv.errors, obj: a})
}
}
}

View File

@@ -147,9 +147,8 @@ export interface PollingOptions {
* */
limit?: number,
/**
* Amount of time to wait between requests to /r/subreddit/new
* Amount of time, in milliseconds, to wait between requests to /r/subreddit/new
*
* @format milliseconds
* @default 10000
* */
interval?: number,
@@ -164,9 +163,8 @@ export interface PollingOptions {
* */
limit?: number,
/**
* Amount of time to wait between requests for new comments
* Amount of time, in milliseconds, to wait between requests for new comments
*
* @format milliseconds
* @default 10000
* */
interval?: number,

View File

@@ -1,11 +1,10 @@
import {Logger} from "winston";
import {createLabelledLogger, loggerMetaShuffle, mergeArr} from "./util";
import {Subreddit} from "snoowrap";
import {CommentCheck} from "./Check/CommentCheck";
import {SubmissionCheck} from "./Check/SubmissionCheck";
import Ajv from 'ajv';
import * as schema from './Schema/schema.json';
import * as schema from './Schema/App.json';
import {JSONConfig} from "./JsonConfig";
import LoggedError from "./Utils/LoggedError";
@@ -13,15 +12,12 @@ const ajv = new Ajv();
export interface ConfigBuilderOptions {
logger?: Logger,
//subreddit: Subreddit,
}
export class ConfigBuilder {
logger: Logger;
//subreddit: Subreddit;
constructor(options: ConfigBuilderOptions) {
// this.subreddit = options.subreddit;
if (options.logger !== undefined) {
this.logger = options.logger.child(loggerMetaShuffle(options.logger, 'Config'), mergeArr);

View File

@@ -1,250 +0,0 @@
/*
* Generated type guards for "RuleSet.ts".
* WARNING: Do not manually change this file.
*/
import { RuleSetJSONConfig } from "./RuleSet";
export function isRuleSetConfig(obj: any, _argumentName?: string): obj is RuleSetJSONConfig {
return (
(obj !== null &&
typeof obj === "object" ||
typeof obj === "function") &&
(obj.condition === "OR" ||
obj.condition === "AND") &&
Array.isArray(obj.rules) &&
obj.rules.every((e: any) =>
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(typeof e.name === "undefined" ||
typeof e.name === "string") &&
(typeof e.authors === "undefined" ||
(e.authors !== null &&
typeof e.authors === "object" ||
typeof e.authors === "function") &&
(typeof e.authors.exclude === "undefined" ||
Array.isArray(e.authors.exclude) &&
e.authors.exclude.every((e: any) =>
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(typeof e.name === "undefined" ||
Array.isArray(e.name) &&
e.name.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairCssClass === "undefined" ||
Array.isArray(e.flairCssClass) &&
e.flairCssClass.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairText === "undefined" ||
Array.isArray(e.flairText) &&
e.flairText.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.isMod === "undefined" ||
e.isMod === false ||
e.isMod === true)
)) &&
(typeof e.authors.include === "undefined" ||
Array.isArray(e.authors.include) &&
e.authors.include.every((e: any) =>
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(typeof e.name === "undefined" ||
Array.isArray(e.name) &&
e.name.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairCssClass === "undefined" ||
Array.isArray(e.flairCssClass) &&
e.flairCssClass.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairText === "undefined" ||
Array.isArray(e.flairText) &&
e.flairText.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.isMod === "undefined" ||
e.isMod === false ||
e.isMod === true)
)))
) &&
Array.isArray(obj.rules) &&
obj.rules.every((e: any) =>
((e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(typeof e.window === "undefined" ||
typeof e.window === "string" ||
typeof e.window === "number") &&
(typeof e.usePostAsReference === "undefined" ||
e.usePostAsReference === false ||
e.usePostAsReference === true) &&
(typeof e.lookAt === "undefined" ||
e.lookAt === "comments" ||
e.lookAt === "submissions") &&
Array.isArray(e.thresholds) &&
e.thresholds.every((e: any) =>
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
Array.isArray(e.subreddits) &&
e.subreddits.every((e: any) =>
typeof e === "string"
) &&
(typeof e.count === "undefined" ||
typeof e.count === "number")
) &&
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(typeof e.name === "undefined" ||
typeof e.name === "string") &&
(typeof e.authors === "undefined" ||
(e.authors !== null &&
typeof e.authors === "object" ||
typeof e.authors === "function") &&
(typeof e.authors.exclude === "undefined" ||
Array.isArray(e.authors.exclude) &&
e.authors.exclude.every((e: any) =>
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(typeof e.name === "undefined" ||
Array.isArray(e.name) &&
e.name.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairCssClass === "undefined" ||
Array.isArray(e.flairCssClass) &&
e.flairCssClass.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairText === "undefined" ||
Array.isArray(e.flairText) &&
e.flairText.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.isMod === "undefined" ||
e.isMod === false ||
e.isMod === true)
)) &&
(typeof e.authors.include === "undefined" ||
Array.isArray(e.authors.include) &&
e.authors.include.every((e: any) =>
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(typeof e.name === "undefined" ||
Array.isArray(e.name) &&
e.name.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairCssClass === "undefined" ||
Array.isArray(e.flairCssClass) &&
e.flairCssClass.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairText === "undefined" ||
Array.isArray(e.flairText) &&
e.flairText.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.isMod === "undefined" ||
e.isMod === false ||
e.isMod === true)
))) &&
(e.kind === "recentActivity" ||
e.kind === "repeatSubmission" ||
e.kind === "author") ||
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
typeof e.threshold === "number" &&
(typeof e.window === "undefined" ||
typeof e.window === "string" ||
typeof e.window === "number") &&
(typeof e.gapAllowance === "undefined" ||
typeof e.gapAllowance === "number") &&
(typeof e.usePostAsReference === "undefined" ||
e.usePostAsReference === false ||
e.usePostAsReference === true) &&
(typeof e.include === "undefined" ||
Array.isArray(e.include) &&
e.include.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.exclude === "undefined" ||
Array.isArray(e.exclude) &&
e.exclude.every((e: any) =>
typeof e === "string"
)) &&
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(typeof e.name === "undefined" ||
typeof e.name === "string") &&
(typeof e.authors === "undefined" ||
(e.authors !== null &&
typeof e.authors === "object" ||
typeof e.authors === "function") &&
(typeof e.authors.exclude === "undefined" ||
Array.isArray(e.authors.exclude) &&
e.authors.exclude.every((e: any) =>
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(typeof e.name === "undefined" ||
Array.isArray(e.name) &&
e.name.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairCssClass === "undefined" ||
Array.isArray(e.flairCssClass) &&
e.flairCssClass.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairText === "undefined" ||
Array.isArray(e.flairText) &&
e.flairText.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.isMod === "undefined" ||
e.isMod === false ||
e.isMod === true)
)) &&
(typeof e.authors.include === "undefined" ||
Array.isArray(e.authors.include) &&
e.authors.include.every((e: any) =>
(e !== null &&
typeof e === "object" ||
typeof e === "function") &&
(typeof e.name === "undefined" ||
Array.isArray(e.name) &&
e.name.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairCssClass === "undefined" ||
Array.isArray(e.flairCssClass) &&
e.flairCssClass.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.flairText === "undefined" ||
Array.isArray(e.flairText) &&
e.flairText.every((e: any) =>
typeof e === "string"
)) &&
(typeof e.isMod === "undefined" ||
e.isMod === false ||
e.isMod === true)
))) &&
(e.kind === "recentActivity" ||
e.kind === "repeatSubmission" ||
e.kind === "author"))
)
)
}

View File

@@ -8,6 +8,10 @@ import {createLabelledLogger, determineNewResults, findResultByPremise, loggerMe
import {Logger} from "winston";
import {AuthorRuleJSONConfig} from "./AuthorRule";
import {JoinCondition, JoinOperands} from "../Common/interfaces";
import * as RuleSchema from '../Schema/Rule.json';
import Ajv from 'ajv';
const ajv = new Ajv();
export class RuleSet implements IRuleSet, Triggerable {
rules: Rule[] = [];
@@ -25,10 +29,15 @@ export class RuleSet implements IRuleSet, Triggerable {
for (const r of rules) {
if (r instanceof Rule) {
this.rules.push(r);
} else if (isRuleConfig(r)) {
// @ts-ignore
r.logger = this.logger;
this.rules.push(ruleFactory(r));
} else {
const valid = ajv.validate(RuleSchema, r);
if (valid) {
// @ts-ignore
r.logger = this.logger;
this.rules.push(ruleFactory(r as RuleJSONConfig));
} else {
this.logger.warn('Could not build rule because of JSON errors', {}, {errors: ajv.errors, obj: r});
}
}
}
}

View File

@@ -1,2 +0,0 @@
"use strict";
//# sourceMappingURL=index.guard.js.map

View File

@@ -1 +0,0 @@
{"version":3,"file":"index.guard.js","sourceRoot":"","sources":["index.guard.ts"],"names":[],"mappings":""}

29
src/Schema/Action.json Normal file
View File

@@ -0,0 +1,29 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"kind": {
"description": "The type of action that will be performed",
"enum": [
"comment",
"flair",
"lock",
"remove",
"report"
],
"type": "string"
},
"name": {
"description": "A friendly name for this Action",
"type": "string"
}
},
"propertyOrder": [
"kind",
"name"
],
"required": [
"kind"
],
"type": "object"
}

View File

@@ -406,8 +406,7 @@
"properties": {
"interval": {
"default": 10000,
"description": "Amount of time to wait between requests for new comments\n\nDefaults to 10 seconds",
"format": "milliseconds",
"description": "Amount of time, in milliseconds, to wait between requests for new comments",
"type": "number"
},
"limit": {
@@ -427,8 +426,7 @@
"properties": {
"interval": {
"default": 10000,
"description": "Amount of time to wait between requests to /r/subreddit/new\n\nDefaults to 10 seconds",
"format": "milliseconds",
"description": "Amount of time, in milliseconds, to wait between requests to /r/subreddit/new",
"type": "number"
},
"limit": {

105
src/Schema/Rule.json Normal file
View File

@@ -0,0 +1,105 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AuthorCriteria": {
"additionalProperties": false,
"description": "Criteria with which to test against the author of an Activity. The outcome of the test is based on:\n\n1. All present properties passing and\n2. If a property is a list then any value from the list matching",
"minProperties": 1,
"properties": {
"flairCssClass": {
"description": "A list of (user) flair css class values from the subreddit to match against",
"items": {
"type": "string"
},
"type": "array"
},
"flairText": {
"description": "A list of (user) flair text values from the subreddit to match against",
"items": {
"type": "string"
},
"type": "array"
},
"isMod": {
"description": "Is the author a moderator?",
"type": "boolean"
},
"name": {
"description": "A list of reddit usernames (case-insensitive) to match against. Do not include the \"u/\" prefix\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
"examples": [
"FoxxMD",
"AnotherUser"
],
"items": {
"type": "string"
},
"type": "array"
}
},
"propertyOrder": [
"name",
"flairCssClass",
"flairText",
"isMod"
],
"type": "object"
},
"AuthorOptions": {
"additionalProperties": false,
"description": "If present then these Author criteria are checked before running the rule. If criteria fails then the rule is skipped.",
"minProperties": 1,
"properties": {
"exclude": {
"description": "Only runs if include is not present. Will \"pass\" if any of set of the AuthorCriteria does not pass",
"items": {
"$ref": "#/definitions/AuthorCriteria"
},
"type": "array"
},
"include": {
"description": "Will \"pass\" if any set of AuthorCriteria passes",
"items": {
"$ref": "#/definitions/AuthorCriteria"
},
"type": "array"
}
},
"propertyOrder": [
"include",
"exclude"
],
"type": "object"
}
},
"properties": {
"authors": {
"$ref": "#/definitions/AuthorOptions",
"additionalProperties": false,
"description": "If present then these Author criteria are checked before running the rule. If criteria fails then the rule is skipped.",
"minProperties": 1
},
"kind": {
"description": "The kind of rule to run",
"enum": [
"author",
"recentActivity",
"repeatSubmission"
],
"type": "string"
},
"name": {
"description": "A friendly, descriptive name for this rule. Highly recommended to make it easier to track logs EX \"repeatCrosspostRule\"",
"type": "string"
}
},
"propertyOrder": [
"kind",
"name",
"authors"
],
"required": [
"kind"
],
"type": "object"
}

465
src/Schema/RuleSet.json Normal file
View File

@@ -0,0 +1,465 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ActivityWindowCriteria": {
"additionalProperties": false,
"description": "If both properties are defined then the first criteria met will be used IE if # of activities = count before duration is reached then count will be used, or vice versa",
"minProperties": 1,
"properties": {
"count": {
"description": "The number of activities (submission/comments) to consider",
"type": "number"
},
"duration": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
},
{
"type": "string"
}
],
"description": "An ISO 8601 duration or Day.js duration object.\n\nThe duration will be subtracted from the time when the rule is run to create a time range like this:\n\nendTime = NOW <----> startTime = (NOW - duration)\n\nEX endTime = 3:00PM <----> startTime = (NOW - 15 minutes) = 2:45PM -- so look for activities between 2:45PM and 3:00PM",
"examples": [
"PT1M",
{
"minutes": 15
}
]
}
},
"propertyOrder": [
"count",
"duration"
],
"type": "object"
},
"AuthorCriteria": {
"additionalProperties": false,
"description": "Criteria with which to test against the author of an Activity. The outcome of the test is based on:\n\n1. All present properties passing and\n2. If a property is a list then any value from the list matching",
"minProperties": 1,
"properties": {
"flairCssClass": {
"description": "A list of (user) flair css class values from the subreddit to match against",
"items": {
"type": "string"
},
"type": "array"
},
"flairText": {
"description": "A list of (user) flair text values from the subreddit to match against",
"items": {
"type": "string"
},
"type": "array"
},
"isMod": {
"description": "Is the author a moderator?",
"type": "boolean"
},
"name": {
"description": "A list of reddit usernames (case-insensitive) to match against. Do not include the \"u/\" prefix\n\n EX to match against /u/FoxxMD and /u/AnotherUser use [\"FoxxMD\",\"AnotherUser\"]",
"examples": [
"FoxxMD",
"AnotherUser"
],
"items": {
"type": "string"
},
"type": "array"
}
},
"propertyOrder": [
"name",
"flairCssClass",
"flairText",
"isMod"
],
"type": "object"
},
"AuthorOptions": {
"additionalProperties": false,
"description": "If present then these Author criteria are checked before running the rule. If criteria fails then the rule is skipped.",
"minProperties": 1,
"properties": {
"exclude": {
"description": "Only runs if include is not present. Will \"pass\" if any of set of the AuthorCriteria does not pass",
"items": {
"$ref": "#/definitions/AuthorCriteria"
},
"type": "array"
},
"include": {
"description": "Will \"pass\" if any set of AuthorCriteria passes",
"items": {
"$ref": "#/definitions/AuthorCriteria"
},
"type": "array"
}
},
"propertyOrder": [
"include",
"exclude"
],
"type": "object"
},
"AuthorRuleJSONConfig": {
"properties": {
"authors": {
"$ref": "#/definitions/AuthorOptions",
"additionalProperties": false,
"description": "If present then these Author criteria are checked before running the rule. If criteria fails then the rule is skipped.",
"minProperties": 1
},
"exclude": {
"description": "Only runs if include is not present. Will \"pass\" if any of set of the AuthorCriteria does not pass",
"items": {
"$ref": "#/definitions/AuthorCriteria"
},
"type": "array"
},
"include": {
"description": "Will \"pass\" if any set of AuthorCriteria passes",
"items": {
"$ref": "#/definitions/AuthorCriteria"
},
"type": "array"
},
"kind": {
"description": "The kind of rule to run",
"enum": [
"author"
],
"type": "string"
},
"name": {
"description": "A friendly, descriptive name for this rule. Highly recommended to make it easier to track logs EX \"repeatCrosspostRule\"",
"type": "string"
}
},
"propertyOrder": [
"kind",
"include",
"exclude",
"name",
"authors"
],
"required": [
"exclude",
"include",
"kind"
],
"type": "object"
},
"DurationObject": {
"additionalProperties": false,
"description": "A Day.js duration object\n\nhttps://day.js.org/docs/en/durations/creating",
"minProperties": 1,
"properties": {
"days": {
"type": "number"
},
"hours": {
"type": "number"
},
"minutes": {
"type": "number"
},
"months": {
"type": "number"
},
"seconds": {
"type": "number"
},
"weeks": {
"type": "number"
},
"years": {
"type": "number"
}
},
"propertyOrder": [
"seconds",
"minutes",
"hours",
"days",
"weeks",
"months",
"years"
],
"type": "object"
},
"RecentActivityRuleJSONConfig": {
"description": "Checks a user's history for any Activity (Submission/Comment) in the subreddits specified in thresholds",
"properties": {
"authors": {
"$ref": "#/definitions/AuthorOptions",
"additionalProperties": false,
"description": "If present then these Author criteria are checked before running the rule. If criteria fails then the rule is skipped.",
"minProperties": 1
},
"kind": {
"description": "The kind of rule to run",
"enum": [
"recentActivity"
],
"type": "string"
},
"lookAt": {
"description": "If present restricts the activities that are considered for count from SubThreshold",
"enum": [
"comments",
"submissions"
],
"type": "string"
},
"name": {
"description": "A friendly, descriptive name for this rule. Highly recommended to make it easier to track logs EX \"repeatCrosspostRule\"",
"type": "string"
},
"thresholds": {
"description": "A list of subreddits/count criteria that may trigger this rule. ANY SubThreshold will trigger this rule.",
"items": {
"$ref": "#/definitions/SubThreshold"
},
"minItems": 1,
"type": "array"
},
"useSubmissionAsReference": {
"default": true,
"description": "If activity is a Submission and is a link (not self-post) then only look at Submissions that contain this link, otherwise consider all activities.",
"type": "boolean"
},
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/ActivityWindowCriteria"
},
{
"type": [
"string",
"number"
]
}
],
"default": 15,
"description": "Criteria for defining what set of activities should be considered.\n\nThe value of this property may be either count OR duration -- to use both write it as an ActivityWindowCriteria\n\nSee ActivityWindowCriteria for descriptions of what count/duration do",
"examples": [
15,
"PT1M",
{
"count": 10
},
{
"duration": {
"hours": 5
}
},
{
"count": 5,
"duration": {
"minutes": 15
}
}
]
}
},
"propertyOrder": [
"kind",
"lookAt",
"thresholds",
"window",
"useSubmissionAsReference",
"name",
"authors"
],
"required": [
"kind",
"thresholds"
],
"type": "object"
},
"RepeatSubmissionJSONConfig": {
"description": "Checks a user's history for Submissions with identical content",
"properties": {
"authors": {
"$ref": "#/definitions/AuthorOptions",
"additionalProperties": false,
"description": "If present then these Author criteria are checked before running the rule. If criteria fails then the rule is skipped.",
"minProperties": 1
},
"exclude": {
"description": "Do not include Submissions from this list of Subreddits.\n\nA list of subreddits (case-insensitive) to look for. Do not include \"r/\" prefix.\n\nEX to match against /r/mealtimevideos and /r/askscience use [\"mealtimevideos\",\"askscience\"]",
"examples": [
"mealtimevideos",
"askscience"
],
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"gapAllowance": {
"description": "The number of allowed non-identical Submissions between identical Submissions that can be ignored when checking against the threshold value",
"type": "number"
},
"include": {
"description": "Only include Submissions from this list of Subreddits.\n\nA list of subreddits (case-insensitive) to look for. Do not include \"r/\" prefix.\n\nEX to match against /r/mealtimevideos and /r/askscience use [\"mealtimevideos\",\"askscience\"]",
"examples": [
"mealtimevideos",
"askscience"
],
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"kind": {
"description": "The kind of rule to run",
"enum": [
"repeatSubmission"
],
"type": "string"
},
"name": {
"description": "A friendly, descriptive name for this rule. Highly recommended to make it easier to track logs EX \"repeatCrosspostRule\"",
"type": "string"
},
"threshold": {
"default": 5,
"description": "The number of repeat submissions that will trigger the rule",
"type": "number"
},
"useSubmissionAsReference": {
"default": true,
"description": "If activity is a Submission and is a link (not self-post) then only look at Submissions that contain this link, otherwise consider all activities.",
"type": "boolean"
},
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/ActivityWindowCriteria"
},
{
"type": [
"string",
"number"
]
}
],
"default": 15,
"description": "Criteria for defining what set of activities should be considered.\n\nThe value of this property may be either count OR duration -- to use both write it as an ActivityWindowCriteria\n\nSee ActivityWindowCriteria for descriptions of what count/duration do",
"examples": [
15,
"PT1M",
{
"count": 10
},
{
"duration": {
"hours": 5
}
},
{
"count": 5,
"duration": {
"minutes": 15
}
}
]
}
},
"propertyOrder": [
"kind",
"threshold",
"gapAllowance",
"include",
"exclude",
"window",
"useSubmissionAsReference",
"name",
"authors"
],
"required": [
"kind"
],
"type": "object"
},
"SubThreshold": {
"properties": {
"count": {
"default": 1,
"description": "The number of activities in each subreddit from the list that will trigger this rule",
"minimum": 1,
"type": "number"
},
"subreddits": {
"description": "A list of subreddits (case-insensitive) to look for. Do not include \"r/\" prefix.\n\nEX to match against /r/mealtimevideos and /r/askscience use [\"mealtimevideos\",\"askscience\"]",
"examples": [
"mealtimevideos",
"askscience"
],
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
}
},
"propertyOrder": [
"count",
"subreddits"
],
"required": [
"subreddits"
],
"type": "object"
}
},
"description": "A RuleSet is a \"nested\" set of Rules that can be used to create more complex AND/OR behavior. Think of the outcome of a RuleSet as the result of all of it's Rules (based on condition)",
"properties": {
"condition": {
"default": "AND",
"description": "Under what condition should a set of rules be considered \"successful\"?\n\nIf \"OR\" then ANY triggered rule results in success.\n\nIf \"AND\" then ALL rules must be triggered to result in success.",
"enum": [
"AND",
"OR"
],
"type": "string"
},
"rules": {
"items": {
"anyOf": [
{
"$ref": "#/definitions/RecentActivityRuleJSONConfig"
},
{
"$ref": "#/definitions/RepeatSubmissionJSONConfig"
},
{
"$ref": "#/definitions/AuthorRuleJSONConfig"
}
]
},
"minItems": 1,
"type": "array"
}
},
"propertyOrder": [
"rules",
"condition"
],
"required": [
"rules"
],
"type": "object"
}

View File

@@ -1,17 +1,14 @@
import {InboxStream, CommentStream, SubmissionStream} from "snoostorm";
import snoowrap from "snoowrap";
import minimist from 'minimist';
import winston, {Logger} from 'winston';
import winston from 'winston';
import 'winston-daily-rotate-file';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import dduration from 'dayjs/plugin/duration.js';
import {labelledFormat} from "./util";
import {ConfigBuilder} from "./ConfigBuilder";
import EventEmitter from "events";
import {Manager} from "./Subreddit/Manager";
import pEvent from "p-event";
import {JSONConfig} from "./JsonConfig";
dayjs.extend(utc);
dayjs.extend(dduration);
@@ -119,7 +116,6 @@ if (subredditsArg.length === 0) {
for (const sub of subsToRun) {
let content = undefined;
let json = undefined;
let config = undefined;
try {
const wiki = sub.getWikiPage('contextbot');
content = await wiki.content_md;