From 5e7706a5a383877c1de49e7d5700114e0eb1d3c6 Mon Sep 17 00:00:00 2001 From: teidesu Date: Sun, 20 Jun 2021 01:29:40 +0300 Subject: [PATCH] feat(dispatcher): callback data builder --- .../dispatcher/src/callback-data-builder.ts | 134 ++++++++++++++++++ packages/dispatcher/src/index.ts | 1 + 2 files changed, 135 insertions(+) create mode 100644 packages/dispatcher/src/callback-data-builder.ts diff --git a/packages/dispatcher/src/callback-data-builder.ts b/packages/dispatcher/src/callback-data-builder.ts new file mode 100644 index 00000000..5e71efd3 --- /dev/null +++ b/packages/dispatcher/src/callback-data-builder.ts @@ -0,0 +1,134 @@ +import { MaybeArray } from '@mtcute/core' +import { CallbackQuery, MtCuteArgumentError } from '@mtcute/client' +import { UpdateFilter } from './filters' + +/** + * Callback data builder, inspired by [aiogram](https://github.com/aiogram/aiogram). + * + * This can be used to simplify management of different callbacks. + * + * [Learn more in the docs](//mt.tei.su/guide/topics/keyboards.html#callback-data-builders) + */ +export class CallbackDataBuilder { + private readonly _fields: T[] + + sep = ':' + + /** + * @param prefix Prefix for the data. Use something unique across your bot. + * @param fields Field names in the order they will be serialized. + */ + constructor(public prefix: string, ...fields: T[]) { + this._fields = fields + } + + /** + * Build a callback data string + * + * @param obj Object containing the data + */ + build(obj: Record): string { + const ret = + this.prefix + + this.sep + + this._fields + .map((f) => { + const val = obj[f] + + if (val.indexOf(this.sep) > -1) + throw new MtCuteArgumentError( + `Value for ${f} ${val} contains separator ${this.sep} and cannot be used.` + ) + + return val + }) + .join(this.sep) + + if (ret.length > 64) { + throw new MtCuteArgumentError( + 'Resulting callback data is too long.' + ) + } + + return ret + } + + /** + * Parse callback data to object + * + * @param data Callback data as string + */ + parse(data: string): Record { + const parts = data.split(this.sep) + + if (parts[0] !== this.prefix) { + throw new MtCuteArgumentError('Invalid data passed') + } + + if (parts.length !== this._fields.length + 1) { + throw new MtCuteArgumentError('Invalid data passed') + } + + const ret = {} as Record + parts.forEach((it, idx) => { + ret[this._fields[idx - 1]] = it + }) + return ret + } + + /** + * Create a filter for this callback data. + * + * > **Note**: `params` object will be compiled to a RegExp, + * > so avoid using characters that have special meaning in regex, + * > or use RegExp directly to let the IDE guide you. + * + * @param params + */ + filter( + params: Partial>> + ): UpdateFilter< + CallbackQuery, + { + match: Record + } + > { + const parts: string[] = [] + + this._fields.forEach((field) => { + if (!(field in params)) { + parts.push(`[^${this.sep}]*?`) + return + } + + const value = params[field]! + if (Array.isArray(value)) { + parts.push( + `(${value + .map((i) => (typeof i === 'string' ? i : i.source)) + .join('|')})` + ) + } else { + parts.push( + typeof value === 'string' ? value : (value as RegExp).source + ) + } + }) + + const regex = new RegExp( + `^${this.prefix}${this.sep}${parts.join(this.sep)}$` + ) + + return (query) => { + const m = query.dataStr?.match(regex) + if (!m) return false + ;(query as CallbackQuery & { + match: Record + }).match = this.parse(m[0]) + return true + } + } +} + +const a = new CallbackDataBuilder('post', 'foo', 'bar') + diff --git a/packages/dispatcher/src/index.ts b/packages/dispatcher/src/index.ts index 53cd49b9..a71cff97 100644 --- a/packages/dispatcher/src/index.ts +++ b/packages/dispatcher/src/index.ts @@ -5,5 +5,6 @@ export * from './handler' export * from './propagation' export * from './updates' export * from './wizard' +export * from './callback-data-builder' export { UpdateState, IStateStorage } from './state'