diff --git a/src/Common/Migrations/Database/Server/1663609045418-mhs.ts b/src/Common/Migrations/Database/Server/1663609045418-mhs.ts new file mode 100644 index 0000000..6dca46c --- /dev/null +++ b/src/Common/Migrations/Database/Server/1663609045418-mhs.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm" +import {RuleType} from "../../../Entities/RuleType"; + +export class mhs1663609045418 implements MigrationInterface { + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.manager.getRepository(RuleType).save([ + new RuleType('mhs'), + ]); + } + + public async down(queryRunner: QueryRunner): Promise { + } + +} diff --git a/src/Common/interfaces.ts b/src/Common/interfaces.ts index 5689cfa..4ba37f4 100644 --- a/src/Common/interfaces.ts +++ b/src/Common/interfaces.ts @@ -1585,6 +1585,9 @@ export interface ThirdPartyCredentialsJsonConfig { youtube?: { apiKey: string } + mhs?: { + apiKey: string + } [key: string]: any } diff --git a/src/Common/types.ts b/src/Common/types.ts index 728fa5b..6fd2db3 100644 --- a/src/Common/types.ts +++ b/src/Common/types.ts @@ -19,10 +19,11 @@ import {DispatchActionJson} from "../Action/DispatchAction"; import {CancelDispatchActionJson} from "../Action/CancelDispatchAction"; import {ContributorActionJson} from "../Action/ContributorAction"; import {SentimentRuleJSONConfig} from "../Rule/SentimentRule"; +import {MHSRuleJSONConfig} from "../Rule/MHSRule"; import {ModNoteActionJson} from "../Action/ModNoteAction"; import {IncludesData} from "./Infrastructure/Includes"; import { SubmissionActionJson } from "../Action/SubmissionAction"; -export type RuleObjectJsonTypes = RecentActivityRuleJSONConfig | RepeatActivityJSONConfig | AuthorRuleJSONConfig | AttributionJSONConfig | HistoryJSONConfig | RegexRuleJSONConfig | RepostRuleJSONConfig | SentimentRuleJSONConfig +export type RuleObjectJsonTypes = RecentActivityRuleJSONConfig | RepeatActivityJSONConfig | AuthorRuleJSONConfig | AttributionJSONConfig | HistoryJSONConfig | RegexRuleJSONConfig | RepostRuleJSONConfig | SentimentRuleJSONConfig | MHSRuleJSONConfig export type ActionJson = CommentActionJson | SubmissionActionJson | FlairActionJson | ReportActionJson | LockActionJson | RemoveActionJson | ApproveActionJson | BanActionJson | UserNoteActionJson | MessageActionJson | UserFlairActionJson | DispatchActionJson | CancelDispatchActionJson | ContributorActionJson | ModNoteActionJson | string | IncludesData; diff --git a/src/Rule/MHSRule.ts b/src/Rule/MHSRule.ts new file mode 100644 index 0000000..383174f --- /dev/null +++ b/src/Rule/MHSRule.ts @@ -0,0 +1,420 @@ +import {Rule, RuleJSONConfig, RuleOptions} from "./index"; +import {Comment} from "snoowrap"; +import Submission from "snoowrap/dist/objects/Submission"; +import { + asComment, boolToString, + formatNumber, + triggeredIndicator, windowConfigToWindowCriteria +} from "../util"; +import got, {HTTPError} from 'got'; +import dayjs from 'dayjs'; +import {map as mapAsync} from 'async'; +import { + comparisonTextOp, + GenericComparison, + parseGenericValueOrPercentComparison, +} from "../Common/Infrastructure/Comparisons"; +import {ActivityWindowConfig, ActivityWindowCriteria} from "../Common/Infrastructure/ActivityWindow"; +import {RuleResult} from "../Common/interfaces"; +import {SnoowrapActivity} from "../Common/Infrastructure/Reddit"; +import {CMError} from "../Utils/Errors"; +import objectHash from "object-hash"; + +const formatConfidence = (val: number) => formatNumber(val * 100, { + suffix: '%', + toFixed: 2 +}); + +export class MHSRule extends Rule { + + criteria: MHSCriteria + + historical?: HistoricalMHS; + + ogConfig: MHSConfig + + constructor(options: MHSRuleOptions) { + super(options); + + if (this.resources.thirdPartyCredentials.mhs?.apiKey === undefined) { + throw new CMError(`MHS (moderatehatespeech.com) API Key has not been specified. It must be present in the bot config or top-level subreddit 'credentials' property.`); + } + + const { + criteria, + historical, + } = options; + + this.ogConfig = { + criteria, + historical + }; + + const { + flagged = true, + confidence, + testOn = ['body'] + } = criteria || {}; + + this.criteria = { + flagged, + confidence: confidence !== undefined ? parseGenericValueOrPercentComparison(confidence) : undefined, + testOn, + } + + if (options.historical !== undefined) { + const { + window, + criteria: historyCriteria, + mustMatchCurrent = false, + totalMatching = '> 0', + } = options.historical + + let usedCriteria: MHSCriteria; + if (historyCriteria === undefined) { + usedCriteria = this.criteria; + } else { + const { + flagged: historyFlagged = true, + confidence: historyConfidence, + testOn: historyTestOn = ['body'] + } = historyCriteria || {}; + usedCriteria = { + flagged: historyFlagged, + confidence: historyConfidence !== undefined ? parseGenericValueOrPercentComparison(historyConfidence) : undefined, + testOn: historyTestOn, + } + } + + this.historical = { + criteria: usedCriteria, + window: windowConfigToWindowCriteria(window), + mustMatchCurrent, + totalMatching: parseGenericValueOrPercentComparison(totalMatching), + }; + } + } + + getKind(): string { + return 'mhs'; + } + + getSpecificPremise(): object { + return this.ogConfig; + } + + protected async process(item: Submission | Comment): Promise<[boolean, RuleResult]> { + + let ogResult = await this.testActivity(item, this.criteria); + let historicResults: MHSCriteriaResult[] | undefined; + let historicalCriteriaTest: string | undefined; + + if (this.historical !== undefined && (!this.historical.mustMatchCurrent || ogResult.passed)) { + const { + criteria, + window, + } = this.historical; + const history = await this.resources.getAuthorActivities(item.author, window); + + historicResults = await mapAsync(history, async (x: SnoowrapActivity) => await this.testActivity(x, criteria)); // history.map(x => this.testActivity(x, sentiment)); + } + + const logSummary: string[] = []; + + let triggered = false; + let humanWindow: string | undefined; + let historicalPassed: string | undefined; + let totalMatchingText: string | undefined; + + if (historicResults === undefined) { + triggered = ogResult.passed; + logSummary.push(`Current Activity MHS Test: ${ogResult.summary}`); + if (!triggered && this.historical !== undefined && this.historical.mustMatchCurrent) { + logSummary.push(`Did not check Historical because 'mustMatchCurrent' is true`); + } + } else { + const { + totalMatching, + criteria, + } = this.historical as HistoricalMHS; + + historicalCriteriaTest = mhsCriteriaTestDisplay(criteria); + + totalMatchingText = totalMatching.displayText; + const allResults = historicResults + const passed = allResults.filter(x => x.passed); + + const firstActivity = allResults[0].activity; + const lastActivity = allResults[allResults.length - 1].activity; + + const humanRange = dayjs.duration(dayjs(firstActivity.created_utc * 1000).diff(dayjs(lastActivity.created_utc * 1000))).humanize(); + + humanWindow = `${allResults.length} Activities (${humanRange})`; + + const {operator, value, isPercent} = totalMatching; + if (isPercent) { + const passPercentVal = passed.length / allResults.length + triggered = comparisonTextOp(passPercentVal, operator, (value / 100)); + historicalPassed = `${passed.length} (${formatNumber(passPercentVal)}%)`; + } else { + triggered = comparisonTextOp(passed.length, operator, value); + historicalPassed = `${passed.length}`; + } + logSummary.push(`${triggeredIndicator(triggered)} ${historicalPassed} historical activities of ${humanWindow} passed MHS criteria '${historicalCriteriaTest}' which ${triggered ? 'MET' : 'DID NOT MEET'} threshold '${totalMatching.displayText}'`); + } + + const result = logSummary.join(' || '); + this.logger.verbose(result); + + return Promise.resolve([triggered, this.getResult(triggered, { + result, + data: { + results: { + triggered, + criteriaTest: mhsCriteriaTestDisplay(this.criteria), + historicalCriteriaTest, + window: humanWindow, + totalMatching: totalMatchingText + } + } + })]); + } + + protected async testActivity(a: SnoowrapActivity, criteria: MHSCriteria): Promise { + const content = []; + if (asComment(a)) { + content.push(a.body); + } else { + if (criteria.testOn.includes('title')) { + content.push(a.title); + } + if (criteria.testOn.includes('body') && a.is_self) { + content.push(a.selftext); + } + } + const mhsResult = await this.getMHSResponse(content.join(' ')); + + const { + flagged, + confidence + } = criteria; + + let flaggedPassed: boolean | undefined; + let confPassed: boolean | undefined; + + let summary = []; + + if (confidence !== undefined) { + const {operator, value} = confidence; + confPassed = comparisonTextOp(mhsResult.confidence * 100, operator, value); + summary.push(`Confidence test (${confidence.displayText}) ${confPassed ? 'PASSED' : 'DID NOT PASS'} MHS confidence of ${formatConfidence(mhsResult.confidence)}`) + } + + if (flagged !== undefined) { + flaggedPassed = flagged ? mhsResult.class === 'flag' : mhsResult.class === 'normal'; + summary.push(`Flagged pass condition of ${flagged} (${flagged ? 'toxic' : 'normal'}) ${flaggedPassed ? 'MATCHED' : 'DID NOT MATCH'} MHS flag '${mhsResult.class === 'flag' ? 'toxic' : 'normal'}' ${confidence === undefined ? ` (${formatConfidence(mhsResult.confidence)} confidence)` : ''}`); + } + + const passed = (flaggedPassed === undefined || flaggedPassed) && (confPassed === undefined || confPassed); + + return { + activity: a, + criteria, + mhsResult, + passed, + summary: `${triggeredIndicator(passed)} ${summary.join(' | ')}` + } + } + + protected async getMHSResponse(content: string): Promise { + const hash = objectHash.sha1({content}); + const key = `mhs-${hash}`; + if (this.resources.wikiTTL !== false) { + let res = await this.resources.cache.get(key) as undefined | null | MHSResponse; + if(res !== undefined && res !== null) { + // don't cache bad responses + if(res.response.toLowerCase() === 'success') + { + return res; + } + } + res = await this.callMHS(content); + if(res.response.toLowerCase() === 'success') { + await this.resources.cache.set(key, res, {ttl: this.resources.wikiTTL}); + } + return res; + } + return this.callMHS(content); + } + + protected async callMHS(content: string): Promise { + try { + return await got.post(`https://api.moderatehatespeech.com/api/v1/moderate/`, { + headers: { + 'Content-Type': `application/json`, + }, + json: { + token: this.resources.thirdPartyCredentials.mhs?.apiKey, + text: content + }, + }).json() as MHSResponse; + } catch (err: any) { + let error: string | undefined = undefined; + if (err instanceof HTTPError) { + error = err.response.statusMessage; + if (typeof err.response.body === 'string') { + error = `(${err.response.statusCode}) ${err.response.body}`; + } + } + throw new CMError(`MHS request failed${error !== undefined ? ` with error: ${error}` : ''}`, {cause: err}); + } + } +} + +const mhsCriteriaTestDisplay = (criteria: MHSCriteria) => { + const summary = []; + if (criteria.flagged !== undefined) { + summary.push(`${criteria.flagged ? 'IS FLAGGED' : 'IS NOT FLAGGED'} as toxic`); + } + if (criteria.confidence !== undefined) { + summary.push(`MHS confidence is ${criteria.confidence.displayText}`); + } + return summary.join(' AND '); +} + +interface MHSResponse { + confidence: number + response: string + class: 'flag' | 'normal' +} + +interface MHSCriteriaResult { + mhsResult: MHSResponse + criteria: MHSCriteria + passed: boolean + summary: string, + activity: SnoowrapActivity +} + +/** + * Test the content of Activities from the Author history against MHS criteria + * + * If this is defined then the `totalMatching` threshold must pass for the Rule to trigger + * + * If `criteria` is defined here it overrides the top-level `criteria` value + * + * */ +interface HistoricalMHSConfig { + window: ActivityWindowConfig + + criteria?: MHSCriteriaConfig + + /** + * When `true` the original Activity being checked MUST pass its criteria before the Rule considers any history + * + * @default false + * */ + mustMatchCurrent?: boolean + + /** + * A string containing a comparison operator and a value to compare Activities from history that pass the given `criteria` test + * + * The syntax is `(< OR > OR <= OR >=) [percent sign]` + * + * * EX `> 12` => greater than 12 activities passed given `criteria` test + * * EX `<= 10%` => less than 10% of all Activities from history passed given `criteria` test + * + * @pattern ^\s*(>|>=|<|<=)\s*(\d+)\s*(%?)(.*)$ + * @default "> 0" + * @examples ["> 0","> 10%"] + * */ + totalMatching: string +} + +interface HistoricalMHS extends Omit { + window: ActivityWindowCriteria + criteria: MHSCriteria + totalMatching: GenericComparison +} + +/** + * Criteria used to trigger based on MHS results + * + * If both `flagged` and `confidence` are specified then both conditions must pass. + * + * By default, only `flagged` is defined as `true` + * */ +interface MHSCriteriaConfig { + /** + * Test if MHS considers content flagged as toxic or not + * + * @default true + * */ + flagged?: boolean + + /** + * A string containing a comparison operator and a value to compare against the confidence returned from MHS + * + * The syntax is `(< OR > OR <= OR >=) ` + * + * * EX `> 50` => MHS confidence is greater than 50% + * + * @pattern ^\s*(>|>=|<|<=)\s*(\d+)\s*(%?)(.*)$ + * @examples ["> 50"] + * */ + confidence?: string + /** + * Which content from an Activity to send to MHS + * + * Only used if the Activity being tested is a Submission -- Comments can be only tested against their body + * + * If more than one type of content is specified then all text is tested together as one string + * + * @default ["body"] + * */ + testOn?: ('title' | 'body')[] +} + +interface MHSCriteria extends Omit { + confidence?: GenericComparison + testOn: ('title' | 'body')[] +} + +interface MHSConfig { + + criteria?: MHSCriteriaConfig + + /** + * run MHS on Activities from the Author history + * + * If this is defined then the `totalMatching` threshold must pass for the Rule to trigger + * + * If `criteria` is defined here it overrides the top-level `criteria` value + * + * */ + historical?: HistoricalMHSConfig +} + +export interface MHSRuleOptions extends MHSConfig, RuleOptions { +} + +/** + * Test content of an Activity against the MHS toxicity model for reddit content + * + * Running this Rule with no configuration will use a default configuration that will cause the Rule to trigger if MHS flags the content of the Activity as toxic. + * + * More info: + * + * * https://moderatehatespeech.com/docs/ + * * https://moderatehatespeech.com/ + * + * */ +export interface MHSRuleJSONConfig extends MHSConfig, RuleJSONConfig { + /** + * @examples ["mhs"] + * @default mhs + * */ + kind: 'mhs' +} + +export default MHSRule; diff --git a/src/Rule/RuleFactory.ts b/src/Rule/RuleFactory.ts index 3d9dbbb..dc50b89 100644 --- a/src/Rule/RuleFactory.ts +++ b/src/Rule/RuleFactory.ts @@ -12,6 +12,7 @@ import {RepostRule, RepostRuleJSONConfig} from "./RepostRule"; import {StructuredFilter} from "../Common/Infrastructure/Filters/FilterShapes"; import {SentimentRule, SentimentRuleJSONConfig} from "./SentimentRule"; import {StructuredRuleConfigObject} from "../Common/Infrastructure/RuleShapes"; +import {MHSRuleJSONConfig, MHSRule} from "./MHSRule"; export function ruleFactory (config: StructuredRuleConfigObject, logger: Logger, subredditName: string, resources: SubredditResources, client: Snoowrap): Rule { @@ -42,6 +43,9 @@ export function ruleFactory case 'sentiment': cfg = config as StructuredFilter; return new SentimentRule({...cfg, logger, subredditName, resources, client}); + case 'mhs': + cfg = config as StructuredFilter; + return new MHSRule({...cfg, logger, subredditName, resources, client}); default: throw new Error(`Rule with kind '${config.kind}' was not recognized.`); } diff --git a/src/Rule/index.ts b/src/Rule/index.ts index 6c1272c..03c45a2 100644 --- a/src/Rule/index.ts +++ b/src/Rule/index.ts @@ -185,5 +185,5 @@ export interface RuleJSONConfig extends IRule { * The kind of rule to run * @examples ["recentActivity", "repeatActivity", "author", "attribution", "history"] */ - kind: 'recentActivity' | 'repeatActivity' | 'author' | 'attribution' | 'history' | 'regex' | 'repost' | 'sentiment' + kind: 'recentActivity' | 'repeatActivity' | 'author' | 'attribution' | 'history' | 'regex' | 'repost' | 'sentiment' | 'mhs' } diff --git a/src/Schema/App.json b/src/Schema/App.json index 0a89f0f..76b22f5 100644 --- a/src/Schema/App.json +++ b/src/Schema/App.json @@ -1650,6 +1650,9 @@ { "$ref": "#/definitions/SentimentRuleJSONConfig" }, + { + "$ref": "#/definitions/MHSRuleJSONConfig" + }, { "$ref": "#/definitions/RuleSetConfigData" }, @@ -2782,6 +2785,55 @@ }, "type": "object" }, + "HistoricalMHSConfig": { + "description": "Test the content of Activities from the Author history against MHS criteria\n\nIf this is defined then the `totalMatching` threshold must pass for the Rule to trigger\n\nIf `criteria` is defined here it overrides the top-level `criteria` value", + "properties": { + "criteria": { + "$ref": "#/definitions/MHSCriteriaConfig", + "description": "Criteria used to trigger based on MHS results\n\nIf both `flagged` and `confidence` are specified then both conditions must pass.\n\nBy default, only `flagged` is defined as `true`" + }, + "mustMatchCurrent": { + "default": false, + "description": "When `true` the original Activity being checked MUST pass its criteria before the Rule considers any history", + "type": "boolean" + }, + "totalMatching": { + "default": "> 0", + "description": "A string containing a comparison operator and a value to compare Activities from history that pass the given `criteria` test\n\nThe syntax is `(< OR > OR <= OR >=) [percent sign]`\n\n* EX `> 12` => greater than 12 activities passed given `criteria` test\n* EX `<= 10%` => less than 10% of all Activities from history passed given `criteria` test", + "examples": [ + "> 0", + "> 10%" + ], + "pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$", + "type": "string" + }, + "window": { + "anyOf": [ + { + "$ref": "#/definitions/DurationObject" + }, + { + "$ref": "#/definitions/FullActivityWindowConfig" + }, + { + "type": [ + "string", + "number" + ] + } + ], + "description": "A value to define the range of Activities to retrieve.\n\nAcceptable values:\n\n**`ActivityWindowCriteria` object**\n\nAllows specify multiple range properties and more specific behavior\n\n**A `number` of Activities to retrieve**\n\n* EX `100` => 100 Activities\n\n*****\n\nAny of the below values that specify the amount of time to subtract from `NOW` to create a time range IE `NOW <---> [duration] ago`\n\nAcceptable values:\n\n**A `string` consisting of a value and a [Day.js](https://day.js.org/docs/en/durations/creating#list-of-all-available-units) time UNIT**\n\n* EX `9 days` => Range is `NOW <---> 9 days ago`\n\n**A [Day.js](https://day.js.org/docs/en/durations/creating) `object`**\n\n* EX `{\"days\": 90, \"minutes\": 15}` => Range is `NOW <---> 90 days and 15 minutes ago`\n\n**An [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) `string`**\n\n* EX `PT15M` => 15 minutes => Range is `NOW <----> 15 minutes ago`", + "examples": [ + "90 days" + ] + } + }, + "required": [ + "totalMatching", + "window" + ], + "type": "object" + }, "HistoricalSentimentConfig": { "description": "Test the Sentiment of Activities from the Author history\n\nIf this is defined then the `totalMatching` threshold must pass for the Rule to trigger\n\nIf `sentiment` is defined here it overrides the top-level `sentiment` value", "properties": { @@ -3251,6 +3303,126 @@ ], "type": "object" }, + "MHSCriteriaConfig": { + "description": "Criteria used to trigger based on MHS results\n\nIf both `flagged` and `confidence` are specified then both conditions must pass.\n\nBy default, only `flagged` is defined as `true`", + "properties": { + "confidence": { + "description": "A string containing a comparison operator and a value to compare against the confidence returned from MHS\n\nThe syntax is `(< OR > OR <= OR >=) `\n\n* EX `> 50` => MHS confidence is greater than 50%", + "examples": [ + "> 50" + ], + "pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$", + "type": "string" + }, + "flagged": { + "default": true, + "description": "Test if MHS considers content flagged as toxic or not", + "type": "boolean" + }, + "testOn": { + "default": [ + "body" + ], + "description": "Which content from an Activity to send to MHS\n\nOnly used if the Activity being tested is a Submission -- Comments can be only tested against their body\n\nIf more than one type of content is specified then all text is tested together as one string", + "items": { + "enum": [ + "body", + "title" + ], + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "MHSRuleJSONConfig": { + "description": "Test content of an Activity against the MHS toxicity model for reddit content\n\nRunning this Rule with no configuration will use a default configuration that will cause the Rule to trigger if MHS flags the content of the Activity as toxic.\n\nMore info:\n\n* https://moderatehatespeech.com/docs/\n* https://moderatehatespeech.com/", + "properties": { + "authorIs": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/AuthorCriteria" + }, + { + "$ref": "#/definitions/NamedCriteria" + }, + { + "type": "string" + } + ] + }, + "type": "array" + }, + { + "$ref": "#/definitions/FilterOptionsJson" + } + ], + "description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail." + }, + "criteria": { + "$ref": "#/definitions/MHSCriteriaConfig", + "description": "Criteria used to trigger based on MHS results\n\nIf both `flagged` and `confidence` are specified then both conditions must pass.\n\nBy default, only `flagged` is defined as `true`" + }, + "historical": { + "$ref": "#/definitions/HistoricalMHSConfig", + "description": "run MHS on Activities from the Author history\n\nIf this is defined then the `totalMatching` threshold must pass for the Rule to trigger\n\nIf `criteria` is defined here it overrides the top-level `criteria` value" + }, + "itemIs": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/SubmissionState" + }, + { + "$ref": "#/definitions/CommentState" + }, + { + "$ref": "#/definitions/NamedCriteria" + }, + { + "type": "string" + } + ] + }, + "type": "array" + }, + { + "$ref": "#/definitions/FilterOptionsJson" + } + ], + "description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]" + }, + "kind": { + "default": "mhs", + "description": "The kind of rule to run", + "enum": [ + "mhs" + ], + "examples": [ + "mhs" + ], + "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.", + "examples": [ + "myNewRule" + ], + "pattern": "^[a-zA-Z]([\\w -]*[\\w])?$", + "type": "string" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, "MessageActionJson": { "description": "Send a private message to the Author of the Activity.", "properties": { @@ -5196,6 +5368,9 @@ { "$ref": "#/definitions/SentimentRuleJSONConfig" }, + { + "$ref": "#/definitions/MHSRuleJSONConfig" + }, { "type": "string" } @@ -5989,6 +6164,9 @@ { "$ref": "#/definitions/SentimentRuleJSONConfig" }, + { + "$ref": "#/definitions/MHSRuleJSONConfig" + }, { "$ref": "#/definitions/RuleSetConfigData" }, @@ -6265,6 +6443,17 @@ "ThirdPartyCredentialsJsonConfig": { "additionalProperties": {}, "properties": { + "mhs": { + "properties": { + "apiKey": { + "type": "string" + } + }, + "required": [ + "apiKey" + ], + "type": "object" + }, "youtube": { "properties": { "apiKey": { diff --git a/src/Schema/Check.json b/src/Schema/Check.json index 39b51d7..64da19c 100644 --- a/src/Schema/Check.json +++ b/src/Schema/Check.json @@ -1473,6 +1473,9 @@ { "$ref": "#/definitions/SentimentRuleJSONConfig" }, + { + "$ref": "#/definitions/MHSRuleJSONConfig" + }, { "$ref": "#/definitions/RuleSetConfigData" }, @@ -2496,6 +2499,55 @@ }, "type": "object" }, + "HistoricalMHSConfig": { + "description": "Test the content of Activities from the Author history against MHS criteria\n\nIf this is defined then the `totalMatching` threshold must pass for the Rule to trigger\n\nIf `criteria` is defined here it overrides the top-level `criteria` value", + "properties": { + "criteria": { + "$ref": "#/definitions/MHSCriteriaConfig", + "description": "Criteria used to trigger based on MHS results\n\nIf both `flagged` and `confidence` are specified then both conditions must pass.\n\nBy default, only `flagged` is defined as `true`" + }, + "mustMatchCurrent": { + "default": false, + "description": "When `true` the original Activity being checked MUST pass its criteria before the Rule considers any history", + "type": "boolean" + }, + "totalMatching": { + "default": "> 0", + "description": "A string containing a comparison operator and a value to compare Activities from history that pass the given `criteria` test\n\nThe syntax is `(< OR > OR <= OR >=) [percent sign]`\n\n* EX `> 12` => greater than 12 activities passed given `criteria` test\n* EX `<= 10%` => less than 10% of all Activities from history passed given `criteria` test", + "examples": [ + "> 0", + "> 10%" + ], + "pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$", + "type": "string" + }, + "window": { + "anyOf": [ + { + "$ref": "#/definitions/DurationObject" + }, + { + "$ref": "#/definitions/FullActivityWindowConfig" + }, + { + "type": [ + "string", + "number" + ] + } + ], + "description": "A value to define the range of Activities to retrieve.\n\nAcceptable values:\n\n**`ActivityWindowCriteria` object**\n\nAllows specify multiple range properties and more specific behavior\n\n**A `number` of Activities to retrieve**\n\n* EX `100` => 100 Activities\n\n*****\n\nAny of the below values that specify the amount of time to subtract from `NOW` to create a time range IE `NOW <---> [duration] ago`\n\nAcceptable values:\n\n**A `string` consisting of a value and a [Day.js](https://day.js.org/docs/en/durations/creating#list-of-all-available-units) time UNIT**\n\n* EX `9 days` => Range is `NOW <---> 9 days ago`\n\n**A [Day.js](https://day.js.org/docs/en/durations/creating) `object`**\n\n* EX `{\"days\": 90, \"minutes\": 15}` => Range is `NOW <---> 90 days and 15 minutes ago`\n\n**An [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) `string`**\n\n* EX `PT15M` => 15 minutes => Range is `NOW <----> 15 minutes ago`", + "examples": [ + "90 days" + ] + } + }, + "required": [ + "totalMatching", + "window" + ], + "type": "object" + }, "HistoricalSentimentConfig": { "description": "Test the Sentiment of Activities from the Author history\n\nIf this is defined then the `totalMatching` threshold must pass for the Rule to trigger\n\nIf `sentiment` is defined here it overrides the top-level `sentiment` value", "properties": { @@ -2965,6 +3017,126 @@ ], "type": "object" }, + "MHSCriteriaConfig": { + "description": "Criteria used to trigger based on MHS results\n\nIf both `flagged` and `confidence` are specified then both conditions must pass.\n\nBy default, only `flagged` is defined as `true`", + "properties": { + "confidence": { + "description": "A string containing a comparison operator and a value to compare against the confidence returned from MHS\n\nThe syntax is `(< OR > OR <= OR >=) `\n\n* EX `> 50` => MHS confidence is greater than 50%", + "examples": [ + "> 50" + ], + "pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$", + "type": "string" + }, + "flagged": { + "default": true, + "description": "Test if MHS considers content flagged as toxic or not", + "type": "boolean" + }, + "testOn": { + "default": [ + "body" + ], + "description": "Which content from an Activity to send to MHS\n\nOnly used if the Activity being tested is a Submission -- Comments can be only tested against their body\n\nIf more than one type of content is specified then all text is tested together as one string", + "items": { + "enum": [ + "body", + "title" + ], + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "MHSRuleJSONConfig": { + "description": "Test content of an Activity against the MHS toxicity model for reddit content\n\nRunning this Rule with no configuration will use a default configuration that will cause the Rule to trigger if MHS flags the content of the Activity as toxic.\n\nMore info:\n\n* https://moderatehatespeech.com/docs/\n* https://moderatehatespeech.com/", + "properties": { + "authorIs": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/AuthorCriteria" + }, + { + "$ref": "#/definitions/NamedCriteria" + }, + { + "type": "string" + } + ] + }, + "type": "array" + }, + { + "$ref": "#/definitions/FilterOptionsJson" + } + ], + "description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail." + }, + "criteria": { + "$ref": "#/definitions/MHSCriteriaConfig", + "description": "Criteria used to trigger based on MHS results\n\nIf both `flagged` and `confidence` are specified then both conditions must pass.\n\nBy default, only `flagged` is defined as `true`" + }, + "historical": { + "$ref": "#/definitions/HistoricalMHSConfig", + "description": "run MHS on Activities from the Author history\n\nIf this is defined then the `totalMatching` threshold must pass for the Rule to trigger\n\nIf `criteria` is defined here it overrides the top-level `criteria` value" + }, + "itemIs": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/SubmissionState" + }, + { + "$ref": "#/definitions/CommentState" + }, + { + "$ref": "#/definitions/NamedCriteria" + }, + { + "type": "string" + } + ] + }, + "type": "array" + }, + { + "$ref": "#/definitions/FilterOptionsJson" + } + ], + "description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]" + }, + "kind": { + "default": "mhs", + "description": "The kind of rule to run", + "enum": [ + "mhs" + ], + "examples": [ + "mhs" + ], + "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.", + "examples": [ + "myNewRule" + ], + "pattern": "^[a-zA-Z]([\\w -]*[\\w])?$", + "type": "string" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, "MessageActionJson": { "description": "Send a private message to the Author of the Activity.", "properties": { @@ -4770,6 +4942,9 @@ { "$ref": "#/definitions/SentimentRuleJSONConfig" }, + { + "$ref": "#/definitions/MHSRuleJSONConfig" + }, { "type": "string" } @@ -5433,6 +5608,9 @@ { "$ref": "#/definitions/SentimentRuleJSONConfig" }, + { + "$ref": "#/definitions/MHSRuleJSONConfig" + }, { "$ref": "#/definitions/RuleSetConfigData" }, diff --git a/src/Schema/OperatorConfig.json b/src/Schema/OperatorConfig.json index 237e2a3..f5605f0 100644 --- a/src/Schema/OperatorConfig.json +++ b/src/Schema/OperatorConfig.json @@ -187,6 +187,17 @@ }, "BotCredentialsJsonConfig": { "properties": { + "mhs": { + "properties": { + "apiKey": { + "type": "string" + } + }, + "required": [ + "apiKey" + ], + "type": "object" + }, "reddit": { "$ref": "#/definitions/RedditCredentials" }, @@ -949,7 +960,7 @@ "file": { "allOf": [ { - "$ref": "#/definitions/Omit" + "$ref": "#/definitions/Omit" }, { "properties": { @@ -1396,7 +1407,7 @@ ], "type": "object" }, - "Omit": { + "Omit": { "properties": { "auditFile": { "description": "A string representing the name of the name of the audit file. (default: './hash-audit.json')", @@ -2077,6 +2088,17 @@ "ThirdPartyCredentialsJsonConfig": { "additionalProperties": {}, "properties": { + "mhs": { + "properties": { + "apiKey": { + "type": "string" + } + }, + "required": [ + "apiKey" + ], + "type": "object" + }, "youtube": { "properties": { "apiKey": { diff --git a/src/Schema/Rule.json b/src/Schema/Rule.json index 37b669e..edd35c7 100644 --- a/src/Schema/Rule.json +++ b/src/Schema/Rule.json @@ -28,6 +28,9 @@ { "$ref": "#/definitions/SentimentRuleJSONConfig" }, + { + "$ref": "#/definitions/MHSRuleJSONConfig" + }, { "type": "string" } @@ -1345,6 +1348,55 @@ }, "type": "object" }, + "HistoricalMHSConfig": { + "description": "Test the content of Activities from the Author history against MHS criteria\n\nIf this is defined then the `totalMatching` threshold must pass for the Rule to trigger\n\nIf `criteria` is defined here it overrides the top-level `criteria` value", + "properties": { + "criteria": { + "$ref": "#/definitions/MHSCriteriaConfig", + "description": "Criteria used to trigger based on MHS results\n\nIf both `flagged` and `confidence` are specified then both conditions must pass.\n\nBy default, only `flagged` is defined as `true`" + }, + "mustMatchCurrent": { + "default": false, + "description": "When `true` the original Activity being checked MUST pass its criteria before the Rule considers any history", + "type": "boolean" + }, + "totalMatching": { + "default": "> 0", + "description": "A string containing a comparison operator and a value to compare Activities from history that pass the given `criteria` test\n\nThe syntax is `(< OR > OR <= OR >=) [percent sign]`\n\n* EX `> 12` => greater than 12 activities passed given `criteria` test\n* EX `<= 10%` => less than 10% of all Activities from history passed given `criteria` test", + "examples": [ + "> 0", + "> 10%" + ], + "pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$", + "type": "string" + }, + "window": { + "anyOf": [ + { + "$ref": "#/definitions/DurationObject" + }, + { + "$ref": "#/definitions/FullActivityWindowConfig" + }, + { + "type": [ + "string", + "number" + ] + } + ], + "description": "A value to define the range of Activities to retrieve.\n\nAcceptable values:\n\n**`ActivityWindowCriteria` object**\n\nAllows specify multiple range properties and more specific behavior\n\n**A `number` of Activities to retrieve**\n\n* EX `100` => 100 Activities\n\n*****\n\nAny of the below values that specify the amount of time to subtract from `NOW` to create a time range IE `NOW <---> [duration] ago`\n\nAcceptable values:\n\n**A `string` consisting of a value and a [Day.js](https://day.js.org/docs/en/durations/creating#list-of-all-available-units) time UNIT**\n\n* EX `9 days` => Range is `NOW <---> 9 days ago`\n\n**A [Day.js](https://day.js.org/docs/en/durations/creating) `object`**\n\n* EX `{\"days\": 90, \"minutes\": 15}` => Range is `NOW <---> 90 days and 15 minutes ago`\n\n**An [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) `string`**\n\n* EX `PT15M` => 15 minutes => Range is `NOW <----> 15 minutes ago`", + "examples": [ + "90 days" + ] + } + }, + "required": [ + "totalMatching", + "window" + ], + "type": "object" + }, "HistoricalSentimentConfig": { "description": "Test the Sentiment of Activities from the Author history\n\nIf this is defined then the `totalMatching` threshold must pass for the Rule to trigger\n\nIf `sentiment` is defined here it overrides the top-level `sentiment` value", "properties": { @@ -1722,6 +1774,126 @@ ], "type": "object" }, + "MHSCriteriaConfig": { + "description": "Criteria used to trigger based on MHS results\n\nIf both `flagged` and `confidence` are specified then both conditions must pass.\n\nBy default, only `flagged` is defined as `true`", + "properties": { + "confidence": { + "description": "A string containing a comparison operator and a value to compare against the confidence returned from MHS\n\nThe syntax is `(< OR > OR <= OR >=) `\n\n* EX `> 50` => MHS confidence is greater than 50%", + "examples": [ + "> 50" + ], + "pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$", + "type": "string" + }, + "flagged": { + "default": true, + "description": "Test if MHS considers content flagged as toxic or not", + "type": "boolean" + }, + "testOn": { + "default": [ + "body" + ], + "description": "Which content from an Activity to send to MHS\n\nOnly used if the Activity being tested is a Submission -- Comments can be only tested against their body\n\nIf more than one type of content is specified then all text is tested together as one string", + "items": { + "enum": [ + "body", + "title" + ], + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "MHSRuleJSONConfig": { + "description": "Test content of an Activity against the MHS toxicity model for reddit content\n\nRunning this Rule with no configuration will use a default configuration that will cause the Rule to trigger if MHS flags the content of the Activity as toxic.\n\nMore info:\n\n* https://moderatehatespeech.com/docs/\n* https://moderatehatespeech.com/", + "properties": { + "authorIs": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/AuthorCriteria" + }, + { + "$ref": "#/definitions/NamedCriteria" + }, + { + "type": "string" + } + ] + }, + "type": "array" + }, + { + "$ref": "#/definitions/FilterOptionsJson" + } + ], + "description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail." + }, + "criteria": { + "$ref": "#/definitions/MHSCriteriaConfig", + "description": "Criteria used to trigger based on MHS results\n\nIf both `flagged` and `confidence` are specified then both conditions must pass.\n\nBy default, only `flagged` is defined as `true`" + }, + "historical": { + "$ref": "#/definitions/HistoricalMHSConfig", + "description": "run MHS on Activities from the Author history\n\nIf this is defined then the `totalMatching` threshold must pass for the Rule to trigger\n\nIf `criteria` is defined here it overrides the top-level `criteria` value" + }, + "itemIs": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/SubmissionState" + }, + { + "$ref": "#/definitions/CommentState" + }, + { + "$ref": "#/definitions/NamedCriteria" + }, + { + "type": "string" + } + ] + }, + "type": "array" + }, + { + "$ref": "#/definitions/FilterOptionsJson" + } + ], + "description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]" + }, + "kind": { + "default": "mhs", + "description": "The kind of rule to run", + "enum": [ + "mhs" + ], + "examples": [ + "mhs" + ], + "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.", + "examples": [ + "myNewRule" + ], + "pattern": "^[a-zA-Z]([\\w -]*[\\w])?$", + "type": "string" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, "ModLogCriteria": { "properties": { "action": { diff --git a/src/Schema/RuleSet.json b/src/Schema/RuleSet.json index 982f4fe..74c1243 100644 --- a/src/Schema/RuleSet.json +++ b/src/Schema/RuleSet.json @@ -1313,6 +1313,55 @@ }, "type": "object" }, + "HistoricalMHSConfig": { + "description": "Test the content of Activities from the Author history against MHS criteria\n\nIf this is defined then the `totalMatching` threshold must pass for the Rule to trigger\n\nIf `criteria` is defined here it overrides the top-level `criteria` value", + "properties": { + "criteria": { + "$ref": "#/definitions/MHSCriteriaConfig", + "description": "Criteria used to trigger based on MHS results\n\nIf both `flagged` and `confidence` are specified then both conditions must pass.\n\nBy default, only `flagged` is defined as `true`" + }, + "mustMatchCurrent": { + "default": false, + "description": "When `true` the original Activity being checked MUST pass its criteria before the Rule considers any history", + "type": "boolean" + }, + "totalMatching": { + "default": "> 0", + "description": "A string containing a comparison operator and a value to compare Activities from history that pass the given `criteria` test\n\nThe syntax is `(< OR > OR <= OR >=) [percent sign]`\n\n* EX `> 12` => greater than 12 activities passed given `criteria` test\n* EX `<= 10%` => less than 10% of all Activities from history passed given `criteria` test", + "examples": [ + "> 0", + "> 10%" + ], + "pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$", + "type": "string" + }, + "window": { + "anyOf": [ + { + "$ref": "#/definitions/DurationObject" + }, + { + "$ref": "#/definitions/FullActivityWindowConfig" + }, + { + "type": [ + "string", + "number" + ] + } + ], + "description": "A value to define the range of Activities to retrieve.\n\nAcceptable values:\n\n**`ActivityWindowCriteria` object**\n\nAllows specify multiple range properties and more specific behavior\n\n**A `number` of Activities to retrieve**\n\n* EX `100` => 100 Activities\n\n*****\n\nAny of the below values that specify the amount of time to subtract from `NOW` to create a time range IE `NOW <---> [duration] ago`\n\nAcceptable values:\n\n**A `string` consisting of a value and a [Day.js](https://day.js.org/docs/en/durations/creating#list-of-all-available-units) time UNIT**\n\n* EX `9 days` => Range is `NOW <---> 9 days ago`\n\n**A [Day.js](https://day.js.org/docs/en/durations/creating) `object`**\n\n* EX `{\"days\": 90, \"minutes\": 15}` => Range is `NOW <---> 90 days and 15 minutes ago`\n\n**An [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) `string`**\n\n* EX `PT15M` => 15 minutes => Range is `NOW <----> 15 minutes ago`", + "examples": [ + "90 days" + ] + } + }, + "required": [ + "totalMatching", + "window" + ], + "type": "object" + }, "HistoricalSentimentConfig": { "description": "Test the Sentiment of Activities from the Author history\n\nIf this is defined then the `totalMatching` threshold must pass for the Rule to trigger\n\nIf `sentiment` is defined here it overrides the top-level `sentiment` value", "properties": { @@ -1690,6 +1739,126 @@ ], "type": "object" }, + "MHSCriteriaConfig": { + "description": "Criteria used to trigger based on MHS results\n\nIf both `flagged` and `confidence` are specified then both conditions must pass.\n\nBy default, only `flagged` is defined as `true`", + "properties": { + "confidence": { + "description": "A string containing a comparison operator and a value to compare against the confidence returned from MHS\n\nThe syntax is `(< OR > OR <= OR >=) `\n\n* EX `> 50` => MHS confidence is greater than 50%", + "examples": [ + "> 50" + ], + "pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$", + "type": "string" + }, + "flagged": { + "default": true, + "description": "Test if MHS considers content flagged as toxic or not", + "type": "boolean" + }, + "testOn": { + "default": [ + "body" + ], + "description": "Which content from an Activity to send to MHS\n\nOnly used if the Activity being tested is a Submission -- Comments can be only tested against their body\n\nIf more than one type of content is specified then all text is tested together as one string", + "items": { + "enum": [ + "body", + "title" + ], + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "MHSRuleJSONConfig": { + "description": "Test content of an Activity against the MHS toxicity model for reddit content\n\nRunning this Rule with no configuration will use a default configuration that will cause the Rule to trigger if MHS flags the content of the Activity as toxic.\n\nMore info:\n\n* https://moderatehatespeech.com/docs/\n* https://moderatehatespeech.com/", + "properties": { + "authorIs": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/AuthorCriteria" + }, + { + "$ref": "#/definitions/NamedCriteria" + }, + { + "type": "string" + } + ] + }, + "type": "array" + }, + { + "$ref": "#/definitions/FilterOptionsJson" + } + ], + "description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail." + }, + "criteria": { + "$ref": "#/definitions/MHSCriteriaConfig", + "description": "Criteria used to trigger based on MHS results\n\nIf both `flagged` and `confidence` are specified then both conditions must pass.\n\nBy default, only `flagged` is defined as `true`" + }, + "historical": { + "$ref": "#/definitions/HistoricalMHSConfig", + "description": "run MHS on Activities from the Author history\n\nIf this is defined then the `totalMatching` threshold must pass for the Rule to trigger\n\nIf `criteria` is defined here it overrides the top-level `criteria` value" + }, + "itemIs": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/SubmissionState" + }, + { + "$ref": "#/definitions/CommentState" + }, + { + "$ref": "#/definitions/NamedCriteria" + }, + { + "type": "string" + } + ] + }, + "type": "array" + }, + { + "$ref": "#/definitions/FilterOptionsJson" + } + ], + "description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]" + }, + "kind": { + "default": "mhs", + "description": "The kind of rule to run", + "enum": [ + "mhs" + ], + "examples": [ + "mhs" + ], + "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.", + "examples": [ + "myNewRule" + ], + "pattern": "^[a-zA-Z]([\\w -]*[\\w])?$", + "type": "string" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, "ModLogCriteria": { "properties": { "action": { @@ -3552,6 +3721,9 @@ { "$ref": "#/definitions/SentimentRuleJSONConfig" }, + { + "$ref": "#/definitions/MHSRuleJSONConfig" + }, { "type": "string" } diff --git a/src/Schema/Run.json b/src/Schema/Run.json index c1de1d9..b7b2850 100644 --- a/src/Schema/Run.json +++ b/src/Schema/Run.json @@ -1470,6 +1470,9 @@ { "$ref": "#/definitions/SentimentRuleJSONConfig" }, + { + "$ref": "#/definitions/MHSRuleJSONConfig" + }, { "$ref": "#/definitions/RuleSetConfigData" }, @@ -2563,6 +2566,55 @@ }, "type": "object" }, + "HistoricalMHSConfig": { + "description": "Test the content of Activities from the Author history against MHS criteria\n\nIf this is defined then the `totalMatching` threshold must pass for the Rule to trigger\n\nIf `criteria` is defined here it overrides the top-level `criteria` value", + "properties": { + "criteria": { + "$ref": "#/definitions/MHSCriteriaConfig", + "description": "Criteria used to trigger based on MHS results\n\nIf both `flagged` and `confidence` are specified then both conditions must pass.\n\nBy default, only `flagged` is defined as `true`" + }, + "mustMatchCurrent": { + "default": false, + "description": "When `true` the original Activity being checked MUST pass its criteria before the Rule considers any history", + "type": "boolean" + }, + "totalMatching": { + "default": "> 0", + "description": "A string containing a comparison operator and a value to compare Activities from history that pass the given `criteria` test\n\nThe syntax is `(< OR > OR <= OR >=) [percent sign]`\n\n* EX `> 12` => greater than 12 activities passed given `criteria` test\n* EX `<= 10%` => less than 10% of all Activities from history passed given `criteria` test", + "examples": [ + "> 0", + "> 10%" + ], + "pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$", + "type": "string" + }, + "window": { + "anyOf": [ + { + "$ref": "#/definitions/DurationObject" + }, + { + "$ref": "#/definitions/FullActivityWindowConfig" + }, + { + "type": [ + "string", + "number" + ] + } + ], + "description": "A value to define the range of Activities to retrieve.\n\nAcceptable values:\n\n**`ActivityWindowCriteria` object**\n\nAllows specify multiple range properties and more specific behavior\n\n**A `number` of Activities to retrieve**\n\n* EX `100` => 100 Activities\n\n*****\n\nAny of the below values that specify the amount of time to subtract from `NOW` to create a time range IE `NOW <---> [duration] ago`\n\nAcceptable values:\n\n**A `string` consisting of a value and a [Day.js](https://day.js.org/docs/en/durations/creating#list-of-all-available-units) time UNIT**\n\n* EX `9 days` => Range is `NOW <---> 9 days ago`\n\n**A [Day.js](https://day.js.org/docs/en/durations/creating) `object`**\n\n* EX `{\"days\": 90, \"minutes\": 15}` => Range is `NOW <---> 90 days and 15 minutes ago`\n\n**An [ISO 8601 duration](https://en.wikipedia.org/wiki/ISO_8601#Durations) `string`**\n\n* EX `PT15M` => 15 minutes => Range is `NOW <----> 15 minutes ago`", + "examples": [ + "90 days" + ] + } + }, + "required": [ + "totalMatching", + "window" + ], + "type": "object" + }, "HistoricalSentimentConfig": { "description": "Test the Sentiment of Activities from the Author history\n\nIf this is defined then the `totalMatching` threshold must pass for the Rule to trigger\n\nIf `sentiment` is defined here it overrides the top-level `sentiment` value", "properties": { @@ -3032,6 +3084,126 @@ ], "type": "object" }, + "MHSCriteriaConfig": { + "description": "Criteria used to trigger based on MHS results\n\nIf both `flagged` and `confidence` are specified then both conditions must pass.\n\nBy default, only `flagged` is defined as `true`", + "properties": { + "confidence": { + "description": "A string containing a comparison operator and a value to compare against the confidence returned from MHS\n\nThe syntax is `(< OR > OR <= OR >=) `\n\n* EX `> 50` => MHS confidence is greater than 50%", + "examples": [ + "> 50" + ], + "pattern": "^\\s*(>|>=|<|<=)\\s*(\\d+)\\s*(%?)(.*)$", + "type": "string" + }, + "flagged": { + "default": true, + "description": "Test if MHS considers content flagged as toxic or not", + "type": "boolean" + }, + "testOn": { + "default": [ + "body" + ], + "description": "Which content from an Activity to send to MHS\n\nOnly used if the Activity being tested is a Submission -- Comments can be only tested against their body\n\nIf more than one type of content is specified then all text is tested together as one string", + "items": { + "enum": [ + "body", + "title" + ], + "type": "string" + }, + "type": "array" + } + }, + "type": "object" + }, + "MHSRuleJSONConfig": { + "description": "Test content of an Activity against the MHS toxicity model for reddit content\n\nRunning this Rule with no configuration will use a default configuration that will cause the Rule to trigger if MHS flags the content of the Activity as toxic.\n\nMore info:\n\n* https://moderatehatespeech.com/docs/\n* https://moderatehatespeech.com/", + "properties": { + "authorIs": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/AuthorCriteria" + }, + { + "$ref": "#/definitions/NamedCriteria" + }, + { + "type": "string" + } + ] + }, + "type": "array" + }, + { + "$ref": "#/definitions/FilterOptionsJson" + } + ], + "description": "If present then these Author criteria are checked before running the Check. If criteria fails then the Check will fail." + }, + "criteria": { + "$ref": "#/definitions/MHSCriteriaConfig", + "description": "Criteria used to trigger based on MHS results\n\nIf both `flagged` and `confidence` are specified then both conditions must pass.\n\nBy default, only `flagged` is defined as `true`" + }, + "historical": { + "$ref": "#/definitions/HistoricalMHSConfig", + "description": "run MHS on Activities from the Author history\n\nIf this is defined then the `totalMatching` threshold must pass for the Rule to trigger\n\nIf `criteria` is defined here it overrides the top-level `criteria` value" + }, + "itemIs": { + "anyOf": [ + { + "items": { + "anyOf": [ + { + "$ref": "#/definitions/SubmissionState" + }, + { + "$ref": "#/definitions/CommentState" + }, + { + "$ref": "#/definitions/NamedCriteria" + }, + { + "type": "string" + } + ] + }, + "type": "array" + }, + { + "$ref": "#/definitions/FilterOptionsJson" + } + ], + "description": "A list of criteria to test the state of the `Activity` against before running the check.\n\nIf any set of criteria passes the Check will be run. If the criteria fails then the Check will fail.\n\n* @examples [[{\"over_18\": true, \"removed': false}]]" + }, + "kind": { + "default": "mhs", + "description": "The kind of rule to run", + "enum": [ + "mhs" + ], + "examples": [ + "mhs" + ], + "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.", + "examples": [ + "myNewRule" + ], + "pattern": "^[a-zA-Z]([\\w -]*[\\w])?$", + "type": "string" + } + }, + "required": [ + "kind" + ], + "type": "object" + }, "MessageActionJson": { "description": "Send a private message to the Author of the Activity.", "properties": { @@ -4837,6 +5009,9 @@ { "$ref": "#/definitions/SentimentRuleJSONConfig" }, + { + "$ref": "#/definitions/MHSRuleJSONConfig" + }, { "type": "string" } @@ -5630,6 +5805,9 @@ { "$ref": "#/definitions/SentimentRuleJSONConfig" }, + { + "$ref": "#/definitions/MHSRuleJSONConfig" + }, { "$ref": "#/definitions/RuleSetConfigData" }, diff --git a/src/Subreddit/SubredditResources.ts b/src/Subreddit/SubredditResources.ts index 8d20af5..3125702 100644 --- a/src/Subreddit/SubredditResources.ts +++ b/src/Subreddit/SubredditResources.ts @@ -223,7 +223,7 @@ export class SubredditResources { protected useSubredditAuthorCache!: boolean; protected authorTTL: number | false = cacheTTLDefaults.authorTTL; protected subredditTTL: number | false = cacheTTLDefaults.subredditTTL; - protected wikiTTL: number | false = cacheTTLDefaults.wikiTTL; + public wikiTTL: number | false = cacheTTLDefaults.wikiTTL; protected submissionTTL: number | false = cacheTTLDefaults.submissionTTL; protected commentTTL: number | false = cacheTTLDefaults.commentTTL; protected filterCriteriaTTL: number | false = cacheTTLDefaults.filterCriteriaTTL;