mirror of
https://github.com/FoxxMD/context-mod.git
synced 2026-04-19 03:00:07 -04:00
refactor: Improve activity source parse and comparison
* Implement a DTO class for activity source to make parts usage (type, identifier) and matching easier * Implement regex to parse type and identifier from activity source string * Refactor activity source interface/types to better distinguish as string, data, and class
This commit is contained in:
34
src/Common/ActivitySource.ts
Normal file
34
src/Common/ActivitySource.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {ActivitySourceData, ActivitySourceTypes} from "./Infrastructure/Atomic";
|
||||
import {strToActivitySourceData} from "../util";
|
||||
|
||||
export class ActivitySource {
|
||||
type: ActivitySourceTypes
|
||||
identifier?: string
|
||||
|
||||
constructor(data: string | ActivitySourceData) {
|
||||
if (typeof data === 'string') {
|
||||
const {type, identifier} = strToActivitySourceData(data);
|
||||
this.type = type;
|
||||
this.identifier = identifier;
|
||||
} else {
|
||||
this.type = data.type;
|
||||
this.identifier = data.identifier;
|
||||
}
|
||||
}
|
||||
|
||||
matches(desired: ActivitySource): boolean {
|
||||
if(desired.type !== this.type) {
|
||||
return false;
|
||||
}
|
||||
// if this source does not have an identifier (we have already matched type) then it is broad enough to match
|
||||
if(this.identifier === undefined) {
|
||||
return true;
|
||||
}
|
||||
// at this point we know this source has an identifier but desired DOES NOT so this source is more restrictive and does not match
|
||||
if(desired.identifier === undefined) {
|
||||
return false;
|
||||
}
|
||||
// otherwise sources match if identifiers are the same
|
||||
return this.identifier.toLowerCase() === desired.identifier.toLowerCase();
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import Submission from "snoowrap/dist/objects/Submission";
|
||||
import Comment from "snoowrap/dist/objects/Comment";
|
||||
import {ColumnDurationTransformer} from "./Transformers";
|
||||
import { RedditUser } from "snoowrap/dist/objects";
|
||||
import {ActivitySourceTypes, DurationVal, NonDispatchActivitySource, onExistingFoundBehavior} from "../Infrastructure/Atomic";
|
||||
import {ActivitySourceTypes, DurationVal, NonDispatchActivitySourceValue, onExistingFoundBehavior} from "../Infrastructure/Atomic";
|
||||
|
||||
@Entity({name: 'DispatchedAction'})
|
||||
export class DispatchedEntity extends TimeAwareRandomBaseEntity {
|
||||
@@ -53,7 +53,7 @@ export class DispatchedEntity extends TimeAwareRandomBaseEntity {
|
||||
identifier?: string
|
||||
|
||||
@Column("varchar", {nullable: true, length: 200})
|
||||
cancelIfQueued?: boolean | NonDispatchActivitySource | NonDispatchActivitySource[]
|
||||
cancelIfQueued?: boolean | NonDispatchActivitySourceValue | NonDispatchActivitySourceValue[]
|
||||
|
||||
@Column({nullable: true})
|
||||
onExistingFound?: onExistingFoundBehavior
|
||||
@@ -127,7 +127,7 @@ export class DispatchedEntity extends TimeAwareRandomBaseEntity {
|
||||
} else if (cVal === 'false') {
|
||||
this.cancelIfQueued = false;
|
||||
} else if (cVal.includes('[')) {
|
||||
this.cancelIfQueued = JSON.parse(cVal) as NonDispatchActivitySource[];
|
||||
this.cancelIfQueued = JSON.parse(cVal) as NonDispatchActivitySourceValue[];
|
||||
}
|
||||
}
|
||||
if(this.goto === null) {
|
||||
|
||||
@@ -168,9 +168,16 @@ export type onExistingFoundBehavior = 'replace' | 'skip' | 'ignore';
|
||||
export type ActionTarget = 'self' | 'parent';
|
||||
export type ArbitraryActionTarget = ActionTarget | string;
|
||||
export type InclusiveActionTarget = ActionTarget | 'any';
|
||||
export type DispatchSource = 'dispatch' | `dispatch:${string}`;
|
||||
export type NonDispatchActivitySource = 'poll' | `poll:${PollOn}` | 'user' | `user:${string}`;
|
||||
export type ActivitySourceTypes = 'poll' | 'dispatch' | 'user'; // TODO
|
||||
export const SOURCE_POLL = 'poll';
|
||||
export type SourcePollStr = 'poll';
|
||||
export const SOURCE_DISPATCH = 'dispatch';
|
||||
export type SourceDispatchStr = 'dispatch';
|
||||
export const SOURCE_USER = 'user';
|
||||
export type SourceUserStr = 'user';
|
||||
|
||||
export type DispatchSourceValue = SourceDispatchStr | `dispatch:${string}`;
|
||||
export type NonDispatchActivitySourceValue = SourcePollStr | `poll:${PollOn}` | SourceUserStr | `user:${string}`;
|
||||
export type ActivitySourceTypes = SourcePollStr | SourceDispatchStr | SourceUserStr; // TODO
|
||||
// https://github.com/YousefED/typescript-json-schema/issues/426
|
||||
// https://github.com/YousefED/typescript-json-schema/issues/425
|
||||
// @pattern ^(((poll|dispatch)(:\w+)?)|user)$
|
||||
@@ -188,7 +195,12 @@ export type ActivitySourceTypes = 'poll' | 'dispatch' | 'user'; // TODO
|
||||
*
|
||||
*
|
||||
* */
|
||||
export type ActivitySource = NonDispatchActivitySource | DispatchSource;
|
||||
export type ActivitySourceValue = NonDispatchActivitySourceValue | DispatchSourceValue;
|
||||
|
||||
export interface ActivitySourceData {
|
||||
type: ActivitySourceTypes
|
||||
identifier?: string
|
||||
}
|
||||
|
||||
export type ConfigFormat = 'json' | 'yaml';
|
||||
export type ActionTypes =
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
DurationVal,
|
||||
EventRetentionPolicyRange,
|
||||
JoinOperands,
|
||||
NonDispatchActivitySource,
|
||||
NonDispatchActivitySourceValue,
|
||||
NotificationEventType,
|
||||
NotificationProvider,
|
||||
onExistingFoundBehavior,
|
||||
@@ -1967,7 +1967,7 @@ export type RequiredItemCrit = Required<(CommentState & SubmissionState)>;
|
||||
|
||||
export interface ActivityDispatchConfig {
|
||||
identifier?: string
|
||||
cancelIfQueued?: boolean | NonDispatchActivitySource | NonDispatchActivitySource[]
|
||||
cancelIfQueued?: boolean | NonDispatchActivitySourceValue | NonDispatchActivitySourceValue[]
|
||||
goto?: string
|
||||
onExistingFound?: onExistingFoundBehavior
|
||||
tardyTolerant?: boolean | DurationVal
|
||||
|
||||
@@ -94,8 +94,7 @@ import {InvokeeType} from "../Common/Entities/InvokeeType";
|
||||
import {RunStateType} from "../Common/Entities/RunStateType";
|
||||
import {EntityRunState} from "../Common/Entities/EntityRunState/EntityRunState";
|
||||
import {
|
||||
ActivitySource,
|
||||
DispatchSource,
|
||||
ActivitySourceValue,
|
||||
EventRetentionPolicyRange,
|
||||
Invokee,
|
||||
PollOn,
|
||||
@@ -128,7 +127,7 @@ export interface runCheckOptions {
|
||||
force?: boolean,
|
||||
gotoContext?: string
|
||||
maxGotoDepth?: number
|
||||
source: ActivitySource
|
||||
source: ActivitySourceValue
|
||||
initialGoto?: string
|
||||
activitySource: ActivitySourceData
|
||||
disableDispatchDelays?: boolean
|
||||
|
||||
@@ -41,7 +41,6 @@ import {
|
||||
redisScanIterator,
|
||||
removeUndefinedKeys,
|
||||
shouldCacheSubredditStateCriteriaResult,
|
||||
strToActivitySource,
|
||||
subredditStateIsNameOnly,
|
||||
testMaybeStringRegex,
|
||||
toStrongSubredditState,
|
||||
@@ -119,7 +118,7 @@ import {
|
||||
UserNoteCriteria
|
||||
} from "../Common/Infrastructure/Filters/FilterCriteria";
|
||||
import {
|
||||
ActivitySource, ConfigFragmentValidationFunc, DurationVal,
|
||||
ActivitySourceValue, ConfigFragmentValidationFunc, DurationVal,
|
||||
EventRetentionPolicyRange, ImageHashCacheData,
|
||||
JoinOperands,
|
||||
ModActionType,
|
||||
@@ -162,6 +161,7 @@ import {parseFromJsonOrYamlToObject} from "../Common/Config/ConfigUtil";
|
||||
import ConfigParseError from "../Utils/ConfigParseError";
|
||||
import {ActivityReport} from "../Common/Entities/ActivityReport";
|
||||
import {ActionResultEntity} from "../Common/Entities/ActionResultEntity";
|
||||
import {ActivitySource} from "../Common/ActivitySource";
|
||||
|
||||
export const DEFAULT_FOOTER = '\r\n*****\r\nThis action was performed by [a bot.]({{botLink}}) Mention a moderator or [send a modmail]({{modmailLink}}) if you have any ideas, questions, or concerns about this action.';
|
||||
|
||||
@@ -2090,7 +2090,7 @@ export class SubredditResources {
|
||||
return res;
|
||||
}
|
||||
|
||||
async testItemCriteria(i: (Comment | Submission), activityStateObj: NamedCriteria<TypedActivityState>, logger: Logger, include = true, source?: ActivitySource): Promise<FilterCriteriaResult<TypedActivityState>> {
|
||||
async testItemCriteria(i: (Comment | Submission), activityStateObj: NamedCriteria<TypedActivityState>, logger: Logger, include = true, source?: ActivitySourceValue): Promise<FilterCriteriaResult<TypedActivityState>> {
|
||||
const {criteria: activityState} = activityStateObj;
|
||||
if(Object.keys(activityState).length === 0) {
|
||||
return {
|
||||
@@ -2254,7 +2254,7 @@ export class SubredditResources {
|
||||
})() as boolean;
|
||||
}
|
||||
|
||||
async isItem (item: Submission | Comment, stateCriteria: TypedActivityState, logger: Logger, include: boolean, source?: ActivitySource): Promise<FilterCriteriaResult<(SubmissionState & CommentState)>> {
|
||||
async isItem (item: Submission | Comment, stateCriteria: TypedActivityState, logger: Logger, include: boolean, source?: ActivitySourceValue): Promise<FilterCriteriaResult<(SubmissionState & CommentState)>> {
|
||||
|
||||
//const definedStateCriteria = (removeUndefinedKeys(stateCriteria) as RequiredItemCrit);
|
||||
|
||||
@@ -2345,10 +2345,12 @@ export class SubredditResources {
|
||||
} else {
|
||||
propResultsMap.source!.found = source;
|
||||
|
||||
const requestedSourcesVal: string[] = !Array.isArray(itemOptVal) ? [itemOptVal] as string[] : itemOptVal as string[];
|
||||
const requestedSources = requestedSourcesVal.map(x => strToActivitySource(x).toLowerCase());
|
||||
const itemSource = new ActivitySource(source);
|
||||
|
||||
propResultsMap.source!.passed = criteriaPassWithIncludeBehavior(requestedSources.some(x => source.toLowerCase().trim() === x.toLowerCase().trim()), include);
|
||||
const requestedSourcesVal: string[] = !Array.isArray(itemOptVal) ? [itemOptVal] as string[] : itemOptVal as string[];
|
||||
const requestedSources = requestedSourcesVal.map(x => new ActivitySource(x));
|
||||
|
||||
propResultsMap.source!.passed = criteriaPassWithIncludeBehavior(requestedSources.some(x => x.matches(itemSource)), include);
|
||||
break;
|
||||
}
|
||||
case 'score':
|
||||
@@ -3786,7 +3788,7 @@ export const checkAuthorFilter = async (item: (Submission | Comment), filter: Au
|
||||
return [true, undefined, {criteriaResults: allCritResults, join: 'OR', passed: true}];
|
||||
}
|
||||
|
||||
export const checkItemFilter = async (item: (Submission | Comment), filter: ItemOptions, resources: SubredditResources, options?: {logger?: Logger, source?: ActivitySource, includeIdentifier?: boolean}): Promise<[boolean, ('inclusive' | 'exclusive' | undefined), FilterResult<TypedActivityState>]> => {
|
||||
export const checkItemFilter = async (item: (Submission | Comment), filter: ItemOptions, resources: SubredditResources, options?: {logger?: Logger, source?: ActivitySourceValue, includeIdentifier?: boolean}): Promise<[boolean, ('inclusive' | 'exclusive' | undefined), FilterResult<TypedActivityState>]> => {
|
||||
|
||||
const {
|
||||
logger: parentLogger = NoopLogger,
|
||||
@@ -3944,7 +3946,7 @@ export const checkItemFilter = async (item: (Submission | Comment), filter: Item
|
||||
return [true, undefined, {criteriaResults: allCritResults, join: 'OR', passed: true}];
|
||||
}
|
||||
|
||||
export const checkCommentSubmissionStates = async (item: Comment, submissionStates: SubmissionState[], resources: SubredditResources, logger: Logger, source?: ActivitySource, excludeCondition?: JoinOperands): Promise<[boolean, FilterCriteriaPropertyResult<CommentState>]> => {
|
||||
export const checkCommentSubmissionStates = async (item: Comment, submissionStates: SubmissionState[], resources: SubredditResources, logger: Logger, source?: ActivitySourceValue, excludeCondition?: JoinOperands): Promise<[boolean, FilterCriteriaPropertyResult<CommentState>]> => {
|
||||
// test submission state first since it's more likely(??) we have crit results or cache data for this submission than for the comment
|
||||
|
||||
// get submission
|
||||
|
||||
26
src/util.ts
26
src/util.ts
@@ -71,7 +71,7 @@ import {
|
||||
UserNoteCriteria
|
||||
} from "./Common/Infrastructure/Filters/FilterCriteria";
|
||||
import {
|
||||
ActivitySource,
|
||||
ActivitySourceValue,
|
||||
ActivitySourceTypes,
|
||||
CacheProvider,
|
||||
ConfigFormat,
|
||||
@@ -86,7 +86,8 @@ import {
|
||||
StatisticFrequency,
|
||||
StatisticFrequencyOption,
|
||||
UrlContext,
|
||||
WikiContext
|
||||
WikiContext,
|
||||
ActivitySourceData
|
||||
} from "./Common/Infrastructure/Atomic";
|
||||
import {
|
||||
AuthorOptions,
|
||||
@@ -2723,17 +2724,30 @@ export const isCommentState = (state: TypedActivityState): state is CommentState
|
||||
const DISPATCH_REGEX: RegExp = /^dispatch:/i;
|
||||
const POLL_REGEX: RegExp = /^poll:/i;
|
||||
const USER_REGEX: RegExp = /^user:/i;
|
||||
export const asActivitySource = (val: string): val is ActivitySource => {
|
||||
const ACTIVITY_SOURCE_REGEX: RegExp = /^(?<type>dispatch|poll|user)(?:$|:(?<identifier>[^\s\r\n]+)$)/i
|
||||
const ACTIVITY_SOURCE_REGEX_URL = 'https://regexr.com/6uqn6';
|
||||
export const asActivitySourceValue = (val: string): val is ActivitySourceValue => {
|
||||
if(['dispatch','poll','user'].some(x => x === val)) {
|
||||
return true;
|
||||
}
|
||||
return DISPATCH_REGEX.test(val) || POLL_REGEX.test(val) || USER_REGEX.test(val);
|
||||
}
|
||||
|
||||
export const strToActivitySource = (val: string): ActivitySource => {
|
||||
export const asActivitySource = (val: any): val is ActivitySourceData => {
|
||||
return null !== val && typeof val === 'object' && 'type' in val;
|
||||
}
|
||||
|
||||
export const strToActivitySourceData = (val: string): ActivitySourceData => {
|
||||
const cleanStr = val.trim();
|
||||
if (asActivitySource(cleanStr)) {
|
||||
return cleanStr;
|
||||
if (asActivitySourceValue(cleanStr)) {
|
||||
const res = parseRegexSingleOrFail(ACTIVITY_SOURCE_REGEX, cleanStr);
|
||||
if (res === undefined) {
|
||||
throw new InvalidRegexError(ACTIVITY_SOURCE_REGEX, cleanStr, ACTIVITY_SOURCE_REGEX_URL, 'Could not parse activity source');
|
||||
}
|
||||
return {
|
||||
type: res.named.type,
|
||||
identifier: res.named.identifier
|
||||
}
|
||||
}
|
||||
throw new SimpleError(`'${cleanStr}' is not a valid ActivitySource. Must be one of: dispatch, dispatch:[identifier], poll, poll:[identifier], user, or user:[identifier]`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user