164 lines
4.8 KiB
TypeScript
164 lines
4.8 KiB
TypeScript
|
import { BranchMiddlewareCondition, Middleware, MiddlewareReturn, NextMiddleware, UnknownObject } from 'middleware-io';
|
||
|
import { Composer as BaseComposer, MessageContext } from 'vk-io';
|
||
|
|
||
|
declare module 'middleware-io' {
|
||
|
class Composer<T extends UnknownObject, R = T> {
|
||
|
filter<V = UnknownObject>(condition: BranchMiddlewareCondition<T & V>, filterMiddleware: Middleware<T & V>): this;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type AllowArray<T> = T | T[];
|
||
|
|
||
|
type HearFunctionCondition<T, V> = (value: V, context: T) => boolean;
|
||
|
type HearCondition<T, V> = HearFunctionCondition<T, V> | RegExp | string | number | boolean;
|
||
|
|
||
|
type HearObjectCondition<T extends Record<string, any>> =
|
||
|
Record<string, AllowArray<HearCondition<T, any>>>
|
||
|
& {
|
||
|
[P in keyof T]?: AllowArray<HearCondition<T, T[P]>>;
|
||
|
};
|
||
|
|
||
|
type HearConditions<T extends Record<string, any>> = (
|
||
|
AllowArray<HearCondition<T, string | undefined>>
|
||
|
| AllowArray<HearObjectCondition<T>>
|
||
|
);
|
||
|
|
||
|
function splitPath(path: string): string[] {
|
||
|
return (
|
||
|
path
|
||
|
.replace(/\[([^[\]]*)\]/g, '.$1.')
|
||
|
.split('.')
|
||
|
.filter(Boolean)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function getObjectValue(source: Record<string, any>, selectors: string[]): any {
|
||
|
let link = source;
|
||
|
|
||
|
for (const selector of selectors) {
|
||
|
if (!link[selector]) {
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
link = link[selector];
|
||
|
}
|
||
|
|
||
|
return link;
|
||
|
}
|
||
|
|
||
|
function unifyCondition(condition: unknown): Function {
|
||
|
if (typeof condition === 'function') {
|
||
|
return condition;
|
||
|
}
|
||
|
|
||
|
if (condition instanceof RegExp) {
|
||
|
return (text: string | undefined): boolean => (
|
||
|
condition.test(text!)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (Array.isArray(condition)) {
|
||
|
const arrayConditions = condition.map(unifyCondition);
|
||
|
|
||
|
return (value: string | undefined): boolean => (
|
||
|
Array.isArray(value)
|
||
|
? arrayConditions.every((cond): boolean => (
|
||
|
value.some((val): boolean => cond(val))
|
||
|
))
|
||
|
: arrayConditions.some((cond): boolean => (
|
||
|
cond(value)
|
||
|
))
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return (value: string | undefined): boolean => value === condition;
|
||
|
}
|
||
|
|
||
|
export class Composer<C extends MessageContext, R = C> extends BaseComposer<C, R> {
|
||
|
constructor() {
|
||
|
super();
|
||
|
}
|
||
|
|
||
|
public hear<T = {}>(
|
||
|
hearConditions: HearConditions<C & T>,
|
||
|
handler: Middleware<C & T>
|
||
|
): this {
|
||
|
const rawConditions = !Array.isArray(hearConditions)
|
||
|
? [hearConditions]
|
||
|
: hearConditions;
|
||
|
|
||
|
const hasConditions = rawConditions.every(Boolean);
|
||
|
|
||
|
if (!hasConditions) {
|
||
|
throw new Error('Condition should be not empty');
|
||
|
}
|
||
|
|
||
|
if (typeof handler !== 'function') {
|
||
|
throw new TypeError('Handler must be a function');
|
||
|
}
|
||
|
|
||
|
let textCondition = false;
|
||
|
let functionCondtion = false;
|
||
|
const conditions = rawConditions.map((condition): Function => {
|
||
|
if (typeof condition === 'object' && !(condition instanceof RegExp)) {
|
||
|
functionCondtion = true;
|
||
|
|
||
|
const entries = Object.entries(condition).map(([path, value]): [string[], Function] => (
|
||
|
[splitPath(path), unifyCondition(value)]
|
||
|
));
|
||
|
|
||
|
return (text: string | undefined, context: C): boolean => (
|
||
|
entries.every(([selectors, callback]): boolean => {
|
||
|
const value = getObjectValue(context, selectors);
|
||
|
|
||
|
return callback(value, context);
|
||
|
})
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (typeof condition === 'function') {
|
||
|
functionCondtion = true;
|
||
|
|
||
|
return condition;
|
||
|
}
|
||
|
|
||
|
textCondition = true;
|
||
|
|
||
|
if (condition instanceof RegExp) {
|
||
|
return (text: string | undefined, context: C): boolean => {
|
||
|
const passed = condition.test(text!);
|
||
|
|
||
|
if (passed) {
|
||
|
context.$match = text!.match(condition)!;
|
||
|
}
|
||
|
|
||
|
return passed;
|
||
|
};
|
||
|
}
|
||
|
|
||
|
const stringCondition = String(condition);
|
||
|
|
||
|
return (text: string | undefined): boolean => text === stringCondition;
|
||
|
});
|
||
|
|
||
|
const needText = textCondition && functionCondtion === false;
|
||
|
|
||
|
this.use((context: C & T, next: NextMiddleware): MiddlewareReturn => {
|
||
|
const { text } = context;
|
||
|
|
||
|
if (needText && text === undefined) {
|
||
|
return next();
|
||
|
}
|
||
|
|
||
|
const hasSome = conditions.some((condition): boolean => (
|
||
|
condition(text, context)
|
||
|
));
|
||
|
|
||
|
return hasSome
|
||
|
? handler(context, next)
|
||
|
: next();
|
||
|
});
|
||
|
|
||
|
return this;
|
||
|
}
|
||
|
}
|