diff --git a/packages/client/scripts/generate-client.js b/packages/client/scripts/generate-client.js index 82832455..da42643e 100644 --- a/packages/client/scripts/generate-client.js +++ b/packages/client/scripts/generate-client.js @@ -4,6 +4,7 @@ const fs = require('fs') const prettier = require('prettier') // not the best way but who cares lol const { createWriter } = require('../../tl/scripts/common') +const updates = require('./generate-updates') const targetDir = path.join(__dirname, '../src') @@ -299,6 +300,32 @@ async function main() { ) output.tab() + output.write(`/** + * Register a raw update handler + * + * @param name Event name + * @param handler Raw update handler + */ + on(name: 'raw_update', handler: ((upd: tl.TypeUpdate | tl.TypeMessage, users: UsersIndex, chats: ChatsIndex) => void)): this +/** + * Register a parsed update handler + * + * @param name Event name + * @param handler Raw update handler + */ + on(name: 'update', handler: ((upd: ParsedUpdate) => void)): this`) + + updates.types.forEach((type) => { + output.write(`/** + * Register ${updates.toSentence(type, 'inline')} + * + * @param name Event name + * @param handler ${updates.toSentence(type, 'full')} + */ +on(name: '${type.typeName}', handler: ((upd: ${type.updateType}) => void)): this`) + }) + + const printer = ts.createPrinter() const classContents = [] diff --git a/packages/client/scripts/generate-updates.js b/packages/client/scripts/generate-updates.js new file mode 100644 index 00000000..89f6f9f5 --- /dev/null +++ b/packages/client/scripts/generate-updates.js @@ -0,0 +1,326 @@ +const fs = require('fs') +const path = require('path') +const prettier = require('prettier') +const { + snakeToCamel, + camelToPascal, + camelToSnake, +} = require('../../tl/scripts/common') + +function parseUpdateTypes() { + const lines = fs + .readFileSync(path.join(__dirname, 'update-types.txt'), 'utf-8') + .split('\n') + .map((it) => it.trim()) + .filter((it) => it && it[0] !== '#') + + const ret = [] + + for (const line of lines) { + const m = line.match(/^([a-z_]+)(?:: ([a-zA-Z]+))? = ([a-zA-Z]+)( \+ State)?$/) + if (!m) throw new Error(`invalid syntax: ${line}`) + ret.push({ + typeName: m[1], + handlerTypeName: m[2] || camelToPascal(snakeToCamel(m[1])), + updateType: m[3], + funcName: m[2] + ? m[2][0].toLowerCase() + m[2].substr(1) + : snakeToCamel(m[1]), + state: !!m[4] + }) + } + + return ret +} + +function replaceSections(filename, sections, dir = __dirname) { + let lines = fs + .readFileSync(path.join(dir, '../src', filename), 'utf-8') + .split('\n') + + const findMarker = (marker) => { + const idx = lines.findIndex((line) => line.trim() === `// ${marker}`) + if (idx === -1) throw new Error(marker + ' not found') + return idx + } + + for (const [name, content] of Object.entries(sections)) { + const start = findMarker(`begin-${name}`) + const end = findMarker(`end-${name}`) + + if (start > end) throw new Error('begin is after end') + + lines.splice(start + 1, end - start - 1, content) + } + + fs.writeFileSync(path.join(dir, '../src', filename), lines.join('\n')) +} + +const types = parseUpdateTypes() + +async function formatFile(filename, dir = __dirname) { + const targetFile = path.join(dir, '../src/', filename) + const prettierConfig = await prettier.resolveConfig(targetFile) + let fullSource = await fs.promises.readFile(targetFile, 'utf-8') + fullSource = await prettier.format(fullSource, { + ...(prettierConfig || {}), + filepath: targetFile, + }) + await fs.promises.writeFile(targetFile, fullSource) +} + +function toSentence(type, stype = 'inline') { + const name = camelToSnake(type.handlerTypeName) + .toLowerCase() + .replace(/_/g, ' ') + + if (stype === 'inline') { + return `${name[0].match(/[aeiouy]/i) ? 'an' : 'a'} ${name} handler` + } else if (stype === 'plain') { + return `${name} handler` + } else { + return `${name[0].toUpperCase()}${name.substr(1)} handler` + } +} + +// function generateHandler() { +// const lines = [] +// const names = ['RawUpdateHandler'] +// +// // imports must be added manually because yeah +// +// types.forEach((type) => { +// if (type.updateType === 'IGNORE') return +// +// lines.push( +// `export type ${type.handlerTypeName}Handler = ParsedUpdateHandler<` + +// `'${type.typeName}', T${type.state ? ', S' : ''}>` +// ) +// names.push(`${type.handlerTypeName}Handler`) +// }) +// +// replaceSections('handler.ts', { +// codegen: +// lines.join('\n') + +// '\n\nexport type UpdateHandler = \n' + +// names.map((i) => ` | ${i}\n`).join(''), +// }) +// } +// +// function generateBuilders() { +// const lines = [] +// const imports = ['UpdateHandler'] +// +// types.forEach((type) => { +// imports.push(`${type.handlerTypeName}Handler`) +// +// if (type.updateType === 'IGNORE') { +// lines.push(` +// /** +// * Create ${toSentence(type)} +// * +// * @param handler ${toSentence(type, 'full')} +// */ +// export function ${type.funcName}( +// handler: ${type.handlerTypeName}Handler['callback'] +// ): ${type.handlerTypeName}Handler +// +// /** +// * Create ${toSentence(type)} with a filter +// * +// * @param filter Predicate to check the update against +// * @param handler ${toSentence(type, 'full')} +// */ +// export function ${type.funcName}( +// filter: ${type.handlerTypeName}Handler['check'], +// handler: ${type.handlerTypeName}Handler['callback'] +// ): ${type.handlerTypeName}Handler +// +// /** @internal */ +// export function ${type.funcName}(filter: any, handler?: any): ${ +// type.handlerTypeName +// }Handler { +// return _create('${type.typeName}', filter, handler) +// } +// `) +// } else { +// lines.push(` +// /** +// * Create ${toSentence(type)} +// * +// * @param handler ${toSentence(type, 'full')} +// */ +// export function ${type.funcName}( +// handler: ${type.handlerTypeName}Handler['callback'] +// ): ${type.handlerTypeName}Handler +// +// /** +// * Create ${toSentence(type)} with a filter +// * +// * @param filter Update filter +// * @param handler ${toSentence(type, 'full')} +// */ +// export function ${type.funcName}( +// filter: UpdateFilter<${type.updateType}, Mod>, +// handler: ${type.handlerTypeName}Handler< +// filters.Modify<${type.updateType}, Mod> +// >['callback'] +// ): ${type.handlerTypeName}Handler +// +// /** @internal */ +// export function ${type.funcName}( +// filter: any, +// handler?: any +// ): ${type.handlerTypeName}Handler { +// return _create('${type.typeName}', filter, handler) +// } +// `) +// } +// }) +// +// replaceSections('builders.ts', { +// codegen: lines.join('\n'), +// 'codegen-imports': +// 'import {\n' + +// imports.map((i) => ` ${i},\n`).join('') + +// "} from './handler'", +// }) +// } +// +// function generateDispatcher() { +// const lines = [] +// const declareLines = [] +// const imports = ['UpdateHandler'] +// +// types.forEach((type) => { +// imports.push(`${type.handlerTypeName}Handler`) +// +// if (type.updateType === 'IGNORE') { +// declareLines.push(` +// /** +// * Register a plain old ${toSentence(type, 'plain')} +// * +// * @param name Event name +// * @param handler ${toSentence(type, 'full')} +// */ +// on(name: '${type.typeName}', handler: ${type.handlerTypeName}Handler['callback']): this +// `) +// +// lines.push(` +// /** +// * Register ${toSentence(type)} without any filters +// * +// * @param handler ${toSentence(type, 'full')} +// * @param group Handler group index +// */ +// on${type.handlerTypeName}(handler: ${type.handlerTypeName}Handler['callback'], group?: number): void +// +// /** +// * Register ${toSentence(type)} with a filter +// * +// * @param filter Update filter function +// * @param handler ${toSentence(type, 'full')} +// * @param group Handler group index +// */ +// on${type.handlerTypeName}( +// filter: ${type.handlerTypeName}Handler['check'], +// handler: ${type.handlerTypeName}Handler['callback'], +// group?: number +// ): void +// +// /** @internal */ +// on${type.handlerTypeName}(filter: any, handler?: any, group?: number): void { +// this._addKnownHandler('${type.funcName}', filter, handler, group) +// } +// `) +// } else { +// declareLines.push(` +// /** +// * Register a plain old ${toSentence(type, 'plain')} +// * +// * @param name Event name +// * @param handler ${toSentence(type, 'full')} +// */ +// on(name: '${type.typeName}', handler: ${type.handlerTypeName}Handler['callback']): this +// +// `) +// lines.push(` +// /** +// * Register ${toSentence(type)} without any filters +// * +// * @param handler ${toSentence(type, 'full')} +// * @param group Handler group index +// */ +// on${type.handlerTypeName}(handler: ${type.handlerTypeName}Handler${type.state ? `<${type.updateType}, State extends never ? never : UpdateState>` : ''}['callback'], group?: number): void +// +// ${type.state ? ` +// /** +// * Register ${toSentence(type)} with a filter +// * +// * @param filter Update filter +// * @param handler ${toSentence(type, 'full')} +// * @param group Handler group index +// */ +// on${type.handlerTypeName}( +// filter: UpdateFilter<${type.updateType}, Mod, State>, +// handler: ${type.handlerTypeName}Handler, State extends never ? never : UpdateState>['callback'], +// group?: number +// ): void +// ` : ''} +// +// /** +// * Register ${toSentence(type)} with a filter +// * +// * @param filter Update filter +// * @param handler ${toSentence(type, 'full')} +// * @param group Handler group index +// */ +// on${type.handlerTypeName}( +// filter: UpdateFilter<${type.updateType}, Mod>, +// handler: ${type.handlerTypeName}Handler${type.state ? ', State extends never ? never : UpdateState' : ''}>['callback'], +// group?: number +// ): void +// +// /** @internal */ +// on${type.handlerTypeName}(filter: any, handler?: any, group?: number): void { +// this._addKnownHandler('${type.funcName}', filter, handler, group) +// } +// `) +// } +// }) +// +// replaceSections('dispatcher.ts', { +// codegen: lines.join('\n'), +// 'codegen-declare': declareLines.join('\n'), +// 'codegen-imports': +// 'import {\n' + +// imports.map((i) => ` ${i},\n`).join('') + +// "} from './handler'", +// }) +// } +// + +function generateParsedUpdate() { + replaceSections('types/updates/index.ts', { + codegen: 'export type ParsedUpdate =\n' + + types.map((typ) => ` | { name: '${typ.typeName}', data: ${typ.updateType} }\n`).join(''), + }) +} + +async function main() { + generateParsedUpdate() + // generateBuilders() + // generateHandler() + // generateDispatcher() + + // await formatFile('builders.ts') + // await formatFile('handler.ts') + // await formatFile('dispatcher.ts') +} + +module.exports = { types, toSentence, replaceSections, formatFile } + +if (require.main === module) { + main().catch(console.error) +} diff --git a/packages/dispatcher/scripts/update-types.txt b/packages/client/scripts/update-types.txt similarity index 86% rename from packages/dispatcher/scripts/update-types.txt rename to packages/client/scripts/update-types.txt index 23a88143..2deb434f 100644 --- a/packages/dispatcher/scripts/update-types.txt +++ b/packages/client/scripts/update-types.txt @@ -1,6 +1,4 @@ # format: type_name[: handler_type_name] = update_type[ + State] -# IGNORE as update_type disables filters modification -raw: RawUpdate = IGNORE new_message = Message + State edit_message = Message + State delete_message = DeleteMessageUpdate diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 3f4d9159..0ceba98d 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -142,12 +142,12 @@ import { getStickerSet } from './methods/stickers/get-sticker-set' import { moveStickerInSet } from './methods/stickers/move-sticker-in-set' import { setStickerSetThumb } from './methods/stickers/set-sticker-set-thumb' import { + _dispatchUpdate, _fetchUpdatesState, _handleUpdate, _loadStorage, _saveStorage, catchUp, - dispatchUpdate, } from './methods/updates' import { blockUser } from './methods/users/block-user' import { deleteProfilePhotos } from './methods/users/delete-profile-photos' @@ -168,17 +168,23 @@ import { Readable } from 'stream' import { ArrayWithTotal, BotCommands, + CallbackQuery, Chat, ChatEvent, ChatInviteLink, ChatMember, + ChatMemberUpdate, ChatPreview, ChatsIndex, + ChosenInlineResult, + DeleteMessageUpdate, Dialog, FileDownloadParameters, FormattedString, GameHighScore, + HistoryReadUpdate, IMessageEntityParser, + InlineQuery, InputFileLike, InputInlineResult, InputMediaLike, @@ -187,10 +193,13 @@ import { MaybeDynamic, Message, MessageMedia, + ParsedUpdate, PartialExcept, PartialOnly, Photo, Poll, + PollUpdate, + PollVoteUpdate, RawDocument, ReplyMarkup, SentCode, @@ -201,6 +210,8 @@ import { UploadFileLike, UploadedFile, User, + UserStatusUpdate, + UserTypingUpdate, UsersIndex, } from './types' import { @@ -212,6 +223,117 @@ import { import { tdFileId } from '@mtcute/file-id' export interface TelegramClient extends BaseTelegramClient { + /** + * Register a raw update handler + * + * @param name Event name + * @param handler Raw update handler + */ + on( + name: 'raw_update', + handler: ( + upd: tl.TypeUpdate | tl.TypeMessage, + users: UsersIndex, + chats: ChatsIndex + ) => void + ): this + /** + * Register a parsed update handler + * + * @param name Event name + * @param handler Raw update handler + */ + on(name: 'update', handler: (upd: ParsedUpdate) => void): this + /** + * Register a new message handler + * + * @param name Event name + * @param handler New message handler + */ + on(name: 'new_message', handler: (upd: Message) => void): this + /** + * Register an edit message handler + * + * @param name Event name + * @param handler Edit message handler + */ + on(name: 'edit_message', handler: (upd: Message) => void): this + /** + * Register a delete message handler + * + * @param name Event name + * @param handler Delete message handler + */ + on( + name: 'delete_message', + handler: (upd: DeleteMessageUpdate) => void + ): this + /** + * Register a chat member update handler + * + * @param name Event name + * @param handler Chat member update handler + */ + on(name: 'chat_member', handler: (upd: ChatMemberUpdate) => void): this + /** + * Register an inline query handler + * + * @param name Event name + * @param handler Inline query handler + */ + on(name: 'inline_query', handler: (upd: InlineQuery) => void): this + /** + * Register a chosen inline result handler + * + * @param name Event name + * @param handler Chosen inline result handler + */ + on( + name: 'chosen_inline_result', + handler: (upd: ChosenInlineResult) => void + ): this + /** + * Register a callback query handler + * + * @param name Event name + * @param handler Callback query handler + */ + on(name: 'callback_query', handler: (upd: CallbackQuery) => void): this + /** + * Register a poll update handler + * + * @param name Event name + * @param handler Poll update handler + */ + on(name: 'poll', handler: (upd: PollUpdate) => void): this + /** + * Register a poll vote handler + * + * @param name Event name + * @param handler Poll vote handler + */ + on(name: 'poll_vote', handler: (upd: PollVoteUpdate) => void): this + /** + * Register an user status update handler + * + * @param name Event name + * @param handler User status update handler + */ + on(name: 'user_status', handler: (upd: UserStatusUpdate) => void): this + /** + * Register an user typing handler + * + * @param name Event name + * @param handler User typing handler + */ + on(name: 'user_typing', handler: (upd: UserTypingUpdate) => void): this + /** + * Register a history read handler + * + * @param name Event name + * @param handler History read handler + */ + on(name: 'history_read', handler: (upd: HistoryReadUpdate) => void): this /** * Accept the given TOS * @@ -3110,29 +3232,6 @@ export interface TelegramClient extends BaseTelegramClient { progressCallback?: (uploaded: number, total: number) => void } ): Promise - /** - * Base function for update handling. Replace or override this function - * and implement your own update handler, and call this function - * to handle externally obtained or manually crafted updates. - * - * Note that this function is called every time an `Update` is received, - * not `Updates`. Low-level updates containers are parsed by the library, - * and you receive ready to use updates and related entities. - * Also note that entity maps may contain entities that are not - * used in this particular update, so do not rely on its contents. - * - * `update` might contain a Message object - in this case, - * it should be interpreted as some kind of `updateNewMessage`. - * - * @param update Update that has just happened - * @param users Map of users in this update - * @param chats Map of chats in this update - */ - dispatchUpdate( - update: tl.TypeUpdate | tl.TypeMessage, - users: UsersIndex, - chats: ChatsIndex - ): void _handleUpdate(update: tl.TypeUpdates, noDispatch?: boolean): void /** * Catch up with the server by loading missed updates. @@ -3524,7 +3623,7 @@ export class TelegramClient extends BaseTelegramClient { protected _fetchUpdatesState = _fetchUpdatesState protected _loadStorage = _loadStorage protected _saveStorage = _saveStorage - dispatchUpdate = dispatchUpdate + protected _dispatchUpdate = _dispatchUpdate _handleUpdate = _handleUpdate catchUp = catchUp blockUser = blockUser diff --git a/packages/client/src/methods/_imports.ts b/packages/client/src/methods/_imports.ts index 168819cc..65048fa0 100644 --- a/packages/client/src/methods/_imports.ts +++ b/packages/client/src/methods/_imports.ts @@ -40,7 +40,18 @@ import { MessageMedia, RawDocument, IMessageEntityParser, - FormattedString + FormattedString, + CallbackQuery, + ChatMemberUpdate, + ChosenInlineResult, + DeleteMessageUpdate, + HistoryReadUpdate, + InlineQuery, + ParsedUpdate, + PollUpdate, + PollVoteUpdate, + UserStatusUpdate, + UserTypingUpdate, } from '../types' // @copy diff --git a/packages/client/src/methods/updates.ts b/packages/client/src/methods/updates.ts index 81096ff3..1aa11bcc 100644 --- a/packages/client/src/methods/updates.ts +++ b/packages/client/src/methods/updates.ts @@ -14,6 +14,7 @@ import { } from '@mtcute/core' import { isDummyUpdate, isDummyUpdates } from '../utils/updates-utils' import { ChatsIndex, UsersIndex } from '../types' +import { _parseUpdate } from '../utils/parse-update' const debug = require('debug')('mtcute:upds') @@ -155,31 +156,21 @@ export async function _saveStorage( } /** - * Base function for update handling. Replace or override this function - * and implement your own update handler, and call this function - * to handle externally obtained or manually crafted updates. - * - * Note that this function is called every time an `Update` is received, - * not `Updates`. Low-level updates containers are parsed by the library, - * and you receive ready to use updates and related entities. - * Also note that entity maps may contain entities that are not - * used in this particular update, so do not rely on its contents. - * - * `update` might contain a Message object - in this case, - * it should be interpreted as some kind of `updateNewMessage`. - * - * @param update Update that has just happened - * @param users Map of users in this update - * @param chats Map of chats in this update * @internal */ -export function dispatchUpdate( +export function _dispatchUpdate( this: TelegramClient, update: tl.TypeUpdate | tl.TypeMessage, users: UsersIndex, chats: ChatsIndex ): void { - // no-op // + this.emit('raw_update', update, users, chats) + + const parsed = _parseUpdate(this, update, users, chats) + if (parsed) { + this.emit('update', parsed) + this.emit(parsed.name, parsed.data) + } } interface NoDispatchIndex { @@ -441,7 +432,7 @@ async function _loadDifference( if (noDispatch.msg[cid]?.[message.id]) return } - this.dispatchUpdate(message, users, chats) + this._dispatchUpdate(message, users, chats) }) for (const upd of diff.otherUpdates) { @@ -495,7 +486,7 @@ async function _loadDifference( if (noDispatch.pts[cid ?? 0]?.[pts]) continue } - this.dispatchUpdate(upd, users, chats) + this._dispatchUpdate(upd, users, chats) } this._pts = state.pts @@ -556,7 +547,7 @@ async function _loadChannelDifference( return if (message._ === 'messageEmpty') return - this.dispatchUpdate(message, users, chats) + this._dispatchUpdate(message, users, chats) }) break } @@ -565,7 +556,7 @@ async function _loadChannelDifference( if (noDispatch && noDispatch.msg[channelId]?.[message.id]) return if (message._ === 'messageEmpty') return - this.dispatchUpdate(message, users, chats) + this._dispatchUpdate(message, users, chats) }) diff.otherUpdates.forEach((upd) => { @@ -582,7 +573,7 @@ async function _loadChannelDifference( if (upd._ === 'updateNewChannelMessage' && upd.message._ === 'messageEmpty') return - this.dispatchUpdate(upd, users, chats) + this._dispatchUpdate(upd, users, chats) }) pts = diff.pts @@ -728,7 +719,7 @@ export function _handleUpdate( } if (!isDummyUpdate(upd) && !noDispatch) { - this.dispatchUpdate(upd, users, chats) + this._dispatchUpdate(upd, users, chats) } if (channelId) { @@ -738,7 +729,7 @@ export function _handleUpdate( this._pts = pts } } else if (!noDispatch) { - this.dispatchUpdate(upd, users, chats) + this._dispatchUpdate(upd, users, chats) } } @@ -769,7 +760,7 @@ export function _handleUpdate( return await _loadDifference.call(this) } - this.dispatchUpdate(upd, peers.users, peers.chats) + this._dispatchUpdate(upd, peers.users, peers.chats) } } @@ -821,7 +812,7 @@ export function _handleUpdate( this._date = update.date this._pts = update.pts - this.dispatchUpdate(message, peers.users, peers.chats) + this._dispatchUpdate(message, peers.users, peers.chats) break } case 'updateShortChatMessage': { @@ -869,7 +860,7 @@ export function _handleUpdate( this._date = update.date this._pts = update.pts - this.dispatchUpdate(message, peers.users, peers.chats) + this._dispatchUpdate(message, peers.users, peers.chats) break } case 'updateShortSentMessage': { diff --git a/packages/dispatcher/src/conversation.ts b/packages/client/src/types/conversation.ts similarity index 82% rename from packages/dispatcher/src/conversation.ts rename to packages/client/src/types/conversation.ts index 0e3e1d9e..2691561c 100644 --- a/packages/dispatcher/src/conversation.ts +++ b/packages/client/src/types/conversation.ts @@ -1,104 +1,18 @@ -import { Dispatcher } from './dispatcher' -import { - FormattedString, - InputMediaLike, - InputPeerLike, - MaybeAsync, - Message, - MtCuteArgumentError, - TelegramClient, - TimeoutError, - tl, -} from '@mtcute/client' -import { AsyncLock, getMarkedPeerId } from '@mtcute/core' +import { AsyncLock, getMarkedPeerId, MaybeAsync } from '@mtcute/core' import { ControllablePromise, createControllablePromise, } from '@mtcute/core/src/utils/controllable-promise' +import { TelegramClient } from '../client' +import { InputMediaLike } from './media' +import { MtCuteArgumentError } from './errors' +import { InputPeerLike } from './peers' import { HistoryReadUpdate } from './updates' - -interface OneWayLinkedListItem { - v: T - n?: OneWayLinkedListItem -} - -class Queue { - first?: OneWayLinkedListItem - last?: OneWayLinkedListItem - - length = 0 - - constructor (readonly limit = 0) { - } - - push(item: T): void { - const it: OneWayLinkedListItem = { v: item } - if (!this.first) { - this.first = this.last = it - } else { - this.last!.n = it - this.last = it - } - - this.length += 1 - - if (this.limit) { - while (this.first && this.length > this.limit) { - this.first = this.first.n - this.length -= 1 - } - } - } - - empty(): boolean { - return this.first === undefined - } - - peek(): T | undefined { - return this.first?.v - } - - pop(): T | undefined { - if (!this.first) return undefined - - const it = this.first - this.first = this.first.n - if (!this.first) this.last = undefined - - this.length -= 1 - return it.v - } - - removeBy(pred: (it: T) => boolean): void { - if (!this.first) return - - let prev: OneWayLinkedListItem | undefined = undefined - let it = this.first - while (it && !pred(it.v)) { - if (!it.n) return - - prev = it - it = it.n - } - - if (!it) return - - if (prev) { - prev.n = it.n - } else { - this.first = it.n - } - - if (!this.first) this.last = undefined - - this.length -= 1 - } - - clear(): void { - this.first = this.last = undefined - this.length = 0 - } -} +import { FormattedString } from './parser' +import { Message } from './messages' +import { tl } from '@mtcute/tl' +import { TimeoutError } from '@mtcute/tl/errors' +import { Queue } from '../utils/queue' interface QueuedHandler { promise: ControllablePromise @@ -120,7 +34,6 @@ interface QueuedHandler { export class Conversation { private _inputPeer: tl.TypeInputPeer private _chatId: number - private _client: TelegramClient private _started = false private _lastMessage: number @@ -136,7 +49,7 @@ export class Conversation { private _pendingRead: Record> = {} constructor( - readonly dispatcher: Dispatcher, + readonly client: TelegramClient, readonly chat: InputPeerLike ) { this._onNewMessage = this._onNewMessage.bind(this) @@ -186,24 +99,16 @@ export class Conversation { async start(): Promise { if (this._started) return - const client = this.dispatcher['_client'] - if (!client) { - throw new MtCuteArgumentError( - 'Dispatcher is not bound to a client!' - ) - } - - this._client = client this._started = true - this._inputPeer = await client.resolvePeer(this.chat) + this._inputPeer = await this.client.resolvePeer(this.chat) this._chatId = getMarkedPeerId(this._inputPeer) - const dialog = await client.getPeerDialogs(this._inputPeer) + const dialog = await this.client.getPeerDialogs(this._inputPeer) this._lastMessage = this._lastReceivedMessage = dialog.lastMessage.id - this.dispatcher.on('new_message', this._onNewMessage) - this.dispatcher.on('edit_message', this._onEditMessage) - this.dispatcher.on('history_read', this._onHistoryRead) + this.client.on('new_message', this._onNewMessage) + this.client.on('edit_message', this._onEditMessage) + this.client.on('history_read', this._onHistoryRead) } /** @@ -212,9 +117,9 @@ export class Conversation { stop(): void { if (!this._started) return - this.dispatcher.off('new_message', this._onNewMessage) - this.dispatcher.off('edit_message', this._onEditMessage) - this.dispatcher.off('history_read', this._onHistoryRead) + this.client.off('new_message', this._onNewMessage) + this.client.off('edit_message', this._onEditMessage) + this.client.off('history_read', this._onHistoryRead) // reset pending status this._queuedNewMessage.clear() @@ -240,7 +145,7 @@ export class Conversation { throw new MtCuteArgumentError("Conversation hasn't started yet") } - const res = await this._client.sendText(this._inputPeer, text, params) + const res = await this.client.sendText(this._inputPeer, text, params) this._lastMessage = res.id return res } @@ -259,7 +164,7 @@ export class Conversation { throw new MtCuteArgumentError("Conversation hasn't started yet") } - const res = await this._client.sendMedia(this._inputPeer, media, params) + const res = await this.client.sendMedia(this._inputPeer, media, params) this._lastMessage = res.id return res } @@ -278,7 +183,7 @@ export class Conversation { throw new MtCuteArgumentError("Conversation hasn't started yet") } - const res = await this._client.sendMediaGroup( + const res = await this.client.sendMediaGroup( this._inputPeer, medias, params @@ -305,7 +210,7 @@ export class Conversation { message = this._lastMessage ?? 0 } - return this._client.readHistory(this._inputPeer, message, clearMentions) + return this.client.readHistory(this._inputPeer, message, clearMentions) } /** @@ -537,7 +442,7 @@ export class Conversation { ) // check if the message is already read - const dialog = await this._client.getPeerDialogs(this._inputPeer) + const dialog = await this.client.getPeerDialogs(this._inputPeer) if (dialog.lastRead >= msgId) return const promise = createControllablePromise() @@ -578,7 +483,7 @@ export class Conversation { this._queuedNewMessage.pop() } } catch (e) { - this._client['_emitError'](e) + this.client['_emitError'](e) } this._lastMessage = this._lastReceivedMessage = msg.id @@ -603,7 +508,7 @@ export class Conversation { it.promise.resolve(msg) delete this._pendingEditMessage[msg.id] } - })().catch((e) => this._client['_emitError'](e)) + })().catch((e) => this.client['_emitError'](e)) } private _onHistoryRead(upd: HistoryReadUpdate) { diff --git a/packages/client/src/types/index.ts b/packages/client/src/types/index.ts index 97d11489..374453c9 100644 --- a/packages/client/src/types/index.ts +++ b/packages/client/src/types/index.ts @@ -5,6 +5,8 @@ export * from './media' export * from './messages' export * from './peers' export * from './misc' +export * from './updates' +export * from './conversation' export * from './errors' export * from './parser' diff --git a/packages/dispatcher/src/updates/chat-member-update.ts b/packages/client/src/types/updates/chat-member-update.ts similarity index 99% rename from packages/dispatcher/src/updates/chat-member-update.ts rename to packages/client/src/types/updates/chat-member-update.ts index 9d3b5535..f89f8cda 100644 --- a/packages/dispatcher/src/updates/chat-member-update.ts +++ b/packages/client/src/types/updates/chat-member-update.ts @@ -8,7 +8,7 @@ import { User, UsersIndex, } from '@mtcute/client' -import { makeInspectable } from '@mtcute/client/src/types/utils' +import { makeInspectable } from '../utils' export namespace ChatMemberUpdate { /** diff --git a/packages/dispatcher/src/updates/chosen-inline-result.ts b/packages/client/src/types/updates/chosen-inline-result.ts similarity index 95% rename from packages/dispatcher/src/updates/chosen-inline-result.ts rename to packages/client/src/types/updates/chosen-inline-result.ts index a55250a4..2d85c10e 100644 --- a/packages/dispatcher/src/updates/chosen-inline-result.ts +++ b/packages/client/src/types/updates/chosen-inline-result.ts @@ -1,4 +1,3 @@ -import { makeInspectable } from '@mtcute/client/src/types/utils' import { tl } from '@mtcute/tl' import { TelegramClient, @@ -7,7 +6,8 @@ import { MtCuteArgumentError, UsersIndex, } from '@mtcute/client' -import { encodeInlineMessageId } from '@mtcute/client/src/utils/inline-utils' +import { encodeInlineMessageId } from '../../utils/inline-utils' +import { makeInspectable } from '../utils' /** * An inline result was chosen by the user and sent to some chat diff --git a/packages/dispatcher/src/updates/delete-message-update.ts b/packages/client/src/types/updates/delete-message-update.ts similarity index 93% rename from packages/dispatcher/src/updates/delete-message-update.ts rename to packages/client/src/types/updates/delete-message-update.ts index b52034c7..7f94381e 100644 --- a/packages/dispatcher/src/updates/delete-message-update.ts +++ b/packages/client/src/types/updates/delete-message-update.ts @@ -1,7 +1,7 @@ import { tl } from '@mtcute/tl' -import { makeInspectable } from '@mtcute/client/src/types/utils' import { MAX_CHANNEL_ID } from '@mtcute/core' import { TelegramClient } from '@mtcute/client' +import { makeInspectable } from '../utils' /** * One or more messages were deleted diff --git a/packages/dispatcher/src/updates/history-read-update.ts b/packages/client/src/types/updates/history-read-update.ts similarity index 98% rename from packages/dispatcher/src/updates/history-read-update.ts rename to packages/client/src/types/updates/history-read-update.ts index d41fbbb8..1d50d1c4 100644 --- a/packages/dispatcher/src/updates/history-read-update.ts +++ b/packages/client/src/types/updates/history-read-update.ts @@ -1,7 +1,7 @@ import { tl } from '@mtcute/tl' import { TelegramClient } from '@mtcute/client' import { getMarkedPeerId, MAX_CHANNEL_ID } from '@mtcute/core' -import { makeInspectable } from '@mtcute/client/src/types/utils' +import { makeInspectable } from '../utils' export class HistoryReadUpdate { constructor ( diff --git a/packages/client/src/types/updates/index.ts b/packages/client/src/types/updates/index.ts new file mode 100644 index 00000000..df03b121 --- /dev/null +++ b/packages/client/src/types/updates/index.ts @@ -0,0 +1,38 @@ +import { CallbackQuery, InlineQuery, Message } from '../..' + +import { DeleteMessageUpdate } from './delete-message-update' +import { ChatMemberUpdate } from './chat-member-update' +import { ChosenInlineResult } from './chosen-inline-result' +import { PollUpdate } from './poll-update' +import { PollVoteUpdate } from './poll-vote' +import { UserStatusUpdate } from './user-status-update' +import { UserTypingUpdate } from './user-typing-update' +import { HistoryReadUpdate } from './history-read-update' + +export { + DeleteMessageUpdate, + ChatMemberUpdate, + ChosenInlineResult, + PollUpdate, + PollVoteUpdate, + UserStatusUpdate, + UserTypingUpdate, + HistoryReadUpdate, +} + +// begin-codegen +export type ParsedUpdate = + | { name: 'new_message', data: Message } + | { name: 'edit_message', data: Message } + | { name: 'delete_message', data: DeleteMessageUpdate } + | { name: 'chat_member', data: ChatMemberUpdate } + | { name: 'inline_query', data: InlineQuery } + | { name: 'chosen_inline_result', data: ChosenInlineResult } + | { name: 'callback_query', data: CallbackQuery } + | { name: 'poll', data: PollUpdate } + | { name: 'poll_vote', data: PollVoteUpdate } + | { name: 'user_status', data: UserStatusUpdate } + | { name: 'user_typing', data: UserTypingUpdate } + | { name: 'history_read', data: HistoryReadUpdate } + +// end-codegen diff --git a/packages/dispatcher/src/updates/poll-update.ts b/packages/client/src/types/updates/poll-update.ts similarity index 97% rename from packages/dispatcher/src/updates/poll-update.ts rename to packages/client/src/types/updates/poll-update.ts index 645d4de4..28f028bd 100644 --- a/packages/dispatcher/src/updates/poll-update.ts +++ b/packages/client/src/types/updates/poll-update.ts @@ -1,6 +1,6 @@ -import { makeInspectable } from '@mtcute/client/src/types/utils' import { TelegramClient, Poll, UsersIndex } from '@mtcute/client' import { tl } from '@mtcute/tl' +import { makeInspectable } from '../utils' /** * Poll state has changed (stopped, somebody diff --git a/packages/dispatcher/src/updates/poll-vote.ts b/packages/client/src/types/updates/poll-vote.ts similarity index 97% rename from packages/dispatcher/src/updates/poll-vote.ts rename to packages/client/src/types/updates/poll-vote.ts index 4dce911b..6cb39107 100644 --- a/packages/dispatcher/src/updates/poll-vote.ts +++ b/packages/client/src/types/updates/poll-vote.ts @@ -5,7 +5,7 @@ import { UsersIndex, } from '@mtcute/client' import { tl } from '@mtcute/tl' -import { makeInspectable } from '@mtcute/client/src/types/utils' +import { makeInspectable } from '../utils' /** * Some user has voted in a public poll. diff --git a/packages/dispatcher/src/updates/user-status-update.ts b/packages/client/src/types/updates/user-status-update.ts similarity index 95% rename from packages/dispatcher/src/updates/user-status-update.ts rename to packages/client/src/types/updates/user-status-update.ts index c6a48657..a81c51d3 100644 --- a/packages/dispatcher/src/updates/user-status-update.ts +++ b/packages/client/src/types/updates/user-status-update.ts @@ -1,6 +1,6 @@ import { TelegramClient, User } from '@mtcute/client' import { tl } from '@mtcute/tl' -import { makeInspectable } from '@mtcute/client/src/types/utils' +import { makeInspectable } from '../utils' /** * User status has changed diff --git a/packages/dispatcher/src/updates/user-typing-update.ts b/packages/client/src/types/updates/user-typing-update.ts similarity index 98% rename from packages/dispatcher/src/updates/user-typing-update.ts rename to packages/client/src/types/updates/user-typing-update.ts index 24b20e2d..f1bf3fa1 100644 --- a/packages/dispatcher/src/updates/user-typing-update.ts +++ b/packages/client/src/types/updates/user-typing-update.ts @@ -8,7 +8,7 @@ import { } from '@mtcute/client' import { tl } from '@mtcute/tl' import { getBarePeerId, MAX_CHANNEL_ID } from '@mtcute/core' -import { makeInspectable } from '@mtcute/client/src/types/utils' +import { makeInspectable } from '../utils' /** * User's typing status has changed. diff --git a/packages/client/src/utils/parse-update.ts b/packages/client/src/utils/parse-update.ts new file mode 100644 index 00000000..38ff13d2 --- /dev/null +++ b/packages/client/src/utils/parse-update.ts @@ -0,0 +1,131 @@ +import { TelegramClient } from '../client' +import { tl } from '@mtcute/tl' +import { + CallbackQuery, + ChatMemberUpdate, + ChatsIndex, + ChosenInlineResult, + DeleteMessageUpdate, + HistoryReadUpdate, + InlineQuery, + Message, + ParsedUpdate, + PollUpdate, + PollVoteUpdate, + UsersIndex, + UserStatusUpdate, + UserTypingUpdate, +} from '../types' + +type ParserFunction = ( + client: TelegramClient, + upd: tl.TypeUpdate | tl.TypeMessage, + users: UsersIndex, + chats: ChatsIndex +) => any +type UpdateParser = [ParsedUpdate['name'], ParserFunction] + +const baseMessageParser: ParserFunction = ( + client: TelegramClient, + upd, + users, + chats +) => + new Message( + client, + tl.isAnyMessage(upd) ? upd : (upd as any).message, + users, + chats, + upd._ === 'updateNewScheduledMessage' + ) + +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 callbackQueryParser: UpdateParser = [ + 'callback_query', + (client, upd, users) => new CallbackQuery(client, upd as any, users), +] +const userTypingParser: UpdateParser = [ + 'user_typing', + (client, upd) => new UserTypingUpdate(client, upd as any), +] +const deleteMessageParser: UpdateParser = [ + 'delete_message', + (client, upd) => new DeleteMessageUpdate(client, upd as any), +] +const historyReadParser: UpdateParser = [ + 'history_read', + (client, upd) => new HistoryReadUpdate(client, upd as any), +] + +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), + ], + updateBotCallbackQuery: callbackQueryParser, + updateInlineBotCallbackQuery: callbackQueryParser, + updateMessagePoll: [ + 'poll', + (client, upd, users) => new PollUpdate(client, upd as any, users), + ], + updateMessagePollVote: [ + 'poll_vote', + (client, upd, users) => new PollVoteUpdate(client, upd as any, users), + ], + updateUserStatus: [ + 'user_status', + (client, upd) => new UserStatusUpdate(client, upd as any), + ], + updateChannelUserTyping: userTypingParser, + updateChatUserTyping: userTypingParser, + updateUserTyping: userTypingParser, + updateDeleteChannelMessages: deleteMessageParser, + updateDeleteMessages: deleteMessageParser, + updateReadHistoryInbox: historyReadParser, + updateReadHistoryOutbox: historyReadParser, + updateReadChannelInbox: historyReadParser, + updateReadChannelOutbox: historyReadParser, + updateReadChannelDiscussionInbox: historyReadParser, + updateReadChannelDiscussionOutbox: historyReadParser, +} + +/** @internal */ +export function _parseUpdate( + client: TelegramClient, + update: tl.TypeUpdate | tl.TypeMessage, + users: UsersIndex, + chats: ChatsIndex +): ParsedUpdate | null { + const pair = PARSERS[update._] + if (pair) { + return { + name: pair[0], + data: pair[1](client, update, users, chats), + } + } else { + return null + } +} diff --git a/packages/client/src/utils/queue.ts b/packages/client/src/utils/queue.ts new file mode 100644 index 00000000..99f3eb05 --- /dev/null +++ b/packages/client/src/utils/queue.ts @@ -0,0 +1,81 @@ +interface OneWayLinkedListItem { + v: T + n?: OneWayLinkedListItem +} + +export class Queue { + first?: OneWayLinkedListItem + last?: OneWayLinkedListItem + + length = 0 + + constructor(readonly limit = 0) {} + + push(item: T): void { + const it: OneWayLinkedListItem = { v: item } + if (!this.first) { + this.first = this.last = it + } else { + this.last!.n = it + this.last = it + } + + this.length += 1 + + if (this.limit) { + while (this.first && this.length > this.limit) { + this.first = this.first.n + this.length -= 1 + } + } + } + + empty(): boolean { + return this.first === undefined + } + + peek(): T | undefined { + return this.first?.v + } + + pop(): T | undefined { + if (!this.first) return undefined + + const it = this.first + this.first = this.first.n + if (!this.first) this.last = undefined + + this.length -= 1 + return it.v + } + + removeBy(pred: (it: T) => boolean): void { + if (!this.first) return + + let prev: OneWayLinkedListItem | undefined = undefined + let it = this.first + while (it && !pred(it.v)) { + if (!it.n) return + + prev = it + it = it.n + } + + if (!it) return + + if (prev) { + prev.n = it.n + } else { + this.first = it.n + } + + if (!this.first) this.last = undefined + + this.length -= 1 + } + + clear(): void { + this.first = this.last = undefined + this.length = 0 + } +} diff --git a/packages/core/src/base-client.ts b/packages/core/src/base-client.ts index 9e5eeef6..f022b041 100644 --- a/packages/core/src/base-client.ts +++ b/packages/core/src/base-client.ts @@ -35,6 +35,7 @@ import bigInt from 'big-integer' import { BinaryWriter } from './utils/binary/binary-writer' import { encodeUrlSafeBase64, parseUrlSafeBase64 } from './utils/buffer-utils' import { BinaryReader } from './utils/binary/binary-reader' +import EventEmitter from 'events' const debug = require('debug')('mtcute:base') @@ -148,7 +149,7 @@ export namespace BaseTelegramClient { } } -export class BaseTelegramClient { +export class BaseTelegramClient extends EventEmitter { /** * `initConnection` params taken from {@link BaseTelegramClient.Options.initConnectionOptions}. */ @@ -242,6 +243,8 @@ export class BaseTelegramClient { protected _handleUpdate(update: tl.TypeUpdates): void {} constructor(opts: BaseTelegramClient.Options) { + super() + const apiId = typeof opts.apiId === 'string' ? parseInt(opts.apiId) : opts.apiId if (isNaN(apiId)) diff --git a/packages/dispatcher/scripts/generate.js b/packages/dispatcher/scripts/generate.js index 1125f21f..89d32bb6 100644 --- a/packages/dispatcher/scripts/generate.js +++ b/packages/dispatcher/scripts/generate.js @@ -1,87 +1,4 @@ -const fs = require('fs') -const path = require('path') -const prettier = require('prettier') -const { - snakeToCamel, - camelToPascal, - camelToSnake, -} = require('../../tl/scripts/common') - -function parseUpdateTypes() { - const lines = fs - .readFileSync(path.join(__dirname, 'update-types.txt'), 'utf-8') - .split('\n') - .map((it) => it.trim()) - .filter((it) => it && it[0] !== '#') - - const ret = [] - - for (const line of lines) { - const m = line.match(/^([a-z_]+)(?:: ([a-zA-Z]+))? = ([a-zA-Z]+)( \+ State)?$/) - if (!m) throw new Error(`invalid syntax: ${line}`) - ret.push({ - typeName: m[1], - handlerTypeName: m[2] || camelToPascal(snakeToCamel(m[1])), - updateType: m[3], - funcName: m[2] - ? m[2][0].toLowerCase() + m[2].substr(1) - : snakeToCamel(m[1]), - state: !!m[4] - }) - } - - return ret -} - -function replaceSections(filename, sections) { - let lines = fs - .readFileSync(path.join(__dirname, '../src', filename), 'utf-8') - .split('\n') - - const findMarker = (marker) => { - const idx = lines.findIndex((line) => line.trim() === `// ${marker}`) - if (idx === -1) throw new Error(marker + ' not found') - return idx - } - - for (const [name, content] of Object.entries(sections)) { - const start = findMarker(`begin-${name}`) - const end = findMarker(`end-${name}`) - - if (start > end) throw new Error('begin is after end') - - lines.splice(start + 1, end - start - 1, content) - } - - fs.writeFileSync(path.join(__dirname, '../src', filename), lines.join('\n')) -} - -const types = parseUpdateTypes() - -async function formatFile(filename) { - const targetFile = path.join(__dirname, '../src/', filename) - const prettierConfig = await prettier.resolveConfig(targetFile) - let fullSource = await fs.promises.readFile(targetFile, 'utf-8') - fullSource = await prettier.format(fullSource, { - ...(prettierConfig || {}), - filepath: targetFile, - }) - await fs.promises.writeFile(targetFile, fullSource) -} - -function toSentence(type, stype = 'inline') { - const name = camelToSnake(type.handlerTypeName) - .toLowerCase() - .replace(/_/g, ' ') - - if (stype === 'inline') { - return `${name[0].match(/[aeiouy]/i) ? 'an' : 'a'} ${name} handler` - } else if (stype === 'plain') { - return `${name} handler` - } else { - return `${name[0].toUpperCase()}${name.substr(1)} handler` - } -} +const { types, toSentence, replaceSections, formatFile } = require('../../client/scripts/generate-updates') function generateHandler() { const lines = [] @@ -90,8 +7,6 @@ function generateHandler() { // imports must be added manually because yeah types.forEach((type) => { - if (type.updateType === 'IGNORE') return - lines.push( `export type ${type.handlerTypeName}Handler = ParsedUpdateHandler<` + @@ -105,147 +20,17 @@ function generateHandler() { lines.join('\n') + '\n\nexport type UpdateHandler = \n' + names.map((i) => ` | ${i}\n`).join(''), - }) -} - -function generateBuilders() { - const lines = [] - const imports = ['UpdateHandler'] - - types.forEach((type) => { - imports.push(`${type.handlerTypeName}Handler`) - - if (type.updateType === 'IGNORE') { - lines.push(` - /** - * Create ${toSentence(type)} - * - * @param handler ${toSentence(type, 'full')} - */ - export function ${type.funcName}( - handler: ${type.handlerTypeName}Handler['callback'] - ): ${type.handlerTypeName}Handler - - /** - * Create ${toSentence(type)} with a filter - * - * @param filter Predicate to check the update against - * @param handler ${toSentence(type, 'full')} - */ - export function ${type.funcName}( - filter: ${type.handlerTypeName}Handler['check'], - handler: ${type.handlerTypeName}Handler['callback'] - ): ${type.handlerTypeName}Handler - - /** @internal */ - export function ${type.funcName}(filter: any, handler?: any): ${ - type.handlerTypeName - }Handler { - return _create('${type.typeName}', filter, handler) - } -`) - } else { - lines.push(` - /** - * Create ${toSentence(type)} - * - * @param handler ${toSentence(type, 'full')} - */ - export function ${type.funcName}( - handler: ${type.handlerTypeName}Handler['callback'] - ): ${type.handlerTypeName}Handler - - /** - * Create ${toSentence(type)} with a filter - * - * @param filter Update filter - * @param handler ${toSentence(type, 'full')} - */ - export function ${type.funcName}( - filter: UpdateFilter<${type.updateType}, Mod>, - handler: ${type.handlerTypeName}Handler< - filters.Modify<${type.updateType}, Mod> - >['callback'] - ): ${type.handlerTypeName}Handler - - /** @internal */ - export function ${type.funcName}( - filter: any, - handler?: any - ): ${type.handlerTypeName}Handler { - return _create('${type.typeName}', filter, handler) - } -`) - } - }) - - replaceSections('builders.ts', { - codegen: lines.join('\n'), - 'codegen-imports': - 'import {\n' + - imports.map((i) => ` ${i},\n`).join('') + - "} from './handler'", - }) + }, __dirname) } function generateDispatcher() { const lines = [] - const declareLines = [] - const imports = ['UpdateHandler'] + const imports = ['UpdateHandler', 'RawUpdateHandler'] types.forEach((type) => { imports.push(`${type.handlerTypeName}Handler`) - if (type.updateType === 'IGNORE') { - declareLines.push(` - /** - * Register a plain old ${toSentence(type, 'plain')} - * - * @param name Event name - * @param handler ${toSentence(type, 'full')} - */ - on(name: '${type.typeName}', handler: ${type.handlerTypeName}Handler['callback']): this -`) - - lines.push(` - /** - * Register ${toSentence(type)} without any filters - * - * @param handler ${toSentence(type, 'full')} - * @param group Handler group index - */ - on${type.handlerTypeName}(handler: ${type.handlerTypeName}Handler['callback'], group?: number): void - - /** - * Register ${toSentence(type)} with a filter - * - * @param filter Update filter function - * @param handler ${toSentence(type, 'full')} - * @param group Handler group index - */ - on${type.handlerTypeName}( - filter: ${type.handlerTypeName}Handler['check'], - handler: ${type.handlerTypeName}Handler['callback'], - group?: number - ): void - - /** @internal */ - on${type.handlerTypeName}(filter: any, handler?: any, group?: number): void { - this._addKnownHandler('${type.funcName}', filter, handler, group) - } -`) - } else { - declareLines.push(` - /** - * Register a plain old ${toSentence(type, 'plain')} - * - * @param name Event name - * @param handler ${toSentence(type, 'full')} - */ - on(name: '${type.typeName}', handler: ${type.handlerTypeName}Handler['callback']): this - -`) - lines.push(` + lines.push(` /** * Register ${toSentence(type)} without any filters * @@ -284,30 +69,31 @@ ${type.state ? ` /** @internal */ on${type.handlerTypeName}(filter: any, handler?: any, group?: number): void { - this._addKnownHandler('${type.funcName}', filter, handler, group) + this._addKnownHandler('${type.typeName}', filter, handler, group) } `) - } }) replaceSections('dispatcher.ts', { codegen: lines.join('\n'), - 'codegen-declare': declareLines.join('\n'), 'codegen-imports': 'import {\n' + imports.map((i) => ` ${i},\n`).join('') + "} from './handler'", - }) + }, __dirname) } + async function main() { - generateBuilders() generateHandler() generateDispatcher() - await formatFile('builders.ts') - await formatFile('handler.ts') - await formatFile('dispatcher.ts') + await formatFile('handler.ts', __dirname) + await formatFile('dispatcher.ts', __dirname) } -main().catch(console.error) +module.exports = { types, toSentence } + +if (require.main === module) { + main().catch(console.error) +} diff --git a/packages/dispatcher/src/builders.ts b/packages/dispatcher/src/builders.ts deleted file mode 100644 index 2f34d152..00000000 --- a/packages/dispatcher/src/builders.ts +++ /dev/null @@ -1,423 +0,0 @@ -/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -// begin-codegen-imports -import { - UpdateHandler, - RawUpdateHandler, - NewMessageHandler, - EditMessageHandler, - DeleteMessageHandler, - ChatMemberUpdateHandler, - InlineQueryHandler, - ChosenInlineResultHandler, - CallbackQueryHandler, - PollUpdateHandler, - PollVoteHandler, - UserStatusUpdateHandler, - UserTypingHandler, - HistoryReadHandler, -} from './handler' -// end-codegen-imports -import { filters, UpdateFilter } from './filters' -import { CallbackQuery, InlineQuery, Message } from '@mtcute/client' -import { - ChatMemberUpdate, - ChosenInlineResult, - PollUpdate, - PollVoteUpdate, - UserStatusUpdate, - UserTypingUpdate, - DeleteMessageUpdate, - HistoryReadUpdate, -} from './updates' - -function _create( - type: T['type'], - filter: any, - handler?: any -): T { - if (handler) { - return { - type, - check: filter, - callback: handler, - } as any - } - - return { - type, - callback: filter, - } as any -} - -export namespace handlers { - // begin-codegen - - /** - * Create a raw update handler - * - * @param handler Raw update handler - */ - export function rawUpdate( - handler: RawUpdateHandler['callback'] - ): RawUpdateHandler - - /** - * Create a raw update handler with a filter - * - * @param filter Predicate to check the update against - * @param handler Raw update handler - */ - export function rawUpdate( - filter: RawUpdateHandler['check'], - handler: RawUpdateHandler['callback'] - ): RawUpdateHandler - - /** @internal */ - export function rawUpdate(filter: any, handler?: any): RawUpdateHandler { - return _create('raw', filter, handler) - } - - /** - * Create a new message handler - * - * @param handler New message handler - */ - export function newMessage( - handler: NewMessageHandler['callback'] - ): NewMessageHandler - - /** - * Create a new message handler with a filter - * - * @param filter Update filter - * @param handler New message handler - */ - export function newMessage( - filter: UpdateFilter, - handler: NewMessageHandler>['callback'] - ): NewMessageHandler - - /** @internal */ - export function newMessage(filter: any, handler?: any): NewMessageHandler { - return _create('new_message', filter, handler) - } - - /** - * Create an edit message handler - * - * @param handler Edit message handler - */ - export function editMessage( - handler: EditMessageHandler['callback'] - ): EditMessageHandler - - /** - * Create an edit message handler with a filter - * - * @param filter Update filter - * @param handler Edit message handler - */ - export function editMessage( - filter: UpdateFilter, - handler: EditMessageHandler>['callback'] - ): EditMessageHandler - - /** @internal */ - export function editMessage( - filter: any, - handler?: any - ): EditMessageHandler { - return _create('edit_message', filter, handler) - } - - /** - * Create a delete message handler - * - * @param handler Delete message handler - */ - export function deleteMessage( - handler: DeleteMessageHandler['callback'] - ): DeleteMessageHandler - - /** - * Create a delete message handler with a filter - * - * @param filter Update filter - * @param handler Delete message handler - */ - export function deleteMessage( - filter: UpdateFilter, - handler: DeleteMessageHandler< - filters.Modify - >['callback'] - ): DeleteMessageHandler - - /** @internal */ - export function deleteMessage( - filter: any, - handler?: any - ): DeleteMessageHandler { - return _create('delete_message', filter, handler) - } - - /** - * Create a chat member update handler - * - * @param handler Chat member update handler - */ - export function chatMemberUpdate( - handler: ChatMemberUpdateHandler['callback'] - ): ChatMemberUpdateHandler - - /** - * Create a chat member update handler with a filter - * - * @param filter Update filter - * @param handler Chat member update handler - */ - export function chatMemberUpdate( - filter: UpdateFilter, - handler: ChatMemberUpdateHandler< - filters.Modify - >['callback'] - ): ChatMemberUpdateHandler - - /** @internal */ - export function chatMemberUpdate( - filter: any, - handler?: any - ): ChatMemberUpdateHandler { - return _create('chat_member', filter, handler) - } - - /** - * Create an inline query handler - * - * @param handler Inline query handler - */ - export function inlineQuery( - handler: InlineQueryHandler['callback'] - ): InlineQueryHandler - - /** - * Create an inline query handler with a filter - * - * @param filter Update filter - * @param handler Inline query handler - */ - export function inlineQuery( - filter: UpdateFilter, - handler: InlineQueryHandler< - filters.Modify - >['callback'] - ): InlineQueryHandler - - /** @internal */ - export function inlineQuery( - filter: any, - handler?: any - ): InlineQueryHandler { - return _create('inline_query', filter, handler) - } - - /** - * Create a chosen inline result handler - * - * @param handler Chosen inline result handler - */ - export function chosenInlineResult( - handler: ChosenInlineResultHandler['callback'] - ): ChosenInlineResultHandler - - /** - * Create a chosen inline result handler with a filter - * - * @param filter Update filter - * @param handler Chosen inline result handler - */ - export function chosenInlineResult( - filter: UpdateFilter, - handler: ChosenInlineResultHandler< - filters.Modify - >['callback'] - ): ChosenInlineResultHandler - - /** @internal */ - export function chosenInlineResult( - filter: any, - handler?: any - ): ChosenInlineResultHandler { - return _create('chosen_inline_result', filter, handler) - } - - /** - * Create a callback query handler - * - * @param handler Callback query handler - */ - export function callbackQuery( - handler: CallbackQueryHandler['callback'] - ): CallbackQueryHandler - - /** - * Create a callback query handler with a filter - * - * @param filter Update filter - * @param handler Callback query handler - */ - export function callbackQuery( - filter: UpdateFilter, - handler: CallbackQueryHandler< - filters.Modify - >['callback'] - ): CallbackQueryHandler - - /** @internal */ - export function callbackQuery( - filter: any, - handler?: any - ): CallbackQueryHandler { - return _create('callback_query', filter, handler) - } - - /** - * Create a poll update handler - * - * @param handler Poll update handler - */ - export function pollUpdate( - handler: PollUpdateHandler['callback'] - ): PollUpdateHandler - - /** - * Create a poll update handler with a filter - * - * @param filter Update filter - * @param handler Poll update handler - */ - export function pollUpdate( - filter: UpdateFilter, - handler: PollUpdateHandler>['callback'] - ): PollUpdateHandler - - /** @internal */ - export function pollUpdate(filter: any, handler?: any): PollUpdateHandler { - return _create('poll', filter, handler) - } - - /** - * Create a poll vote handler - * - * @param handler Poll vote handler - */ - export function pollVote( - handler: PollVoteHandler['callback'] - ): PollVoteHandler - - /** - * Create a poll vote handler with a filter - * - * @param filter Update filter - * @param handler Poll vote handler - */ - export function pollVote( - filter: UpdateFilter, - handler: PollVoteHandler< - filters.Modify - >['callback'] - ): PollVoteHandler - - /** @internal */ - export function pollVote(filter: any, handler?: any): PollVoteHandler { - return _create('poll_vote', filter, handler) - } - - /** - * Create an user status update handler - * - * @param handler User status update handler - */ - export function userStatusUpdate( - handler: UserStatusUpdateHandler['callback'] - ): UserStatusUpdateHandler - - /** - * Create an user status update handler with a filter - * - * @param filter Update filter - * @param handler User status update handler - */ - export function userStatusUpdate( - filter: UpdateFilter, - handler: UserStatusUpdateHandler< - filters.Modify - >['callback'] - ): UserStatusUpdateHandler - - /** @internal */ - export function userStatusUpdate( - filter: any, - handler?: any - ): UserStatusUpdateHandler { - return _create('user_status', filter, handler) - } - - /** - * Create an user typing handler - * - * @param handler User typing handler - */ - export function userTyping( - handler: UserTypingHandler['callback'] - ): UserTypingHandler - - /** - * Create an user typing handler with a filter - * - * @param filter Update filter - * @param handler User typing handler - */ - export function userTyping( - filter: UpdateFilter, - handler: UserTypingHandler< - filters.Modify - >['callback'] - ): UserTypingHandler - - /** @internal */ - export function userTyping(filter: any, handler?: any): UserTypingHandler { - return _create('user_typing', filter, handler) - } - - /** - * Create a history read handler - * - * @param handler History read handler - */ - export function historyRead( - handler: HistoryReadHandler['callback'] - ): HistoryReadHandler - - /** - * Create a history read handler with a filter - * - * @param filter Update filter - * @param handler History read handler - */ - export function historyRead( - filter: UpdateFilter, - handler: HistoryReadHandler< - filters.Modify - >['callback'] - ): HistoryReadHandler - - /** @internal */ - export function historyRead( - filter: any, - handler?: any - ): HistoryReadHandler { - return _create('history_read', filter, handler) - } - - // end-codegen -} diff --git a/packages/dispatcher/src/dispatcher.ts b/packages/dispatcher/src/dispatcher.ts index 679efde7..d5260845 100644 --- a/packages/dispatcher/src/dispatcher.ts +++ b/packages/dispatcher/src/dispatcher.ts @@ -8,6 +8,15 @@ import { MtCuteArgumentError, TelegramClient, UsersIndex, + ChatMemberUpdate, + ChosenInlineResult, + PollUpdate, + PollVoteUpdate, + UserStatusUpdate, + UserTypingUpdate, + DeleteMessageUpdate, + HistoryReadUpdate, + ParsedUpdate, } from '@mtcute/client' import { tl } from '@mtcute/tl' // begin-codegen-imports @@ -28,262 +37,20 @@ import { HistoryReadHandler, } from './handler' // end-codegen-imports -import { ParsedUpdate } from './handler' import { filters, UpdateFilter } from './filters' -import { handlers } from './builders' -import { - ChatMemberUpdate, - ChosenInlineResult, - PollUpdate, - PollVoteUpdate, - UserStatusUpdate, - UserTypingUpdate, - DeleteMessageUpdate, - HistoryReadUpdate, -} from './updates' import { IStateStorage, UpdateState, StateKeyDelegate } from './state' import { defaultStateKeyDelegate } from './state' import { PropagationAction } from './propagation' -import EventEmitter from 'events' const noop = () => {} -type ParserFunction = ( - client: TelegramClient, - upd: tl.TypeUpdate | tl.TypeMessage, - users: UsersIndex, - chats: ChatsIndex -) => 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, - upd._ === 'updateNewScheduledMessage' - ) - -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 callbackQueryParser: UpdateParser = [ - 'callback_query', - (client, upd, users) => new CallbackQuery(client, upd as any, users), -] -const userTypingParser: UpdateParser = [ - 'user_typing', - (client, upd) => new UserTypingUpdate(client, upd as any), -] -const deleteMessageParser: UpdateParser = [ - 'delete_message', - (client, upd) => new DeleteMessageUpdate(client, upd as any), -] -const historyReadParser: UpdateParser = [ - 'history_read', - (client, upd) => new HistoryReadUpdate(client, upd as any), -] - -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), - ], - updateBotCallbackQuery: callbackQueryParser, - updateInlineBotCallbackQuery: callbackQueryParser, - updateMessagePoll: [ - 'poll', - (client, upd, users) => new PollUpdate(client, upd as any, users), - ], - updateMessagePollVote: [ - 'poll_vote', - (client, upd, users) => new PollVoteUpdate(client, upd as any, users), - ], - updateUserStatus: [ - 'user_status', - (client, upd) => new UserStatusUpdate(client, upd as any), - ], - updateChannelUserTyping: userTypingParser, - updateChatUserTyping: userTypingParser, - updateUserTyping: userTypingParser, - updateDeleteChannelMessages: deleteMessageParser, - updateDeleteMessages: deleteMessageParser, - updateReadHistoryInbox: historyReadParser, - updateReadHistoryOutbox: historyReadParser, - updateReadChannelInbox: historyReadParser, - updateReadChannelOutbox: historyReadParser, - updateReadChannelDiscussionInbox: historyReadParser, - updateReadChannelDiscussionOutbox: historyReadParser, -} - -const HANDLER_TYPE_TO_UPDATE: Record = {} -Object.keys(PARSERS).forEach((upd: keyof typeof PARSERS) => { - const handler = PARSERS[upd]![0] - if (!(handler in HANDLER_TYPE_TO_UPDATE)) - HANDLER_TYPE_TO_UPDATE[handler] = [] - HANDLER_TYPE_TO_UPDATE[handler].push(upd) -}) - -export declare interface Dispatcher< - // eslint-disable-next-line @typescript-eslint/no-unused-vars - State = never, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - SceneName extends string = string -> { - on( - name: 'update', - handler: (update: ParsedUpdate & T) => void - ): this - - // begin-codegen-declare - - /** - * Register a plain old raw update handler - * - * @param name Event name - * @param handler Raw update handler - */ - on(name: 'raw', handler: RawUpdateHandler['callback']): this - - /** - * Register a plain old new message handler - * - * @param name Event name - * @param handler New message handler - */ - on(name: 'new_message', handler: NewMessageHandler['callback']): this - - /** - * Register a plain old edit message handler - * - * @param name Event name - * @param handler Edit message handler - */ - on(name: 'edit_message', handler: EditMessageHandler['callback']): this - - /** - * Register a plain old delete message handler - * - * @param name Event name - * @param handler Delete message handler - */ - on(name: 'delete_message', handler: DeleteMessageHandler['callback']): this - - /** - * Register a plain old chat member update handler - * - * @param name Event name - * @param handler Chat member update handler - */ - on(name: 'chat_member', handler: ChatMemberUpdateHandler['callback']): this - - /** - * Register a plain old inline query handler - * - * @param name Event name - * @param handler Inline query handler - */ - on(name: 'inline_query', handler: InlineQueryHandler['callback']): this - - /** - * Register a plain old chosen inline result handler - * - * @param name Event name - * @param handler Chosen inline result handler - */ - on( - name: 'chosen_inline_result', - handler: ChosenInlineResultHandler['callback'] - ): this - - /** - * Register a plain old callback query handler - * - * @param name Event name - * @param handler Callback query handler - */ - on(name: 'callback_query', handler: CallbackQueryHandler['callback']): this - - /** - * Register a plain old poll update handler - * - * @param name Event name - * @param handler Poll update handler - */ - on(name: 'poll', handler: PollUpdateHandler['callback']): this - - /** - * Register a plain old poll vote handler - * - * @param name Event name - * @param handler Poll vote handler - */ - on(name: 'poll_vote', handler: PollVoteHandler['callback']): this - - /** - * Register a plain old user status update handler - * - * @param name Event name - * @param handler User status update handler - */ - on(name: 'user_status', handler: UserStatusUpdateHandler['callback']): this - - /** - * Register a plain old user typing handler - * - * @param name Event name - * @param handler User typing handler - */ - on(name: 'user_typing', handler: UserTypingHandler['callback']): this - - /** - * Register a plain old history read handler - * - * @param name Event name - * @param handler History read handler - */ - on(name: 'history_read', handler: HistoryReadHandler['callback']): this - - // end-codegen-declare -} - /** * Updates dispatcher */ -export class Dispatcher< - State = never, - SceneName extends string = string -> extends EventEmitter { +export class Dispatcher { private _groups: Record< number, - Record + Record > = {} private _groupsOrder: number[] = [] @@ -304,8 +71,6 @@ export class Dispatcher< private _customStateKeyDelegate?: StateKeyDelegate private _customStorage?: IStateStorage - private _handlersCount: Record = {} - private _errorHandler?: ( err: Error, update: ParsedUpdate & T, @@ -346,7 +111,8 @@ export class Dispatcher< storage?: IStateStorage | StateKeyDelegate, key?: StateKeyDelegate ) { - super() + this.dispatchRawUpdate = this.dispatchRawUpdate.bind(this) + this.dispatchUpdate = this.dispatchUpdate.bind(this) if (client) { if (client instanceof TelegramClient) { @@ -383,7 +149,9 @@ export class Dispatcher< * Dispatcher also uses bound client to throw errors */ bindToClient(client: TelegramClient): void { - client['dispatchUpdate'] = this.dispatchUpdate.bind(this) + client.on('update', this.dispatchUpdate) + client.on('raw_update', this.dispatchRawUpdate) + this._client = client } @@ -395,11 +163,103 @@ export class Dispatcher< */ unbind(): void { if (this._client) { - this._client['dispatchUpdate'] = noop + this._client.off('update', this.dispatchUpdate) + this._client.off('raw_update', this.dispatchRawUpdate) + this._client = undefined } } + /** + * Process a raw update with this dispatcher. + * Calling this method without bound client will not work. + * + * Under the hood asynchronously calls {@link dispatchRawUpdateNow} + * with error handler set to client's one. + * + * @param update Update to process + * @param users Users map + * @param chats Chats map + */ + dispatchRawUpdate( + update: tl.TypeUpdate | tl.TypeMessage, + users: UsersIndex, + chats: ChatsIndex + ): void { + if (!this._client) return + + // order does not matter in the dispatcher, + // so we can handle each update in its own task + this.dispatchRawUpdateNow(update, users, chats).catch((err) => + this._client!['_emitError'](err) + ) + } + + /** + * Process a raw update right now in the current stack. + * + * Unlike {@link dispatchRawUpdate}, 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 Users map + * @param chats Chats map + * @returns Whether the update was handled + */ + async dispatchRawUpdateNow( + update: tl.TypeUpdate | tl.TypeMessage, + users: UsersIndex, + chats: ChatsIndex + ): Promise { + if (!this._client) return false + + let handled = false + + outer: for (const grp of this._groupsOrder) { + const group = this._groups[grp] + + if ('raw' in group) { + const handlers = group['raw'] as RawUpdateHandler[] + + for (const h of handlers) { + let result: void | PropagationAction + + if ( + !h.check || + (await h.check(this._client, update, users, chats)) + ) { + result = await h.callback( + this._client, + update, + users, + chats + ) + handled = true + } else continue + + switch (result) { + case 'continue': + continue + case 'stop': + break outer + case 'stop-children': + return handled + } + + break + } + } + } + + for (const child of this._children) { + handled ||= await child.dispatchRawUpdateNow(update, users, chats) + } + + return handled + } + /** * Process an update with this dispatcher. * Calling this method without bound client will not work. @@ -408,19 +268,13 @@ export class Dispatcher< * 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: UsersIndex, - chats: ChatsIndex - ): void { + dispatchUpdate(update: ParsedUpdate): 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.dispatchUpdateNow(update).catch((err) => this._client!['_emitError'](err) ) } @@ -434,58 +288,31 @@ export class Dispatcher< * 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 * @returns Whether the update was handled */ - async dispatchUpdateNow( - update: tl.TypeUpdate | tl.TypeMessage, - users: UsersIndex, - chats: ChatsIndex - ): Promise { - return this._dispatchUpdateNowImpl(update, users, chats) + async dispatchUpdateNow(update: ParsedUpdate): Promise { + return this._dispatchUpdateNowImpl(update) } private async _dispatchUpdateNowImpl( - update: tl.TypeUpdate | tl.TypeMessage | null, - users: UsersIndex | null, - chats: ChatsIndex | null, + update: ParsedUpdate, // this is getting a bit crazy lol - parsed?: any, - parsedType?: Exclude | null, parsedState?: UpdateState | null, parsedScene?: string | null, forceScene?: true ): Promise { if (!this._client) return false - const isRawMessage = update && tl.isAnyMessage(update) - - if (parsed === undefined) { - const pair = PARSERS[update!._] - if (pair) { - if ( - this._handlersCount[update!._] || - this.listenerCount(pair[0]) - ) { - parsed = pair[1](this._client, update!, users!, chats!) - parsedType = pair[0] - } - } else { - parsed = parsedType = null - } - } - if (parsedScene === undefined) { if ( this._storage && this._scenes && - (parsedType === 'new_message' || - parsedType === 'edit_message' || - parsedType === 'callback_query') + (update.name === 'new_message' || + update.name === 'edit_message' || + update.name === 'callback_query') ) { // no need to fetch scene if there are no registered scenes - const key = await this._stateKeyDelegate!(parsed) + const key = await this._stateKeyDelegate!(update.data) if (key) { parsedScene = await this._storage.getCurrentScene(key) } else { @@ -508,10 +335,6 @@ export class Dispatcher< return this._scenes[parsedScene]._dispatchUpdateNowImpl( update, - users, - chats, - parsed, - parsedType, parsedState, parsedScene, true @@ -522,16 +345,18 @@ export class Dispatcher< if (parsedState === undefined) { if ( this._storage && - (parsedType === 'new_message' || - parsedType === 'edit_message' || - parsedType === 'callback_query') + (update.name === 'new_message' || + update.name === 'edit_message' || + update.name === 'callback_query') ) { - const key = await this._stateKeyDelegate!(parsed) + const key = await this._stateKeyDelegate!(update.data) if (key) { let customKey if ( !this._customStateKeyDelegate || - (customKey = await this._customStateKeyDelegate(parsed)) + (customKey = await this._customStateKeyDelegate( + update.data + )) ) { parsedState = new UpdateState( this._storage!, @@ -552,79 +377,23 @@ export class Dispatcher< let shouldDispatch = true let shouldDispatchChildren = true - let wasHandled = false - let updateInfo: any = null + let handled = false - if (parsed) { - updateInfo = { type: parsedType, data: parsed } - switch ( - await this._preUpdateHandler?.( - updateInfo as any, - parsedState as any - ) - ) { - case 'stop': - shouldDispatch = false - break - case 'stop-children': - return false - } + switch (await this._preUpdateHandler?.(update, parsedState as any)) { + case 'stop': + shouldDispatch = false + break + case 'stop-children': + return false } if (shouldDispatch) { - if (update && !isRawMessage) { - this.emit('raw', update, users, chats) - } - - if (parsedType) { - this.emit('update', updateInfo) - this.emit(parsedType, parsed) - } - outer: for (const grp of this._groupsOrder) { const group = this._groups[grp] - if (update && !isRawMessage && 'raw' in group) { - const handlers = group['raw'] as RawUpdateHandler[] - - for (const h of handlers) { - let result: void | PropagationAction - - if ( - !h.check || - (await h.check( - this._client, - update as any, - users!, - chats! - )) - ) { - result = await h.callback( - this._client, - update as any, - users!, - chats! - ) - wasHandled = true - } else continue - - switch (result) { - case 'continue': - continue - case 'stop': - break outer - case 'stop-children': - shouldDispatchChildren = false - break outer - } - - break - } - } - - if (parsedType && parsedType in group) { + if (update.name in group) { // raw is not handled here, so we can safely assume this - const handlers = group[parsedType] as Exclude< + const handlers = group[update.name] as Exclude< UpdateHandler, RawUpdateHandler >[] @@ -635,13 +404,16 @@ export class Dispatcher< if ( !h.check || - (await h.check(parsed, parsedState as never)) + (await h.check( + update.data as any, + parsedState as never + )) ) { result = await h.callback( - parsed, + update.data as any, parsedState as never ) - wasHandled = true + handled = true } else continue switch (result) { @@ -669,10 +441,6 @@ export class Dispatcher< scene ]._dispatchUpdateNowImpl( update, - users, - chats, - parsed, - parsedType, undefined, scene, true @@ -686,7 +454,7 @@ export class Dispatcher< if (this._errorHandler) { const handled = await this._errorHandler( e, - updateInfo as any, + update, parsedState as never ) if (!handled) throw e @@ -700,25 +468,13 @@ export class Dispatcher< if (shouldDispatchChildren) { for (const child of this._children) { - wasHandled ||= await child._dispatchUpdateNowImpl( - update, - users, - chats, - parsed, - parsedType - ) + handled ||= await child._dispatchUpdateNowImpl(update) } } - if (updateInfo) { - this._postUpdateHandler?.( - wasHandled, - updateInfo, - parsedState as any - ) - } + this._postUpdateHandler?.(handled, update, parsedState as any) - return wasHandled + return handled } /** @@ -734,28 +490,23 @@ export class Dispatcher< this._groupsOrder.sort((a, b) => a - b) } - if (!(handler.type in this._groups[group])) { - this._groups[group][handler.type] = [] + if (!(handler.name in this._groups[group])) { + this._groups[group][handler.name] = [] } - HANDLER_TYPE_TO_UPDATE[handler.type].forEach((upd) => { - if (!(upd in this._handlersCount)) this._handlersCount[upd] = 0 - this._handlersCount[upd] += 1 - }) - - this._groups[group][handler.type].push(handler) + this._groups[group][handler.name].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 handler Update handler to remove, its name or `'all'` to remove all * @param group Handler group index (-1 to affect all groups) * @internal */ removeUpdateHandler( - handler: UpdateHandler | UpdateHandler['type'] | 'all', + handler: UpdateHandler | UpdateHandler['name'] | 'all', group = 0 ): void { if (group !== -1 && !(group in this._groups)) { @@ -766,38 +517,22 @@ export class Dispatcher< if (handler === 'all') { if (group === -1) { this._groups = {} - this._handlersCount = {} } else { - const grp = this._groups[group] as any - Object.keys(grp).forEach((handler) => { - HANDLER_TYPE_TO_UPDATE[handler].forEach((upd) => { - this._handlersCount[upd] -= grp[handler].length - }) - }) delete this._groups[group] } } else { - HANDLER_TYPE_TO_UPDATE[handler].forEach((upd) => { - this._handlersCount[upd] -= this._groups[group][ - handler - ].length - }) delete this._groups[group][handler] } return } - if (!(handler.type in this._groups[group])) { + if (!(handler.name in this._groups[group])) { return } - const idx = this._groups[group][handler.type].indexOf(handler) - if (idx > 0) { - this._groups[group][handler.type].splice(idx, 1) - - HANDLER_TYPE_TO_UPDATE[handler.type].forEach((upd) => { - this._handlersCount[upd] -= 1 - }) + const idx = this._groups[group][handler.name].indexOf(handler) + if (idx > -1) { + this._groups[group][handler.name].splice(idx, 1) } } @@ -1071,10 +806,6 @@ export class Dispatcher< } }) - Object.keys(other._handlersCount).forEach((typ) => { - this._handlersCount[typ] += other._handlersCount[typ] - }) - other._children.forEach((it) => { it._unparent() this.addChild(it as any) @@ -1121,14 +852,13 @@ export class Dispatcher< dp._groups[idx] = {} as any Object.keys(this._groups[idx]).forEach( - (type: UpdateHandler['type']) => { + (type: UpdateHandler['name']) => { dp._groups[idx][type] = [...this._groups[idx][type]] } ) }) dp._groupsOrder = [...this._groupsOrder] - dp._handlersCount = { ...this._handlersCount } dp._errorHandler = this._errorHandler dp._customStateKeyDelegate = this._customStateKeyDelegate dp._customStorage = this._customStorage @@ -1268,16 +998,23 @@ export class Dispatcher< // addUpdateHandler convenience wrappers // private _addKnownHandler( - name: keyof typeof handlers, + name: UpdateHandler['name'], filter: any, handler?: any, group?: number ): void { if (typeof handler === 'number') { - this.addUpdateHandler((handlers as any)[name](filter), handler) + this.addUpdateHandler({ + name, + callback: filter + }, handler) } else { this.addUpdateHandler( - (handlers as any)[name](filter, handler), + { + name, + callback: handler, + check: filter + }, group ) } @@ -1285,32 +1022,6 @@ export class Dispatcher< // 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 * @@ -1359,7 +1070,7 @@ export class Dispatcher< /** @internal */ onNewMessage(filter: any, handler?: any, group?: number): void { - this._addKnownHandler('newMessage', filter, handler, group) + this._addKnownHandler('new_message', filter, handler, group) } /** @@ -1410,7 +1121,7 @@ export class Dispatcher< /** @internal */ onEditMessage(filter: any, handler?: any, group?: number): void { - this._addKnownHandler('editMessage', filter, handler, group) + this._addKnownHandler('edit_message', filter, handler, group) } /** @@ -1441,7 +1152,7 @@ export class Dispatcher< /** @internal */ onDeleteMessage(filter: any, handler?: any, group?: number): void { - this._addKnownHandler('deleteMessage', filter, handler, group) + this._addKnownHandler('delete_message', filter, handler, group) } /** @@ -1472,7 +1183,7 @@ export class Dispatcher< /** @internal */ onChatMemberUpdate(filter: any, handler?: any, group?: number): void { - this._addKnownHandler('chatMemberUpdate', filter, handler, group) + this._addKnownHandler('chat_member', filter, handler, group) } /** @@ -1500,7 +1211,7 @@ export class Dispatcher< /** @internal */ onInlineQuery(filter: any, handler?: any, group?: number): void { - this._addKnownHandler('inlineQuery', filter, handler, group) + this._addKnownHandler('inline_query', filter, handler, group) } /** @@ -1531,7 +1242,7 @@ export class Dispatcher< /** @internal */ onChosenInlineResult(filter: any, handler?: any, group?: number): void { - this._addKnownHandler('chosenInlineResult', filter, handler, group) + this._addKnownHandler('chosen_inline_result', filter, handler, group) } /** @@ -1582,7 +1293,7 @@ export class Dispatcher< /** @internal */ onCallbackQuery(filter: any, handler?: any, group?: number): void { - this._addKnownHandler('callbackQuery', filter, handler, group) + this._addKnownHandler('callback_query', filter, handler, group) } /** @@ -1608,7 +1319,7 @@ export class Dispatcher< /** @internal */ onPollUpdate(filter: any, handler?: any, group?: number): void { - this._addKnownHandler('pollUpdate', filter, handler, group) + this._addKnownHandler('poll', filter, handler, group) } /** @@ -1636,7 +1347,7 @@ export class Dispatcher< /** @internal */ onPollVote(filter: any, handler?: any, group?: number): void { - this._addKnownHandler('pollVote', filter, handler, group) + this._addKnownHandler('poll_vote', filter, handler, group) } /** @@ -1667,7 +1378,7 @@ export class Dispatcher< /** @internal */ onUserStatusUpdate(filter: any, handler?: any, group?: number): void { - this._addKnownHandler('userStatusUpdate', filter, handler, group) + this._addKnownHandler('user_status', filter, handler, group) } /** @@ -1695,7 +1406,7 @@ export class Dispatcher< /** @internal */ onUserTyping(filter: any, handler?: any, group?: number): void { - this._addKnownHandler('userTyping', filter, handler, group) + this._addKnownHandler('user_typing', filter, handler, group) } /** @@ -1723,7 +1434,7 @@ export class Dispatcher< /** @internal */ onHistoryRead(filter: any, handler?: any, group?: number): void { - this._addKnownHandler('historyRead', filter, handler, group) + this._addKnownHandler('history_read', filter, handler, group) } // end-codegen diff --git a/packages/dispatcher/src/filters.ts b/packages/dispatcher/src/filters.ts index 581b0355..829b6d28 100644 --- a/packages/dispatcher/src/filters.ts +++ b/packages/dispatcher/src/filters.ts @@ -23,14 +23,14 @@ import { WebPage, MessageAction, RawLocation, + ChatMemberUpdate, + ChosenInlineResult, + UserStatusUpdate, + PollVoteUpdate, + UserTypingUpdate, } from '@mtcute/client' import { MaybeArray } from '@mtcute/core' -import { ChatMemberUpdate } from './updates' -import { ChosenInlineResult } from './updates/chosen-inline-result' import { UpdateState } from './state' -import { UserStatusUpdate } from './updates/user-status-update' -import { PollVoteUpdate } from './updates/poll-vote' -import { UserTypingUpdate } from './updates/user-typing-update' function extractText( obj: Message | InlineQuery | ChosenInlineResult | CallbackQuery diff --git a/packages/dispatcher/src/handler.ts b/packages/dispatcher/src/handler.ts index 01b99efd..cedde32b 100644 --- a/packages/dispatcher/src/handler.ts +++ b/packages/dispatcher/src/handler.ts @@ -6,52 +6,42 @@ import { CallbackQuery, UsersIndex, ChatsIndex, + ChatMemberUpdate, + PollVoteUpdate, + UserStatusUpdate, + ChosenInlineResult, + HistoryReadUpdate, + DeleteMessageUpdate, + PollUpdate, + UserTypingUpdate, } from '@mtcute/client' import { tl } from '@mtcute/tl' import { PropagationAction } from './propagation' -import { - ChatMemberUpdate, - ChosenInlineResult, - PollUpdate, - PollVoteUpdate, - UserStatusUpdate, - UserTypingUpdate, - DeleteMessageUpdate, - HistoryReadUpdate, -} from './updates' -interface BaseUpdateHandler { - type: Type +interface BaseUpdateHandler { + name: Name callback: Handler check?: Checker } -type ParsedUpdateHandler = BaseUpdateHandler< - Type, +type ParsedUpdateHandler = BaseUpdateHandler< + Name, (update: Update, state: State) => MaybeAsync, (update: Update, state: State) => MaybeAsync > -type _ParsedUpdate = T extends ParsedUpdateHandler - ? { - readonly type: K - readonly data: Q - } - : never -export type ParsedUpdate = _ParsedUpdate - export type RawUpdateHandler = BaseUpdateHandler< 'raw', ( client: TelegramClient, - update: tl.TypeUpdate, + update: tl.TypeUpdate | tl.TypeMessage, users: UsersIndex, chats: ChatsIndex ) => MaybeAsync, ( client: TelegramClient, - update: tl.TypeUpdate, + update: tl.TypeUpdate | tl.TypeMessage, users: UsersIndex, chats: ChatsIndex ) => MaybeAsync diff --git a/packages/dispatcher/src/index.ts b/packages/dispatcher/src/index.ts index c9c0a557..9508875f 100644 --- a/packages/dispatcher/src/index.ts +++ b/packages/dispatcher/src/index.ts @@ -1,11 +1,8 @@ -export * from './builders' export * from './dispatcher' export * from './filters' export * from './handler' export * from './propagation' -export * from './updates' export * from './wizard' export * from './callback-data-builder' -export * from './conversation' export { UpdateState, IStateStorage } from './state' diff --git a/packages/dispatcher/src/updates/index.ts b/packages/dispatcher/src/updates/index.ts deleted file mode 100644 index 7a5c6eb3..00000000 --- a/packages/dispatcher/src/updates/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './chat-member-update' -export * from './chosen-inline-result' -export * from './delete-message-update' -export * from './poll-update' -export * from './poll-vote' -export * from './user-status-update' -export * from './user-typing-update' -export * from './history-read-update'