Refactor most thresholds to use comparison operators (like automod)

This commit is contained in:
FoxxMD
2021-06-21 11:40:04 -04:00
parent 532f6aa3d8
commit 4bef85e1e4
11 changed files with 249 additions and 243 deletions

View File

@@ -404,7 +404,7 @@ export interface ManagerOptions {
/**
* A string containing a comparison operator and a value to compare against
*
* The syntax is `< OR > OR <= OR >=] [number][?percent sign]`
* The syntax is `(< OR > OR <= OR >=) <number>[percent sign]`
*
* * EX `> 100` => greater than 100
* * EX `<= 75%` => less than or equal to 75%

View File

@@ -4,7 +4,7 @@ import {Rule, RuleJSONConfig, RuleOptions, RuleResult} from "./index";
import Submission from "snoowrap/dist/objects/Submission";
import {getAuthorActivities} from "../Utils/SnoowrapUtils";
import dayjs from "dayjs";
import {comparisonTextOp, formatNumber, parseGenericComparison, percentFromString} from "../util";
import {comparisonTextOp, formatNumber, parseGenericValueOrPercentComparison, percentFromString} from "../util";
export interface CommentThresholdCriteria extends ThresholdCriteria {
/**
@@ -23,7 +23,7 @@ export interface HistoryCriteria {
/**
* A string containing a comparison operator and a value to compare submissions against
*
* The syntax is `[< OR > OR <= OR >=] [number][?percent sign]`
* The syntax is `(< OR > OR <= OR >=) <number>[percent sign]`
*
* * EX `> 100` => greater than 100 submissions
* * EX `<= 75%` => submissions are equal to or less than 75% of all Activities
@@ -34,12 +34,12 @@ export interface HistoryCriteria {
/**
* A string containing a comparison operator and a value to compare comments against
*
* The syntax is `[< OR > OR <= OR >=] [number][?percent sign]`
* The syntax is `(< OR > OR <= OR >=) <number>[percent sign] [OP]`
*
* * EX `> 100` => greater than 100 comments
* * EX `<= 75%` => comments are equal to or less than 75% of all Activities
*
* If your string also contains the text `OP` somewhere **after** `[number][?percent sign]`...:
* If your string also contains the text `OP` somewhere **after** `<number>[percent sign]`...:
*
* * EX `> 100 OP` => greater than 100 comments as OP
* * EX `<= 25% as OP` => Comments as OP were less then or equal to 25% of **all Comments**
@@ -131,7 +131,7 @@ export class HistoryRule extends Rule {
let commentTrigger = undefined;
if(comment !== undefined) {
const {operator, value, isPercent, extra = ''} = parseGenericComparison(comment);
const {operator, value, isPercent, extra = ''} = parseGenericValueOrPercentComparison(comment);
const asOp = extra.toLowerCase().includes('op');
if(isPercent) {
const per = value / 100;
@@ -151,7 +151,7 @@ export class HistoryRule extends Rule {
let submissionTrigger = undefined;
if(submission !== undefined) {
const {operator, value, isPercent} = parseGenericComparison(submission);
const {operator, value, isPercent} = parseGenericValueOrPercentComparison(submission);
if(isPercent) {
const per = value / 100;
submissionTrigger = comparisonTextOp(submissionTotal / activityTotal, operator, per);
@@ -218,14 +218,14 @@ export class HistoryRule extends Rule {
let submissionSummary;
let commentSummary;
if(submission !== undefined) {
const {operator, value, isPercent, displayText} = parseGenericComparison(submission);
const {operator, value, isPercent, displayText} = parseGenericValueOrPercentComparison(submission);
const suffix = !isPercent ? 'Items' : `(${formatNumber((submissionTotal/activityTotal)*100)}%) of ${activityTotal} Total`;
submissionSummary = `Submissions (${submissionTotal}) were ${displayText} ${suffix}`;
data.submissionSummary = submissionSummary;
thresholdSummary.push(submissionSummary);
}
if(comment !== undefined) {
const {operator, value, isPercent, displayText, extra = ''} = parseGenericComparison(comment);
const {operator, value, isPercent, displayText, extra = ''} = parseGenericValueOrPercentComparison(comment);
const asOp = extra.toLowerCase().includes('op');
const totalType = asOp ? 'Comments' : 'Activities'
const countType = asOp ? 'Comments as OP' : 'Comments';

View File

@@ -1,7 +1,7 @@
import {Rule, RuleJSONConfig, RuleOptions, RulePremise, RuleResult} from "./index";
import {Comment, VoteableContent} from "snoowrap";
import Submission from "snoowrap/dist/objects/Submission";
import {activityWindowText, parseUsableLinkIdentifier} from "../util";
import {activityWindowText, comparisonTextOp, parseGenericValueOrPercentComparison, parseUsableLinkIdentifier} from "../util";
import {
ActivityWindow,
ActivityWindowCriteria,
@@ -59,7 +59,6 @@ export class RecentActivityRule extends Rule {
break;
}
let viableActivity = activities;
if (this.useSubmissionAsReference) {
if (!(item instanceof Submission)) {
@@ -84,53 +83,51 @@ export class RecentActivityRule extends Rule {
grouped[s] = (grouped[s] || []).concat(activity);
return grouped;
}, {} as Record<string, (Submission | Comment)[]>);
let triggeredPerSub = [];
let totalTriggeredOn;
for (const triggerSet of this.thresholds) {
triggeredPerSub = [];
let currCount = 0;
let presentSubs = [];
const {count: subCount = 1, totalCount = 1, subreddits = []} = triggerSet;
let presentSubs;
const {threshold = '>= 1', subreddits = []} = triggerSet;
for (const sub of subreddits) {
const isub = sub.toLowerCase();
const {[isub]: tSub = []} = groupedActivity;
if(tSub.length > 0) {
if (tSub.length > 0) {
currCount += tSub.length;
presentSubs.push(sub);
if (subCount !== undefined && tSub.length >= subCount) {
triggeredPerSub.push({subreddit: sub, count: tSub.length, threshold: subCount});
if(presentSubs === undefined) {
presentSubs = [];
}
presentSubs.push(sub);
}
}
if(totalCount !== undefined && currCount >= totalCount) {
totalTriggeredOn = {subreddits: presentSubs, count: currCount, threshold: totalCount};
const {operator, value, isPercent} = parseGenericValueOrPercentComparison(threshold);
if (threshold !== undefined) {
if (isPercent) {
if (comparisonTextOp(currCount / viableActivity.length, operator, value / 100)) {
totalTriggeredOn = {subreddits: presentSubs || subreddits, count: currCount, threshold};
}
} else if (comparisonTextOp(currCount, operator, value)) {
totalTriggeredOn = {subreddits: presentSubs || subreddits, count: currCount, threshold};
}
}
// if either trigger condition is hit end the iteration early
if(triggeredPerSub.length > 0 || totalTriggeredOn !== undefined) {
if (totalTriggeredOn !== undefined) {
break;
}
}
if (triggeredPerSub.length > 0 || totalTriggeredOn !== undefined) {
if (totalTriggeredOn !== undefined) {
let resultArr = [];
const data: any = {};
if(triggeredPerSub.length > 0) {
data.perSubCount = triggeredPerSub.length;
data.perSubTotal = triggeredPerSub.reduce((acc, x) => acc + x.count, 0);
data.perSubSubredditsSummary = triggeredPerSub.map(x => x.subreddit).join(', ');
data.perSubSummary = triggeredPerSub.map(x => `${x.subreddit}(${x.count})`).join(', ');
data.perSubThreshold = triggeredPerSub[0].threshold;
resultArr.push(`${triggeredPerSub.length} subs have >${triggeredPerSub[0].threshold} activities (${data.perSubTotal} Total)`);
}
if(totalTriggeredOn !== undefined) {
data.totalCount = totalTriggeredOn.count;
data.totalSubredditsCount = totalTriggeredOn.subreddits.length;
data.totalSubredditsSummary = totalTriggeredOn.subreddits.join(', ')
data.totalThreshold = totalTriggeredOn.threshold;
data.totalSummary = `${data.totalCount} (>${totalTriggeredOn.threshold}) activities over ${totalTriggeredOn.subreddits.length} subreddits`;
resultArr.push(data.totalSummary);
}
data.totalCount = totalTriggeredOn.count;
data.totalSubredditsCount = totalTriggeredOn.subreddits.length;
data.totalSubredditsSummary = totalTriggeredOn.subreddits.join(', ')
data.totalThreshold = totalTriggeredOn.threshold;
data.totalSummary = `${data.totalCount} (${totalTriggeredOn.threshold}) activities over ${totalTriggeredOn.subreddits.length} subreddits`;
resultArr.push(data.totalSummary);
let summary;
if(resultArr.length === 2) {
if (resultArr.length === 2) {
// need a shortened summary
summary = `${data.perSubCount} per-sub triggers (${data.perSubThreshold}) and ${data.totalCount} total (${data.totalThreshold})`
} else {
@@ -142,11 +139,11 @@ export class RecentActivityRule extends Rule {
result,
data: {
window: typeof this.window === 'number' ? `${activities.length} Items` : activityWindowText(viableActivity),
triggeredOn: triggeredPerSub,
//triggeredOn: triggeredPerSub,
summary,
subSummary: data.totalSubredditsSummary|| data.perSubSubredditsSummary,
subCount: data.totalSubredditsCount || data.perSubCount,
totalCount: data.totalCount || data.perSubTotal
subSummary: data.totalSubredditsSummary,
subCount: data.totalSubredditsCount,
totalCount: data.totalCount
}
})]]);
}
@@ -163,19 +160,20 @@ export class RecentActivityRule extends Rule {
* */
export interface SubThreshold extends SubredditCriteria {
/**
* The number of activities in each subreddit from the list that will trigger this rule
* @minimum 1
* @default 1
* @examples [1]
* A string containing a comparison operator and a value to compare recent activities against
*
* The syntax is `(< OR > OR <= OR >=) <number>[percent sign]`
*
* * EX `> 3` => greater than 3 activities found in the listed subreddits
* * EX `<= 75%` => number of Activities in the subreddits listed are equal to or less than 75% of all Activities
*
* **Note:** If you use percentage comparison here as well as `useSubmissionAsReference` then "all Activities" is only pertains to Activities that had the Link of the Submission, rather than all Activities from this window.
*
* @pattern ^\s*(>|>=|<|<=)\s*(\d+)\s*(%?)(.*)$
* @default ">= 1"
* @examples [">= 1"]
* */
count?: number,
/**
* The total number of activities across all listed subreddits that will trigger this rule
* @minimum 1
* @default 1
* @examples [1]
* */
totalCount?: number
threshold?: string
}
interface RecentActivityConfig extends ActivityWindow, ReferenceSubmission {

View File

@@ -4,18 +4,22 @@ import {RuleOptions, RuleResult} from "../index";
import Submission from "snoowrap/dist/objects/Submission";
import {getAttributionIdentifier} from "../../Utils/SnoowrapUtils";
import dayjs from "dayjs";
import {comparisonTextOp, parseGenericValueOrPercentComparison} from "../../util";
export interface AttributionCriteria {
/**
* The number or percentage to trigger this rule at
* A string containing a comparison operator and a value to compare comments against
*
* * If `threshold` is a `number` then it is the absolute number of attribution instances to trigger at
* * 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
* The syntax is `(< OR > OR <= OR >=) <number>[percent sign]`
*
* @default 10%
* * EX `> 12` => greater than 12 activities originate from same attribution
* * EX `<= 10%` => less than 10% of all Activities have the same attribution
*
* @pattern ^\s*(>|>=|<|<=)\s*(\d+)\s*(%?)(.*)$
* @default "> 10%"
* */
threshold: number | string
threshold: string
window: ActivityWindowType
/**
* What activities to use for total count when determining what percentage an attribution comprises
@@ -107,12 +111,8 @@ export class AttributionRule extends SubmissionRule {
for (const criteria of this.criteria) {
const {threshold, window, thresholdOn = 'all', minActivityCount = 5} = criteria;
let percentVal;
if (typeof threshold === 'string') {
percentVal = Number.parseInt(threshold.replace('%', '')) / 100;
}
const {threshold = '> 10%', window, thresholdOn = 'all', minActivityCount = 10} = criteria;
const {operator, value, isPercent, extra = ''} = parseGenericValueOrPercentComparison(threshold);
let activities = thresholdOn === 'submissions' ? await this.resources.getAuthorSubmissions(item.author, {window: window}) : await this.resources.getAuthorActivities(item.author, {window: window});
activities = activities.filter(act => {
@@ -172,11 +172,11 @@ export class AttributionRule extends SubmissionRule {
let triggeredDomains = [];
for (const [domain, subCount] of aggregatedSubmissions) {
let triggered = false;
if (percentVal !== undefined) {
triggered = percentVal <= subCount / activityTotal;
} else if (subCount >= threshold) {
triggered = true;
if(isPercent) {
triggered = comparisonTextOp(subCount / activityTotal, operator, (value/100));
}
else {
triggered = comparisonTextOp(subCount, operator, value);
}
if (triggered) {

View File

@@ -1,7 +1,12 @@
import {SubmissionRule, SubmissionRuleJSONConfig} from "./index";
import {RuleOptions, RuleResult} from "../index";
import {Comment} from "snoowrap";
import {activityWindowText, parseUsableLinkIdentifier as linkParser} from "../../util";
import {
activityWindowText,
comparisonTextOp,
parseGenericValueComparison,
parseUsableLinkIdentifier as linkParser
} from "../../util";
import {ActivityWindow, ActivityWindowType, ReferenceSubmission} from "../../Common/interfaces";
import Submission from "snoowrap/dist/objects/Submission";
import dayjs from "dayjs";
@@ -33,7 +38,7 @@ const getActivityIdentifier = (activity: (Submission | Comment), length = 200) =
}
export class RepeatActivityRule extends SubmissionRule {
threshold: number;
threshold: string;
window: ActivityWindowType;
gapAllowance?: number;
useSubmissionAsReference: boolean;
@@ -44,7 +49,7 @@ export class RepeatActivityRule extends SubmissionRule {
constructor(options: RepeatActivityOptions) {
super(options);
const {
threshold = 5,
threshold = '> 5',
window = 100,
gapAllowance,
useSubmissionAsReference = true,
@@ -139,6 +144,10 @@ export class RepeatActivityRule extends SubmissionRule {
applicableGroupedActivities.set(getActivityIdentifier(item), referenceSubmissions || [])
}
const {operator, value: thresholdValue} = parseGenericValueComparison(this.threshold);
const greaterThan = operator.includes('>');
let allLessThan = true;
const identifiersSummary: SummaryData[] = [];
for (let [key, value] of applicableGroupedActivities) {
const summaryData = {
@@ -150,23 +159,39 @@ export class RepeatActivityRule extends SubmissionRule {
triggeringSetsMarkdown: [],
};
for (let set of value) {
if (set.length >= this.threshold) {
// @ts-ignore
summaryData.triggeringSets.push(set);
summaryData.totalTriggeringSets++;
summaryData.largestTrigger = Math.max(summaryData.largestTrigger, set.length);
const md = set.map((x: (Comment | Submission)) => `[${x instanceof Submission ? x.title : getActivityIdentifier(x, 50)}](https://reddit.com${x.permalink}) in ${x.subreddit_name_prefixed} on ${dayjs(x.created_utc * 1000).utc().format()}`);
// @ts-ignore
summaryData.triggeringSetsMarkdown.push(md);
const test = comparisonTextOp(set.length, operator, thresholdValue);
if(test) {
// if(greaterThan) {
// @ts-ignore
summaryData.triggeringSets.push(set);
summaryData.totalTriggeringSets++;
summaryData.largestTrigger = Math.max(summaryData.largestTrigger, set.length);
const md = set.map((x: (Comment | Submission)) => `[${x instanceof Submission ? x.title : getActivityIdentifier(x, 50)}](https://reddit.com${x.permalink}) in ${x.subreddit_name_prefixed} on ${dayjs(x.created_utc * 1000).utc().format()}`);
// @ts-ignore
summaryData.triggeringSetsMarkdown.push(md);
// }
} else if(!greaterThan) {
allLessThan = false;
}
// if ((test && greaterThan) || (!test && !greaterThan)) {
// // @ts-ignore
// summaryData.triggeringSets.push(set);
// summaryData.totalTriggeringSets++;
// summaryData.largestTrigger = Math.max(summaryData.largestTrigger, set.length);
// const md = set.map((x: (Comment | Submission)) => `[${x instanceof Submission ? x.title : getActivityIdentifier(x, 50)}](https://reddit.com${x.permalink}) in ${x.subreddit_name_prefixed} on ${dayjs(x.created_utc * 1000).utc().format()}`);
// // @ts-ignore
// summaryData.triggeringSetsMarkdown.push(md);
// }
}
if(greaterThan || (!greaterThan && allLessThan)) {
identifiersSummary.push(summaryData);
}
identifiersSummary.push(summaryData);
}
const triggeringSummaries = identifiersSummary.filter(x => x.totalTriggeringSets > 0)
if (triggeringSummaries.length > 0) {
const largestRepeat = triggeringSummaries.reduce((acc, summ) => Math.max(summ.largestTrigger, acc), 0);
const result = `${triggeringSummaries.length} of ${identifiersSummary.length} unique items repeated >=${this.threshold} (threshold) times, largest repeat: ${largestRepeat}`;
const result = `${triggeringSummaries.length} of ${identifiersSummary.length} unique items repeated ${this.threshold} (threshold) times, largest repeat: ${largestRepeat}`;
this.logger.verbose(result);
return Promise.resolve([true, [this.getResult(true, {
result,
@@ -198,9 +223,9 @@ interface SummaryData {
interface RepeatActivityConfig extends ActivityWindow, ReferenceSubmission {
/**
* The number of repeat submissions that will trigger the rule
* @default 5
* @default ">= 5"
* */
threshold?: number,
threshold?: string,
/**
* The number of allowed non-identical Submissions between identical Submissions that can be ignored when checking against the threshold value
* */

View File

@@ -149,31 +149,51 @@ export class Author implements AuthorCriteria {
export interface UserNoteCriteria {
/**
* User Note type key
* User Note type key to search for
* @examples ["spamwarn"]
* */
type: string;
/**
* Number of occurrences of this type. Ignored if `search` is `current`
* @examples [1]
* @default 1
*
* A string containing a comparison operator and/or a value to compare number of occurrences against
*
* The syntax is `(< OR > OR <= OR >=) <number>[percent sign] [ascending|descending]`
*
* @examples [">= 1"]
* @default ">= 1"
* @pattern ^\s*(?<opStr>>|>=|<|<=)\s*(?<value>\d+)\s*(?<percent>%?)\s*(?<extra>asc.*|desc.*)*$
* */
count?: number;
count?: string;
/**
* * If `current` then only the most recent note is checked
* * If `consecutive` then `count` number of `type` notes must be found in a row, based on `order` direction
* * If `total` then `count` number of `type` must be found within all notes
* How to test the notes for this Author:
*
* ### current
*
* Only the most recent note is checked for `type`
*
* ### total
*
* The `count` comparison of `type` must be found within all notes
*
* * EX `count: > 3` => Must have more than 3 notes of `type`, total
* * EX `count: <= 25%` => Must have 25% or less of notes of `type`, total
*
* ### consecutive
*
* The `count` **number** of `type` notes must be found in a row.
*
* You may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`
*
* * EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order
* * EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order
* * EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order
*
* @examples ["current"]
* @default current
* */
search?: 'current' | 'consecutive' | 'total'
/**
* Time-based order to search Notes in for `consecutive` search
* @examples ["descending"]
* @default descending
* */
order?: 'ascending' | 'descending'
}
/**
@@ -231,7 +251,7 @@ export interface AuthorCriteria {
/**
* Test the age of the Author's account (when it was created) against this comparison
*
* The syntax is `[< OR > OR <= OR >=] [number] [unit]`
* The syntax is `(< OR > OR <= OR >=) <number> <unit>`
*
* * EX `> 100 days` => Passes if Author's account is older than 100 days
* * EX `<= 2 months` => Passes if Author's account is younger than or equal to 2 months
@@ -248,7 +268,7 @@ export interface AuthorCriteria {
/**
* A duration and how to compare it against a value
*
* The syntax is `[< OR > OR <= OR >=] [number] [unit]` EX `> 100 days`, `<= 2 months`
* The syntax is `(< OR > OR <= OR >=) <number> <unit>` EX `> 100 days`, `<= 2 months`
*
* * EX `> 100 days` => Passes if the date being compared is before 100 days ago
* * EX `<= 2 months` => Passes if the date being compared is after or equal to 2 months

View File

@@ -98,12 +98,10 @@
"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"
]
"default": "> 10%",
"description": "A string containing a comparison operator and a value to compare comments against\n\nThe syntax is `[< OR > OR <= OR >=] [number][?percent sign]`\n\n* EX `> 12` => greater than 12 activities originate from same attribution\n* EX `<= 10%` => less than 10% of all Activities have the same attribution",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
"type": "string"
},
"thresholdOn": {
"default": "all",
@@ -1361,9 +1359,9 @@
"type": "string"
},
"threshold": {
"default": 5,
"default": ">= 5",
"description": "The number of repeat submissions that will trigger the rule",
"type": "number"
"type": "string"
},
"useSubmissionAsReference": {
"default": true,
@@ -1491,15 +1489,6 @@
"description": "At least one count property must be present. If both are present then either can trigger the rule",
"minProperties": 1,
"properties": {
"count": {
"default": 1,
"description": "The number of activities in each subreddit from the list that will trigger this rule",
"examples": [
1
],
"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": [
@@ -1514,14 +1503,14 @@
"minItems": 2,
"type": "array"
},
"totalCount": {
"default": 1,
"description": "The total number of activities across all listed subreddits that will trigger this rule",
"threshold": {
"default": ">= 1",
"description": "A string containing a comparison operator and a value to compare recent activities against\n\nThe syntax is `[< OR > OR <= OR >=] [number][?percent sign]`\n\n* EX `> 3` => greater than 3 activities found in the listed subreddits\n* EX `<= 75%` => number of Activities in the subreddits listed are equal to or less than 75% of all Activities\n\n**Note:** If you use percentage comparison here as well as `useSubmissionAsReference` then \"all Activities\" is only pertains to Activities that had the Link of the Submission, rather than all Activities from this window.",
"examples": [
1
">= 1"
],
"minimum": 1,
"type": "number"
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
"type": "string"
}
},
"required": [
@@ -1816,28 +1805,17 @@
"UserNoteCriteria": {
"properties": {
"count": {
"default": 1,
"description": "Number of occurrences of this type. Ignored if `search` is `current`",
"default": ">= 1",
"description": "Number of occurrences of this type. Ignored if `search` is `current`\n\nA string containing a comparison operator and/or a value to compare number of occurrences against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign] [ascending|descending]`",
"examples": [
1
],
"type": "number"
},
"order": {
"default": "descending",
"description": "Time-based order to search Notes in for `consecutive` search",
"enum": [
"ascending",
"descending"
],
"examples": [
"descending"
],
"pattern": "^\\s*(?<opStr>>|>=|<|<=)\\s*(?<value>\\d+)\\s*(?<percent>%?)\\s*(?<extra>asc.*|desc.*)*$",
"type": "string"
},
"search": {
"default": "current",
"description": "* If `current` then only the most recent note is checked\n* If `consecutive` then `count` number of `type` notes must be found in a row, based on `order` direction\n* If `total` then `count` number of `type` must be found within all notes",
"description": "How to test the notes for this Author\n\n### current\n\nOnly the most recent note is checked for `type`\n\n### total\n\nThe `count` comparison of `type` must be found within all notes\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n\n### consecutive\n\nThe `count` **number** of `type` notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
"enum": [
"consecutive",
"current",
@@ -1849,7 +1827,7 @@
"type": "string"
},
"type": {
"description": "User Note type key",
"description": "User Note type key to search for",
"examples": [
"spamwarn"
],

View File

@@ -85,12 +85,10 @@
"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"
]
"default": "> 10%",
"description": "A string containing a comparison operator and a value to compare comments against\n\nThe syntax is `[< OR > OR <= OR >=] [number][?percent sign]`\n\n* EX `> 12` => greater than 12 activities originate from same attribution\n* EX `<= 10%` => less than 10% of all Activities have the same attribution",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
"type": "string"
},
"thresholdOn": {
"default": "all",
@@ -868,9 +866,9 @@
"type": "string"
},
"threshold": {
"default": 5,
"default": ">= 5",
"description": "The number of repeat submissions that will trigger the rule",
"type": "number"
"type": "string"
},
"useSubmissionAsReference": {
"default": true,
@@ -908,15 +906,6 @@
"description": "At least one count property must be present. If both are present then either can trigger the rule",
"minProperties": 1,
"properties": {
"count": {
"default": 1,
"description": "The number of activities in each subreddit from the list that will trigger this rule",
"examples": [
1
],
"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": [
@@ -931,14 +920,14 @@
"minItems": 2,
"type": "array"
},
"totalCount": {
"default": 1,
"description": "The total number of activities across all listed subreddits that will trigger this rule",
"threshold": {
"default": ">= 1",
"description": "A string containing a comparison operator and a value to compare recent activities against\n\nThe syntax is `[< OR > OR <= OR >=] [number][?percent sign]`\n\n* EX `> 3` => greater than 3 activities found in the listed subreddits\n* EX `<= 75%` => number of Activities in the subreddits listed are equal to or less than 75% of all Activities\n\n**Note:** If you use percentage comparison here as well as `useSubmissionAsReference` then \"all Activities\" is only pertains to Activities that had the Link of the Submission, rather than all Activities from this window.",
"examples": [
1
">= 1"
],
"minimum": 1,
"type": "number"
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
"type": "string"
}
},
"required": [
@@ -992,28 +981,17 @@
"UserNoteCriteria": {
"properties": {
"count": {
"default": 1,
"description": "Number of occurrences of this type. Ignored if `search` is `current`",
"default": ">= 1",
"description": "Number of occurrences of this type. Ignored if `search` is `current`\n\nA string containing a comparison operator and/or a value to compare number of occurrences against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign] [ascending|descending]`",
"examples": [
1
],
"type": "number"
},
"order": {
"default": "descending",
"description": "Time-based order to search Notes in for `consecutive` search",
"enum": [
"ascending",
"descending"
],
"examples": [
"descending"
],
"pattern": "^\\s*(?<opStr>>|>=|<|<=)\\s*(?<value>\\d+)\\s*(?<percent>%?)\\s*(?<extra>asc.*|desc.*)*$",
"type": "string"
},
"search": {
"default": "current",
"description": "* If `current` then only the most recent note is checked\n* If `consecutive` then `count` number of `type` notes must be found in a row, based on `order` direction\n* If `total` then `count` number of `type` must be found within all notes",
"description": "How to test the notes for this Author\n\n### current\n\nOnly the most recent note is checked for `type`\n\n### total\n\nThe `count` comparison of `type` must be found within all notes\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n\n### consecutive\n\nThe `count` **number** of `type` notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
"enum": [
"consecutive",
"current",
@@ -1025,7 +1003,7 @@
"type": "string"
},
"type": {
"description": "User Note type key",
"description": "User Note type key to search for",
"examples": [
"spamwarn"
],

View File

@@ -65,12 +65,10 @@
"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"
]
"default": "> 10%",
"description": "A string containing a comparison operator and a value to compare comments against\n\nThe syntax is `[< OR > OR <= OR >=] [number][?percent sign]`\n\n* EX `> 12` => greater than 12 activities originate from same attribution\n* EX `<= 10%` => less than 10% of all Activities have the same attribution",
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
"type": "string"
},
"thresholdOn": {
"default": "all",
@@ -848,9 +846,9 @@
"type": "string"
},
"threshold": {
"default": 5,
"default": ">= 5",
"description": "The number of repeat submissions that will trigger the rule",
"type": "number"
"type": "string"
},
"useSubmissionAsReference": {
"default": true,
@@ -888,15 +886,6 @@
"description": "At least one count property must be present. If both are present then either can trigger the rule",
"minProperties": 1,
"properties": {
"count": {
"default": 1,
"description": "The number of activities in each subreddit from the list that will trigger this rule",
"examples": [
1
],
"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": [
@@ -911,14 +900,14 @@
"minItems": 2,
"type": "array"
},
"totalCount": {
"default": 1,
"description": "The total number of activities across all listed subreddits that will trigger this rule",
"threshold": {
"default": ">= 1",
"description": "A string containing a comparison operator and a value to compare recent activities against\n\nThe syntax is `[< OR > OR <= OR >=] [number][?percent sign]`\n\n* EX `> 3` => greater than 3 activities found in the listed subreddits\n* EX `<= 75%` => number of Activities in the subreddits listed are equal to or less than 75% of all Activities\n\n**Note:** If you use percentage comparison here as well as `useSubmissionAsReference` then \"all Activities\" is only pertains to Activities that had the Link of the Submission, rather than all Activities from this window.",
"examples": [
1
">= 1"
],
"minimum": 1,
"type": "number"
"pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$",
"type": "string"
}
},
"required": [
@@ -972,28 +961,17 @@
"UserNoteCriteria": {
"properties": {
"count": {
"default": 1,
"description": "Number of occurrences of this type. Ignored if `search` is `current`",
"default": ">= 1",
"description": "Number of occurrences of this type. Ignored if `search` is `current`\n\nA string containing a comparison operator and/or a value to compare number of occurrences against\n\nThe syntax is `(< OR > OR <= OR >=) <number>[percent sign] [ascending|descending]`",
"examples": [
1
],
"type": "number"
},
"order": {
"default": "descending",
"description": "Time-based order to search Notes in for `consecutive` search",
"enum": [
"ascending",
"descending"
],
"examples": [
"descending"
],
"pattern": "^\\s*(?<opStr>>|>=|<|<=)\\s*(?<value>\\d+)\\s*(?<percent>%?)\\s*(?<extra>asc.*|desc.*)*$",
"type": "string"
},
"search": {
"default": "current",
"description": "* If `current` then only the most recent note is checked\n* If `consecutive` then `count` number of `type` notes must be found in a row, based on `order` direction\n* If `total` then `count` number of `type` must be found within all notes",
"description": "How to test the notes for this Author\n\n### current\n\nOnly the most recent note is checked for `type`\n\n### total\n\nThe `count` comparison of `type` must be found within all notes\n\n* EX `count: > 3` => Must have more than 3 notes of `type`, total\n* EX `count: <= 25%` => Must have 25% or less of notes of `type`, total\n\n### consecutive\n\nThe `count` **number** of `type` notes must be found in a row.\n\nYou may also specify the time-based order in which to search the notes by specifying `ascending (asc)` or `descending (desc)` in the `count` value. Default is `descending`\n\n* EX `count: >= 3` => Must have 3 or more notes of `type` consecutively, in descending order\n* EX `count: < 2` => Must have less than 2 notes of `type` consecutively, in descending order\n* EX `count: > 4 asc` => Must have greater than 4 notes of `type` consecutively, in ascending order",
"enum": [
"consecutive",
"current",
@@ -1005,7 +983,7 @@
"type": "string"
},
"type": {
"description": "User Note type key",
"description": "User Note type key to search for",
"examples": [
"spamwarn"
],

View File

@@ -13,15 +13,16 @@ import {
TypedActivityStates
} from "../Common/interfaces";
import {
compareDurationValue,
compareDurationValue, comparisonTextOp,
isActivityWindowCriteria,
normalizeName, parseDuration,
parseDurationComparison,
parseDurationComparison, parseGenericValueOrPercentComparison,
truncateStringToLength
} from "../util";
import UserNotes from "../Subreddit/UserNotes";
import {Logger} from "winston";
import InvalidRegexError from "./InvalidRegexError";
import SimpleError from "./SimpleError";
export const BOT_LINK = 'https://www.reddit.com/r/ContextModBot/comments/o1dugk/introduction_to_contextmodbot_and_rcb';
@@ -266,7 +267,8 @@ export const renderContent = async (template: string, data: (Submission | Commen
export const testAuthorCriteria = async (item: (Comment | Submission), authorOpts: AuthorCriteria, include = true, userNotes: UserNotes) => {
// @ts-ignore
const author: RedditUser = await item.author;
const author: RedditUser = await item.author.fetch();
debugger;
for (const k of Object.keys(authorOpts)) {
// @ts-ignore
if (authorOpts[k] !== undefined) {
@@ -336,7 +338,9 @@ export const testAuthorCriteria = async (item: (Comment | Submission), authorOpt
const notes = await userNotes.getUserNotes(item.author);
const notePass = () => {
for (const noteCriteria of authorOpts[k] as UserNoteCriteria[]) {
const {count = 1, order = 'descending', search = 'current', type} = noteCriteria;
const {count = '>= 1', search = 'current', type} = noteCriteria;
const {value, operator, isPercent, extra = ''} = parseGenericValueOrPercentComparison(count);
const order = extra.includes('asc') ? 'ascending' : 'descending';
switch (search) {
case 'current':
if (notes.length > 0 && notes[notes.length - 1].noteType === type) {
@@ -356,13 +360,20 @@ export const testAuthorCriteria = async (item: (Comment | Submission), authorOpt
} else {
currCount = 0;
}
if (currCount >= count) {
if(isPercent) {
throw new SimpleError(`When comparing UserNotes with 'consecutive' search 'count' cannot be a percentage. Given: ${count}`);
}
if (comparisonTextOp(currCount, operator, value)) {
return true;
}
}
break;
case 'total':
if (notes.filter(x => x.noteType === type).length >= count) {
if(isPercent) {
if(comparisonTextOp(notes.filter(x => x.noteType === type).length / notes.length, operator, value/100)) {
return true;
}
} else if(comparisonTextOp(notes.filter(x => x.noteType === type).length, operator, value)) {
return true;
}
}

View File

@@ -207,21 +207,6 @@ export const createAjvFactory = (logger: Logger) => {
return new Ajv({logger: logger, verbose: true, strict: "log", allowUnionTypes: true});
}
export const comparisonTextOp = (val1: number, strOp: string, val2: number): boolean => {
switch (strOp) {
case '>':
return val1 > val2;
case '>=':
return val1 >= val2;
case '<':
return val1 < val2;
case '<=':
return val1 <= val2;
default:
throw new Error(`${strOp} was not a recognized operator`);
}
}
export const percentFromString = (str: string): number => {
const n = Number.parseInt(str.replace('%', ''));
if(Number.isNaN(n)) {
@@ -385,9 +370,42 @@ export const parseFromJsonOrYamlToObject = (content: string): [object?, Error?,
return [obj, jsonErr, yamlErr];
}
export const comparisonTextOp = (val1: number, strOp: string, val2: number): boolean => {
switch (strOp) {
case '>':
return val1 > val2;
case '>=':
return val1 >= val2;
case '<':
return val1 < val2;
case '<=':
return val1 <= val2;
default:
throw new Error(`${strOp} was not a recognized operator`);
}
}
const GENERIC_VALUE_COMPARISON = /^\s*(?<opStr>>|>=|<|<=)\s*(?<value>\d+)(?<extra>\s+.*)*$/
const GENERIC_VALUE_COMPARISON_URL = 'https://regexr.com/60dq4';
export const parseGenericValueComparison = (val: string): GenericComparison => {
const matches = val.match(GENERIC_VALUE_COMPARISON);
if (matches === null) {
throw new InvalidRegexError(GENERIC_VALUE_COMPARISON, val, GENERIC_VALUE_COMPARISON_URL)
}
const groups = matches.groups as any;
return {
operator: groups.opStr as StringOperator,
value: Number.parseFloat(groups.value),
isPercent: false,
extra: groups.extra,
displayText: `${groups.opStr} ${groups.number}`
}
}
const GENERIC_VALUE_PERCENT_COMPARISON = /^\s*(?<opStr>>|>=|<|<=)\s*(?<value>\d+)\s*(?<percent>%?)(?<extra>.*)$/
const GENERIC_VALUE_PERCENT_COMPARISON_URL = 'https://regexr.com/60a16';
export const parseGenericComparison = (val: string): GenericComparison => {
export const parseGenericValueOrPercentComparison = (val: string): GenericComparison => {
const matches = val.match(GENERIC_VALUE_PERCENT_COMPARISON);
if (matches === null) {
throw new InvalidRegexError(GENERIC_VALUE_PERCENT_COMPARISON, val, GENERIC_VALUE_PERCENT_COMPARISON_URL)
@@ -396,8 +414,8 @@ export const parseGenericComparison = (val: string): GenericComparison => {
return {
operator: groups.opStr as StringOperator,
value: Number.parseFloat(groups.number),
isPercent: groups.percent !== undefined,
value: Number.parseFloat(groups.value),
isPercent: groups.percent !== '',
extra: groups.extra,
displayText: `${groups.opStr} ${groups.number}${groups.percent === undefined ? '': '%'}`
}