Implement name references for actions and rules

Action/Rule objects they can now be referenced by name from anywhere in the configuration
This commit is contained in:
FoxxMD
2021-06-08 12:40:00 -04:00
parent d239d3c6cc
commit 5905c910b0
21 changed files with 826 additions and 161 deletions

View File

@@ -23,7 +23,8 @@ Some feature highlights:
* All text-based actions support [mustache](https://mustache.github.io) templating
* History-based rules support multiple "valid window" types -- [ISO 8601 Durations](https://en.wikipedia.org/wiki/ISO_8601#Durations), [Day.js Durations](https://day.js.org/docs/en/durations/creating), and submission/comment count limits.
* All rules support skipping behavior based on author criteria -- name, css flair/text, and moderator status
* Docker container support *(coming soon...)*
* Rules and Actions support named references so you write rules/actions once and reference them anywhere
* Docker container support
# Table of Contents

View File

@@ -10,10 +10,12 @@
"guard": "ts-auto-guard src/JsonConfig.ts",
"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"
"schema-ruleset": "typescript-json-schema tsconfig.json RuleSetJson --out src/Schema/RuleSet.json --required --tsNodeRegister --refs --propOrder",
"schema-rule": "typescript-json-schema tsconfig.json RuleJson --out src/Schema/Rule.json --required --tsNodeRegister --refs --propOrder",
"schema-action": "typescript-json-schema tsconfig.json ActionJson --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",
"circular": "madge --circular --extensions ts src/index.ts",
"circular-graph": "madge --image graph.svg --circular --extensions ts src/index.ts"
},
"engines": {
"node": ">=15"

View File

@@ -1,23 +1,23 @@
import {CommentAction, CommentActionJSONConfig} from "./CommentAction";
import {CommentAction, CommentActionJson} from "./CommentAction";
import LockAction from "./LockAction";
import {RemoveAction} from "./RemoveAction";
import {ReportAction, ReportActionJSONConfig} from "./ReportAction";
import {FlairAction, FlairActionJSONConfig} from "./SubmissionAction/FlairAction";
import Action, {ActionJSONConfig} from "./index";
import {ReportAction, ReportActionJson} from "./ReportAction";
import {FlairAction, FlairActionJson} from "./SubmissionAction/FlairAction";
import Action, {ActionJson} from "./index";
export function actionFactory
(config: ActionJSONConfig): Action {
(config: ActionJson): Action {
switch (config.kind) {
case 'comment':
return new CommentAction(config as CommentActionJSONConfig);
return new CommentAction(config as CommentActionJson);
case 'lock':
return new LockAction();
case 'remove':
return new RemoveAction();
case 'report':
return new ReportAction(config as ReportActionJSONConfig);
return new ReportAction(config as ReportActionJson);
case 'flair':
return new FlairAction(config as FlairActionJSONConfig);
return new FlairAction(config as FlairActionJson);
default:
throw new Error('rule "kind" was not recognized.');
}

View File

@@ -1,4 +1,4 @@
import Action, {ActionJSONConfig, ActionConfig, ActionOptions} from "./index";
import Action, {ActionJson, ActionConfig, ActionOptions} from "./index";
import Snoowrap, {Comment} from "snoowrap";
import Submission from "snoowrap/dist/objects/Submission";
import dayjs, {Dayjs} from "dayjs";
@@ -81,6 +81,6 @@ export interface CommentActionOptions extends CommentActionConfig,ActionOptions
/**
* Reply to the Activity. For a submission the reply will be a top-level comment.
* */
export interface CommentActionJSONConfig extends CommentActionConfig, ActionJSONConfig {
export interface CommentActionJson extends CommentActionConfig, ActionJson {
}

View File

@@ -1,4 +1,4 @@
import {ActionJSONConfig, ActionConfig} from "./index";
import {ActionJson, ActionConfig} from "./index";
import Action from "./index";
import Snoowrap, {Comment, Submission} from "snoowrap";
import {RuleResult} from "../Rule";
@@ -20,7 +20,7 @@ export interface LockActionConfig extends ActionConfig {
/**
* Lock the Activity
* */
export interface LockActionJSONConfig extends LockActionConfig, ActionJSONConfig {
export interface LockActionJson extends LockActionConfig, ActionJson {
}

View File

@@ -1,4 +1,4 @@
import {ActionJSONConfig, ActionConfig} from "./index";
import {ActionJson, ActionConfig} from "./index";
import Action from "./index";
import Snoowrap, {Comment, Submission} from "snoowrap";
import {RuleResult} from "../Rule";
@@ -18,6 +18,6 @@ export interface RemoveActionConfig extends ActionConfig {
/**
* Remove the Activity
* */
export interface RemoveActionJSONConfig extends RemoveActionConfig, ActionJSONConfig {
export interface RemoveActionJson extends RemoveActionConfig, ActionJson {
}

View File

@@ -1,4 +1,4 @@
import {ActionJSONConfig, ActionConfig, ActionOptions} from "./index";
import {ActionJson, ActionConfig, ActionOptions} from "./index";
import Action from "./index";
import Snoowrap, {Comment, Submission} from "snoowrap";
import {truncateStringToLength} from "../util";
@@ -7,7 +7,8 @@ import {RuleResult} from "../Rule";
// https://www.reddit.com/dev/api/oauth#POST_api_report
// denotes 100 characters maximum
const reportTrunc = truncateStringToLength(100);
// const reportTrunc = truncateStringToLength(100);
// actually only applies to VISIBLE text on OLD reddit... on old reddit rest of text is visible on hover. on new reddit the whole thing displays (up to at least 400 characters)
export class ReportAction extends Action {
content: string;
@@ -20,9 +21,9 @@ export class ReportAction extends Action {
async handle(item: Comment | Submission, ruleResults: RuleResult[]): Promise<void> {
const renderedContent = await renderContent(this.content, item, ruleResults);
const truncatedContent = reportTrunc(renderedContent);
//const truncatedContent = reportTrunc(renderedContent);
// @ts-ignore
await item.report({reason: truncatedContent});
await item.report({reason: renderedContent});
}
}
@@ -39,6 +40,6 @@ export interface ReportActionOptions extends ReportActionConfig, ActionOptions {
/**
* Report the Activity
* */
export interface ReportActionJSONConfig extends ReportActionConfig, ActionJSONConfig {
export interface ReportActionJson extends ReportActionConfig, ActionJson {
}

View File

@@ -1,5 +1,5 @@
import {SubmissionActionConfig} from "./index";
import Action, {ActionJSONConfig} from "../index";
import Action, {ActionJson} from "../index";
import Snoowrap, {Comment, Submission} from "snoowrap";
import {RuleResult} from "../../Rule";
@@ -43,6 +43,6 @@ export interface FlairActionOptions extends SubmissionActionConfig {
/**
* Flair the Submission
* */
export interface FlairActionJSONConfig extends FlairActionOptions, ActionJSONConfig {
export interface FlairActionJson extends FlairActionOptions, ActionJson {
}

View File

@@ -44,13 +44,16 @@ export interface ActionConfig {
name?: string;
}
/** @see {isActionConfig} ts-auto-guard:type-guard */
export interface ActionJSONConfig extends ActionConfig {
export interface ActionJson extends ActionConfig {
/**
* The type of action that will be performed
*/
kind: 'comment' | 'lock' | 'remove' | 'report' | 'flair'
}
export const isActionJson = (obj: object): obj is ActionJson => {
return (obj as ActionJson).kind !== undefined;
}
export default Action;

View File

@@ -1,25 +1,17 @@
import {RuleSet, IRuleSet, RuleSetJSONConfig} from "../Rule/RuleSet";
import {IRule, Triggerable, Rule, RuleJSONConfig, RuleResult} from "../Rule";
import Action, {ActionConfig, ActionJSONConfig} from "../Action";
import {RuleSet, IRuleSet, RuleSetJson, RuleSetObjectJson} from "../Rule/RuleSet";
import {IRule,Rule, RuleJSONConfig, RuleResult} from "../Rule";
import Action, {ActionConfig, ActionJson} from "../Action";
import {Logger} from "winston";
import Snoowrap, {Comment, Submission} from "snoowrap";
import {RecentActivityRuleJSONConfig} from "../Rule/RecentActivityRule";
import {RepeatActivityJSONConfig} from "../Rule/SubmissionRule/RepeatActivityRule";
import {FlairActionJSONConfig} from "../Action/SubmissionAction/FlairAction";
import {CommentActionJSONConfig} from "../Action/CommentAction";
import {Comment, Submission} from "snoowrap";
import {actionFactory} from "../Action/ActionFactory";
import {ruleFactory} from "../Rule/RuleFactory";
import {createLabelledLogger, determineNewResults, loggerMetaShuffle, mergeArr} from "../util";
import {AuthorRuleJSONConfig} from "../Rule/AuthorRule";
import {ReportActionJSONConfig} from "../Action/ReportAction";
import {LockActionJSONConfig} from "../Action/LockAction";
import {RemoveActionJSONConfig} from "../Action/RemoveAction";
import {createLabelledLogger, loggerMetaShuffle, mergeArr} from "../util";;
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';
import {AttributionJSONConfig} from "../Rule/SubmissionRule/AttributionRule";
import {ActionObjectJson, RuleJson, RuleObjectJson, ActionJson as ActionTypeJson} from "../Common/types";
const ajv = new Ajv();
@@ -60,7 +52,7 @@ export class Check implements ICheck {
if (valid) {
// @ts-ignore
r.logger = this.logger;
this.rules.push(new RuleSet(r as RuleSetJSONConfig));
this.rules.push(new RuleSet(r as RuleSetObjectJson));
} else {
setErrors = ajv.errors;
valid = ajv.validate(RuleSchema, r);
@@ -87,7 +79,7 @@ export class Check implements ICheck {
} else {
let valid = ajv.validate(ActionSchema, a);
if (valid) {
this.actions.push(actionFactory(a as ActionJSONConfig));
this.actions.push(actionFactory(a as ActionJson));
// @ts-ignore
a.logger = this.logger;
} else {
@@ -151,23 +143,29 @@ export interface CheckOptions extends ICheck {
logger?: Logger
}
/**
* An object consisting of Rules (tests) and Actions to perform if Rules are triggered
* @see {isCheckConfig} ts-auto-guard:type-guard
* */
export interface CheckJSONConfig extends ICheck {
export interface CheckJson extends ICheck {
/**
* The type of event (new submission or new comment) this check should be run against
*/
kind: 'submission' | 'comment'
/**
* Rules are run in the order found in configuration. Can be Rules or RuleSets
* A list of Rules to run. If `Rule` objects are triggered based on `condition` then `Actions` will be performed.
*
* Can be `Rule`, `RuleSet`, or the `name` of any **named** `Rule` in your subreddit's configuration
* @minItems 1
* */
rules: Array<RuleSetJSONConfig | RecentActivityRuleJSONConfig | RepeatActivityJSONConfig | AuthorRuleJSONConfig | AttributionJSONConfig>
rules: Array<RuleSetJson | RuleJson>
/**
* The actions to run after the check is successfully triggered. ALL actions will run in the order they are listed
* The `Actions` to run after the check is successfully triggered. ALL `Actions` will run in the order they are listed
*
* Can be `Action` or the `name` of any **named** `Action` in your subreddit's configuration
*
* @minItems 1
* */
actions: Array<FlairActionJSONConfig | CommentActionJSONConfig | ReportActionJSONConfig | LockActionJSONConfig | RemoveActionJSONConfig>
actions: Array<ActionTypeJson>
}
export interface CheckStructuredJson extends CheckJson {
rules: Array<RuleSetObjectJson | RuleObjectJson>
actions: Array<ActionObjectJson>
}

View File

@@ -122,11 +122,11 @@ export type JoinOperands = 'OR' | 'AND';
export interface JoinCondition {
/**
* Under what condition should a set of rules be considered "successful"?
* Under what condition should a set of run `Rule` objects be considered "successful"?
*
* If "OR" then ANY triggered rule results in success.
* If `OR` then **any** triggered `Rule` object results in success.
*
* If "AND" then ALL rules must be triggered to result in success.
* If `AND` then **all** `Rule` objects must be triggered to result in success.
*
* @default "AND"
* */
@@ -170,3 +170,16 @@ export interface PollingOptions {
interval?: number,
}
}
export interface ManagerOptions {
polling?: PollingOptions
/**
* 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
* */
heartbeatInterval?: number
/**
* When Reddit API limit remaining reaches this number context bot will start warning on every poll interval
* @default 250
* */
apiLimitWarning?: number
}

15
src/Common/types.ts Normal file
View File

@@ -0,0 +1,15 @@
import {RecentActivityRuleJSONConfig} from "../Rule/RecentActivityRule";
import {RepeatActivityJSONConfig} from "../Rule/SubmissionRule/RepeatActivityRule";
import {AuthorRuleJSONConfig} from "../Rule/AuthorRule";
import {AttributionJSONConfig} from "../Rule/SubmissionRule/AttributionRule";
import {FlairActionJson} from "../Action/SubmissionAction/FlairAction";
import {CommentActionJson} from "../Action/CommentAction";
import {ReportActionJson} from "../Action/ReportAction";
import {LockActionJson} from "../Action/LockAction";
import {RemoveActionJson} from "../Action/RemoveAction";
export type RuleJson = RecentActivityRuleJSONConfig | RepeatActivityJSONConfig | AuthorRuleJSONConfig | AttributionJSONConfig | string;
export type RuleObjectJson = Exclude<RuleJson, string>
export type ActionJson = FlairActionJson | CommentActionJson | ReportActionJson | LockActionJson | RemoveActionJson | string;
export type ActionObjectJson = Exclude<ActionJson, string>;

View File

@@ -7,7 +7,12 @@ import Ajv from 'ajv';
import * as schema from './Schema/App.json';
import {JSONConfig} from "./JsonConfig";
import LoggedError from "./Utils/LoggedError";
import {ManagerOptions} from "./Subreddit/Manager";
import {CheckStructuredJson} from "./Check";
import {ManagerOptions} from "./Common/interfaces";
import {isRuleSetJSON, RuleSetJson, RuleSetObjectJson} from "./Rule/RuleSet";
import deepEqual from "fast-deep-equal";
import {ActionJson, ActionObjectJson, RuleJson, RuleObjectJson} from "./Common/types";
import {isActionJson} from "./Action";
const ajv = new Ajv();
@@ -30,13 +35,30 @@ export class ConfigBuilder {
buildFromJson(config: object): [Array<SubmissionCheck>,Array<CommentCheck>,ManagerOptions] {
const commentChecks: Array<CommentCheck> = [];
const subChecks: Array<SubmissionCheck> = [];
let namedRules: Map<string,RuleObjectJson> = new Map();
let namedActions: Map<string,ActionObjectJson> = new Map();
const valid = ajv.validate(schema, config);
let managerOptions: ManagerOptions = {};
if(valid) {
const validConfig = config as JSONConfig;
const {checks = [], ...rest} = validConfig;
for(const c of checks) {
namedRules = extractNamedRules(c.rules, namedRules);
namedActions = extractNamedActions(c.actions, namedActions);
}
const structuredChecks: CheckStructuredJson[] = [];
for(const c of checks) {
const strongRules = insertNamedRules(c.rules, namedRules);
const strongActions = insertNamedActions(c.actions, namedActions);
const strongCheck = {...c, rules: strongRules, actions: strongActions} as CheckStructuredJson;
structuredChecks.push(strongCheck);
}
managerOptions = rest;
for (const jCheck of checks) {
for (const jCheck of structuredChecks) {
if (jCheck.kind === 'comment') {
commentChecks.push(new CommentCheck({...jCheck, logger: this.logger}));
} else if (jCheck.kind === 'submission') {
@@ -63,3 +85,100 @@ export class ConfigBuilder {
return [subChecks, commentChecks, managerOptions];
}
}
export const extractNamedRules = (rules: Array<RuleSetJson | RuleJson>, namedRules: Map<string, RuleObjectJson> = new Map()): Map<string, RuleObjectJson> => {
//const namedRules = new Map();
for (const r of rules) {
let rulesToAdd: RuleObjectJson[] = [];
if ((typeof r === 'object')) {
if ((r as RuleObjectJson).kind !== undefined) {
// itsa rule
const rule = r as RuleObjectJson;
if (rule.name !== undefined) {
rulesToAdd.push(rule);
}
} else {
const ruleSet = r as RuleSetJson;
const nestedNamed = extractNamedRules(ruleSet.rules);
rulesToAdd = [...nestedNamed.values()];
}
for (const rule of rulesToAdd) {
const name = rule.name as string;
const normalName = name.toLowerCase();
const {name: n, ...rest} = rule;
const ruleNoName = {...rest};
if (namedRules.has(normalName)) {
const {name: nn, ...ruleRest} = namedRules.get(normalName) as RuleObjectJson;
if (!deepEqual(ruleRest, ruleNoName)) {
throw new Error(`Rule names must be unique (case-insensitive). Conflicting name: ${name}`);
}
} else {
namedRules.set(normalName, rule);
}
}
}
}
return namedRules;
}
export const insertNamedRules = (rules: Array<RuleSetJson | RuleJson>, namedRules: Map<string, RuleObjectJson> = new Map()): Array<RuleSetObjectJson | RuleObjectJson> => {
const strongRules: Array<RuleSetObjectJson | RuleObjectJson> = [];
for (const r of rules) {
if (typeof r === 'string') {
const foundRule = namedRules.get(r.toLowerCase());
if (foundRule === undefined) {
throw new Error(`No named Rule with the name ${r} was found`);
}
strongRules.push(foundRule);
} else if (isRuleSetJSON(r)) {
const {rules: sr, ...rest} = r;
const setRules = insertNamedRules(sr, namedRules);
const strongSet = {rules: setRules, ...rest} as RuleSetObjectJson;
strongRules.push(strongSet);
} else {
strongRules.push(r);
}
}
return strongRules;
}
export const extractNamedActions = (actions: Array<ActionJson>, namedActions: Map<string, ActionObjectJson> = new Map()): Map<string, ActionObjectJson> => {
for (const a of actions) {
if(!(typeof a === 'string')) {
if (isActionJson(a) && a.name !== undefined) {
const normalName = a.name.toLowerCase();
const {name: n, ...rest} = a;
const actionNoName = {...rest};
if (namedActions.has(normalName)) {
// @ts-ignore
const {name: nn, ...aRest} = namedActions.get(normalName) as ActionObjectJson;
if (!deepEqual(aRest, actionNoName)) {
throw new Error(`Actions names must be unique (case-insensitive). Conflicting name: ${a.name}`);
}
} else {
namedActions.set(normalName, a);
}
}
}
}
return namedActions;
}
export const insertNamedActions = (actions: Array<ActionJson>, namedActions: Map<string, ActionObjectJson> = new Map()): Array<ActionObjectJson> => {
const strongActions: Array<ActionObjectJson> = [];
for (const a of actions) {
if (typeof a === 'string') {
const foundAction = namedActions.get(a.toLowerCase());
if (foundAction === undefined) {
throw new Error(`No named Action with the name ${a} was found`);
}
strongActions.push(foundAction);
}else {
strongActions.push(a);
}
}
return strongActions;
}

View File

@@ -1,5 +1,5 @@
import {CheckJSONConfig} from "./Check";
import {ManagerOptions} from "./Subreddit/Manager";
import {CheckJson} from "./Check";
import {ManagerOptions} from "./Common/interfaces";
export interface JSONConfig extends ManagerOptions {
/**
@@ -12,5 +12,5 @@ export interface JSONConfig extends ManagerOptions {
* When a check "passes", and actions are performed, then all subsequent checks are skipped.
* @minItems 1
* */
checks: CheckJSONConfig[]
checks: CheckJson[]
}

View File

@@ -1,15 +1,12 @@
import {IRule, Triggerable, Rule, RuleJSONConfig, RuleResult} from "./index";
import {Comment, Submission} from "snoowrap";
import {ruleFactory} from "./RuleFactory";
import {RecentActivityRuleJSONConfig} from "./RecentActivityRule";
import {RepeatActivityJSONConfig} from "./SubmissionRule/RepeatActivityRule";
import {createLabelledLogger, determineNewResults, findResultByPremise, loggerMetaShuffle} from "../util";
import {createLabelledLogger, loggerMetaShuffle} from "../util";
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';
import {AttributionJSONConfig} from "./SubmissionRule/AttributionRule";
import {RuleJson, RuleObjectJson} from "../Common/types";
const ajv = new Ajv();
@@ -84,12 +81,20 @@ export interface RuleSetOptions extends IRuleSet {
}
/**
* 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)
* @see {isRuleSetConfig} ts-auto-guard:type-guard
* A RuleSet is a "nested" set of `Rule` objects that can be used to create more complex AND/OR behavior. Think of the outcome of a `RuleSet` as the result of all of its run `Rule` objects (based on `condition`)
* */
export interface RuleSetJSONConfig extends IRuleSet {
export interface RuleSetJson extends JoinCondition {
/**
* Can be `Rule` or the `name` of any **named** `Rule` in your subreddit's configuration
* @minItems 1
* */
rules: Array<RecentActivityRuleJSONConfig | RepeatActivityJSONConfig | AuthorRuleJSONConfig | AttributionJSONConfig>
rules: Array<RuleJson>
}
export interface RuleSetObjectJson extends RuleSetJson {
rules: Array<RuleObjectJson>
}
export const isRuleSetJSON = (obj: object): obj is RuleSetJson => {
return (obj as RuleSetJson).rules !== undefined;
}

View File

@@ -67,6 +67,7 @@ export abstract class Rule implements IRule, Triggerable {
this.logger.debug('Starting');
const existingResult = findResultByPremise(this.getPremise(), existingResults);
if (existingResult) {
this.logger.debug('Returning existing result');
return Promise.resolve([existingResult.triggered, [{...existingResult, name: this.name}]]);
}
if (this.authors.include !== undefined && this.authors.include.length > 0) {
@@ -194,7 +195,6 @@ export interface IRule {
authors?: AuthorOptions
}
/** @see {isRuleConfig} ts-auto-guard:type-guard */
export interface RuleJSONConfig extends IRule {
/**
* The kind of rule to run

View File

@@ -145,15 +145,6 @@
"minItems": 1,
"type": "array"
},
"includeInTotal": {
"default": "submissions",
"description": "What activities to use for total count when determining what percentage an attribution comprises\n\nEX:\n\nAuthor has 100 activities, 40 are submissions and 60 are comments\n\n* If `submission` then if 10 submission are for Youtube Channel A then percentage => 10/40 = 25%\n* If `all` then if 10 submission are for Youtube Channel A then percentage => 10/100 = 10%",
"enum": [
"all",
"submissions"
],
"type": "string"
},
"includeSelf": {
"default": false,
"description": "Include reddit `self.*` domains in aggregation?\n\nSelf-posts are aggregated under the domain `self.[subreddit]`. If you wish to include these domains in aggregation set this to `true`",
@@ -192,7 +183,6 @@
"criteriaJoin",
"include",
"exclude",
"includeInTotal",
"lookAt",
"aggregateMediaDomains",
"includeSelf",
@@ -323,27 +313,29 @@
],
"type": "object"
},
"CheckJSONConfig": {
"description": "An object consisting of Rules (tests) and Actions to perform if Rules are triggered",
"CheckJson": {
"properties": {
"actions": {
"description": "The actions to run after the check is successfully triggered. ALL actions will run in the order they are listed",
"description": "The `Actions` to run after the check is successfully triggered. ALL `Actions` will run in the order they are listed\n\n Can be `Action` or the `name` of any **named** `Action` in your subreddit's configuration",
"items": {
"anyOf": [
{
"$ref": "#/definitions/FlairActionJSONConfig"
"$ref": "#/definitions/FlairActionJson"
},
{
"$ref": "#/definitions/CommentActionJSONConfig"
"$ref": "#/definitions/CommentActionJson"
},
{
"$ref": "#/definitions/LockActionJSONConfig"
"$ref": "#/definitions/ReportActionJson"
},
{
"$ref": "#/definitions/RemoveActionJSONConfig"
"$ref": "#/definitions/LockActionJson"
},
{
"$ref": "#/definitions/ReportActionJSONConfig"
"$ref": "#/definitions/RemoveActionJson"
},
{
"type": "string"
}
]
},
@@ -352,7 +344,7 @@
},
"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.",
"description": "Under what condition should a set of run `Rule` objects be considered \"successful\"?\n\nIf `OR` then **any** triggered `Rule` object results in success.\n\nIf `AND` then **all** `Rule` objects must be triggered to result in success.",
"enum": [
"AND",
"OR"
@@ -376,7 +368,7 @@
"type": "string"
},
"rules": {
"description": "Rules are run in the order found in configuration. Can be Rules or RuleSets",
"description": "A list of Rules to run. If `Rule` objects are triggered based on `condition` then `Actions` will be performed.\n\nCan be `Rule`, `RuleSet`, or the `name` of any **named** `Rule` in your subreddit's configuration",
"items": {
"anyOf": [
{
@@ -392,7 +384,10 @@
"$ref": "#/definitions/AttributionJSONConfig"
},
{
"$ref": "#/definitions/RuleSetJSONConfig"
"$ref": "#/definitions/RuleSetJson"
},
{
"type": "string"
}
]
},
@@ -416,7 +411,7 @@
],
"type": "object"
},
"CommentActionJSONConfig": {
"CommentActionJson": {
"description": "Reply to the Activity. For a submission the reply will be a top-level comment.",
"properties": {
"content": {
@@ -509,7 +504,7 @@
],
"type": "object"
},
"FlairActionJSONConfig": {
"FlairActionJson": {
"description": "Flair the Submission",
"properties": {
"css": {
@@ -548,7 +543,7 @@
],
"type": "object"
},
"LockActionJSONConfig": {
"LockActionJson": {
"description": "Lock the Activity",
"properties": {
"kind": {
@@ -721,7 +716,7 @@
],
"type": "object"
},
"RemoveActionJSONConfig": {
"RemoveActionJson": {
"description": "Remove the Activity",
"properties": {
"kind": {
@@ -872,7 +867,7 @@
],
"type": "object"
},
"ReportActionJSONConfig": {
"ReportActionJson": {
"description": "Report the Activity",
"properties": {
"content": {
@@ -907,12 +902,12 @@
],
"type": "object"
},
"RuleSetJSONConfig": {
"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)",
"RuleSetJson": {
"description": "A RuleSet is a \"nested\" set of `Rule` objects that can be used to create more complex AND/OR behavior. Think of the outcome of a `RuleSet` as the result of all of its run `Rule` objects (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.",
"description": "Under what condition should a set of run `Rule` objects be considered \"successful\"?\n\nIf `OR` then **any** triggered `Rule` object results in success.\n\nIf `AND` then **all** `Rule` objects must be triggered to result in success.",
"enum": [
"AND",
"OR"
@@ -920,6 +915,7 @@
"type": "string"
},
"rules": {
"description": "Can be `Rule` or the `name` of any **named** `Rule` in your subreddit's configuration",
"items": {
"anyOf": [
{
@@ -933,6 +929,9 @@
},
{
"$ref": "#/definitions/AttributionJSONConfig"
},
{
"type": "string"
}
]
},
@@ -989,7 +988,7 @@
"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": {
"$ref": "#/definitions/CheckJSONConfig"
"$ref": "#/definitions/CheckJson"
},
"minItems": 1,
"type": "array"

View File

@@ -1,6 +1,217 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"anyOf": [
{
"$ref": "#/definitions/RecentActivityRuleJSONConfig"
},
{
"$ref": "#/definitions/RepeatActivityJSONConfig"
},
{
"$ref": "#/definitions/AuthorRuleJSONConfig"
},
{
"$ref": "#/definitions/AttributionJSONConfig"
},
{
"type": "string"
}
],
"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"
},
"AttributionCriteria": {
"properties": {
"minActivityCount": {
"default": 5,
"description": "The minimum number of activities (activities defined in `includeInTotal`) that must exist for this criteria to run",
"type": "number"
},
"name": {
"type": "string"
},
"threshold": {
"default": "10%",
"description": "The number or percentage to trigger this rule at\n\n* If `threshold` is a `number` then it is the absolute number of attribution instances to trigger at\n* If `threshold` is a `string` with percentage (EX `40%`) then it is the percentage of the total (see `lookAt`) this attribution must reach to trigger",
"type": [
"string",
"number"
]
},
"thresholdOn": {
"default": "all",
"description": "What activities to use for total count when determining what percentage an attribution comprises\n\nEX:\n\nAuthor has 100 activities, 40 are submissions and 60 are comments\n\n* If `submission` then if 10 submission are for Youtube Channel A then percentage => 10/40 = 25%\n* If `all` then if 10 submission are for Youtube Channel A then percentage => 10/100 = 10%",
"enum": [
"all",
"submissions"
],
"type": "string"
},
"window": {
"anyOf": [
{
"$ref": "#/definitions/DurationObject"
},
{
"$ref": "#/definitions/ActivityWindowCriteria"
},
{
"type": [
"string",
"number"
]
}
]
}
},
"propertyOrder": [
"threshold",
"window",
"thresholdOn",
"minActivityCount",
"name"
],
"required": [
"threshold",
"window"
],
"type": "object"
},
"AttributionJSONConfig": {
"description": "Aggregates all of the domain/media accounts attributed to an author's Submission history. If any domain is over the threshold the rule is triggered\n\nAvailable data for [Action templating](https://github.com/FoxxMD/reddit-context-bot#action-templating):\n\n```\ncount => Total number of repeat Submissions\nthreshold => The threshold you configured for this Rule to trigger\nurl => Url of the submission that triggered the rule\n```",
"properties": {
"aggregateMediaDomains": {
"default": false,
"description": "Should the rule aggregate recognized media domains into the parent domain?\n\nSubmissions to major media domains (youtube, vimeo) can be identified by individual Channel/Author...\n\n* If `false` then aggregate will occur at the channel level IE Youtube Channel A (2 counts), Youtube Channel B (3 counts)\n* If `true` then then aggregation will occur at the domain level IE youtube.com (5 counts)",
"type": "boolean"
},
"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
},
"criteria": {
"description": "A list threshold-window values to test attribution against\n\nIf none is provided the default set used is:\n\n```\nthreshold: 10%\nwindow: 100\n```",
"items": {
"$ref": "#/definitions/AttributionCriteria"
},
"minItems": 1,
"type": "array"
},
"criteriaJoin": {
"description": "* If `OR` then any set of AttributionCriteria that produce an Attribution over the threshold will trigger the rule.\n* If `AND` then all AttributionCriteria sets must product an Attribution over the threshold to trigger the rule.",
"enum": [
"AND",
"OR"
],
"type": "string"
},
"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"
},
"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"
},
"includeSelf": {
"default": false,
"description": "Include reddit `self.*` domains in aggregation?\n\nSelf-posts are aggregated under the domain `self.[subreddit]`. If you wish to include these domains in aggregation set this to `true`",
"type": "boolean"
},
"kind": {
"description": "The kind of rule to run",
"enum": [
"attribution"
],
"type": "string"
},
"lookAt": {
"default": "all",
"description": "Determines which type of attribution to look at\n\n* If `media` then only the author's submission history which reddit recognizes as media (youtube, vimeo, etc.) will be considered\n* If `all` then all domains (EX youtube.com, twitter.com) from the author's submission history will be considered",
"enum": [
"all",
"media"
],
"type": "string"
},
"name": {
"description": "An optional, but highly recommended, friendly name for this rule. If not present will default to `kind`.\n\nCan only contain letters, numbers, underscore, spaces, and dashes\n\nname is used to reference Rule result data during Action content templating. See CommentAction or ReportAction for more details.",
"pattern": "^[a-zA-Z]([\\w -]*[\\w])?$",
"type": "string"
},
"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"
}
},
"propertyOrder": [
"kind",
"criteria",
"criteriaJoin",
"include",
"exclude",
"lookAt",
"aggregateMediaDomains",
"includeSelf",
"useSubmissionAsReference",
"name",
"authors"
],
"required": [
"kind"
],
"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",
@@ -69,39 +280,340 @@
"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": [
"attribution",
"author",
"recentActivity",
"repeatActivity"
"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": "An optional, but highly recommended, friendly name for this rule. If not present will default to `kind`.\n\nCan only contain letters, numbers, underscore, spaces, and dashes\n\nname is used to reference Rule result data during Action content templating. See CommentAction or ReportAction for more details.",
"pattern": "^[a-zA-Z]([\\w -]*[\\w])?$",
"type": "string"
}
},
"propertyOrder": [
"kind",
"include",
"exclude",
"name",
"authors"
],
"type": "string"
"required": [
"exclude",
"include",
"kind"
],
"type": "object"
},
"name": {
"description": "An optional, but highly recommended, friendly name for this rule. If not present will default to `kind`.\n\nCan only contain letters, numbers, underscore, spaces, and dashes\n\nname is used to reference Rule result data during Action content templating. See CommentAction or ReportAction for more details.",
"pattern": "^[a-zA-Z]([\\w -]*[\\w])?$",
"type": "string"
"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\n\nAvailable data for [Action templating](https://github.com/FoxxMD/reddit-context-bot#action-templating):\n\n```\nsummary => comma-deliminated list of subreddits that hit the threshold and their count EX subredditA(1), subredditB(4),...\nsubCount => Total number of subreddits that hit the threshold\ntotalCount => Total number of all activity occurrences in subreddits\n```",
"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": "An optional, but highly recommended, friendly name for this rule. If not present will default to `kind`.\n\nCan only contain letters, numbers, underscore, spaces, and dashes\n\nname is used to reference Rule result data during Action content templating. See CommentAction or ReportAction for more details.",
"pattern": "^[a-zA-Z]([\\w -]*[\\w])?$",
"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"
},
"RepeatActivityJSONConfig": {
"description": "Checks a user's history for Submissions with identical content\n\nAvailable data for [Action templating](https://github.com/FoxxMD/reddit-context-bot#action-templating):\n\n```\ncount => Total number of repeat Submissions\nthreshold => The threshold you configured for this Rule to trigger\nurl => Url of the submission that triggered the rule\n```",
"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": [
"repeatActivity"
],
"type": "string"
},
"lookAt": {
"default": "all",
"description": "If present determines which activities to consider for gapAllowance.\n\n* If `submissions` then only the author's submission history is considered IE gapAllowance = 2 ===> can have gap of two submissions between repeats\n* If `all` then the author's entire history (submissions/comments) is considered IE gapAllowance = 2 ===> can only have gap of two activities (submissions or comments) between repeats",
"enum": [
"all",
"submissions"
],
"type": "string"
},
"name": {
"description": "An optional, but highly recommended, friendly name for this rule. If not present will default to `kind`.\n\nCan only contain letters, numbers, underscore, spaces, and dashes\n\nname is used to reference Rule result data during Action content templating. See CommentAction or ReportAction for more details.",
"pattern": "^[a-zA-Z]([\\w -]*[\\w])?$",
"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",
"lookAt",
"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"
}
},
"propertyOrder": [
"kind",
"name",
"authors"
],
"required": [
"kind"
],
"type": "object"
}
}

View File

@@ -145,15 +145,6 @@
"minItems": 1,
"type": "array"
},
"includeInTotal": {
"default": "submissions",
"description": "What activities to use for total count when determining what percentage an attribution comprises\n\nEX:\n\nAuthor has 100 activities, 40 are submissions and 60 are comments\n\n* If `submission` then if 10 submission are for Youtube Channel A then percentage => 10/40 = 25%\n* If `all` then if 10 submission are for Youtube Channel A then percentage => 10/100 = 10%",
"enum": [
"all",
"submissions"
],
"type": "string"
},
"includeSelf": {
"default": false,
"description": "Include reddit `self.*` domains in aggregation?\n\nSelf-posts are aggregated under the domain `self.[subreddit]`. If you wish to include these domains in aggregation set this to `true`",
@@ -192,7 +183,6 @@
"criteriaJoin",
"include",
"exclude",
"includeInTotal",
"lookAt",
"aggregateMediaDomains",
"includeSelf",
@@ -608,11 +598,11 @@
"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)",
"description": "A RuleSet is a \"nested\" set of `Rule` objects that can be used to create more complex AND/OR behavior. Think of the outcome of a `RuleSet` as the result of all of its run `Rule` objects (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.",
"description": "Under what condition should a set of run `Rule` objects be considered \"successful\"?\n\nIf `OR` then **any** triggered `Rule` object results in success.\n\nIf `AND` then **all** `Rule` objects must be triggered to result in success.",
"enum": [
"AND",
"OR"
@@ -620,6 +610,7 @@
"type": "string"
},
"rules": {
"description": "Can be `Rule` or the `name` of any **named** `Rule` in your subreddit's configuration",
"items": {
"anyOf": [
{
@@ -633,6 +624,9 @@
},
{
"$ref": "#/definitions/AttributionJSONConfig"
},
{
"type": "string"
}
]
},

View File

@@ -11,24 +11,11 @@ import {CommentStream, SubmissionStream} from "snoostorm";
import pEvent from "p-event";
import {RuleResult} from "../Rule";
import {ConfigBuilder} from "../ConfigBuilder";
import {PollingOptions} from "../Common/interfaces";
import {ManagerOptions, PollingOptions} from "../Common/interfaces";
import Submission from "snoowrap/dist/objects/Submission";
import {itemContentPeek} from "../Utils/SnoowrapUtils";
import dayjs from "dayjs";
export interface ManagerOptions {
polling?: PollingOptions
/**
* 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
* */
heartbeatInterval?: number
/**
* When Reddit API limit remaining reaches this number context bot will start warning on every poll interval
* @default 250
* */
apiLimitWarning?: number
}
export class Manager {
subreddit: Subreddit;
client: Snoowrap;

View File

@@ -30,7 +30,19 @@ export const loggerMetaShuffle = (logger: Logger, newLeaf: (string | undefined |
let longestLabel = 3;
// @ts-ignore
export const defaultFormat = printf(({level, message, label = 'App', labels = [], leaf, itemId, timestamp, [SPLAT]: splatObj, stack, ...rest}) => {
export const defaultFormat = printf(({
level,
message,
label = 'App',
labels = [],
leaf,
itemId,
timestamp,
// @ts-ignore
[SPLAT]: splatObj,
stack,
...rest
}) => {
let stringifyValue = splatObj !== undefined ? jsonStringify(splatObj) : '';
if (label.length > longestLabel) {
longestLabel = label.length;
@@ -48,9 +60,9 @@ export const defaultFormat = printf(({level, message, label = 'App', labels = []
}
let labelContent = `[${label.padEnd(longestLabel)}]`;
if(labels.length > 0 || (leaf !== null && leaf !== undefined)) {
if (labels.length > 0 || (leaf !== null && leaf !== undefined)) {
let nodes = labels;
if(leaf !== null) {
if (leaf !== null) {
nodes.push(leaf);
}
//labelContent = `${labels.slice(0, labels.length).map((x: string) => `[${x}]`).join(' ')}`
@@ -173,8 +185,12 @@ export const determineNewResults = (existing: RuleResult[], val: RuleResult | Ru
return newResults;
}
export const mergeArr = (objValue: [], srcValue: []): (any[]|undefined) => {
export const mergeArr = (objValue: [], srcValue: []): (any[] | undefined) => {
if (Array.isArray(objValue)) {
return objValue.concat(srcValue);
}
}