import { InlineQuery, Message, MtCuteArgumentError, TelegramClient, } from '@mtcute/client' import { tl } from '@mtcute/tl' import { ContinuePropagation, PropagationSymbol, StopChildrenPropagation, StopPropagation, } from './propagation' // begin-codegen-imports import { UpdateHandler, RawUpdateHandler, NewMessageHandler, EditMessageHandler, ChatMemberUpdateHandler, InlineQueryHandler, ChosenInlineResultHandler, } from './handler' // end-codegen-imports import { filters, UpdateFilter } from './filters' import { handlers } from './builders' import { ChatMemberUpdate } from './updates' import { ChosenInlineResult } from './updates/chosen-inline-result' const noop = () => {} type ParserFunction = ( client: TelegramClient, upd: tl.TypeUpdate | tl.TypeMessage, users: Record, chats: Record ) => any type UpdateParser = [Exclude, ParserFunction] const baseMessageParser: ParserFunction = ( client: TelegramClient, upd, users, chats ) => new Message( client, tl.isAnyMessage(upd) ? upd : (upd as any).message, users, chats ) const newMessageParser: UpdateParser = ['new_message', baseMessageParser] const editMessageParser: UpdateParser = ['edit_message', baseMessageParser] const chatMemberParser: UpdateParser = [ 'chat_member', (client, upd, users, chats) => new ChatMemberUpdate(client, upd as any, users, chats), ] const PARSERS: Partial< Record<(tl.TypeUpdate | tl.TypeMessage)['_'], UpdateParser> > = { message: newMessageParser, messageEmpty: newMessageParser, messageService: newMessageParser, updateNewMessage: newMessageParser, updateNewChannelMessage: newMessageParser, updateNewScheduledMessage: newMessageParser, updateEditMessage: editMessageParser, updateEditChannelMessage: editMessageParser, updateChatParticipant: chatMemberParser, updateChannelParticipant: chatMemberParser, updateBotInlineQuery: [ 'inline_query', (client, upd, users) => new InlineQuery(client, upd as any, users), ], updateBotInlineSend: [ 'chosen_inline_result', (client, upd, users) => new ChosenInlineResult(client, upd as any, users), ], } /** * The dispatcher */ export class Dispatcher { private _groups: Record = {} private _groupsOrder: number[] = [] private _client?: TelegramClient private _parent?: Dispatcher private _children: Dispatcher[] = [] /** * Create a new dispatcher, optionally binding it to the client. */ constructor(client?: TelegramClient) { if (client) { this.bindToClient(client) } } /** * Bind the dispatcher to the client. * Called by the constructor automatically if * `client` was passed. * * Under the hood, this replaces client's `dispatchUpdate` * function, meaning you can't bind two different * dispatchers to the same client at the same time. * Instead, use {@link extend}, {@link addChild} * or {@link addScene} on the existing, already bound dispatcher. * * Dispatcher also uses bound client to throw errors */ bindToClient(client: TelegramClient): void { client['dispatchUpdate'] = this.dispatchUpdate.bind(this) this._client = client } /** * Unbind a dispatcher from the client. * * This will replace client's dispatchUpdate with a no-op. * If this dispatcher is not bound, nothing will happen. */ unbind(): void { if (this._client) { this._client['dispatchUpdate'] = noop this._client = undefined } } /** * Process an update with this dispatcher. * Calling this method without bound client will not work. * * Under the hood asynchronously calls {@link dispatchUpdateNow} * with error handler set to client's one. * * @param update Update to process * @param users Map of users * @param chats Map of chats */ dispatchUpdate( update: tl.TypeUpdate | tl.TypeMessage, users: Record, chats: Record ): void { if (!this._client) return // order does not matter in the dispatcher, // so we can handle each update in its own task this.dispatchUpdateNow(update, users, chats).catch((err) => this._client!['_emitError'](err) ) } /** * Process an update right now in the current stack. * * Unlike {@link dispatchUpdate}, this does not schedule * the update to be dispatched, but dispatches it immediately, * and after `await`ing this method you can be certain that the update * was fully processed by all the registered handlers, including children. * * @param update Update to process * @param users Map of users * @param chats Map of chats */ async dispatchUpdateNow( update: tl.TypeUpdate | tl.TypeMessage, users: Record, chats: Record ): Promise { if (!this._client) return const isRawMessage = tl.isAnyMessage(update) const pair = PARSERS[update._] const parsed = pair ? pair[1](this._client, update, users, chats) : undefined outer: for (const grp of this._groupsOrder) { for (const handler of this._groups[grp]) { let result: void | PropagationSymbol if ( handler.type === 'raw' && !isRawMessage && (!handler.check || (await handler.check( this._client, update as any, users, chats ))) ) { result = await handler.callback( this._client, update as any, users, chats ) } else if ( pair && handler.type === pair[0] && (!handler.check || (await handler.check(parsed, this._client))) ) { result = await handler.callback(parsed, this._client) } else continue if (result === ContinuePropagation) continue if (result === StopPropagation) break outer if (result === StopChildrenPropagation) return break } } for (const child of this._children) { await child.dispatchUpdateNow(update, users, chats) } } /** * Add an update handler to a given handlers group * * @param handler Update handler * @param group Handler group index */ addUpdateHandler(handler: UpdateHandler, group = 0): void { if (!(group in this._groups)) { this._groups[group] = [] this._groupsOrder.push(group) this._groupsOrder.sort((a, b) => a - b) } this._groups[group].push(handler) } /** * Remove an update handler (or handlers) from a given * handler group. * * @param handler Update handler to remove, its type or `'all'` to remove all * @param group Handler group index * @internal */ removeUpdateHandler( handler: UpdateHandler | UpdateHandler['type'] | 'all', group = 0 ): void { if (!(group in this._groups)) { return } if (typeof handler === 'string') { if (handler === 'all') { delete this._groups[group] } else { this._groups[group] = this._groups[group].filter( (h) => h.type !== handler ) } return } if (!(handler.type in this._groups[group])) { return } const idx = this._groups[group].indexOf(handler) if (idx > 0) { this._groups[group].splice(idx, 1) } } // children // /** * Get parent dispatcher if current dispatcher is a child. * Otherwise, return `null` */ get parent(): Dispatcher | null { return this._parent ?? null } /** * Add a child dispatcher. * * Child dispatchers are called when dispatching updates * just like normal, except they can be controlled * externally. Additionally, child dispatcher have their own * independent handler grouping that does not interfere with parent's, * including `StopPropagation` (i.e. returning `StopPropagation` will * still call children. To entirely stop, use `StopChildrenPropagation`) * * Note that child dispatchers share the same TelegramClient * binding as the parent, don't bind them manually. * * @param other Other dispatcher */ addChild(other: Dispatcher): void { if (this._children.indexOf(other) > -1) return if (other._client) { throw new MtCuteArgumentError( 'Provided dispatcher is ' + (other._parent ? 'already a child. Use parent.removeChild() before calling addChild()' : 'already bound to a client. Use unbind() before calling addChild()') ) } other._parent = this other._client = this._client this._children.push(other) } /** * Remove a child dispatcher. * * Removing child dispatcher will also remove * child dispatcher's client binding. * * If the provided dispatcher is not a child of current, * this function will silently fail. * * @param other Other dispatcher */ removeChild(other: Dispatcher): void { const idx = this._children.indexOf(other) if (idx > -1) { other._parent = undefined other._client = undefined this._children.splice(idx, 1) } } /** * Extend current dispatcher by copying other dispatcher's * handlers and children to the current. * * This might be more efficient for simple cases, but do note that the handler * groups will get merged (unlike {@link addChild}, where they * are independent). Also note that unlike with children, * when adding handlers to `other` *after* you extended * the current dispatcher, changes will not be applied. * * @param other Other dispatcher */ extend(other: Dispatcher): void { other._groupsOrder.forEach((group) => { if (!(group in this._groups)) { this._groups[group] = [] this._groupsOrder.push(group) } this._groups[group].push(...other._groups[group]) }) this._groupsOrder.sort((a, b) => a - b) } // addUpdateHandler convenience wrappers // private _addKnownHandler( name: keyof typeof handlers, filter: any, handler?: any, group?: number ): void { if (typeof handler === 'number') { this.addUpdateHandler((handlers as any)[name](filter), handler) } else { this.addUpdateHandler( (handlers as any)[name](filter, handler), group ) } } // begin-codegen /** * Register a raw update handler without any filters * * @param handler Raw update handler * @param group Handler group index */ onRawUpdate(handler: RawUpdateHandler['callback'], group?: number): void /** * Register a raw update handler with a filter * * @param filter Update filter function * @param handler Raw update handler * @param group Handler group index */ onRawUpdate( filter: RawUpdateHandler['check'], handler: RawUpdateHandler['callback'], group?: number ): void /** @internal */ onRawUpdate(filter: any, handler?: any, group?: number): void { this._addKnownHandler('rawUpdate', filter, handler, group) } /** * Register a new message handler without any filters * * @param handler New message handler * @param group Handler group index * @internal */ onNewMessage(handler: NewMessageHandler['callback'], group?: number): void /** * Register a new message handler with a filter * * @param filter Update filter * @param handler New message handler * @param group Handler group index */ onNewMessage( filter: UpdateFilter, handler: NewMessageHandler>['callback'], group?: number ): void /** @internal */ onNewMessage(filter: any, handler?: any, group?: number): void { this._addKnownHandler('newMessage', filter, handler, group) } /** * Register an edit message handler without any filters * * @param handler Edit message handler * @param group Handler group index * @internal */ onEditMessage(handler: EditMessageHandler['callback'], group?: number): void /** * Register an edit message handler with a filter * * @param filter Update filter * @param handler Edit message handler * @param group Handler group index */ onEditMessage( filter: UpdateFilter, handler: EditMessageHandler>['callback'], group?: number ): void /** @internal */ onEditMessage(filter: any, handler?: any, group?: number): void { this._addKnownHandler('editMessage', filter, handler, group) } /** * Register a chat member update handler without any filters * * @param handler Chat member update handler * @param group Handler group index * @internal */ onChatMemberUpdate( handler: ChatMemberUpdateHandler['callback'], group?: number ): void /** * Register a chat member update handler with a filter * * @param filter Update filter * @param handler Chat member update handler * @param group Handler group index */ onChatMemberUpdate( filter: UpdateFilter, handler: ChatMemberUpdateHandler< filters.Modify >['callback'], group?: number ): void /** @internal */ onChatMemberUpdate(filter: any, handler?: any, group?: number): void { this._addKnownHandler('chatMemberUpdate', filter, handler, group) } /** * Register an inline query handler without any filters * * @param handler Inline query handler * @param group Handler group index * @internal */ onInlineQuery(handler: InlineQueryHandler['callback'], group?: number): void /** * Register an inline query handler with a filter * * @param filter Update filter * @param handler Inline query handler * @param group Handler group index */ onInlineQuery( filter: UpdateFilter, handler: InlineQueryHandler< filters.Modify >['callback'], group?: number ): void /** @internal */ onInlineQuery(filter: any, handler?: any, group?: number): void { this._addKnownHandler('inlineQuery', filter, handler, group) } /** * Register a chosen inline result handler without any filters * * @param handler Chosen inline result handler * @param group Handler group index * @internal */ onChosenInlineResult( handler: ChosenInlineResultHandler['callback'], group?: number ): void /** * Register a chosen inline result handler with a filter * * @param filter Update filter * @param handler Chosen inline result handler * @param group Handler group index */ onChosenInlineResult( filter: UpdateFilter, handler: ChosenInlineResultHandler< filters.Modify >['callback'], group?: number ): void /** @internal */ onChosenInlineResult(filter: any, handler?: any, group?: number): void { this._addKnownHandler('chosenInlineResult', filter, handler, group) } // end-codegen }