UNPKG

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