diff --git a/src/Common/ActivitySource.ts b/src/Common/ActivitySource.ts new file mode 100644 index 0000000..89ff334 --- /dev/null +++ b/src/Common/ActivitySource.ts @@ -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(); + } +} diff --git a/src/Common/Entities/DispatchedEntity.ts b/src/Common/Entities/DispatchedEntity.ts index 74b32a4..0c61ef0 100644 --- a/src/Common/Entities/DispatchedEntity.ts +++ b/src/Common/Entities/DispatchedEntity.ts @@ -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) { diff --git a/src/Common/Infrastructure/Atomic.ts b/src/Common/Infrastructure/Atomic.ts index 8ef6b3f..65a24ac 100644 --- a/src/Common/Infrastructure/Atomic.ts +++ b/src/Common/Infrastructure/Atomic.ts @@ -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 = diff --git a/src/Common/interfaces.ts b/src/Common/interfaces.ts index 4ba37f4..a9eead3 100644 --- a/src/Common/interfaces.ts +++ b/src/Common/interfaces.ts @@ -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 diff --git a/src/Subreddit/Manager.ts b/src/Subreddit/Manager.ts index 33fa3eb..3db023e 100644 --- a/src/Subreddit/Manager.ts +++ b/src/Subreddit/Manager.ts @@ -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 diff --git a/src/Subreddit/SubredditResources.ts b/src/Subreddit/SubredditResources.ts index a0d7b34..a00bfda 100644 --- a/src/Subreddit/SubredditResources.ts +++ b/src/Subreddit/SubredditResources.ts @@ -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, logger: Logger, include = true, source?: ActivitySource): Promise> { + async testItemCriteria(i: (Comment | Submission), activityStateObj: NamedCriteria, logger: Logger, include = true, source?: ActivitySourceValue): Promise> { 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> { + async isItem (item: Submission | Comment, stateCriteria: TypedActivityState, logger: Logger, include: boolean, source?: ActivitySourceValue): Promise> { //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]> => { +export const checkItemFilter = async (item: (Submission | Comment), filter: ItemOptions, resources: SubredditResources, options?: {logger?: Logger, source?: ActivitySourceValue, includeIdentifier?: boolean}): Promise<[boolean, ('inclusive' | 'exclusive' | undefined), FilterResult]> => { 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]> => { +export const checkCommentSubmissionStates = async (item: Comment, submissionStates: SubmissionState[], resources: SubredditResources, logger: Logger, source?: ActivitySourceValue, excludeCondition?: JoinOperands): Promise<[boolean, FilterCriteriaPropertyResult]> => { // 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 diff --git a/src/util.ts b/src/util.ts index 258b0ad..4761e08 100644 --- a/src/util.ts +++ b/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 = /^(?dispatch|poll|user)(?:$|:(?[^\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]`); }