UNPKG

24.7 kBPlain TextView Raw
1/** @format */
2
3import * as tg from './core/types/typegram'
4import * as tt from './telegram-types'
5import { Middleware, MiddlewareFn, MiddlewareObj } from './middleware'
6import Context from './context'
7
8export type MaybeArray<T> = T | T[]
9export type MaybePromise<T> = T | Promise<T>
10export type NonemptyReadonlyArray<T> = readonly [T, ...T[]]
11export type Triggers<C> = MaybeArray<
12 string | RegExp | ((value: string, ctx: C) => RegExpExecArray | null)
13>
14export type Predicate<T> = (t: T) => boolean
15export type AsyncPredicate<T> = (t: T) => Promise<boolean>
16
17export 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/** Takes: a context type and an update type (or message subtype).
25 Produces: a context that has some properties required, and some undefined.
26 The required ones are those that are always present when the given update (or message) arrives.
27 The undefined ones are those that are always absent when the given update (or message) arrives. */
28/** @deprecated */
29type MatchedContext<
30 C extends Context,
31 T extends tt.UpdateType | tt.MessageSubType
32> = NarrowedContext<C, tt.MountMap[T]>
33
34/**
35 * Narrows down `C['update']` (and derived getters)
36 * to specific update type `U`.
37 *
38 * Used by [[`Composer`]],
39 * possibly useful for splitting a bot into multiple files.
40 */
41export type NarrowedContext<
42 C extends Context,
43 U extends tg.Update
44> = Context<U> & Omit<C, keyof Context>
45
46export interface GameQueryUpdate extends tg.Update.CallbackQueryUpdate {
47 callback_query: tg.CallbackQuery.GameShortGameCallbackQuery
48}
49
50function always<T>(x: T) {
51 return () => x
52}
53const anoop = always(Promise.resolve())
54
55export 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 * Registers a middleware.
64 */
65 use(...fns: ReadonlyArray<Middleware<C>>) {
66 this.handler = Composer.compose([this.handler, ...fns])
67 return this
68 }
69
70 /**
71 * Registers middleware for handling updates
72 * matching given type guard function.
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 * Registers middleware for handling provided update types.
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 * Registers middleware for handling matching text messages.
93 */
94 hears(triggers: Triggers<C>, ...fns: MatchedMiddleware<C, 'text'>) {
95 return this.use(Composer.hears<C>(triggers, ...fns))
96 }
97
98 /**
99 * Registers middleware for handling specified commands.
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 * Registers middleware for handling matching callback queries.
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 * Registers middleware for handling matching inline queries.
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 * Registers middleware for handling game queries
130 */
131 gameQuery(
132 ...fns: NonemptyReadonlyArray<
133 Middleware<NarrowedContext<C, GameQueryUpdate>>
134 >
135 ) {
136 return this.use(Composer.gameQuery(...fns))
137 }
138
139 /**
140 * Registers middleware for dropping matching updates.
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 // prettier-ignore
241 return (ctx, next) => Promise.resolve(handler(ctx, next))
242 .catch((err) => errorHandler(err, ctx))
243 }
244
245 /**
246 * Generates middleware that runs in the background.
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 * Generates middleware that gives up control to the next middleware.
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 * @param trueMiddleware middleware to run if the predicate returns true
289 * @param falseMiddleware middleware to run if the predicate returns false
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
781function escapeRegExp(s: string) {
782 // $& means the whole matched string
783 return s.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&')
784}
785
786function 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
810function 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}
816function 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
827function 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
835export default Composer
836
\No newline at end of file