More annotation improvements

This commit is contained in:
FoxxMD
2021-06-03 14:32:56 -04:00
parent 81e95e2cc8
commit c632efbbb2
17 changed files with 608 additions and 188 deletions

View File

@@ -8,7 +8,7 @@
"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",
"schema": "typescript-json-schema tsconfig.json JSONConfig --out src/Schema/schema.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

@@ -3,6 +3,7 @@ import Snoowrap, {Comment} from "snoowrap";
import Submission from "snoowrap/dist/objects/Submission";
import dayjs, {Dayjs} from "dayjs";
import {renderContent} from "../Utils/SnoowrapUtils";
import {RichContent} from "../Common/interfaces";
export const WIKI_DESCRIM = 'wiki:';
@@ -58,20 +59,27 @@ export class CommentAction extends Action {
}
}
export interface CommentActionConfig {
export interface CommentActionConfig extends RichContent {
/**
* Content is interpreted as reddit-flavored Markdown. If value starts with 'wiki:' then the proceeding value will be use to get a wiki page
* EX wiki:botconfig/mybot ==> try to get http://reddit.com/mySubredditExample/wiki/botconfig/mybot
* */
content: string,
* Lock the comment after creation?
* */
lock?: boolean,
/**
* Stick the comment after creation?
* */
sticky?: boolean,
/**
* Distinguish the comment after creation?
* */
distinguish?: boolean,
}
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 {
}

View File

@@ -16,6 +16,9 @@ export interface LockActionConfig extends ActionConfig {
}
/**
* Lock the Activity
* */
export interface LockActionJSONConfig extends LockActionConfig, ActionJSONConfig {
}

View File

@@ -14,6 +14,9 @@ export interface RemoveActionConfig extends ActionConfig {
}
/**
* Remove the Activity
* */
export interface RemoveActionJSONConfig extends RemoveActionConfig, ActionJSONConfig {
}

View File

@@ -17,13 +17,19 @@ export class ReportAction extends Action {
}
}
export interface ReportActionConfig{
export interface ReportActionConfig {
/**
* The text of the report
* */
content: string,
}
export interface ReportActionOptions extends ReportActionConfig,ActionOptions {
export interface ReportActionOptions extends ReportActionConfig, ActionOptions {
}
/**
* Report the Activity
* */
export interface ReportActionJSONConfig extends ReportActionConfig, ActionJSONConfig {
}

View File

@@ -24,6 +24,10 @@ export class FlairAction extends Action {
}
}
/**
* @minProperties 1
* @additionalProperties false
* */
export interface FlairActionOptions extends SubmissionActionConfig {
/**
* The text of the flair to apply
@@ -36,7 +40,7 @@ export interface FlairActionOptions extends SubmissionActionConfig {
}
/**
* text and css cannot both be empty
* Flair the Submission
* */
export interface FlairActionJSONConfig extends FlairActionOptions, ActionJSONConfig {

View File

@@ -17,12 +17,13 @@ import {AuthorRuleJSONConfig} from "../Rule/AuthorRule";
import {ReportActionJSONConfig} from "../Action/ReportAction";
import {LockActionJSONConfig} from "../Action/LockAction";
import {RemoveActionJSONConfig} from "../Action/RemoveAction";
import {JoinCondition, JoinOperands} from "../Common/interfaces";
export class Check implements ICheck {
actions: Action[] = [];
description?: string;
name: string;
ruleJoin: "OR" | "AND";
condition: JoinOperands;
rules: Array<RuleSet | Rule> = [];
logger: Logger;
@@ -30,9 +31,9 @@ export class Check implements ICheck {
const {
name,
description,
ruleJoin = 'AND',
rules,
actions,
condition = 'AND',
rules = [],
actions = [],
} = options;
if (options.logger !== undefined) {
@@ -44,7 +45,7 @@ export class Check implements ICheck {
this.name = name;
this.description = description;
this.ruleJoin = ruleJoin;
this.condition = condition;
for (const r of rules) {
if (r instanceof Rule || r instanceof RuleSet) {
this.rules.push(r);
@@ -82,10 +83,10 @@ export class Check implements ICheck {
}
runOne = true;
if (passed) {
if (this.ruleJoin === 'OR') {
if (this.condition === 'OR') {
return [true, allResults];
}
} else if (this.ruleJoin === 'AND') {
} else if (this.condition === 'AND') {
return [false, allResults];
}
}
@@ -102,16 +103,12 @@ export class Check implements ICheck {
}
}
export interface ICheck {
export interface ICheck extends JoinCondition {
/**
* A friendly name for this check (highly recommended) -- EX "repeatCrosspostReport"
* */
name: string,
description?: string,
/**
* Under what condition should a check's rules be "successful"? If 'OR' then ANY triggered rule will cause actions to run. If 'AND' then ALL rules must be triggered for actions to run.
* */
ruleJoin?: 'OR' | 'AND',
}
export interface CheckOptions extends ICheck {

View File

@@ -1,11 +1,10 @@
import {DurationUnitsObjectType} from "dayjs/plugin/duration";
/**
* An ISO 8601 Duration
* @pattern ^(-?)P(?=\d|T\d)(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)([DW]))?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$
* */
export type ISO8601 = string;
export type ActivityWindowType = ISO8601 | number | ActivityWindowCriteria;
export type ActivityWindowType = Duration | number | ActivityWindowCriteria;
export type Duration = ISO8601 | DurationObject;
/**
* 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
@@ -18,15 +17,22 @@ export interface ActivityWindowCriteria {
* */
count?: number,
/**
* An ISO 8601 duration or Day.js duration object
* An ISO 8601 duration or Day.js duration object.
*
* The duration will be subtracted from the time when the rule is run to create a time range like this:
*
* endTime = NOW <----> startTime = (NOW - duration)
*
* EX endTime = 3:00PM <----> startTime = (NOW - 15 minutes) = 2:45PM -- so look for activities between 2:45PM and 3:00PM
* @examples ["PT1M", {"minutes": 15}]
* */
duration?: ISO8601 | DurationObject
duration?: Duration
}
/**
* A Day.js duration object
* @see https://day.js.org/docs/en/durations/creating
*
* https://day.js.org/docs/en/durations/creating
* @minProperties 1
* @additionalProperties false
* */
@@ -63,10 +69,106 @@ export const windowExample: ActivityWindowType[] = [
export interface ActivityWindow {
/**
* Criteria for defining what set of activities should be considered. See ActivityWindowCriteria for descriptions of what different data types will do
* //@examples require('./interfaces.ts').windowExample
* Criteria for defining what set of activities should be considered.
*
* The value of this property may be either count OR duration -- to use both write it as an ActivityWindowCriteria
*
* See ActivityWindowCriteria for descriptions of what count/duration do
* @examples require('./interfaces.ts').windowExample
* @default 15
*/
window?: ActivityWindowType,
}
// export type AtLeastOne<T> = { [K in keyof T]: Pick<T, K> }[keyof T]
export interface ReferenceSubmission {
/**
* 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.
* @default true
* */
useSubmissionAsReference?: boolean,
}
export interface RichContent {
/**
* The Content to submit for this Action. Content is interpreted as reddit-flavored Markdown.
*
* If value starts with 'wiki:' then the proceeding value will be used to get a wiki page
*
* EX "wiki:botconfig/mybot" tries to get https://reddit.com/mySubredditExample/wiki/botconfig/mybot
*
* EX "this is plain text"
*
* EX "this is **bold** markdown text"
*
* @examples ["this is plain text", "this is **bold** markdown text", "wiki:botconfig/acomment" ]
* */
content: string,
}
/**
* A list of subreddits (case-insensitive) to look for. Do not include "r/" prefix.
*
* EX to match against /r/mealtimevideos and /r/askscience use ["mealtimevideos","askscience"]
* @examples ["mealtimevideos","askscience"]
* @minItems 1
* */
export type SubredditList = string[];
export interface SubredditCriteria {
subreddits: SubredditList
}
export type JoinOperands = 'OR' | 'AND';
export interface JoinCondition {
/**
* Under what condition should a set of rules be considered "successful"?
*
* If "OR" then ANY triggered rule results in success.
*
* If "AND" then ALL rules must be triggered to result in success.
*
* @default "AND"
* */
condition?: JoinOperands,
}
/**
* You may specify polling options independently for submissions/comments
* */
export interface PollingOptions {
/**
* Polling options for submission events
* */
submissions?: {
/**
* The number of submissions to pull from /r/subreddit/new on every request
* @default 10
* */
limit?: number,
/**
* Amount of time to wait between requests to /r/subreddit/new
*
* @format milliseconds
* @default 10000
* */
interval?: number,
},
/**
* Polling options for comment events
* */
comments?: {
/**
* The number of new comments to pull on every request
* @default 10
* */
limit?: number,
/**
* Amount of time to wait between requests for new comments
*
* @format milliseconds
* @default 10000
* */
interval?: number,
}
}

View File

@@ -1,9 +1,17 @@
import {CheckJSONConfig} from "./Check";
import {PollingOptions} from "./Common/interfaces";
export interface JSONConfig {
/**
* A list of all the checks that should be run for a subreddit. Checks are split into two lists -- submission or comment -- based on kind and run independently. Checks in each list are run in the order found in the configuration. When a check "passes" and actions are performed any subsequent checks are skipped.
* A list of all the checks that should be run for a subreddit.
*
* Checks are split into two lists -- submission or comment -- based on kind and run independently.
*
* Checks in each list are run in the order found in the configuration.
*
* When a check "passes", and actions are performed, then all subsequent checks are skipped.
* @minItems 1
* */
checks: CheckJSONConfig[]
polling?: PollingOptions
}

View File

@@ -1,11 +1,22 @@
import {Author, AuthorOptions, IAuthor, Rule, RuleJSONConfig, RuleOptions, RuleResult} from "./index";
import {Author, AuthorOptions, AuthorCriteria, Rule, RuleJSONConfig, RuleOptions, RuleResult} from "./index";
import {Comment} from "snoowrap";
import Submission from "snoowrap/dist/objects/Submission";
import {testAuthorCriteria} from "../Utils/SnoowrapUtils";
export interface AuthorRuleConfig extends AuthorOptions {
include: IAuthor[];
exclude: IAuthor[];
/**
* Checks the author of the Activity against AuthorCriteria. This differs from a Rule's AuthorOptions as this is a full Rule and will only pass/fail, not skip.
* @minProperties 1
* @additionalProperties false
* */
export interface AuthorRuleConfig {
/**
* Will "pass" if any set of AuthorCriteria passes
* */
include: AuthorCriteria[];
/**
* Only runs if include is not present. Will "pass" if any of set of the AuthorCriteria does not pass
* */
exclude: AuthorCriteria[];
}
export interface AuthorRuleOptions extends AuthorRuleConfig, RuleOptions {
@@ -13,12 +24,12 @@ export interface AuthorRuleOptions extends AuthorRuleConfig, RuleOptions {
}
export interface AuthorRuleJSONConfig extends AuthorRuleConfig, RuleJSONConfig {
kind: 'author'
}
export class AuthorRule extends Rule {
include: IAuthor[] = [];
exclude: IAuthor[] = [];
include: AuthorCriteria[] = [];
exclude: AuthorCriteria[] = [];
constructor(options: AuthorRuleOptions) {
super(options);

View File

@@ -3,25 +3,31 @@ import {Comment, VoteableContent} from "snoowrap";
import Submission from "snoowrap/dist/objects/Submission";
import {getAuthorActivities, getAuthorComments, getAuthorSubmissions} from "../Utils/SnoowrapUtils";
import {parseUsableLinkIdentifier} from "../util";
import {ActivityWindow, ActivityWindowCriteria, ActivityWindowType} from "../Common/interfaces";
import {
ActivityWindow,
ActivityWindowCriteria,
ActivityWindowType,
ReferenceSubmission,
SubredditCriteria
} from "../Common/interfaces";
const parseLink = parseUsableLinkIdentifier();
export class RecentActivityRule extends Rule {
window: ActivityWindowType;
thresholds: SubThreshold[];
usePostAsReference: boolean;
useSubmissionAsReference: boolean;
lookAt?: 'comments' | 'submissions';
constructor(options: RecentActivityRuleOptions) {
super(options);
const {
window = 15,
usePostAsReference = true,
useSubmissionAsReference = true,
lookAt,
} = options || {};
this.lookAt = lookAt;
this.usePostAsReference = usePostAsReference;
this.useSubmissionAsReference = useSubmissionAsReference;
this.window = window;
this.thresholds = options.thresholds;
}
@@ -34,7 +40,7 @@ export class RecentActivityRule extends Rule {
return {
window: this.window,
thresholds: this.thresholds,
usePostAsReference: this.usePostAsReference,
useSubmissionAsReference: this.useSubmissionAsReference,
lookAt: this.lookAt
}
}
@@ -56,7 +62,7 @@ export class RecentActivityRule extends Rule {
let viableActivity = activities;
if (this.usePostAsReference) {
if (this.useSubmissionAsReference) {
if (!(item instanceof Submission)) {
this.logger.debug('Cannot use post as reference because triggered item is not a Submission');
} else if (item.is_self) {
@@ -101,43 +107,35 @@ export class RecentActivityRule extends Rule {
}
}
export interface SubThreshold {
/**
* A list of subreddits (case-insensitive) to look for
* @minItems 1
* */
subreddits: string[],
export interface SubThreshold extends SubredditCriteria {
/**
* The number of activities in each subreddit from the list that will trigger this rule
* @default 1
* @minimum 1
* */
count?: number,
}
interface RecentActivityConfig extends ActivityWindow {
/**
* 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.
* */
usePostAsReference?: boolean,
interface RecentActivityConfig extends ActivityWindow, ReferenceSubmission {
/**
* If present restricts the activities that are considered for count from SubThreshold
* */
lookAt?: 'comments' | 'submissions',
/**
* A list of subreddits/count criteria that may trigger this rule. ANY SubThreshold will trigger this rule.
* @minItems 1
* */
thresholds: SubThreshold[],
/**
* @default 15
* */
window: ActivityWindowType;
}
export interface RecentActivityRuleOptions extends RecentActivityConfig, RuleOptions {
}
/**
* Checks a user's history for any Activity (Submission/Comment) in the subreddits specified in thresholds
* */
export interface RecentActivityRuleJSONConfig extends RecentActivityConfig, RuleJSONConfig {
kind: 'recentActivity'
}
export default RecentActivityRule;

View File

@@ -7,20 +7,22 @@ import {RepeatSubmissionJSONConfig} from "./SubmissionRule/RepeatSubmissionRule"
import {createLabelledLogger, determineNewResults, findResultByPremise, loggerMetaShuffle} from "../util";
import {Logger} from "winston";
import {AuthorRuleJSONConfig} from "./AuthorRule";
import {JoinCondition, JoinOperands} from "../Common/interfaces";
export class RuleSet implements IRuleSet, Triggerable {
rules: Rule[] = [];
condition: 'OR' | 'AND';
condition: JoinOperands;
logger: Logger;
constructor(options: RuleSetOptions) {
if (options.logger !== undefined) {
this.logger = options.logger.child(loggerMetaShuffle(options.logger, 'Rule Set'));
const {logger, condition = 'AND', rules = []} = options;
if (logger !== undefined) {
this.logger = logger.child(loggerMetaShuffle(logger, 'Rule Set'));
} else {
this.logger = createLabelledLogger('Rule Set');
}
this.condition = options.condition;
for (const r of options.rules) {
this.condition = condition;
for (const r of rules) {
if (r instanceof Rule) {
this.rules.push(r);
} else if (isRuleConfig(r)) {
@@ -60,11 +62,7 @@ export class RuleSet implements IRuleSet, Triggerable {
}
}
export interface IRuleSet {
/**
* Under what condition should a RuleSet's rules be "successful"? If 'OR' then ANY triggered rule result in a true outcome. If 'AND' then ALL rules must be triggered for the result to be true.
* */
condition: 'OR' | 'AND',
export interface IRuleSet extends JoinCondition {
/**
* @minItems 1
* */
@@ -81,5 +79,8 @@ export interface RuleSetOptions extends IRuleSet {
* @see {isRuleSetConfig} ts-auto-guard:type-guard
* */
export interface RuleSetJSONConfig extends IRuleSet {
/**
* @minItems 1
* */
rules: Array<RecentActivityRuleJSONConfig | RepeatSubmissionJSONConfig | AuthorRuleJSONConfig>
}

View File

@@ -3,15 +3,16 @@ import {Rule, RuleOptions, RulePremise, RuleResult} from "../index";
import {Submission} from "snoowrap";
import {getAuthorSubmissions} from "../../Utils/SnoowrapUtils";
import {groupBy, parseUsableLinkIdentifier as linkParser} from "../../util";
import {ActivityWindow, ActivityWindowType, ReferenceSubmission} from "../../Common/interfaces";
const groupByUrl = groupBy(['urlIdentifier']);
const parseUsableLinkIdentifier = linkParser()
export class RepeatSubmissionRule extends SubmissionRule {
threshold: number;
window: string | number;
window: ActivityWindowType;
gapAllowance?: number;
usePostAsReference: boolean;
useSubmissionAsReference: boolean;
include: string[];
exclude: string[];
@@ -21,14 +22,14 @@ export class RepeatSubmissionRule extends SubmissionRule {
threshold = 5,
window = 15,
gapAllowance,
usePostAsReference = true,
useSubmissionAsReference = true,
include = [],
exclude = []
} = options;
this.threshold = threshold;
this.window = window;
this.gapAllowance = gapAllowance;
this.usePostAsReference = usePostAsReference;
this.useSubmissionAsReference = useSubmissionAsReference;
this.include = include;
this.exclude = exclude;
}
@@ -42,7 +43,7 @@ export class RepeatSubmissionRule extends SubmissionRule {
threshold: this.threshold,
window: this.window,
gapAllowance: this.gapAllowance,
usePostAsReference: this.usePostAsReference,
useSubmissionAsReference: this.useSubmissionAsReference,
include: this.include,
exclude: this.exclude,
}
@@ -50,7 +51,7 @@ export class RepeatSubmissionRule extends SubmissionRule {
async process(item: Submission): Promise<[boolean, RuleResult[]]> {
const referenceUrl = await item.url;
if (referenceUrl === undefined && this.usePostAsReference) {
if (referenceUrl === undefined && this.useSubmissionAsReference) {
throw new Error(`Cannot run Rule ${this.name} because submission is not a link`);
}
const submissions = await getAuthorSubmissions(item.author, {window: this.window});
@@ -97,7 +98,7 @@ export class RepeatSubmissionRule extends SubmissionRule {
urlIdentifier: parseUsableLinkIdentifier(x.url)
})));
let groupsToCheck = [];
if (this.usePostAsReference) {
if (this.useSubmissionAsReference) {
const identifier = parseUsableLinkIdentifier(referenceUrl);
const {[identifier as string]: refGroup = []} = groupedPosts;
groupsToCheck.push(refGroup);
@@ -116,24 +117,46 @@ export class RepeatSubmissionRule extends SubmissionRule {
}
}
interface RepeatSubmissionConfig {
threshold: number,
window?: string | number,
interface RepeatSubmissionConfig extends ActivityWindow, ReferenceSubmission {
/**
* The number of repeat submissions that will trigger the rule
* @default 5
* */
threshold?: number,
/**
* The number of allowed non-identical Submissions between identical Submissions that can be ignored when checking against the threshold value
* */
gapAllowance?: number,
/**
* 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.
* Only include Submissions from this list of Subreddits.
*
* A list of subreddits (case-insensitive) to look for. Do not include "r/" prefix.
*
* EX to match against /r/mealtimevideos and /r/askscience use ["mealtimevideos","askscience"]
* @examples ["mealtimevideos","askscience"]
* @minItems 1
* */
usePostAsReference?: boolean,
include?: string[],
/**
* Do not include Submissions from this list of Subreddits.
*
* A list of subreddits (case-insensitive) to look for. Do not include "r/" prefix.
*
* EX to match against /r/mealtimevideos and /r/askscience use ["mealtimevideos","askscience"]
* @examples ["mealtimevideos","askscience"]
* @minItems 1
* */
exclude?: string[],
}
export interface RepeatSubmissionOptions extends RepeatSubmissionConfig, RuleOptions {
}
/**
* Checks a user's history for Submissions with identical content
* */
export interface RepeatSubmissionJSONConfig extends RepeatSubmissionConfig, SubmissionRuleJSONConfig {
kind: 'repeatSubmission'
}
export default RepeatSubmissionRule;

View File

@@ -117,13 +117,13 @@ export abstract class Rule implements IRule, Triggerable {
}
}
export class Author implements IAuthor {
export class Author implements AuthorCriteria {
name?: string[];
flairCssClass?: string[];
flairText?: string[];
isMod?: boolean;
constructor(options: IAuthor) {
constructor(options: AuthorCriteria) {
this.name = options.name;
this.flairCssClass = options.flairCssClass;
this.flairText = options.flairText;
@@ -132,25 +132,36 @@ export class Author implements IAuthor {
}
/**
* If present then these Author criteria are checked before running the rule. If criteria fails then the rule is skipped. Note that when used on AuthorRule this becomes pass/fail (no skip)
* If present then these Author criteria are checked before running the rule. If criteria fails then the rule is skipped.
* @minProperties 1
* @additionalProperties false
* */
export interface AuthorOptions {
/**
* Only runs if include is not present. Will "pass" if any of set of the Author criteria do not pass
* Will "pass" if any set of AuthorCriteria passes
* */
exclude?: IAuthor[];
include?: AuthorCriteria[];
/**
* Will "pass" if any set of the Author criteria passes
* Only runs if include is not present. Will "pass" if any of set of the AuthorCriteria does not pass
* */
include?: IAuthor[];
exclude?: AuthorCriteria[];
}
/**
* Criteria with which to test against the author of a submission/comment. The outcome of the test is based on 1. any list criteria matching and then 2. all present criteria passing
* Criteria with which to test against the author of an Activity. The outcome of the test is based on:
*
* 1. All present properties passing and
* 2. If a property is a list then any value from the list matching
*
* @minProperties 1
* @additionalProperties false
* */
export interface IAuthor {
export interface AuthorCriteria {
/**
* A list of reddit usernames (case-insensitive) to match against
* A list of reddit usernames (case-insensitive) to match against. Do not include the "u/" prefix
*
* EX to match against /u/FoxxMD and /u/AnotherUser use ["FoxxMD","AnotherUser"]
* @examples ["FoxxMD","AnotherUser"]
* */
name?: string[],
/**
@@ -173,7 +184,7 @@ export interface IRule {
* */
name?: string
/**
* If present then these Author criteria are checked before running the rule. If criteria fails then the rule is skipped. Note this is NOT the same as AuthorRule.
* If present then these Author criteria are checked before running the rule. If criteria fails then the rule is skipped.
* */
authors?: AuthorOptions
}

View File

@@ -19,7 +19,7 @@
"type": "string"
}
],
"description": "An ISO 8601 duration or Day.js duration object",
"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",
{
@@ -28,54 +28,107 @@
]
}
},
"propertyOrder": [
"count",
"duration"
],
"type": "object"
},
"AuthorOptions": {
"description": "If present then these Author criteria are checked before running the rule. If criteria fails then the rule is skipped. Note that when used on AuthorRule this becomes pass/fail (no skip)",
"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": {
"exclude": {
"description": "Only runs if include is not present. Will \"pass\" if any of set of the Author criteria do not pass",
"flairCssClass": {
"description": "A list of (user) flair css class values from the subreddit to match against",
"items": {
"$ref": "#/definitions/IAuthor"
"type": "string"
},
"type": "array"
},
"include": {
"description": "Will \"pass\" if any set of the Author criteria passes",
"flairText": {
"description": "A list of (user) flair text values from the subreddit to match against",
"items": {
"$ref": "#/definitions/IAuthor"
"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",
"description": "If present then these Author criteria are checked before running the rule. If criteria fails then the rule is skipped. Note this is NOT the same as AuthorRule."
"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 Author criteria do not pass",
"description": "Only runs if include is not present. Will \"pass\" if any of set of the AuthorCriteria does not pass",
"items": {
"$ref": "#/definitions/IAuthor"
"$ref": "#/definitions/AuthorCriteria"
},
"type": "array"
},
"include": {
"description": "Will \"pass\" if any set of the Author criteria passes",
"description": "Will \"pass\" if any set of AuthorCriteria passes",
"items": {
"$ref": "#/definitions/IAuthor"
"$ref": "#/definitions/AuthorCriteria"
},
"type": "array"
},
"kind": {
"description": "The kind of rule to run",
"enum": [
"author",
"recentActivity",
"repeatSubmission"
"author"
],
"type": "string"
},
@@ -84,6 +137,13 @@
"type": "string"
}
},
"propertyOrder": [
"kind",
"include",
"exclude",
"name",
"authors"
],
"required": [
"exclude",
"include",
@@ -118,6 +178,15 @@
"minItems": 1,
"type": "array"
},
"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"
},
"description": {
"type": "string"
},
@@ -133,14 +202,6 @@
"description": "A friendly name for this check (highly recommended) -- EX \"repeatCrosspostReport\"",
"type": "string"
},
"ruleJoin": {
"description": "Under what condition should a check's rules be \"successful\"? If 'OR' then ANY triggered rule will cause actions to run. If 'AND' then ALL rules must be triggered for actions to run.",
"enum": [
"AND",
"OR"
],
"type": "string"
},
"rules": {
"description": "Rules are run in the order found in configuration. Can be Rules or RuleSets",
"items": {
@@ -163,6 +224,14 @@
"type": "array"
}
},
"propertyOrder": [
"kind",
"rules",
"actions",
"name",
"description",
"condition"
],
"required": [
"actions",
"kind",
@@ -172,12 +241,19 @@
"type": "object"
},
"CommentActionJSONConfig": {
"description": "Reply to the Activity. For a submission the reply will be a top-level comment.",
"properties": {
"content": {
"description": "Content is interpreted as reddit-flavored Markdown. If value starts with 'wiki:' then the proceeding value will be use to get a wiki page\nEX wiki:botconfig/mybot ==> try to get http://reddit.com/mySubredditExample/wiki/botconfig/mybot",
"description": "The Content to submit for this Action. Content is interpreted as reddit-flavored Markdown.\n\nIf value starts with 'wiki:' then the proceeding value will be used to get a wiki page\n\nEX \"wiki:botconfig/mybot\" tries to get https://reddit.com/mySubredditExample/wiki/botconfig/mybot\n\nEX \"this is plain text\"\n\nEX \"this is **bold** markdown text\"",
"examples": [
"this is plain text",
"this is **bold** markdown text",
"wiki:botconfig/acomment"
],
"type": "string"
},
"distinguish": {
"description": "Distinguish the comment after creation?",
"type": "boolean"
},
"kind": {
@@ -192,6 +268,7 @@
"type": "string"
},
"lock": {
"description": "Lock the comment after creation?",
"type": "boolean"
},
"name": {
@@ -199,9 +276,18 @@
"type": "string"
},
"sticky": {
"description": "Stick the comment after creation?",
"type": "boolean"
}
},
"propertyOrder": [
"lock",
"sticky",
"distinguish",
"content",
"kind",
"name"
],
"required": [
"content",
"kind"
@@ -210,7 +296,7 @@
},
"DurationObject": {
"additionalProperties": false,
"description": "A Day.js duration object",
"description": "A Day.js duration object\n\nhttps://day.js.org/docs/en/durations/creating",
"minProperties": 1,
"properties": {
"days": {
@@ -235,10 +321,19 @@
"type": "number"
}
},
"propertyOrder": [
"seconds",
"minutes",
"hours",
"days",
"weeks",
"months",
"years"
],
"type": "object"
},
"FlairActionJSONConfig": {
"description": "text and css cannot both be empty",
"description": "Flair the Submission",
"properties": {
"css": {
"description": "The text of the css class of the flair to apply",
@@ -264,43 +359,19 @@
"type": "string"
}
},
"propertyOrder": [
"text",
"css",
"name",
"kind"
],
"required": [
"kind"
],
"type": "object"
},
"IAuthor": {
"description": "Criteria with which to test against the author of a submission/comment. The outcome of the test is based on 1. any list criteria matching and then 2. all present criteria passing",
"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",
"items": {
"type": "string"
},
"type": "array"
}
},
"type": "object"
},
"LockActionJSONConfig": {
"description": "Lock the Activity",
"properties": {
"kind": {
"description": "The type of action that will be performed",
@@ -318,23 +389,80 @@
"type": "string"
}
},
"propertyOrder": [
"name",
"kind"
],
"required": [
"kind"
],
"type": "object"
},
"PollingOptions": {
"description": "You may specify polling options independently for submissions/comments",
"properties": {
"comments": {
"description": "Polling options for comment events",
"properties": {
"interval": {
"default": 10000,
"description": "Amount of time to wait between requests for new comments\n\nDefaults to 10 seconds",
"format": "milliseconds",
"type": "number"
},
"limit": {
"default": 10,
"description": "The number of new comments to pull on every request",
"type": "number"
}
},
"propertyOrder": [
"limit",
"interval"
],
"type": "object"
},
"submissions": {
"description": "Polling options for submission events",
"properties": {
"interval": {
"default": 10000,
"description": "Amount of time to wait between requests to /r/subreddit/new\n\nDefaults to 10 seconds",
"format": "milliseconds",
"type": "number"
},
"limit": {
"default": 10,
"description": "The number of submissions to pull from /r/subreddit/new on every request",
"type": "number"
}
},
"propertyOrder": [
"limit",
"interval"
],
"type": "object"
}
},
"propertyOrder": [
"submissions",
"comments"
],
"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",
"description": "If present then these Author criteria are checked before running the rule. If criteria fails then the rule is skipped. Note this is NOT the same as AuthorRule."
"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"
"recentActivity"
],
"type": "string"
},
@@ -355,14 +483,19 @@
"items": {
"$ref": "#/definitions/SubThreshold"
},
"minItems": 1,
"type": "array"
},
"usePostAsReference": {
"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"
},
@@ -374,17 +507,44 @@
}
],
"default": 15,
"description": "Criteria for defining what set of activities should be considered. See ActivityWindowCriteria for descriptions of what different data types will do\n//@examples require('./interfaces.ts').windowExample"
"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",
"window"
"thresholds"
],
"type": "object"
},
"RemoveActionJSONConfig": {
"description": "Remove the Activity",
"properties": {
"kind": {
"description": "The type of action that will be performed",
@@ -402,37 +562,55 @@
"type": "string"
}
},
"propertyOrder": [
"name",
"kind"
],
"required": [
"kind"
],
"type": "object"
},
"RepeatSubmissionJSONConfig": {
"description": "Checks a user's history for Submissions with identical content",
"properties": {
"authors": {
"$ref": "#/definitions/AuthorOptions",
"description": "If present then these Author criteria are checked before running the rule. If criteria fails then the rule is skipped. Note this is NOT the same as AuthorRule."
"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": [
"author",
"recentActivity",
"repeatSubmission"
],
"type": "string"
@@ -442,28 +620,73 @@
"type": "string"
},
"threshold": {
"default": 5,
"description": "The number of repeat submissions that will trigger the rule",
"type": "number"
},
"usePostAsReference": {
"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": {
"type": [
"string",
"number"
"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
}
}
]
}
},
"required": [
"propertyOrder": [
"kind",
"threshold"
"threshold",
"gapAllowance",
"include",
"exclude",
"window",
"useSubmissionAsReference",
"name",
"authors"
],
"required": [
"kind"
],
"type": "object"
},
"ReportActionJSONConfig": {
"description": "Report the Activity",
"properties": {
"content": {
"description": "The text of the report",
"type": "string"
},
"kind": {
@@ -482,6 +705,11 @@
"type": "string"
}
},
"propertyOrder": [
"content",
"kind",
"name"
],
"required": [
"content",
"kind"
@@ -492,7 +720,8 @@
"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": {
"description": "Under what condition should a RuleSet's rules be \"successful\"? If 'OR' then ANY triggered rule result in a true outcome. If 'AND' then ALL rules must be triggered for the result to be true.",
"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"
@@ -513,11 +742,15 @@
}
]
},
"minItems": 1,
"type": "array"
}
},
"propertyOrder": [
"rules",
"condition"
],
"required": [
"condition",
"rules"
],
"type": "object"
@@ -525,11 +758,17 @@
"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",
"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"
},
@@ -537,6 +776,10 @@
"type": "array"
}
},
"propertyOrder": [
"count",
"subreddits"
],
"required": [
"subreddits"
],
@@ -545,14 +788,22 @@
},
"properties": {
"checks": {
"description": "A list of all the checks that should be run for a subreddit. Checks are split into two lists -- submission or comment -- based on kind and run independently. Checks in each list are run in the order found in the configuration. When a check \"passes\" and actions are performed any subsequent checks are skipped.",
"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"
},
"minItems": 1,
"type": "array"
},
"polling": {
"$ref": "#/definitions/PollingOptions",
"description": "You may specify polling options independently for submissions/comments"
}
},
"propertyOrder": [
"checks",
"polling"
],
"required": [
"checks"
],

View File

@@ -7,23 +7,17 @@ import {CommentStream, SubmissionStream} from "snoostorm";
import pEvent from "p-event";
import {RuleResult} from "../Rule";
import {ConfigBuilder} from "../ConfigBuilder";
import {PollingOptions} from "../Common/interfaces";
export interface ManagerOptions {
submissions?: {
limit?: number,
interval?: number,
},
comments?: {
limit?: number,
interval?: number,
}
polling?: PollingOptions
}
export class Manager {
subreddit: Subreddit;
client: Snoowrap;
logger: Logger;
pollOptions: ManagerOptions;
pollOptions: PollingOptions;
submissionChecks: SubmissionCheck[];
commentChecks: CommentCheck[];
@@ -37,7 +31,7 @@ export class Manager {
const configBuilder = new ConfigBuilder({logger: this.logger});
const [subChecks, commentChecks] = configBuilder.buildFromJson(sourceData);
this.pollOptions = opts;
this.pollOptions = opts.polling || {};
this.subreddit = sub;
this.client = client;
this.submissionChecks = subChecks;

View File

@@ -3,7 +3,7 @@ import Submission from "snoowrap/dist/objects/Submission";
import {Duration, DurationUnitsObjectType} from "dayjs/plugin/duration";
import dayjs, {Dayjs} from "dayjs";
import Mustache from "mustache";
import {AuthorOptions, IAuthor} from "../Rule";
import {AuthorOptions, AuthorCriteria} from "../Rule";
import {ActivityWindowCriteria, ActivityWindowType} from "../Common/interfaces";
export interface AuthorTypedActivitiesOptions extends AuthorActivitiesOptions {
@@ -97,7 +97,7 @@ export const renderContent = async (content: string, data: (Submission | Comment
return Mustache.render(content, {...templateData, ...additionalData});
}
export const testAuthorCriteria = async (item: (Comment|Submission), authorOpts: IAuthor, include = true) => {
export const testAuthorCriteria = async (item: (Comment|Submission), authorOpts: AuthorCriteria, include = true) => {
// @ts-ignore
const author: RedditUser = await item.author;
for(const k of Object.keys(authorOpts)) {