balabola-vk/src/composer.ts

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;
}
}