refactor(dispatcher): big refactor, moved updates parsing to client, separated raw updates from parsed, moved Conversation to client package

This commit is contained in:
teidesu 2021-07-17 17:26:31 +03:00
parent 8fb099cfeb
commit 627fdbed2f
27 changed files with 1008 additions and 1343 deletions

View file

@ -4,6 +4,7 @@ const fs = require('fs')
const prettier = require('prettier') const prettier = require('prettier')
// not the best way but who cares lol // not the best way but who cares lol
const { createWriter } = require('../../tl/scripts/common') const { createWriter } = require('../../tl/scripts/common')
const updates = require('./generate-updates')
const targetDir = path.join(__dirname, '../src') const targetDir = path.join(__dirname, '../src')
@ -299,6 +300,32 @@ async function main() {
) )
output.tab() 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 printer = ts.createPrinter()
const classContents = [] const classContents = []

View file

@ -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<T = ${type.updateType}` +
// `${type.state ? ', S = never' : ''}> = 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}<Mod>(
// 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<State, SceneName>>` : ''}['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}<Mod>(
// filter: UpdateFilter<${type.updateType}, Mod, State>,
// handler: ${type.handlerTypeName}Handler<filters.Modify<${type.updateType}, Mod>, State extends never ? never : UpdateState<State, SceneName>>['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}<Mod>(
// filter: UpdateFilter<${type.updateType}, Mod>,
// handler: ${type.handlerTypeName}Handler<filters.Modify<${type.updateType}, Mod>${type.state ? ', State extends never ? never : UpdateState<State, SceneName>' : ''}>['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)
}

View file

@ -1,6 +1,4 @@
# format: type_name[: handler_type_name] = update_type[ + State] # format: type_name[: handler_type_name] = update_type[ + State]
# IGNORE as update_type disables filters modification
raw: RawUpdate = IGNORE
new_message = Message + State new_message = Message + State
edit_message = Message + State edit_message = Message + State
delete_message = DeleteMessageUpdate delete_message = DeleteMessageUpdate

View file

@ -142,12 +142,12 @@ import { getStickerSet } from './methods/stickers/get-sticker-set'
import { moveStickerInSet } from './methods/stickers/move-sticker-in-set' import { moveStickerInSet } from './methods/stickers/move-sticker-in-set'
import { setStickerSetThumb } from './methods/stickers/set-sticker-set-thumb' import { setStickerSetThumb } from './methods/stickers/set-sticker-set-thumb'
import { import {
_dispatchUpdate,
_fetchUpdatesState, _fetchUpdatesState,
_handleUpdate, _handleUpdate,
_loadStorage, _loadStorage,
_saveStorage, _saveStorage,
catchUp, catchUp,
dispatchUpdate,
} from './methods/updates' } from './methods/updates'
import { blockUser } from './methods/users/block-user' import { blockUser } from './methods/users/block-user'
import { deleteProfilePhotos } from './methods/users/delete-profile-photos' import { deleteProfilePhotos } from './methods/users/delete-profile-photos'
@ -168,17 +168,23 @@ import { Readable } from 'stream'
import { import {
ArrayWithTotal, ArrayWithTotal,
BotCommands, BotCommands,
CallbackQuery,
Chat, Chat,
ChatEvent, ChatEvent,
ChatInviteLink, ChatInviteLink,
ChatMember, ChatMember,
ChatMemberUpdate,
ChatPreview, ChatPreview,
ChatsIndex, ChatsIndex,
ChosenInlineResult,
DeleteMessageUpdate,
Dialog, Dialog,
FileDownloadParameters, FileDownloadParameters,
FormattedString, FormattedString,
GameHighScore, GameHighScore,
HistoryReadUpdate,
IMessageEntityParser, IMessageEntityParser,
InlineQuery,
InputFileLike, InputFileLike,
InputInlineResult, InputInlineResult,
InputMediaLike, InputMediaLike,
@ -187,10 +193,13 @@ import {
MaybeDynamic, MaybeDynamic,
Message, Message,
MessageMedia, MessageMedia,
ParsedUpdate,
PartialExcept, PartialExcept,
PartialOnly, PartialOnly,
Photo, Photo,
Poll, Poll,
PollUpdate,
PollVoteUpdate,
RawDocument, RawDocument,
ReplyMarkup, ReplyMarkup,
SentCode, SentCode,
@ -201,6 +210,8 @@ import {
UploadFileLike, UploadFileLike,
UploadedFile, UploadedFile,
User, User,
UserStatusUpdate,
UserTypingUpdate,
UsersIndex, UsersIndex,
} from './types' } from './types'
import { import {
@ -212,6 +223,117 @@ import {
import { tdFileId } from '@mtcute/file-id' import { tdFileId } from '@mtcute/file-id'
export interface TelegramClient extends BaseTelegramClient { 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 * Accept the given TOS
* *
@ -3110,29 +3232,6 @@ export interface TelegramClient extends BaseTelegramClient {
progressCallback?: (uploaded: number, total: number) => void progressCallback?: (uploaded: number, total: number) => void
} }
): Promise<StickerSet> ): Promise<StickerSet>
/**
* 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 _handleUpdate(update: tl.TypeUpdates, noDispatch?: boolean): void
/** /**
* Catch up with the server by loading missed updates. * Catch up with the server by loading missed updates.
@ -3524,7 +3623,7 @@ export class TelegramClient extends BaseTelegramClient {
protected _fetchUpdatesState = _fetchUpdatesState protected _fetchUpdatesState = _fetchUpdatesState
protected _loadStorage = _loadStorage protected _loadStorage = _loadStorage
protected _saveStorage = _saveStorage protected _saveStorage = _saveStorage
dispatchUpdate = dispatchUpdate protected _dispatchUpdate = _dispatchUpdate
_handleUpdate = _handleUpdate _handleUpdate = _handleUpdate
catchUp = catchUp catchUp = catchUp
blockUser = blockUser blockUser = blockUser

View file

@ -40,7 +40,18 @@ import {
MessageMedia, MessageMedia,
RawDocument, RawDocument,
IMessageEntityParser, IMessageEntityParser,
FormattedString FormattedString,
CallbackQuery,
ChatMemberUpdate,
ChosenInlineResult,
DeleteMessageUpdate,
HistoryReadUpdate,
InlineQuery,
ParsedUpdate,
PollUpdate,
PollVoteUpdate,
UserStatusUpdate,
UserTypingUpdate,
} from '../types' } from '../types'
// @copy // @copy

View file

@ -14,6 +14,7 @@ import {
} from '@mtcute/core' } from '@mtcute/core'
import { isDummyUpdate, isDummyUpdates } from '../utils/updates-utils' import { isDummyUpdate, isDummyUpdates } from '../utils/updates-utils'
import { ChatsIndex, UsersIndex } from '../types' import { ChatsIndex, UsersIndex } from '../types'
import { _parseUpdate } from '../utils/parse-update'
const debug = require('debug')('mtcute:upds') 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 * @internal
*/ */
export function dispatchUpdate( export function _dispatchUpdate(
this: TelegramClient, this: TelegramClient,
update: tl.TypeUpdate | tl.TypeMessage, update: tl.TypeUpdate | tl.TypeMessage,
users: UsersIndex, users: UsersIndex,
chats: ChatsIndex chats: ChatsIndex
): void { ): 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 { interface NoDispatchIndex {
@ -441,7 +432,7 @@ async function _loadDifference(
if (noDispatch.msg[cid]?.[message.id]) return if (noDispatch.msg[cid]?.[message.id]) return
} }
this.dispatchUpdate(message, users, chats) this._dispatchUpdate(message, users, chats)
}) })
for (const upd of diff.otherUpdates) { for (const upd of diff.otherUpdates) {
@ -495,7 +486,7 @@ async function _loadDifference(
if (noDispatch.pts[cid ?? 0]?.[pts]) continue if (noDispatch.pts[cid ?? 0]?.[pts]) continue
} }
this.dispatchUpdate(upd, users, chats) this._dispatchUpdate(upd, users, chats)
} }
this._pts = state.pts this._pts = state.pts
@ -556,7 +547,7 @@ async function _loadChannelDifference(
return return
if (message._ === 'messageEmpty') return if (message._ === 'messageEmpty') return
this.dispatchUpdate(message, users, chats) this._dispatchUpdate(message, users, chats)
}) })
break break
} }
@ -565,7 +556,7 @@ async function _loadChannelDifference(
if (noDispatch && noDispatch.msg[channelId]?.[message.id]) return if (noDispatch && noDispatch.msg[channelId]?.[message.id]) return
if (message._ === 'messageEmpty') return if (message._ === 'messageEmpty') return
this.dispatchUpdate(message, users, chats) this._dispatchUpdate(message, users, chats)
}) })
diff.otherUpdates.forEach((upd) => { diff.otherUpdates.forEach((upd) => {
@ -582,7 +573,7 @@ async function _loadChannelDifference(
if (upd._ === 'updateNewChannelMessage' && upd.message._ === 'messageEmpty') if (upd._ === 'updateNewChannelMessage' && upd.message._ === 'messageEmpty')
return return
this.dispatchUpdate(upd, users, chats) this._dispatchUpdate(upd, users, chats)
}) })
pts = diff.pts pts = diff.pts
@ -728,7 +719,7 @@ export function _handleUpdate(
} }
if (!isDummyUpdate(upd) && !noDispatch) { if (!isDummyUpdate(upd) && !noDispatch) {
this.dispatchUpdate(upd, users, chats) this._dispatchUpdate(upd, users, chats)
} }
if (channelId) { if (channelId) {
@ -738,7 +729,7 @@ export function _handleUpdate(
this._pts = pts this._pts = pts
} }
} else if (!noDispatch) { } 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) 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._date = update.date
this._pts = update.pts this._pts = update.pts
this.dispatchUpdate(message, peers.users, peers.chats) this._dispatchUpdate(message, peers.users, peers.chats)
break break
} }
case 'updateShortChatMessage': { case 'updateShortChatMessage': {
@ -869,7 +860,7 @@ export function _handleUpdate(
this._date = update.date this._date = update.date
this._pts = update.pts this._pts = update.pts
this.dispatchUpdate(message, peers.users, peers.chats) this._dispatchUpdate(message, peers.users, peers.chats)
break break
} }
case 'updateShortSentMessage': { case 'updateShortSentMessage': {

View file

@ -1,104 +1,18 @@
import { Dispatcher } from './dispatcher' import { AsyncLock, getMarkedPeerId, MaybeAsync } from '@mtcute/core'
import {
FormattedString,
InputMediaLike,
InputPeerLike,
MaybeAsync,
Message,
MtCuteArgumentError,
TelegramClient,
TimeoutError,
tl,
} from '@mtcute/client'
import { AsyncLock, getMarkedPeerId } from '@mtcute/core'
import { import {
ControllablePromise, ControllablePromise,
createControllablePromise, createControllablePromise,
} from '@mtcute/core/src/utils/controllable-promise' } 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' import { HistoryReadUpdate } from './updates'
import { FormattedString } from './parser'
interface OneWayLinkedListItem<T> { import { Message } from './messages'
v: T import { tl } from '@mtcute/tl'
n?: OneWayLinkedListItem<T> import { TimeoutError } from '@mtcute/tl/errors'
} import { Queue } from '../utils/queue'
class Queue<T> {
first?: OneWayLinkedListItem<T>
last?: OneWayLinkedListItem<T>
length = 0
constructor (readonly limit = 0) {
}
push(item: T): void {
const it: OneWayLinkedListItem<T> = { 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<T> | 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
}
}
interface QueuedHandler<T> { interface QueuedHandler<T> {
promise: ControllablePromise<T> promise: ControllablePromise<T>
@ -120,7 +34,6 @@ interface QueuedHandler<T> {
export class Conversation { export class Conversation {
private _inputPeer: tl.TypeInputPeer private _inputPeer: tl.TypeInputPeer
private _chatId: number private _chatId: number
private _client: TelegramClient
private _started = false private _started = false
private _lastMessage: number private _lastMessage: number
@ -136,7 +49,7 @@ export class Conversation {
private _pendingRead: Record<number, QueuedHandler<void>> = {} private _pendingRead: Record<number, QueuedHandler<void>> = {}
constructor( constructor(
readonly dispatcher: Dispatcher<any, any>, readonly client: TelegramClient,
readonly chat: InputPeerLike readonly chat: InputPeerLike
) { ) {
this._onNewMessage = this._onNewMessage.bind(this) this._onNewMessage = this._onNewMessage.bind(this)
@ -186,24 +99,16 @@ export class Conversation {
async start(): Promise<void> { async start(): Promise<void> {
if (this._started) return 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._started = true
this._inputPeer = await client.resolvePeer(this.chat) this._inputPeer = await this.client.resolvePeer(this.chat)
this._chatId = getMarkedPeerId(this._inputPeer) 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._lastMessage = this._lastReceivedMessage = dialog.lastMessage.id
this.dispatcher.on('new_message', this._onNewMessage) this.client.on('new_message', this._onNewMessage)
this.dispatcher.on('edit_message', this._onEditMessage) this.client.on('edit_message', this._onEditMessage)
this.dispatcher.on('history_read', this._onHistoryRead) this.client.on('history_read', this._onHistoryRead)
} }
/** /**
@ -212,9 +117,9 @@ export class Conversation {
stop(): void { stop(): void {
if (!this._started) return if (!this._started) return
this.dispatcher.off('new_message', this._onNewMessage) this.client.off('new_message', this._onNewMessage)
this.dispatcher.off('edit_message', this._onEditMessage) this.client.off('edit_message', this._onEditMessage)
this.dispatcher.off('history_read', this._onHistoryRead) this.client.off('history_read', this._onHistoryRead)
// reset pending status // reset pending status
this._queuedNewMessage.clear() this._queuedNewMessage.clear()
@ -240,7 +145,7 @@ export class Conversation {
throw new MtCuteArgumentError("Conversation hasn't started yet") 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 this._lastMessage = res.id
return res return res
} }
@ -259,7 +164,7 @@ export class Conversation {
throw new MtCuteArgumentError("Conversation hasn't started yet") 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 this._lastMessage = res.id
return res return res
} }
@ -278,7 +183,7 @@ export class Conversation {
throw new MtCuteArgumentError("Conversation hasn't started yet") throw new MtCuteArgumentError("Conversation hasn't started yet")
} }
const res = await this._client.sendMediaGroup( const res = await this.client.sendMediaGroup(
this._inputPeer, this._inputPeer,
medias, medias,
params params
@ -305,7 +210,7 @@ export class Conversation {
message = this._lastMessage ?? 0 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 // 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 if (dialog.lastRead >= msgId) return
const promise = createControllablePromise<void>() const promise = createControllablePromise<void>()
@ -578,7 +483,7 @@ export class Conversation {
this._queuedNewMessage.pop() this._queuedNewMessage.pop()
} }
} catch (e) { } catch (e) {
this._client['_emitError'](e) this.client['_emitError'](e)
} }
this._lastMessage = this._lastReceivedMessage = msg.id this._lastMessage = this._lastReceivedMessage = msg.id
@ -603,7 +508,7 @@ export class Conversation {
it.promise.resolve(msg) it.promise.resolve(msg)
delete this._pendingEditMessage[msg.id] delete this._pendingEditMessage[msg.id]
} }
})().catch((e) => this._client['_emitError'](e)) })().catch((e) => this.client['_emitError'](e))
} }
private _onHistoryRead(upd: HistoryReadUpdate) { private _onHistoryRead(upd: HistoryReadUpdate) {

View file

@ -5,6 +5,8 @@ export * from './media'
export * from './messages' export * from './messages'
export * from './peers' export * from './peers'
export * from './misc' export * from './misc'
export * from './updates'
export * from './conversation'
export * from './errors' export * from './errors'
export * from './parser' export * from './parser'

View file

@ -8,7 +8,7 @@ import {
User, User,
UsersIndex, UsersIndex,
} from '@mtcute/client' } from '@mtcute/client'
import { makeInspectable } from '@mtcute/client/src/types/utils' import { makeInspectable } from '../utils'
export namespace ChatMemberUpdate { export namespace ChatMemberUpdate {
/** /**

View file

@ -1,4 +1,3 @@
import { makeInspectable } from '@mtcute/client/src/types/utils'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { import {
TelegramClient, TelegramClient,
@ -7,7 +6,8 @@ import {
MtCuteArgumentError, MtCuteArgumentError,
UsersIndex, UsersIndex,
} from '@mtcute/client' } 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 * An inline result was chosen by the user and sent to some chat

View file

@ -1,7 +1,7 @@
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { makeInspectable } from '@mtcute/client/src/types/utils'
import { MAX_CHANNEL_ID } from '@mtcute/core' import { MAX_CHANNEL_ID } from '@mtcute/core'
import { TelegramClient } from '@mtcute/client' import { TelegramClient } from '@mtcute/client'
import { makeInspectable } from '../utils'
/** /**
* One or more messages were deleted * One or more messages were deleted

View file

@ -1,7 +1,7 @@
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { TelegramClient } from '@mtcute/client' import { TelegramClient } from '@mtcute/client'
import { getMarkedPeerId, MAX_CHANNEL_ID } from '@mtcute/core' import { getMarkedPeerId, MAX_CHANNEL_ID } from '@mtcute/core'
import { makeInspectable } from '@mtcute/client/src/types/utils' import { makeInspectable } from '../utils'
export class HistoryReadUpdate { export class HistoryReadUpdate {
constructor ( constructor (

View file

@ -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

View file

@ -1,6 +1,6 @@
import { makeInspectable } from '@mtcute/client/src/types/utils'
import { TelegramClient, Poll, UsersIndex } from '@mtcute/client' import { TelegramClient, Poll, UsersIndex } from '@mtcute/client'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { makeInspectable } from '../utils'
/** /**
* Poll state has changed (stopped, somebody * Poll state has changed (stopped, somebody

View file

@ -5,7 +5,7 @@ import {
UsersIndex, UsersIndex,
} from '@mtcute/client' } from '@mtcute/client'
import { tl } from '@mtcute/tl' 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. * Some user has voted in a public poll.

View file

@ -1,6 +1,6 @@
import { TelegramClient, User } from '@mtcute/client' import { TelegramClient, User } from '@mtcute/client'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { makeInspectable } from '@mtcute/client/src/types/utils' import { makeInspectable } from '../utils'
/** /**
* User status has changed * User status has changed

View file

@ -8,7 +8,7 @@ import {
} from '@mtcute/client' } from '@mtcute/client'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { getBarePeerId, MAX_CHANNEL_ID } from '@mtcute/core' 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. * User's typing status has changed.

View file

@ -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
}
}

View file

@ -0,0 +1,81 @@
interface OneWayLinkedListItem<T> {
v: T
n?: OneWayLinkedListItem<T>
}
export class Queue<T> {
first?: OneWayLinkedListItem<T>
last?: OneWayLinkedListItem<T>
length = 0
constructor(readonly limit = 0) {}
push(item: T): void {
const it: OneWayLinkedListItem<T> = { 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<T> | 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
}
}

View file

@ -35,6 +35,7 @@ import bigInt from 'big-integer'
import { BinaryWriter } from './utils/binary/binary-writer' import { BinaryWriter } from './utils/binary/binary-writer'
import { encodeUrlSafeBase64, parseUrlSafeBase64 } from './utils/buffer-utils' import { encodeUrlSafeBase64, parseUrlSafeBase64 } from './utils/buffer-utils'
import { BinaryReader } from './utils/binary/binary-reader' import { BinaryReader } from './utils/binary/binary-reader'
import EventEmitter from 'events'
const debug = require('debug')('mtcute:base') 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}. * `initConnection` params taken from {@link BaseTelegramClient.Options.initConnectionOptions}.
*/ */
@ -242,6 +243,8 @@ export class BaseTelegramClient {
protected _handleUpdate(update: tl.TypeUpdates): void {} protected _handleUpdate(update: tl.TypeUpdates): void {}
constructor(opts: BaseTelegramClient.Options) { constructor(opts: BaseTelegramClient.Options) {
super()
const apiId = const apiId =
typeof opts.apiId === 'string' ? parseInt(opts.apiId) : opts.apiId typeof opts.apiId === 'string' ? parseInt(opts.apiId) : opts.apiId
if (isNaN(apiId)) if (isNaN(apiId))

View file

@ -1,87 +1,4 @@
const fs = require('fs') const { types, toSentence, replaceSections, formatFile } = require('../../client/scripts/generate-updates')
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`
}
}
function generateHandler() { function generateHandler() {
const lines = [] const lines = []
@ -90,8 +7,6 @@ function generateHandler() {
// imports must be added manually because yeah // imports must be added manually because yeah
types.forEach((type) => { types.forEach((type) => {
if (type.updateType === 'IGNORE') return
lines.push( lines.push(
`export type ${type.handlerTypeName}Handler<T = ${type.updateType}` + `export type ${type.handlerTypeName}Handler<T = ${type.updateType}` +
`${type.state ? ', S = never' : ''}> = ParsedUpdateHandler<` + `${type.state ? ', S = never' : ''}> = ParsedUpdateHandler<` +
@ -105,147 +20,17 @@ function generateHandler() {
lines.join('\n') + lines.join('\n') +
'\n\nexport type UpdateHandler = \n' + '\n\nexport type UpdateHandler = \n' +
names.map((i) => ` | ${i}\n`).join(''), names.map((i) => ` | ${i}\n`).join(''),
}) }, __dirname)
}
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}<Mod>(
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() { function generateDispatcher() {
const lines = [] const lines = []
const declareLines = [] const imports = ['UpdateHandler', 'RawUpdateHandler']
const imports = ['UpdateHandler']
types.forEach((type) => { types.forEach((type) => {
imports.push(`${type.handlerTypeName}Handler`) imports.push(`${type.handlerTypeName}Handler`)
if (type.updateType === 'IGNORE') { lines.push(`
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 * Register ${toSentence(type)} without any filters
* *
@ -284,30 +69,31 @@ ${type.state ? `
/** @internal */ /** @internal */
on${type.handlerTypeName}(filter: any, handler?: any, group?: number): void { 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', { replaceSections('dispatcher.ts', {
codegen: lines.join('\n'), codegen: lines.join('\n'),
'codegen-declare': declareLines.join('\n'),
'codegen-imports': 'codegen-imports':
'import {\n' + 'import {\n' +
imports.map((i) => ` ${i},\n`).join('') + imports.map((i) => ` ${i},\n`).join('') +
"} from './handler'", "} from './handler'",
}) }, __dirname)
} }
async function main() { async function main() {
generateBuilders()
generateHandler() generateHandler()
generateDispatcher() generateDispatcher()
await formatFile('builders.ts') await formatFile('handler.ts', __dirname)
await formatFile('handler.ts') await formatFile('dispatcher.ts', __dirname)
await formatFile('dispatcher.ts')
} }
main().catch(console.error) module.exports = { types, toSentence }
if (require.main === module) {
main().catch(console.error)
}

View file

@ -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<T extends UpdateHandler>(
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<Mod>(
filter: UpdateFilter<Message, Mod>,
handler: NewMessageHandler<filters.Modify<Message, Mod>>['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<Mod>(
filter: UpdateFilter<Message, Mod>,
handler: EditMessageHandler<filters.Modify<Message, Mod>>['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<Mod>(
filter: UpdateFilter<DeleteMessageUpdate, Mod>,
handler: DeleteMessageHandler<
filters.Modify<DeleteMessageUpdate, Mod>
>['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<Mod>(
filter: UpdateFilter<ChatMemberUpdate, Mod>,
handler: ChatMemberUpdateHandler<
filters.Modify<ChatMemberUpdate, Mod>
>['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<Mod>(
filter: UpdateFilter<InlineQuery, Mod>,
handler: InlineQueryHandler<
filters.Modify<InlineQuery, Mod>
>['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<Mod>(
filter: UpdateFilter<ChosenInlineResult, Mod>,
handler: ChosenInlineResultHandler<
filters.Modify<ChosenInlineResult, Mod>
>['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<Mod>(
filter: UpdateFilter<CallbackQuery, Mod>,
handler: CallbackQueryHandler<
filters.Modify<CallbackQuery, Mod>
>['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<Mod>(
filter: UpdateFilter<PollUpdate, Mod>,
handler: PollUpdateHandler<filters.Modify<PollUpdate, Mod>>['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<Mod>(
filter: UpdateFilter<PollVoteUpdate, Mod>,
handler: PollVoteHandler<
filters.Modify<PollVoteUpdate, Mod>
>['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<Mod>(
filter: UpdateFilter<UserStatusUpdate, Mod>,
handler: UserStatusUpdateHandler<
filters.Modify<UserStatusUpdate, Mod>
>['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<Mod>(
filter: UpdateFilter<UserTypingUpdate, Mod>,
handler: UserTypingHandler<
filters.Modify<UserTypingUpdate, Mod>
>['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<Mod>(
filter: UpdateFilter<HistoryReadUpdate, Mod>,
handler: HistoryReadHandler<
filters.Modify<HistoryReadUpdate, Mod>
>['callback']
): HistoryReadHandler
/** @internal */
export function historyRead(
filter: any,
handler?: any
): HistoryReadHandler {
return _create('history_read', filter, handler)
}
// end-codegen
}

View file

@ -8,6 +8,15 @@ import {
MtCuteArgumentError, MtCuteArgumentError,
TelegramClient, TelegramClient,
UsersIndex, UsersIndex,
ChatMemberUpdate,
ChosenInlineResult,
PollUpdate,
PollVoteUpdate,
UserStatusUpdate,
UserTypingUpdate,
DeleteMessageUpdate,
HistoryReadUpdate,
ParsedUpdate,
} from '@mtcute/client' } from '@mtcute/client'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
// begin-codegen-imports // begin-codegen-imports
@ -28,262 +37,20 @@ import {
HistoryReadHandler, HistoryReadHandler,
} from './handler' } from './handler'
// end-codegen-imports // end-codegen-imports
import { ParsedUpdate } from './handler'
import { filters, UpdateFilter } from './filters' 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 { IStateStorage, UpdateState, StateKeyDelegate } from './state'
import { defaultStateKeyDelegate } from './state' import { defaultStateKeyDelegate } from './state'
import { PropagationAction } from './propagation' import { PropagationAction } from './propagation'
import EventEmitter from 'events'
const noop = () => {} const noop = () => {}
type ParserFunction = (
client: TelegramClient,
upd: tl.TypeUpdate | tl.TypeMessage,
users: UsersIndex,
chats: ChatsIndex
) => any
type UpdateParser = [Exclude<UpdateHandler['type'], 'raw'>, 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<string, string[]> = {}
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<T = {}>(
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 * Updates dispatcher
*/ */
export class Dispatcher< export class Dispatcher<State = never, SceneName extends string = string> {
State = never,
SceneName extends string = string
> extends EventEmitter {
private _groups: Record< private _groups: Record<
number, number,
Record<UpdateHandler['type'], UpdateHandler[]> Record<UpdateHandler['name'], UpdateHandler[]>
> = {} > = {}
private _groupsOrder: number[] = [] private _groupsOrder: number[] = []
@ -304,8 +71,6 @@ export class Dispatcher<
private _customStateKeyDelegate?: StateKeyDelegate private _customStateKeyDelegate?: StateKeyDelegate
private _customStorage?: IStateStorage private _customStorage?: IStateStorage
private _handlersCount: Record<string, number> = {}
private _errorHandler?: <T = {}>( private _errorHandler?: <T = {}>(
err: Error, err: Error,
update: ParsedUpdate & T, update: ParsedUpdate & T,
@ -346,7 +111,8 @@ export class Dispatcher<
storage?: IStateStorage | StateKeyDelegate, storage?: IStateStorage | StateKeyDelegate,
key?: StateKeyDelegate key?: StateKeyDelegate
) { ) {
super() this.dispatchRawUpdate = this.dispatchRawUpdate.bind(this)
this.dispatchUpdate = this.dispatchUpdate.bind(this)
if (client) { if (client) {
if (client instanceof TelegramClient) { if (client instanceof TelegramClient) {
@ -383,7 +149,9 @@ export class Dispatcher<
* Dispatcher also uses bound client to throw errors * Dispatcher also uses bound client to throw errors
*/ */
bindToClient(client: TelegramClient): void { bindToClient(client: TelegramClient): void {
client['dispatchUpdate'] = this.dispatchUpdate.bind(this) client.on('update', this.dispatchUpdate)
client.on('raw_update', this.dispatchRawUpdate)
this._client = client this._client = client
} }
@ -395,11 +163,103 @@ export class Dispatcher<
*/ */
unbind(): void { unbind(): void {
if (this._client) { if (this._client) {
this._client['dispatchUpdate'] = noop this._client.off('update', this.dispatchUpdate)
this._client.off('raw_update', this.dispatchRawUpdate)
this._client = undefined 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<boolean> {
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. * Process an update with this dispatcher.
* Calling this method without bound client will not work. * Calling this method without bound client will not work.
@ -408,19 +268,13 @@ export class Dispatcher<
* with error handler set to client's one. * with error handler set to client's one.
* *
* @param update Update to process * @param update Update to process
* @param users Map of users
* @param chats Map of chats
*/ */
dispatchUpdate( dispatchUpdate(update: ParsedUpdate): void {
update: tl.TypeUpdate | tl.TypeMessage,
users: UsersIndex,
chats: ChatsIndex
): void {
if (!this._client) return if (!this._client) return
// order does not matter in the dispatcher, // order does not matter in the dispatcher,
// so we can handle each update in its own task // 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) this._client!['_emitError'](err)
) )
} }
@ -434,58 +288,31 @@ export class Dispatcher<
* was fully processed by all the registered handlers, including children. * was fully processed by all the registered handlers, including children.
* *
* @param update Update to process * @param update Update to process
* @param users Map of users
* @param chats Map of chats
* @returns Whether the update was handled * @returns Whether the update was handled
*/ */
async dispatchUpdateNow( async dispatchUpdateNow(update: ParsedUpdate): Promise<boolean> {
update: tl.TypeUpdate | tl.TypeMessage, return this._dispatchUpdateNowImpl(update)
users: UsersIndex,
chats: ChatsIndex
): Promise<boolean> {
return this._dispatchUpdateNowImpl(update, users, chats)
} }
private async _dispatchUpdateNowImpl( private async _dispatchUpdateNowImpl(
update: tl.TypeUpdate | tl.TypeMessage | null, update: ParsedUpdate,
users: UsersIndex | null,
chats: ChatsIndex | null,
// this is getting a bit crazy lol // this is getting a bit crazy lol
parsed?: any,
parsedType?: Exclude<UpdateHandler['type'], 'raw'> | null,
parsedState?: UpdateState<State, SceneName> | null, parsedState?: UpdateState<State, SceneName> | null,
parsedScene?: string | null, parsedScene?: string | null,
forceScene?: true forceScene?: true
): Promise<boolean> { ): Promise<boolean> {
if (!this._client) return false 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 (parsedScene === undefined) {
if ( if (
this._storage && this._storage &&
this._scenes && this._scenes &&
(parsedType === 'new_message' || (update.name === 'new_message' ||
parsedType === 'edit_message' || update.name === 'edit_message' ||
parsedType === 'callback_query') update.name === 'callback_query')
) { ) {
// no need to fetch scene if there are no registered scenes // 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) { if (key) {
parsedScene = await this._storage.getCurrentScene(key) parsedScene = await this._storage.getCurrentScene(key)
} else { } else {
@ -508,10 +335,6 @@ export class Dispatcher<
return this._scenes[parsedScene]._dispatchUpdateNowImpl( return this._scenes[parsedScene]._dispatchUpdateNowImpl(
update, update,
users,
chats,
parsed,
parsedType,
parsedState, parsedState,
parsedScene, parsedScene,
true true
@ -522,16 +345,18 @@ export class Dispatcher<
if (parsedState === undefined) { if (parsedState === undefined) {
if ( if (
this._storage && this._storage &&
(parsedType === 'new_message' || (update.name === 'new_message' ||
parsedType === 'edit_message' || update.name === 'edit_message' ||
parsedType === 'callback_query') update.name === 'callback_query')
) { ) {
const key = await this._stateKeyDelegate!(parsed) const key = await this._stateKeyDelegate!(update.data)
if (key) { if (key) {
let customKey let customKey
if ( if (
!this._customStateKeyDelegate || !this._customStateKeyDelegate ||
(customKey = await this._customStateKeyDelegate(parsed)) (customKey = await this._customStateKeyDelegate(
update.data
))
) { ) {
parsedState = new UpdateState( parsedState = new UpdateState(
this._storage!, this._storage!,
@ -552,79 +377,23 @@ export class Dispatcher<
let shouldDispatch = true let shouldDispatch = true
let shouldDispatchChildren = true let shouldDispatchChildren = true
let wasHandled = false let handled = false
let updateInfo: any = null
if (parsed) { switch (await this._preUpdateHandler?.(update, parsedState as any)) {
updateInfo = { type: parsedType, data: parsed } case 'stop':
switch ( shouldDispatch = false
await this._preUpdateHandler?.( break
updateInfo as any, case 'stop-children':
parsedState as any return false
)
) {
case 'stop':
shouldDispatch = false
break
case 'stop-children':
return false
}
} }
if (shouldDispatch) { 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) { outer: for (const grp of this._groupsOrder) {
const group = this._groups[grp] const group = this._groups[grp]
if (update && !isRawMessage && 'raw' in group) { if (update.name 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) {
// raw is not handled here, so we can safely assume this // 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, UpdateHandler,
RawUpdateHandler RawUpdateHandler
>[] >[]
@ -635,13 +404,16 @@ export class Dispatcher<
if ( if (
!h.check || !h.check ||
(await h.check(parsed, parsedState as never)) (await h.check(
update.data as any,
parsedState as never
))
) { ) {
result = await h.callback( result = await h.callback(
parsed, update.data as any,
parsedState as never parsedState as never
) )
wasHandled = true handled = true
} else continue } else continue
switch (result) { switch (result) {
@ -669,10 +441,6 @@ export class Dispatcher<
scene scene
]._dispatchUpdateNowImpl( ]._dispatchUpdateNowImpl(
update, update,
users,
chats,
parsed,
parsedType,
undefined, undefined,
scene, scene,
true true
@ -686,7 +454,7 @@ export class Dispatcher<
if (this._errorHandler) { if (this._errorHandler) {
const handled = await this._errorHandler( const handled = await this._errorHandler(
e, e,
updateInfo as any, update,
parsedState as never parsedState as never
) )
if (!handled) throw e if (!handled) throw e
@ -700,25 +468,13 @@ export class Dispatcher<
if (shouldDispatchChildren) { if (shouldDispatchChildren) {
for (const child of this._children) { for (const child of this._children) {
wasHandled ||= await child._dispatchUpdateNowImpl( handled ||= await child._dispatchUpdateNowImpl(update)
update,
users,
chats,
parsed,
parsedType
)
} }
} }
if (updateInfo) { this._postUpdateHandler?.(handled, update, parsedState as any)
this._postUpdateHandler?.(
wasHandled,
updateInfo,
parsedState as any
)
}
return wasHandled return handled
} }
/** /**
@ -734,28 +490,23 @@ export class Dispatcher<
this._groupsOrder.sort((a, b) => a - b) this._groupsOrder.sort((a, b) => a - b)
} }
if (!(handler.type in this._groups[group])) { if (!(handler.name in this._groups[group])) {
this._groups[group][handler.type] = [] this._groups[group][handler.name] = []
} }
HANDLER_TYPE_TO_UPDATE[handler.type].forEach((upd) => { this._groups[group][handler.name].push(handler)
if (!(upd in this._handlersCount)) this._handlersCount[upd] = 0
this._handlersCount[upd] += 1
})
this._groups[group][handler.type].push(handler)
} }
/** /**
* Remove an update handler (or handlers) from a given * Remove an update handler (or handlers) from a given
* handler group. * 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) * @param group Handler group index (-1 to affect all groups)
* @internal * @internal
*/ */
removeUpdateHandler( removeUpdateHandler(
handler: UpdateHandler | UpdateHandler['type'] | 'all', handler: UpdateHandler | UpdateHandler['name'] | 'all',
group = 0 group = 0
): void { ): void {
if (group !== -1 && !(group in this._groups)) { if (group !== -1 && !(group in this._groups)) {
@ -766,38 +517,22 @@ export class Dispatcher<
if (handler === 'all') { if (handler === 'all') {
if (group === -1) { if (group === -1) {
this._groups = {} this._groups = {}
this._handlersCount = {}
} else { } 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] delete this._groups[group]
} }
} else { } else {
HANDLER_TYPE_TO_UPDATE[handler].forEach((upd) => {
this._handlersCount[upd] -= this._groups[group][
handler
].length
})
delete this._groups[group][handler] delete this._groups[group][handler]
} }
return return
} }
if (!(handler.type in this._groups[group])) { if (!(handler.name in this._groups[group])) {
return return
} }
const idx = this._groups[group][handler.type].indexOf(handler) const idx = this._groups[group][handler.name].indexOf(handler)
if (idx > 0) { if (idx > -1) {
this._groups[group][handler.type].splice(idx, 1) this._groups[group][handler.name].splice(idx, 1)
HANDLER_TYPE_TO_UPDATE[handler.type].forEach((upd) => {
this._handlersCount[upd] -= 1
})
} }
} }
@ -1071,10 +806,6 @@ export class Dispatcher<
} }
}) })
Object.keys(other._handlersCount).forEach((typ) => {
this._handlersCount[typ] += other._handlersCount[typ]
})
other._children.forEach((it) => { other._children.forEach((it) => {
it._unparent() it._unparent()
this.addChild(it as any) this.addChild(it as any)
@ -1121,14 +852,13 @@ export class Dispatcher<
dp._groups[idx] = {} as any dp._groups[idx] = {} as any
Object.keys(this._groups[idx]).forEach( Object.keys(this._groups[idx]).forEach(
(type: UpdateHandler['type']) => { (type: UpdateHandler['name']) => {
dp._groups[idx][type] = [...this._groups[idx][type]] dp._groups[idx][type] = [...this._groups[idx][type]]
} }
) )
}) })
dp._groupsOrder = [...this._groupsOrder] dp._groupsOrder = [...this._groupsOrder]
dp._handlersCount = { ...this._handlersCount }
dp._errorHandler = this._errorHandler dp._errorHandler = this._errorHandler
dp._customStateKeyDelegate = this._customStateKeyDelegate dp._customStateKeyDelegate = this._customStateKeyDelegate
dp._customStorage = this._customStorage dp._customStorage = this._customStorage
@ -1268,16 +998,23 @@ export class Dispatcher<
// addUpdateHandler convenience wrappers // // addUpdateHandler convenience wrappers //
private _addKnownHandler( private _addKnownHandler(
name: keyof typeof handlers, name: UpdateHandler['name'],
filter: any, filter: any,
handler?: any, handler?: any,
group?: number group?: number
): void { ): void {
if (typeof handler === 'number') { if (typeof handler === 'number') {
this.addUpdateHandler((handlers as any)[name](filter), handler) this.addUpdateHandler({
name,
callback: filter
}, handler)
} else { } else {
this.addUpdateHandler( this.addUpdateHandler(
(handlers as any)[name](filter, handler), {
name,
callback: handler,
check: filter
},
group group
) )
} }
@ -1285,32 +1022,6 @@ export class Dispatcher<
// begin-codegen // 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 * Register a new message handler without any filters
* *
@ -1359,7 +1070,7 @@ export class Dispatcher<
/** @internal */ /** @internal */
onNewMessage(filter: any, handler?: any, group?: number): void { 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 */ /** @internal */
onEditMessage(filter: any, handler?: any, group?: number): void { 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 */ /** @internal */
onDeleteMessage(filter: any, handler?: any, group?: number): void { 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 */ /** @internal */
onChatMemberUpdate(filter: any, handler?: any, group?: number): void { 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 */ /** @internal */
onInlineQuery(filter: any, handler?: any, group?: number): void { 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 */ /** @internal */
onChosenInlineResult(filter: any, handler?: any, group?: number): void { 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 */ /** @internal */
onCallbackQuery(filter: any, handler?: any, group?: number): void { 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 */ /** @internal */
onPollUpdate(filter: any, handler?: any, group?: number): void { 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 */ /** @internal */
onPollVote(filter: any, handler?: any, group?: number): void { 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 */ /** @internal */
onUserStatusUpdate(filter: any, handler?: any, group?: number): void { 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 */ /** @internal */
onUserTyping(filter: any, handler?: any, group?: number): void { 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 */ /** @internal */
onHistoryRead(filter: any, handler?: any, group?: number): void { onHistoryRead(filter: any, handler?: any, group?: number): void {
this._addKnownHandler('historyRead', filter, handler, group) this._addKnownHandler('history_read', filter, handler, group)
} }
// end-codegen // end-codegen

View file

@ -23,14 +23,14 @@ import {
WebPage, WebPage,
MessageAction, MessageAction,
RawLocation, RawLocation,
ChatMemberUpdate,
ChosenInlineResult,
UserStatusUpdate,
PollVoteUpdate,
UserTypingUpdate,
} from '@mtcute/client' } from '@mtcute/client'
import { MaybeArray } from '@mtcute/core' import { MaybeArray } from '@mtcute/core'
import { ChatMemberUpdate } from './updates'
import { ChosenInlineResult } from './updates/chosen-inline-result'
import { UpdateState } from './state' 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( function extractText(
obj: Message | InlineQuery | ChosenInlineResult | CallbackQuery obj: Message | InlineQuery | ChosenInlineResult | CallbackQuery

View file

@ -6,52 +6,42 @@ import {
CallbackQuery, CallbackQuery,
UsersIndex, UsersIndex,
ChatsIndex, ChatsIndex,
ChatMemberUpdate,
PollVoteUpdate,
UserStatusUpdate,
ChosenInlineResult,
HistoryReadUpdate,
DeleteMessageUpdate,
PollUpdate,
UserTypingUpdate,
} from '@mtcute/client' } from '@mtcute/client'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { PropagationAction } from './propagation' import { PropagationAction } from './propagation'
import {
ChatMemberUpdate,
ChosenInlineResult,
PollUpdate,
PollVoteUpdate,
UserStatusUpdate,
UserTypingUpdate,
DeleteMessageUpdate,
HistoryReadUpdate,
} from './updates'
interface BaseUpdateHandler<Type, Handler, Checker> { interface BaseUpdateHandler<Name, Handler, Checker> {
type: Type name: Name
callback: Handler callback: Handler
check?: Checker check?: Checker
} }
type ParsedUpdateHandler<Type, Update, State = never> = BaseUpdateHandler< type ParsedUpdateHandler<Name, Update, State = never> = BaseUpdateHandler<
Type, Name,
(update: Update, state: State) => MaybeAsync<void | PropagationAction>, (update: Update, state: State) => MaybeAsync<void | PropagationAction>,
(update: Update, state: State) => MaybeAsync<boolean> (update: Update, state: State) => MaybeAsync<boolean>
> >
type _ParsedUpdate<T> = T extends ParsedUpdateHandler<infer K, infer Q>
? {
readonly type: K
readonly data: Q
}
: never
export type ParsedUpdate = _ParsedUpdate<UpdateHandler>
export type RawUpdateHandler = BaseUpdateHandler< export type RawUpdateHandler = BaseUpdateHandler<
'raw', 'raw',
( (
client: TelegramClient, client: TelegramClient,
update: tl.TypeUpdate, update: tl.TypeUpdate | tl.TypeMessage,
users: UsersIndex, users: UsersIndex,
chats: ChatsIndex chats: ChatsIndex
) => MaybeAsync<void | PropagationAction>, ) => MaybeAsync<void | PropagationAction>,
( (
client: TelegramClient, client: TelegramClient,
update: tl.TypeUpdate, update: tl.TypeUpdate | tl.TypeMessage,
users: UsersIndex, users: UsersIndex,
chats: ChatsIndex chats: ChatsIndex
) => MaybeAsync<boolean> ) => MaybeAsync<boolean>

View file

@ -1,11 +1,8 @@
export * from './builders'
export * from './dispatcher' export * from './dispatcher'
export * from './filters' export * from './filters'
export * from './handler' export * from './handler'
export * from './propagation' export * from './propagation'
export * from './updates'
export * from './wizard' export * from './wizard'
export * from './callback-data-builder' export * from './callback-data-builder'
export * from './conversation'
export { UpdateState, IStateStorage } from './state' export { UpdateState, IStateStorage } from './state'

View file

@ -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'