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:
FoxxMD
2022-09-26 12:07:24 -04:00
parent 1b20122ffc
commit 027f4087e3
7 changed files with 88 additions and 27 deletions

View 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();
}
}

View File

@@ -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) {

View File

@@ -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 =

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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]`);
}