1 |
|
2 |
|
3 | import * as tg from './core/types/typegram'
|
4 | import * as tt from './telegram-types'
|
5 | import { Middleware, MiddlewareFn, MiddlewareObj } from './middleware'
|
6 | import Context from './context'
|
7 |
|
8 | export type MaybeArray<T> = T | T[]
|
9 | export type MaybePromise<T> = T | Promise<T>
|
10 | export type NonemptyReadonlyArray<T> = readonly [T, ...T[]]
|
11 | export type Triggers<C> = MaybeArray<
|
12 | string | RegExp | ((value: string, ctx: C) => RegExpExecArray | null)
|
13 | >
|
14 | export type Predicate<T> = (t: T) => boolean
|
15 | export type AsyncPredicate<T> = (t: T) => Promise<boolean>
|
16 |
|
17 | export type MatchedMiddleware<
|
18 | C extends Context,
|
19 | T extends tt.UpdateType | tt.MessageSubType = 'message' | 'channel_post'
|
20 | > = NonemptyReadonlyArray<
|
21 | Middleware<MatchedContext<C & { match: RegExpExecArray }, T>>
|
22 | >
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 | type MatchedContext<
|
30 | C extends Context,
|
31 | T extends tt.UpdateType | tt.MessageSubType
|
32 | > = NarrowedContext<C, tt.MountMap[T]>
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 | export type NarrowedContext<
|
42 | C extends Context,
|
43 | U extends tg.Update
|
44 | > = Context<U> & Omit<C, keyof Context>
|
45 |
|
46 | export interface GameQueryUpdate extends tg.Update.CallbackQueryUpdate {
|
47 | callback_query: tg.CallbackQuery.GameShortGameCallbackQuery
|
48 | }
|
49 |
|
50 | function always<T>(x: T) {
|
51 | return () => x
|
52 | }
|
53 | const anoop = always(Promise.resolve())
|
54 |
|
55 | export class Composer<C extends Context> implements MiddlewareObj<C> {
|
56 | private handler: MiddlewareFn<C>
|
57 |
|
58 | constructor(...fns: ReadonlyArray<Middleware<C>>) {
|
59 | this.handler = Composer.compose(fns)
|
60 | }
|
61 |
|
62 | |
63 |
|
64 |
|
65 | use(...fns: ReadonlyArray<Middleware<C>>) {
|
66 | this.handler = Composer.compose([this.handler, ...fns])
|
67 | return this
|
68 | }
|
69 |
|
70 | |
71 |
|
72 |
|
73 |
|
74 | guard<U extends tg.Update>(
|
75 | guardFn: (update: tg.Update) => update is U,
|
76 | ...fns: NonemptyReadonlyArray<Middleware<NarrowedContext<C, U>>>
|
77 | ) {
|
78 | return this.use(Composer.guard<C, U>(guardFn, ...fns))
|
79 | }
|
80 |
|
81 | |
82 |
|
83 |
|
84 | on<T extends tt.UpdateType | tt.MessageSubType>(
|
85 | updateType: MaybeArray<T>,
|
86 | ...fns: NonemptyReadonlyArray<Middleware<MatchedContext<C, T>>>
|
87 | ) {
|
88 | return this.use(Composer.mount<C, T>(updateType, ...fns))
|
89 | }
|
90 |
|
91 | |
92 |
|
93 |
|
94 | hears(triggers: Triggers<C>, ...fns: MatchedMiddleware<C, 'text'>) {
|
95 | return this.use(Composer.hears<C>(triggers, ...fns))
|
96 | }
|
97 |
|
98 | |
99 |
|
100 |
|
101 | command(
|
102 | command: MaybeArray<string>,
|
103 | ...fns: NonemptyReadonlyArray<Middleware<MatchedContext<C, 'text'>>>
|
104 | ) {
|
105 | return this.use(Composer.command<C>(command, ...fns))
|
106 | }
|
107 |
|
108 | |
109 |
|
110 |
|
111 | action(
|
112 | triggers: Triggers<C>,
|
113 | ...fns: MatchedMiddleware<C, 'callback_query'>
|
114 | ) {
|
115 | return this.use(Composer.action<C>(triggers, ...fns))
|
116 | }
|
117 |
|
118 | |
119 |
|
120 |
|
121 | inlineQuery(
|
122 | triggers: Triggers<C>,
|
123 | ...fns: MatchedMiddleware<C, 'inline_query'>
|
124 | ) {
|
125 | return this.use(Composer.inlineQuery<C>(triggers, ...fns))
|
126 | }
|
127 |
|
128 | |
129 |
|
130 |
|
131 | gameQuery(
|
132 | ...fns: NonemptyReadonlyArray<
|
133 | Middleware<NarrowedContext<C, GameQueryUpdate>>
|
134 | >
|
135 | ) {
|
136 | return this.use(Composer.gameQuery(...fns))
|
137 | }
|
138 |
|
139 | |
140 |
|
141 |
|
142 | drop(predicate: Predicate<C>) {
|
143 | return this.use(Composer.drop(predicate))
|
144 | }
|
145 |
|
146 | filter(predicate: Predicate<C>) {
|
147 | return this.use(Composer.filter(predicate))
|
148 | }
|
149 |
|
150 | private entity<
|
151 | T extends 'message' | 'channel_post' | tt.MessageSubType =
|
152 | | 'message'
|
153 | | 'channel_post'
|
154 | >(
|
155 | predicate:
|
156 | | MaybeArray<string>
|
157 | | ((entity: tg.MessageEntity, s: string, ctx: C) => boolean),
|
158 | ...fns: ReadonlyArray<Middleware<MatchedContext<C, T>>>
|
159 | ) {
|
160 | return this.use(Composer.entity<C, T>(predicate, ...fns))
|
161 | }
|
162 |
|
163 | email(email: Triggers<C>, ...fns: MatchedMiddleware<C>) {
|
164 | return this.use(Composer.email<C>(email, ...fns))
|
165 | }
|
166 |
|
167 | url(url: Triggers<C>, ...fns: MatchedMiddleware<C>) {
|
168 | return this.use(Composer.url<C>(url, ...fns))
|
169 | }
|
170 |
|
171 | textLink(link: Triggers<C>, ...fns: MatchedMiddleware<C>) {
|
172 | return this.use(Composer.textLink<C>(link, ...fns))
|
173 | }
|
174 |
|
175 | textMention(mention: Triggers<C>, ...fns: MatchedMiddleware<C>) {
|
176 | return this.use(Composer.textMention<C>(mention, ...fns))
|
177 | }
|
178 |
|
179 | mention(mention: MaybeArray<string>, ...fns: MatchedMiddleware<C>) {
|
180 | return this.use(Composer.mention<C>(mention, ...fns))
|
181 | }
|
182 |
|
183 | phone(number: Triggers<C>, ...fns: MatchedMiddleware<C>) {
|
184 | return this.use(Composer.phone<C>(number, ...fns))
|
185 | }
|
186 |
|
187 | hashtag(hashtag: MaybeArray<string>, ...fns: MatchedMiddleware<C>) {
|
188 | return this.use(Composer.hashtag<C>(hashtag, ...fns))
|
189 | }
|
190 |
|
191 | cashtag(cashtag: MaybeArray<string>, ...fns: MatchedMiddleware<C>) {
|
192 | return this.use(Composer.cashtag<C>(cashtag, ...fns))
|
193 | }
|
194 |
|
195 | /**
|
196 | * Registers a middleware for handling /start
|
197 | */
|
198 | start(
|
199 | ...fns: NonemptyReadonlyArray<
|
200 | Middleware<MatchedContext<C, 'text'> & { startPayload: string }>
|
201 | >
|
202 | ) {
|
203 | const handler = Composer.compose(fns)
|
204 | return this.command('start', (ctx, next) => {
|
205 | const entity = ctx.message.entities![0]!
|
206 | const startPayload = ctx.message.text.slice(entity.length + 1)
|
207 | return handler(Object.assign(ctx, { startPayload }), next)
|
208 | })
|
209 | }
|
210 |
|
211 | /**
|
212 | * Registers a middleware for handling /help
|
213 | */
|
214 | help(...fns: NonemptyReadonlyArray<Middleware<MatchedContext<C, 'text'>>>) {
|
215 | return this.command('help', ...fns)
|
216 | }
|
217 |
|
218 | /**
|
219 | * Registers a middleware for handling /settings
|
220 | */
|
221 | settings(
|
222 | ...fns: NonemptyReadonlyArray<Middleware<MatchedContext<C, 'text'>>>
|
223 | ) {
|
224 | return this.command('settings', ...fns)
|
225 | }
|
226 |
|
227 | middleware() {
|
228 | return this.handler
|
229 | }
|
230 |
|
231 | static reply(...args: Parameters<Context['reply']>): MiddlewareFn<Context> {
|
232 | return (ctx) => ctx.reply(...args)
|
233 | }
|
234 |
|
235 | static catch<C extends Context>(
|
236 | errorHandler: (err: unknown, ctx: C) => void,
|
237 | ...fns: ReadonlyArray<Middleware<C>>
|
238 | ): MiddlewareFn<C> {
|
239 | const handler = Composer.compose(fns)
|
240 |
|
241 | return (ctx, next) => Promise.resolve(handler(ctx, next))
|
242 | .catch((err) => errorHandler(err, ctx))
|
243 | }
|
244 |
|
245 | |
246 |
|
247 |
|
248 | static fork<C extends Context>(middleware: Middleware<C>): MiddlewareFn<C> {
|
249 | const handler = Composer.unwrap(middleware)
|
250 | return async (ctx, next) => {
|
251 | await Promise.all([handler(ctx, anoop), next()])
|
252 | }
|
253 | }
|
254 |
|
255 | static tap<C extends Context>(middleware: Middleware<C>): MiddlewareFn<C> {
|
256 | const handler = Composer.unwrap(middleware)
|
257 | return (ctx, next) =>
|
258 | Promise.resolve(handler(ctx, anoop)).then(() => next())
|
259 | }
|
260 |
|
261 | |
262 |
|
263 |
|
264 | static passThru(): MiddlewareFn<Context> {
|
265 | return (ctx, next) => next()
|
266 | }
|
267 |
|
268 | static lazy<C extends Context>(
|
269 | factoryFn: (ctx: C) => MaybePromise<Middleware<C>>
|
270 | ): MiddlewareFn<C> {
|
271 | if (typeof factoryFn !== 'function') {
|
272 | throw new Error('Argument must be a function')
|
273 | }
|
274 | return (ctx, next) =>
|
275 | Promise.resolve(factoryFn(ctx)).then((middleware) =>
|
276 | Composer.unwrap(middleware)(ctx, next)
|
277 | )
|
278 | }
|
279 |
|
280 | static log(logFn: (s: string) => void = console.log): MiddlewareFn<Context> {
|
281 | return (ctx, next) => {
|
282 | logFn(JSON.stringify(ctx.update, null, 2))
|
283 | return next()
|
284 | }
|
285 | }
|
286 |
|
287 | |
288 |
|
289 |
|
290 |
|
291 | static branch<C extends Context>(
|
292 | predicate: Predicate<C> | AsyncPredicate<C>,
|
293 | trueMiddleware: Middleware<C>,
|
294 | falseMiddleware: Middleware<C>
|
295 | ): MiddlewareFn<C> {
|
296 | if (typeof predicate !== 'function') {
|
297 | return Composer.unwrap(predicate ? trueMiddleware : falseMiddleware)
|
298 | }
|
299 | return Composer.lazy<C>((ctx) =>
|
300 | Promise.resolve(predicate(ctx)).then((value) =>
|
301 | value ? trueMiddleware : falseMiddleware
|
302 | )
|
303 | )
|
304 | }
|
305 |
|
306 | /**
|
307 | * Generates optional middleware.
|
308 | * @param predicate predicate to decide on a context object whether to run the middleware
|
309 | * @param middleware middleware to run if the predicate returns true
|
310 | */
|
311 | static optional<C extends Context>(
|
312 | predicate: Predicate<C> | AsyncPredicate<C>,
|
313 | ...fns: NonemptyReadonlyArray<Middleware<C>>
|
314 | ): MiddlewareFn<C> {
|
315 | return Composer.branch(
|
316 | predicate,
|
317 | Composer.compose(fns),
|
318 | Composer.passThru()
|
319 | )
|
320 | }
|
321 |
|
322 | static filter<C extends Context>(predicate: Predicate<C>): MiddlewareFn<C> {
|
323 | return Composer.branch(predicate, Composer.passThru(), anoop)
|
324 | }
|
325 |
|
326 | /**
|
327 | * Generates middleware for dropping matching updates.
|
328 | */
|
329 | static drop<C extends Context>(predicate: Predicate<C>): MiddlewareFn<C> {
|
330 | return Composer.branch(predicate, anoop, Composer.passThru())
|
331 | }
|
332 |
|
333 | static dispatch<
|
334 | C extends Context,
|
335 | Handlers extends Record<string | number | symbol, Middleware<C>>
|
336 | >(
|
337 | routeFn: (ctx: C) => MaybePromise<keyof Handlers>,
|
338 | handlers: Handlers
|
339 | ): Middleware<C> {
|
340 | return Composer.lazy<C>((ctx) =>
|
341 | Promise.resolve(routeFn(ctx)).then((value) => handlers[value])
|
342 | )
|
343 | }
|
344 |
|
345 | // EXPLANATION FOR THE ts-expect-error ANNOTATIONS
|
346 |
|
347 | // The annotations around function invocations with `...fns` are there
|
348 | // whenever we perform validation logic that the flow analysis of TypeScript
|
349 | // cannot comprehend. We always make sure that the middleware functions are
|
350 | // only invoked with properly constrained context objects, but this cannot be
|
351 | // determined automatically.
|
352 |
|
353 | /**
|
354 | * Generates optional middleware based on a predicate that only operates on `ctx.update`.
|
355 | *
|
356 | * Example:
|
357 | * ```ts
|
358 | * import { Composer, Update } from 'telegraf'
|
359 | *
|
360 | * const predicate = (u): u is Update.MessageUpdate => 'message' in u
|
361 | * const middleware = Composer.guard(predicate, (ctx) => {
|
362 | * const message = ctx.update.message
|
363 | * })
|
364 | * ```
|
365 | *
|
366 | * Note that `Composer.mount('message')` is preferred over this.
|
367 | *
|
368 | * @param guardFn predicate to decide whether to run the middleware based on the `ctx.update` object
|
369 | * @param fns middleware to run if the predicate returns true
|
370 | * @see `Composer.optional` for a more generic version of this method that allows the predicate to operate on `ctx` itself
|
371 | */
|
372 | static guard<C extends Context, U extends tg.Update>(
|
373 | guardFn: (u: tg.Update) => u is U,
|
374 | ...fns: NonemptyReadonlyArray<Middleware<NarrowedContext<C, U>>>
|
375 | ): MiddlewareFn<C> {
|
376 | return Composer.optional<C>(
|
377 | (ctx) => guardFn(ctx.update),
|
378 | // @ts-expect-error see explanation above
|
379 | ...fns
|
380 | )
|
381 | }
|
382 |
|
383 | /**
|
384 | * Generates middleware for handling provided update types.
|
385 | * @deprecated use `Composer.on`
|
386 | */
|
387 | static mount<C extends Context, T extends tt.UpdateType | tt.MessageSubType>(
|
388 | updateType: MaybeArray<T>,
|
389 | ...fns: NonemptyReadonlyArray<Middleware<MatchedContext<C, T>>>
|
390 | ): MiddlewareFn<C> {
|
391 | return Composer.on(updateType, ...fns)
|
392 | }
|
393 |
|
394 | /**
|
395 | * Generates middleware for handling provided update types.
|
396 | */
|
397 | static on<C extends Context, T extends tt.UpdateType | tt.MessageSubType>(
|
398 | updateType: MaybeArray<T>,
|
399 | ...fns: NonemptyReadonlyArray<Middleware<MatchedContext<C, T>>>
|
400 | ): MiddlewareFn<C> {
|
401 | const updateTypes = normalizeTextArguments(updateType)
|
402 |
|
403 | const predicate = (
|
404 | update: tg.Update
|
405 | ): update is tg.Update & tt.MountMap[T] =>
|
406 | updateTypes.some(
|
407 | (type) =>
|
408 | // Check update type
|
409 | type in update ||
|
410 | // Check message sub-type
|
411 | ('message' in update && type in update.message)
|
412 | )
|
413 |
|
414 | return Composer.guard<C, tt.MountMap[T]>(predicate, ...fns)
|
415 | }
|
416 |
|
417 | private static entity<
|
418 | C extends Context,
|
419 | T extends 'message' | 'channel_post' | tt.MessageSubType =
|
420 | | 'message'
|
421 | | 'channel_post'
|
422 | >(
|
423 | predicate:
|
424 | | MaybeArray<string>
|
425 | | ((entity: tg.MessageEntity, s: string, ctx: C) => boolean),
|
426 | ...fns: ReadonlyArray<Middleware<MatchedContext<C, T>>>
|
427 | ): MiddlewareFn<C> {
|
428 | if (typeof predicate !== 'function') {
|
429 | const entityTypes = normalizeTextArguments(predicate)
|
430 | return Composer.entity(({ type }) => entityTypes.includes(type), ...fns)
|
431 | }
|
432 | return Composer.optional<C>((ctx) => {
|
433 | const msg: tg.Message | undefined = ctx.message ?? ctx.channelPost
|
434 | if (msg === undefined) {
|
435 | return false
|
436 | }
|
437 | const text = getText(msg)
|
438 | const entities = getEntities(msg)
|
439 | if (text === undefined) return false
|
440 | return entities.some((entity) =>
|
441 | predicate(
|
442 | entity,
|
443 | text.substring(entity.offset, entity.offset + entity.length),
|
444 | ctx
|
445 | )
|
446 | )
|
447 | // @ts-expect-error see explanation above
|
448 | }, ...fns)
|
449 | }
|
450 |
|
451 | static entityText<C extends Context>(
|
452 | entityType: MaybeArray<string>,
|
453 | predicate: Triggers<C>,
|
454 | ...fns: MatchedMiddleware<C>
|
455 | ): MiddlewareFn<C> {
|
456 | if (fns.length === 0) {
|
457 | // prettier-ignore
|
458 | return Array.isArray(predicate)
|
459 | // @ts-expect-error predicate is really the middleware
|
460 | ? Composer.entity(entityType, ...predicate)
|
461 | // @ts-expect-error predicate is really the middleware
|
462 | : Composer.entity(entityType, predicate)
|
463 | }
|
464 | const triggers = normalizeTriggers(predicate)
|
465 | return Composer.entity<C>(({ type }, value, ctx) => {
|
466 | if (type !== entityType) {
|
467 | return false
|
468 | }
|
469 | for (const trigger of triggers) {
|
470 | // @ts-expect-error define so far unknown property `match`
|
471 | if ((ctx.match = trigger(value, ctx))) {
|
472 | return true
|
473 | }
|
474 | }
|
475 | return false
|
476 | // @ts-expect-error see explanation above
|
477 | }, ...fns)
|
478 | }
|
479 |
|
480 | static email<C extends Context>(
|
481 | email: Triggers<C>,
|
482 | ...fns: MatchedMiddleware<C>
|
483 | ): MiddlewareFn<C> {
|
484 | return Composer.entityText<C>('email', email, ...fns)
|
485 | }
|
486 |
|
487 | static phone<C extends Context>(
|
488 | number: Triggers<C>,
|
489 | ...fns: MatchedMiddleware<C>
|
490 | ): MiddlewareFn<C> {
|
491 | return Composer.entityText<C>('phone_number', number, ...fns)
|
492 | }
|
493 |
|
494 | static url<C extends Context>(
|
495 | url: Triggers<C>,
|
496 | ...fns: MatchedMiddleware<C>
|
497 | ): MiddlewareFn<C> {
|
498 | return Composer.entityText<C>('url', url, ...fns)
|
499 | }
|
500 |
|
501 | static textLink<C extends Context>(
|
502 | link: Triggers<C>,
|
503 | ...fns: MatchedMiddleware<C>
|
504 | ): MiddlewareFn<C> {
|
505 | return Composer.entityText<C>('text_link', link, ...fns)
|
506 | }
|
507 |
|
508 | static textMention<C extends Context>(
|
509 | mention: Triggers<C>,
|
510 | ...fns: MatchedMiddleware<C>
|
511 | ): MiddlewareFn<C> {
|
512 | return Composer.entityText<C>('text_mention', mention, ...fns)
|
513 | }
|
514 |
|
515 | static mention<C extends Context>(
|
516 | mention: MaybeArray<string>,
|
517 | ...fns: MatchedMiddleware<C>
|
518 | ): MiddlewareFn<C> {
|
519 | return Composer.entityText<C>(
|
520 | 'mention',
|
521 | normalizeTextArguments(mention, '@'),
|
522 | ...fns
|
523 | )
|
524 | }
|
525 |
|
526 | static hashtag<C extends Context>(
|
527 | hashtag: MaybeArray<string>,
|
528 | ...fns: MatchedMiddleware<C>
|
529 | ): MiddlewareFn<C> {
|
530 | return Composer.entityText<C>(
|
531 | 'hashtag',
|
532 | normalizeTextArguments(hashtag, '#'),
|
533 | ...fns
|
534 | )
|
535 | }
|
536 |
|
537 | static cashtag<C extends Context>(
|
538 | cashtag: MaybeArray<string>,
|
539 | ...fns: MatchedMiddleware<C>
|
540 | ): MiddlewareFn<C> {
|
541 | return Composer.entityText<C>(
|
542 | 'cashtag',
|
543 | normalizeTextArguments(cashtag, '$'),
|
544 | ...fns
|
545 | )
|
546 | }
|
547 |
|
548 | private static match<
|
549 | C extends Context,
|
550 | T extends
|
551 | | 'message'
|
552 | | 'channel_post'
|
553 | | 'callback_query'
|
554 | | 'inline_query'
|
555 | | tt.MessageSubType
|
556 | >(
|
557 | triggers: ReadonlyArray<(text: string, ctx: C) => RegExpExecArray | null>,
|
558 | ...fns: MatchedMiddleware<C, T>
|
559 | ): MiddlewareFn<MatchedContext<C, T>> {
|
560 | const handler = Composer.compose(fns)
|
561 | return (ctx, next) => {
|
562 | const text =
|
563 | getText(ctx.message) ??
|
564 | getText(ctx.channelPost) ??
|
565 | getText(ctx.callbackQuery) ??
|
566 | ctx.inlineQuery?.query
|
567 | if (text === undefined) return next()
|
568 | for (const trigger of triggers) {
|
569 | // @ts-expect-error
|
570 | const match = trigger(text, ctx)
|
571 | if (match) {
|
572 | // @ts-expect-error define so far unknown property `match`
|
573 | return handler(Object.assign(ctx, { match }), next)
|
574 | }
|
575 | }
|
576 | return next()
|
577 | }
|
578 | }
|
579 |
|
580 | /**
|
581 | * Generates middleware for handling matching text messages.
|
582 | */
|
583 | static hears<C extends Context>(
|
584 | triggers: Triggers<C>,
|
585 | ...fns: MatchedMiddleware<C, 'text'>
|
586 | ): MiddlewareFn<C> {
|
587 | return Composer.mount(
|
588 | 'text',
|
589 | Composer.match<C, 'text'>(normalizeTriggers(triggers), ...fns)
|
590 | )
|
591 | }
|
592 |
|
593 | /**
|
594 | * Generates middleware for handling specified commands.
|
595 | */
|
596 | static command<C extends Context>(
|
597 | command: MaybeArray<string>,
|
598 | ...fns: NonemptyReadonlyArray<Middleware<MatchedContext<C, 'text'>>>
|
599 | ): MiddlewareFn<C> {
|
600 | if (fns.length === 0) {
|
601 | // @ts-expect-error command is really the middleware
|
602 | return Composer.entity('bot_command', command)
|
603 | }
|
604 | const commands = normalizeTextArguments(command, '/')
|
605 | return Composer.mount<C, 'text'>(
|
606 | 'text',
|
607 | Composer.lazy<MatchedContext<C, 'text'>>((ctx) => {
|
608 | const groupCommands =
|
609 | ctx.me && ctx.chat?.type.endsWith('group')
|
610 | ? commands.map((command) => `${command}@${ctx.me}`)
|
611 | : []
|
612 | return Composer.entity<MatchedContext<C, 'text'>>(
|
613 | ({ offset, type }, value) =>
|
614 | offset === 0 &&
|
615 | type === 'bot_command' &&
|
616 | (commands.includes(value) || groupCommands.includes(value)),
|
617 | // @ts-expect-error see explanation above
|
618 | ...fns
|
619 | )
|
620 | })
|
621 | )
|
622 | }
|
623 |
|
624 | /**
|
625 | * Generates middleware for handling matching callback queries.
|
626 | */
|
627 | static action<C extends Context>(
|
628 | triggers: Triggers<C>,
|
629 | ...fns: MatchedMiddleware<C, 'callback_query'>
|
630 | ): MiddlewareFn<C> {
|
631 | return Composer.mount(
|
632 | 'callback_query',
|
633 | Composer.match<C, 'callback_query'>(normalizeTriggers(triggers), ...fns)
|
634 | )
|
635 | }
|
636 |
|
637 | /**
|
638 | * Generates middleware for handling matching inline queries.
|
639 | */
|
640 | static inlineQuery<C extends Context>(
|
641 | triggers: Triggers<C>,
|
642 | ...fns: MatchedMiddleware<C, 'inline_query'>
|
643 | ): MiddlewareFn<C> {
|
644 | return Composer.mount(
|
645 | 'inline_query',
|
646 | Composer.match<C, 'inline_query'>(normalizeTriggers(triggers), ...fns)
|
647 | )
|
648 | }
|
649 |
|
650 | /**
|
651 | * Generates middleware responding only to specified users.
|
652 | */
|
653 | static acl<C extends Context>(
|
654 | userId: MaybeArray<number>,
|
655 | ...fns: NonemptyReadonlyArray<Middleware<C>>
|
656 | ): MiddlewareFn<C> {
|
657 | if (typeof userId === 'function') {
|
658 | return Composer.optional(userId, ...fns)
|
659 | }
|
660 | const allowed = Array.isArray(userId) ? userId : [userId]
|
661 | // prettier-ignore
|
662 | return Composer.optional((ctx) => !ctx.from || allowed.includes(ctx.from.id), ...fns)
|
663 | }
|
664 |
|
665 | private static memberStatus<C extends Context>(
|
666 | status: MaybeArray<tg.ChatMember['status']>,
|
667 | ...fns: NonemptyReadonlyArray<Middleware<C>>
|
668 | ): MiddlewareFn<C> {
|
669 | const statuses = Array.isArray(status) ? status : [status]
|
670 | return Composer.optional(async (ctx) => {
|
671 | if (ctx.message === undefined) return false
|
672 | const member = await ctx.getChatMember(ctx.message.from.id)
|
673 | return statuses.includes(member.status)
|
674 | }, ...fns)
|
675 | }
|
676 |
|
677 | /**
|
678 | * Generates middleware responding only to chat admins and chat creator.
|
679 | */
|
680 | static admin<C extends Context>(
|
681 | ...fns: NonemptyReadonlyArray<Middleware<C>>
|
682 | ): MiddlewareFn<C> {
|
683 | return Composer.memberStatus(['administrator', 'creator'], ...fns)
|
684 | }
|
685 |
|
686 | /**
|
687 | * Generates middleware responding only to chat creator.
|
688 | */
|
689 | static creator<C extends Context>(
|
690 | ...fns: NonemptyReadonlyArray<Middleware<C>>
|
691 | ): MiddlewareFn<C> {
|
692 | return Composer.memberStatus('creator', ...fns)
|
693 | }
|
694 |
|
695 | /**
|
696 | * Generates middleware running only in specified chat types.
|
697 | */
|
698 | static chatType<C extends Context>(
|
699 | type: MaybeArray<tg.Chat['type']>,
|
700 | ...fns: NonemptyReadonlyArray<Middleware<C>>
|
701 | ): MiddlewareFn<C> {
|
702 | const types = Array.isArray(type) ? type : [type]
|
703 | return Composer.optional((ctx) => {
|
704 | const chat = ctx.chat
|
705 | return chat !== undefined && types.includes(chat.type)
|
706 | }, ...fns)
|
707 | }
|
708 |
|
709 | /**
|
710 | * Generates middleware running only in private chats.
|
711 | */
|
712 | static privateChat<C extends Context>(
|
713 | ...fns: NonemptyReadonlyArray<Middleware<C>>
|
714 | ): MiddlewareFn<C> {
|
715 | return Composer.chatType('private', ...fns)
|
716 | }
|
717 |
|
718 | /**
|
719 | * Generates middleware running only in groups and supergroups.
|
720 | */
|
721 | static groupChat<C extends Context>(
|
722 | ...fns: NonemptyReadonlyArray<Middleware<C>>
|
723 | ): MiddlewareFn<C> {
|
724 | return Composer.chatType(['group', 'supergroup'], ...fns)
|
725 | }
|
726 |
|
727 | /**
|
728 | * Generates middleware for handling game queries.
|
729 | */
|
730 | static gameQuery<C extends Context>(
|
731 | ...fns: NonemptyReadonlyArray<
|
732 | Middleware<NarrowedContext<C, GameQueryUpdate>>
|
733 | >
|
734 | ): MiddlewareFn<C> {
|
735 | return Composer.guard(
|
736 | (u): u is GameQueryUpdate =>
|
737 | 'callback_query' in u && 'game_short_name' in u.callback_query,
|
738 | ...fns
|
739 | )
|
740 | }
|
741 |
|
742 | static unwrap<C extends Context>(handler: Middleware<C>): MiddlewareFn<C> {
|
743 | if (!handler) {
|
744 | throw new Error('Handler is undefined')
|
745 | }
|
746 | return 'middleware' in handler ? handler.middleware() : handler
|
747 | }
|
748 |
|
749 | static compose<C extends Context>(
|
750 | middlewares: ReadonlyArray<Middleware<C>>
|
751 | ): MiddlewareFn<C> {
|
752 | if (!Array.isArray(middlewares)) {
|
753 | throw new Error('Middlewares must be an array')
|
754 | }
|
755 | if (middlewares.length === 0) {
|
756 | return Composer.passThru()
|
757 | }
|
758 | if (middlewares.length === 1) {
|
759 | return Composer.unwrap(middlewares[0]!)
|
760 | }
|
761 | return (ctx, next) => {
|
762 | let index = -1
|
763 | return execute(0, ctx)
|
764 | async function execute(i: number, context: C): Promise<void> {
|
765 | if (!(context instanceof Context)) {
|
766 | throw new Error('next(ctx) called with invalid context')
|
767 | }
|
768 | if (i <= index) {
|
769 | throw new Error('next() called multiple times')
|
770 | }
|
771 | index = i
|
772 | const handler = Composer.unwrap(middlewares[i] ?? next)
|
773 | await handler(context, async (ctx = context) => {
|
774 | await execute(i + 1, ctx)
|
775 | })
|
776 | }
|
777 | }
|
778 | }
|
779 | }
|
780 |
|
781 | function escapeRegExp(s: string) {
|
782 | // $& means the whole matched string
|
783 | return s.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&')
|
784 | }
|
785 |
|
786 | function normalizeTriggers<C extends Context>(
|
787 | triggers: Triggers<C>
|
788 | ): Array<(value: string, ctx: C) => RegExpExecArray | null> {
|
789 | if (!Array.isArray(triggers)) {
|
790 | triggers = [triggers]
|
791 | }
|
792 | return triggers.map((trigger) => {
|
793 | if (!trigger) {
|
794 | throw new Error('Invalid trigger')
|
795 | }
|
796 | if (typeof trigger === 'function') {
|
797 | return trigger
|
798 | }
|
799 | if (trigger instanceof RegExp) {
|
800 | return (value = '') => {
|
801 | trigger.lastIndex = 0
|
802 | return trigger.exec(value)
|
803 | }
|
804 | }
|
805 | const regex = new RegExp(`^${escapeRegExp(trigger)}$`)
|
806 | return (value: string) => regex.exec(value)
|
807 | })
|
808 | }
|
809 |
|
810 | function getEntities(msg: tg.Message | undefined): tg.MessageEntity[] {
|
811 | if (msg == null) return []
|
812 | if ('caption_entities' in msg) return msg.caption_entities ?? []
|
813 | if ('entities' in msg) return msg.entities ?? []
|
814 | return []
|
815 | }
|
816 | function getText(
|
817 | msg: tg.Message | tg.CallbackQuery | undefined
|
818 | ): string | undefined {
|
819 | if (msg == null) return undefined
|
820 | if ('caption' in msg) return msg.caption
|
821 | if ('text' in msg) return msg.text
|
822 | if ('data' in msg) return msg.data
|
823 | if ('game_short_name' in msg) return msg.game_short_name
|
824 | return undefined
|
825 | }
|
826 |
|
827 | function normalizeTextArguments(argument: MaybeArray<string>, prefix = '') {
|
828 | const args = Array.isArray(argument) ? argument : [argument]
|
829 | // prettier-ignore
|
830 | return args
|
831 | .filter(Boolean)
|
832 | .map((arg) => prefix && typeof arg === 'string' && !arg.startsWith(prefix) ? `${prefix}${arg}` : arg)
|
833 | }
|
834 |
|
835 | export default Composer
|
836 |
|
\ | No newline at end of file |