feat: message groups
This commit is contained in:
parent
85ca3b4603
commit
c7d82d41f0
13 changed files with 504 additions and 205 deletions
|
@ -631,50 +631,7 @@ on(name: '${type.typeName}', handler: ((upd: ${type.updateType}) => void)): this
|
||||||
)
|
)
|
||||||
output.write('}\n')
|
output.write('}\n')
|
||||||
|
|
||||||
output.write(
|
output.write('\nexport { TelegramClientOptions }\n')
|
||||||
`
|
|
||||||
export interface TelegramClientOptions extends BaseTelegramClientOptions {
|
|
||||||
/**
|
|
||||||
* **ADVANCED**
|
|
||||||
*
|
|
||||||
* Whether to disable no-dispatch mechanism.
|
|
||||||
*
|
|
||||||
* No-dispatch is a mechanism that allows you to call methods
|
|
||||||
* that return updates and correctly handle them, without
|
|
||||||
* actually dispatching them to the event handlers.
|
|
||||||
*
|
|
||||||
* In other words, the following code will work differently:
|
|
||||||
* \`\`\`ts
|
|
||||||
* dp.onNewMessage(console.log)
|
|
||||||
* console.log(tg.sendText('me', 'hello'))
|
|
||||||
* \`\`\`
|
|
||||||
* - if \`disableNoDispatch\` is \`true\`, the sent message will be
|
|
||||||
* dispatched to the event handler, thus it will be printed twice
|
|
||||||
* - if \`disableNoDispatch\` is \`false\`, the sent message will not be
|
|
||||||
* dispatched to the event handler, thus it will onlt be printed once
|
|
||||||
*
|
|
||||||
* Disabling it also may improve performance, but it's not guaranteed.
|
|
||||||
*
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
disableNoDispatch?: boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Limit of {@link resolvePeerMany} internal async pool.
|
|
||||||
*
|
|
||||||
* Higher value means more parallel requests, but also
|
|
||||||
* higher risk of getting flood-wait errors.
|
|
||||||
* Most resolves will however likely be a DB cache hit.
|
|
||||||
*
|
|
||||||
* Only change this if you know what you're doing.
|
|
||||||
*
|
|
||||||
* @default 8
|
|
||||||
*/
|
|
||||||
resolvePeerManyPoolLimit?: number
|
|
||||||
}
|
|
||||||
`.trim(),
|
|
||||||
)
|
|
||||||
|
|
||||||
output.write('\nexport class TelegramClient extends BaseTelegramClient {\n')
|
output.write('\nexport class TelegramClient extends BaseTelegramClient {\n')
|
||||||
|
|
||||||
state.fields.forEach(({ code }) => output.write(`protected ${code}\n`))
|
state.fields.forEach(({ code }) => output.write(`protected ${code}\n`))
|
||||||
|
|
|
@ -8,16 +8,12 @@ const snakeToCamel = (s) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const camelToPascal = (s) =>
|
const camelToPascal = (s) => s[0].toUpperCase() + s.substr(1)
|
||||||
s[0].toUpperCase() + s.substr(1)
|
|
||||||
|
|
||||||
const camelToSnake = (s) => {
|
const camelToSnake = (s) => {
|
||||||
return s.replace(
|
return s.replace(/(?<=[a-zA-Z0-9])([A-Z0-9]+(?=[A-Z]|$)|[A-Z0-9])/g, ($1) => {
|
||||||
/(?<=[a-zA-Z0-9])([A-Z0-9]+(?=[A-Z]|$)|[A-Z0-9])/g,
|
return '_' + $1.toLowerCase()
|
||||||
($1) => {
|
})
|
||||||
return '_' + $1.toLowerCase()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseUpdateTypes() {
|
function parseUpdateTypes() {
|
||||||
|
@ -30,15 +26,13 @@ function parseUpdateTypes() {
|
||||||
const ret = []
|
const ret = []
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const m = line.match(/^([a-z_]+)(?:: ([a-zA-Z]+))? = ([a-zA-Z]+)( \+ State)?$/)
|
const m = line.match(/^([a-z_]+)(?:: ([a-zA-Z]+))? = ([a-zA-Z]+(?:\[\])?)( \+ State)?$/)
|
||||||
if (!m) throw new Error(`invalid syntax: ${line}`)
|
if (!m) throw new Error(`invalid syntax: ${line}`)
|
||||||
ret.push({
|
ret.push({
|
||||||
typeName: m[1],
|
typeName: m[1],
|
||||||
handlerTypeName: m[2] || camelToPascal(snakeToCamel(m[1])),
|
handlerTypeName: m[2] || camelToPascal(snakeToCamel(m[1])),
|
||||||
updateType: m[3],
|
updateType: m[3],
|
||||||
funcName: m[2] ?
|
funcName: m[2] ? m[2][0].toLowerCase() + m[2].substr(1) : snakeToCamel(m[1]),
|
||||||
m[2][0].toLowerCase() + m[2].substr(1) :
|
|
||||||
snakeToCamel(m[1]),
|
|
||||||
state: Boolean(m[4]),
|
state: Boolean(m[4]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -47,9 +41,7 @@ function parseUpdateTypes() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceSections(filename, sections, dir = __dirname) {
|
function replaceSections(filename, sections, dir = __dirname) {
|
||||||
let lines = fs
|
let lines = fs.readFileSync(path.join(dir, '../src', filename), 'utf-8').split('\n')
|
||||||
.readFileSync(path.join(dir, '../src', filename), 'utf-8')
|
|
||||||
.split('\n')
|
|
||||||
|
|
||||||
const findMarker = (marker) => {
|
const findMarker = (marker) => {
|
||||||
const idx = lines.findIndex((line) => line.trim() === `// ${marker}`)
|
const idx = lines.findIndex((line) => line.trim() === `// ${marker}`)
|
||||||
|
@ -84,9 +76,7 @@ async function formatFile(filename, dir = __dirname) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function toSentence(type, stype = 'inline') {
|
function toSentence(type, stype = 'inline') {
|
||||||
const name = camelToSnake(type.handlerTypeName)
|
const name = camelToSnake(type.handlerTypeName).toLowerCase().replace(/_/g, ' ')
|
||||||
.toLowerCase()
|
|
||||||
.replace(/_/g, ' ')
|
|
||||||
|
|
||||||
if (stype === 'inline') {
|
if (stype === 'inline') {
|
||||||
return `${name[0].match(/[aeiouy]/i) ? 'an' : 'a'} ${name} handler`
|
return `${name[0].match(/[aeiouy]/i) ? 'an' : 'a'} ${name} handler`
|
||||||
|
@ -99,7 +89,8 @@ function toSentence(type, stype = 'inline') {
|
||||||
|
|
||||||
function generateParsedUpdate() {
|
function generateParsedUpdate() {
|
||||||
replaceSections('types/updates/index.ts', {
|
replaceSections('types/updates/index.ts', {
|
||||||
codegen: 'export type ParsedUpdate =\n' +
|
codegen:
|
||||||
|
'export type ParsedUpdate =\n' +
|
||||||
types.map((typ) => ` | { name: '${typ.typeName}'; data: ${typ.updateType} }\n`).join(''),
|
types.map((typ) => ` | { name: '${typ.typeName}'; data: ${typ.updateType} }\n`).join(''),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# format: type_name[: handler_type_name] = update_type[ + State]
|
# format: type_name[: handler_type_name] = update_type[ + State]
|
||||||
new_message = Message + State
|
new_message = Message + State
|
||||||
edit_message = Message + State
|
edit_message = Message + State
|
||||||
|
message_group = Message[] + State
|
||||||
delete_message = DeleteMessageUpdate
|
delete_message = DeleteMessageUpdate
|
||||||
chat_member: ChatMemberUpdate = ChatMemberUpdate
|
chat_member: ChatMemberUpdate = ChatMemberUpdate
|
||||||
inline_query = InlineQuery
|
inline_query = InlineQuery
|
||||||
|
|
|
@ -329,6 +329,66 @@ import {
|
||||||
} from './types'
|
} from './types'
|
||||||
import { RpsMeter } from './utils/rps-meter'
|
import { RpsMeter } from './utils/rps-meter'
|
||||||
|
|
||||||
|
// from methods/_options.ts
|
||||||
|
interface TelegramClientOptions extends BaseTelegramClientOptions {
|
||||||
|
/**
|
||||||
|
* **ADVANCED**
|
||||||
|
*
|
||||||
|
* Whether to disable no-dispatch mechanism.
|
||||||
|
*
|
||||||
|
* No-dispatch is a mechanism that allows you to call methods
|
||||||
|
* that return updates and correctly handle them, without
|
||||||
|
* actually dispatching them to the event handlers.
|
||||||
|
*
|
||||||
|
* In other words, the following code will work differently:
|
||||||
|
* ```ts
|
||||||
|
* dp.onNewMessage(console.log)
|
||||||
|
* console.log(await tg.sendText('me', 'hello'))
|
||||||
|
* ```
|
||||||
|
* - if `disableNoDispatch` is `true`, the sent message will be
|
||||||
|
* dispatched to the event handler, thus it will be printed twice
|
||||||
|
* - if `disableNoDispatch` is `false`, the sent message will not be
|
||||||
|
* dispatched to the event handler, thus it will onlt be printed once
|
||||||
|
*
|
||||||
|
* Disabling it also may improve performance, but it's not guaranteed.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
disableNoDispatch?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit of {@link resolvePeerMany} internal async pool.
|
||||||
|
*
|
||||||
|
* Higher value means more parallel requests, but also
|
||||||
|
* higher risk of getting flood-wait errors.
|
||||||
|
* Most resolves will however likely be a DB cache hit.
|
||||||
|
*
|
||||||
|
* Only change this if you know what you're doing.
|
||||||
|
*
|
||||||
|
* @default 8
|
||||||
|
*/
|
||||||
|
resolvePeerManyPoolLimit?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When non-zero, allows the library to automatically handle Telegram
|
||||||
|
* media groups (e.g. albums) in {@link MessageGroup} updates
|
||||||
|
* in a given time interval (in ms).
|
||||||
|
*
|
||||||
|
* **Note**: this does not catch messages that happen to be consecutive,
|
||||||
|
* only messages belonging to the same "media group".
|
||||||
|
*
|
||||||
|
* This will cause up to `messageGroupingInterval` delay
|
||||||
|
* in handling media group messages.
|
||||||
|
*
|
||||||
|
* This option only applies to `new_message` updates,
|
||||||
|
* and the updates being grouped **will not** be dispatched on their own.
|
||||||
|
*
|
||||||
|
* Recommended value is 250 ms.
|
||||||
|
*
|
||||||
|
* @default 0 (disabled)
|
||||||
|
*/
|
||||||
|
messageGroupingInterval?: number
|
||||||
|
}
|
||||||
// from methods/updates.ts
|
// from methods/updates.ts
|
||||||
interface PendingUpdateContainer {
|
interface PendingUpdateContainer {
|
||||||
upd: tl.TypeUpdates
|
upd: tl.TypeUpdates
|
||||||
|
@ -5482,45 +5542,9 @@ export interface TelegramClient extends BaseTelegramClient {
|
||||||
bio?: string
|
bio?: string
|
||||||
}): Promise<User>
|
}): Promise<User>
|
||||||
}
|
}
|
||||||
export interface TelegramClientOptions extends BaseTelegramClientOptions {
|
|
||||||
/**
|
|
||||||
* **ADVANCED**
|
|
||||||
*
|
|
||||||
* Whether to disable no-dispatch mechanism.
|
|
||||||
*
|
|
||||||
* No-dispatch is a mechanism that allows you to call methods
|
|
||||||
* that return updates and correctly handle them, without
|
|
||||||
* actually dispatching them to the event handlers.
|
|
||||||
*
|
|
||||||
* In other words, the following code will work differently:
|
|
||||||
* ```ts
|
|
||||||
* dp.onNewMessage(console.log)
|
|
||||||
* console.log(tg.sendText('me', 'hello'))
|
|
||||||
* ```
|
|
||||||
* - if `disableNoDispatch` is `true`, the sent message will be
|
|
||||||
* dispatched to the event handler, thus it will be printed twice
|
|
||||||
* - if `disableNoDispatch` is `false`, the sent message will not be
|
|
||||||
* dispatched to the event handler, thus it will onlt be printed once
|
|
||||||
*
|
|
||||||
* Disabling it also may improve performance, but it's not guaranteed.
|
|
||||||
*
|
|
||||||
* @default false
|
|
||||||
*/
|
|
||||||
disableNoDispatch?: boolean
|
|
||||||
|
|
||||||
/**
|
export { TelegramClientOptions }
|
||||||
* Limit of {@link resolvePeerMany} internal async pool.
|
|
||||||
*
|
|
||||||
* Higher value means more parallel requests, but also
|
|
||||||
* higher risk of getting flood-wait errors.
|
|
||||||
* Most resolves will however likely be a DB cache hit.
|
|
||||||
*
|
|
||||||
* Only change this if you know what you're doing.
|
|
||||||
*
|
|
||||||
* @default 8
|
|
||||||
*/
|
|
||||||
resolvePeerManyPoolLimit?: number
|
|
||||||
}
|
|
||||||
export class TelegramClient extends BaseTelegramClient {
|
export class TelegramClient extends BaseTelegramClient {
|
||||||
protected _userId: number | null
|
protected _userId: number | null
|
||||||
protected _isBot: boolean
|
protected _isBot: boolean
|
||||||
|
@ -5544,6 +5568,8 @@ export class TelegramClient extends BaseTelegramClient {
|
||||||
protected _updLock: AsyncLock
|
protected _updLock: AsyncLock
|
||||||
protected _rpsIncoming?: RpsMeter
|
protected _rpsIncoming?: RpsMeter
|
||||||
protected _rpsProcessing?: RpsMeter
|
protected _rpsProcessing?: RpsMeter
|
||||||
|
protected _messageGroupingInterval: number
|
||||||
|
protected _messageGroupingPending: Map<string, [Message[], NodeJS.Timeout]>
|
||||||
protected _pts?: number
|
protected _pts?: number
|
||||||
protected _qts?: number
|
protected _qts?: number
|
||||||
protected _date?: number
|
protected _date?: number
|
||||||
|
@ -5583,6 +5609,9 @@ export class TelegramClient extends BaseTelegramClient {
|
||||||
this._noDispatchPts = new Map()
|
this._noDispatchPts = new Map()
|
||||||
this._noDispatchQts = new Set()
|
this._noDispatchQts = new Set()
|
||||||
|
|
||||||
|
this._messageGroupingInterval = opts.messageGroupingInterval ?? 0
|
||||||
|
this._messageGroupingPending = new Map()
|
||||||
|
|
||||||
this._updLock = new AsyncLock()
|
this._updLock = new AsyncLock()
|
||||||
// we dont need to initialize state fields since
|
// we dont need to initialize state fields since
|
||||||
// they are always loaded either from the server, or from storage.
|
// they are always loaded either from the server, or from storage.
|
||||||
|
|
64
packages/client/src/methods/_options.ts
Normal file
64
packages/client/src/methods/_options.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
|
||||||
|
import { BaseTelegramClientOptions } from '@mtcute/core'
|
||||||
|
|
||||||
|
// @copy
|
||||||
|
interface TelegramClientOptions extends BaseTelegramClientOptions {
|
||||||
|
/**
|
||||||
|
* **ADVANCED**
|
||||||
|
*
|
||||||
|
* Whether to disable no-dispatch mechanism.
|
||||||
|
*
|
||||||
|
* No-dispatch is a mechanism that allows you to call methods
|
||||||
|
* that return updates and correctly handle them, without
|
||||||
|
* actually dispatching them to the event handlers.
|
||||||
|
*
|
||||||
|
* In other words, the following code will work differently:
|
||||||
|
* ```ts
|
||||||
|
* dp.onNewMessage(console.log)
|
||||||
|
* console.log(await tg.sendText('me', 'hello'))
|
||||||
|
* ```
|
||||||
|
* - if `disableNoDispatch` is `true`, the sent message will be
|
||||||
|
* dispatched to the event handler, thus it will be printed twice
|
||||||
|
* - if `disableNoDispatch` is `false`, the sent message will not be
|
||||||
|
* dispatched to the event handler, thus it will onlt be printed once
|
||||||
|
*
|
||||||
|
* Disabling it also may improve performance, but it's not guaranteed.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
disableNoDispatch?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit of {@link resolvePeerMany} internal async pool.
|
||||||
|
*
|
||||||
|
* Higher value means more parallel requests, but also
|
||||||
|
* higher risk of getting flood-wait errors.
|
||||||
|
* Most resolves will however likely be a DB cache hit.
|
||||||
|
*
|
||||||
|
* Only change this if you know what you're doing.
|
||||||
|
*
|
||||||
|
* @default 8
|
||||||
|
*/
|
||||||
|
resolvePeerManyPoolLimit?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When non-zero, allows the library to automatically handle Telegram
|
||||||
|
* media groups (e.g. albums) in {@link MessageGroup} updates
|
||||||
|
* in a given time interval (in ms).
|
||||||
|
*
|
||||||
|
* **Note**: this does not catch messages that happen to be consecutive,
|
||||||
|
* only messages belonging to the same "media group".
|
||||||
|
*
|
||||||
|
* This will cause up to `messageGroupingInterval` delay
|
||||||
|
* in handling media group messages.
|
||||||
|
*
|
||||||
|
* This option only applies to `new_message` updates,
|
||||||
|
* and the updates being grouped **will not** be dispatched on their own.
|
||||||
|
*
|
||||||
|
* Recommended value is 250 ms.
|
||||||
|
*
|
||||||
|
* @default 0 (disabled)
|
||||||
|
*/
|
||||||
|
messageGroupingInterval?: number
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ import {
|
||||||
} from '@mtcute/core/utils'
|
} from '@mtcute/core/utils'
|
||||||
|
|
||||||
import { TelegramClient, TelegramClientOptions } from '../client'
|
import { TelegramClient, TelegramClientOptions } from '../client'
|
||||||
import { PeersIndex } from '../types'
|
import { Message, PeersIndex } from '../types'
|
||||||
import { _parseUpdate } from '../types/updates/parse-update'
|
import { _parseUpdate } from '../types/updates/parse-update'
|
||||||
import { extractChannelIdFromUpdate } from '../utils/misc-utils'
|
import { extractChannelIdFromUpdate } from '../utils/misc-utils'
|
||||||
import { normalizeToInputChannel } from '../utils/peer-utils'
|
import { normalizeToInputChannel } from '../utils/peer-utils'
|
||||||
|
@ -65,6 +65,9 @@ interface UpdatesState {
|
||||||
_rpsIncoming?: RpsMeter
|
_rpsIncoming?: RpsMeter
|
||||||
_rpsProcessing?: RpsMeter
|
_rpsProcessing?: RpsMeter
|
||||||
|
|
||||||
|
_messageGroupingInterval: number
|
||||||
|
_messageGroupingPending: Map<string, [Message[], NodeJS.Timeout]>
|
||||||
|
|
||||||
// accessing storage every time might be expensive,
|
// accessing storage every time might be expensive,
|
||||||
// so store everything here, and load & save
|
// so store everything here, and load & save
|
||||||
// every time session is loaded & saved.
|
// every time session is loaded & saved.
|
||||||
|
@ -109,6 +112,9 @@ function _initializeUpdates(this: TelegramClient, opts: TelegramClientOptions) {
|
||||||
this._noDispatchPts = new Map()
|
this._noDispatchPts = new Map()
|
||||||
this._noDispatchQts = new Set()
|
this._noDispatchQts = new Set()
|
||||||
|
|
||||||
|
this._messageGroupingInterval = opts.messageGroupingInterval ?? 0
|
||||||
|
this._messageGroupingPending = new Map()
|
||||||
|
|
||||||
this._updLock = new AsyncLock()
|
this._updLock = new AsyncLock()
|
||||||
// we dont need to initialize state fields since
|
// we dont need to initialize state fields since
|
||||||
// they are always loaded either from the server, or from storage.
|
// they are always loaded either from the server, or from storage.
|
||||||
|
@ -375,6 +381,29 @@ export function _dispatchUpdate(this: TelegramClient, update: tl.TypeUpdate, pee
|
||||||
const parsed = _parseUpdate(this, update, peers)
|
const parsed = _parseUpdate(this, update, peers)
|
||||||
|
|
||||||
if (parsed) {
|
if (parsed) {
|
||||||
|
if (this._messageGroupingInterval && parsed.name === 'new_message') {
|
||||||
|
const group = parsed.data.groupedIdUnique
|
||||||
|
|
||||||
|
if (group) {
|
||||||
|
const pendingGroup = this._messageGroupingPending.get(group)
|
||||||
|
|
||||||
|
if (pendingGroup) {
|
||||||
|
pendingGroup[0].push(parsed.data)
|
||||||
|
} else {
|
||||||
|
const messages = [parsed.data]
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
this._messageGroupingPending.delete(group)
|
||||||
|
this.emit('update', { name: 'message_group', data: messages })
|
||||||
|
this.emit('message_group', messages)
|
||||||
|
}, this._messageGroupingInterval)
|
||||||
|
|
||||||
|
this._messageGroupingPending.set(group, [messages, timeout])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.emit('update', parsed)
|
this.emit('update', parsed)
|
||||||
this.emit(parsed.name, parsed.data)
|
this.emit(parsed.name, parsed.data)
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,6 +161,15 @@ export class Message {
|
||||||
return this.raw._ === 'message' ? this.raw.groupedId ?? null : null
|
return this.raw._ === 'message' ? this.raw.groupedId ?? null : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as {@link groupedId}, but is globally unique across chats.
|
||||||
|
*/
|
||||||
|
get groupedIdUnique(): string | null {
|
||||||
|
if (!(this.raw._ === 'message' && this.raw.groupedId !== undefined)) return null
|
||||||
|
|
||||||
|
return `${this.raw.groupedId.low}|${this.raw.groupedId.high}|${getMarkedPeerId(this.raw.peerId)}`
|
||||||
|
}
|
||||||
|
|
||||||
private _sender?: User | Chat
|
private _sender?: User | Chat
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -36,6 +36,7 @@ export {
|
||||||
export type ParsedUpdate =
|
export type ParsedUpdate =
|
||||||
| { name: 'new_message'; data: Message }
|
| { name: 'new_message'; data: Message }
|
||||||
| { name: 'edit_message'; data: Message }
|
| { name: 'edit_message'; data: Message }
|
||||||
|
| { name: 'message_group'; data: Message[] }
|
||||||
| { name: 'delete_message'; data: DeleteMessageUpdate }
|
| { name: 'delete_message'; data: DeleteMessageUpdate }
|
||||||
| { name: 'chat_member'; data: ChatMemberUpdate }
|
| { name: 'chat_member'; data: ChatMemberUpdate }
|
||||||
| { name: 'inline_query'; data: InlineQuery }
|
| { name: 'inline_query'; data: InlineQuery }
|
||||||
|
|
|
@ -41,6 +41,7 @@ import {
|
||||||
EditMessageHandler,
|
EditMessageHandler,
|
||||||
HistoryReadHandler,
|
HistoryReadHandler,
|
||||||
InlineQueryHandler,
|
InlineQueryHandler,
|
||||||
|
MessageGroupHandler,
|
||||||
NewMessageHandler,
|
NewMessageHandler,
|
||||||
PollUpdateHandler,
|
PollUpdateHandler,
|
||||||
PollVoteHandler,
|
PollVoteHandler,
|
||||||
|
@ -109,7 +110,10 @@ export class Dispatcher<State = never, SceneName extends string = string> {
|
||||||
* Create a new dispatcher and bind it to client and optionally
|
* Create a new dispatcher and bind it to client and optionally
|
||||||
* FSM storage
|
* FSM storage
|
||||||
*/
|
*/
|
||||||
constructor(client: TelegramClient, ...args: State extends never ? [] : [IStateStorage, StateKeyDelegate?])
|
constructor(
|
||||||
|
client: TelegramClient,
|
||||||
|
...args: (() => State) extends () => never ? [] : [IStateStorage, StateKeyDelegate?]
|
||||||
|
)
|
||||||
constructor(
|
constructor(
|
||||||
client?: TelegramClient | IStateStorage | StateKeyDelegate,
|
client?: TelegramClient | IStateStorage | StateKeyDelegate,
|
||||||
storage?: IStateStorage | StateKeyDelegate,
|
storage?: IStateStorage | StateKeyDelegate,
|
||||||
|
@ -290,11 +294,16 @@ export class Dispatcher<State = never, SceneName extends string = string> {
|
||||||
if (
|
if (
|
||||||
this._storage &&
|
this._storage &&
|
||||||
this._scenes &&
|
this._scenes &&
|
||||||
(update.name === 'new_message' || update.name === 'edit_message' || update.name === 'callback_query')
|
(update.name === 'new_message' ||
|
||||||
|
update.name === 'edit_message' ||
|
||||||
|
update.name === 'callback_query' ||
|
||||||
|
update.name === 'message_group')
|
||||||
) {
|
) {
|
||||||
// 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!(update.data)
|
const key = await this._stateKeyDelegate!(
|
||||||
|
update.name === 'message_group' ? update.data[0] : update.data,
|
||||||
|
)
|
||||||
|
|
||||||
if (key) {
|
if (key) {
|
||||||
parsedScene = await this._storage.getCurrentScene(key)
|
parsedScene = await this._storage.getCurrentScene(key)
|
||||||
|
@ -1034,6 +1043,57 @@ export class Dispatcher<State = never, SceneName extends string = string> {
|
||||||
this._addKnownHandler('edit_message', filter, handler, group)
|
this._addKnownHandler('edit_message', filter, handler, group)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a message group handler without any filters
|
||||||
|
*
|
||||||
|
* @param handler Message group handler
|
||||||
|
* @param group Handler group index
|
||||||
|
*/
|
||||||
|
onMessageGroup(
|
||||||
|
handler: MessageGroupHandler<
|
||||||
|
Message[],
|
||||||
|
State extends never ? never : UpdateState<State, SceneName>
|
||||||
|
>['callback'],
|
||||||
|
group?: number,
|
||||||
|
): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a message group handler with a filter
|
||||||
|
*
|
||||||
|
* @param filter Update filter
|
||||||
|
* @param handler Message group handler
|
||||||
|
* @param group Handler group index
|
||||||
|
*/
|
||||||
|
onMessageGroup<Mod>(
|
||||||
|
filter: UpdateFilter<Message[], Mod, State>,
|
||||||
|
handler: MessageGroupHandler<
|
||||||
|
filters.Modify<Message[], Mod>,
|
||||||
|
State extends never ? never : UpdateState<State, SceneName>
|
||||||
|
>['callback'],
|
||||||
|
group?: number,
|
||||||
|
): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a message group handler with a filter
|
||||||
|
*
|
||||||
|
* @param filter Update filter
|
||||||
|
* @param handler Message group handler
|
||||||
|
* @param group Handler group index
|
||||||
|
*/
|
||||||
|
onMessageGroup<Mod>(
|
||||||
|
filter: UpdateFilter<Message[], Mod>,
|
||||||
|
handler: MessageGroupHandler<
|
||||||
|
filters.Modify<Message[], Mod>,
|
||||||
|
State extends never ? never : UpdateState<State, SceneName>
|
||||||
|
>['callback'],
|
||||||
|
group?: number,
|
||||||
|
): void
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
onMessageGroup(filter: any, handler?: any, group?: number): void {
|
||||||
|
this._addKnownHandler('message_group', filter, handler, group)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a delete message handler without any filters
|
* Register a delete message handler without any filters
|
||||||
*
|
*
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
|
|
||||||
import { MaybeAsync } from '@mtcute/core'
|
import { MaybeAsync } from '@mtcute/core'
|
||||||
|
|
||||||
import { UpdateState } from '../state'
|
import { ExtractBaseMany, ExtractMod, Invert, UnionToIntersection, UpdateFilter } from './types'
|
||||||
import { ExtractBaseMany, ExtractMod, ExtractState, Invert, UnionToIntersection, UpdateFilter } from './types'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filter that matches any update
|
* Filter that matches any update
|
||||||
|
@ -35,9 +34,90 @@ export function not<Base, Mod, State>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// i couldn't come up with proper types for these 😭
|
||||||
|
// if you know how to do this better - PRs are welcome!
|
||||||
|
|
||||||
|
export function and<Base1, Mod1, State1, Base2, Mod2, State2>(
|
||||||
|
fn1: UpdateFilter<Base1, Mod1, State1>,
|
||||||
|
fn2: UpdateFilter<Base2, Mod2, State2>,
|
||||||
|
): UpdateFilter<Base1 & Base2, Mod1 & Mod2, State1 | State2>
|
||||||
|
export function and<Base1, Mod1, State1, Base2, Mod2, State2, Base3, Mod3, State3>(
|
||||||
|
fn1: UpdateFilter<Base1, Mod1, State1>,
|
||||||
|
fn2: UpdateFilter<Base2, Mod2, State2>,
|
||||||
|
fn3: UpdateFilter<Base3, Mod3, State3>,
|
||||||
|
): UpdateFilter<Base1 & Base2 & Base3, Mod1 & Mod2 & Mod3, State1 | State2 | State3>
|
||||||
|
export function and<Base1, Mod1, State1, Base2, Mod2, State2, Base3, Mod3, State3, Base4, Mod4, State4>(
|
||||||
|
fn1: UpdateFilter<Base1, Mod1, State1>,
|
||||||
|
fn2: UpdateFilter<Base2, Mod2, State2>,
|
||||||
|
fn3: UpdateFilter<Base3, Mod3, State3>,
|
||||||
|
fn4: UpdateFilter<Base4, Mod4, State4>,
|
||||||
|
): UpdateFilter<Base1 & Base2 & Base3 & Base4, Mod1 & Mod2 & Mod3 & Mod4, State1 | State2 | State3 | State4>
|
||||||
|
export function and<
|
||||||
|
Base1,
|
||||||
|
Mod1,
|
||||||
|
State1,
|
||||||
|
Base2,
|
||||||
|
Mod2,
|
||||||
|
State2,
|
||||||
|
Base3,
|
||||||
|
Mod3,
|
||||||
|
State3,
|
||||||
|
Base4,
|
||||||
|
Mod4,
|
||||||
|
State4,
|
||||||
|
Base5,
|
||||||
|
Mod5,
|
||||||
|
State5,
|
||||||
|
>(
|
||||||
|
fn1: UpdateFilter<Base1, Mod1, State1>,
|
||||||
|
fn2: UpdateFilter<Base2, Mod2, State2>,
|
||||||
|
fn3: UpdateFilter<Base3, Mod3, State3>,
|
||||||
|
fn4: UpdateFilter<Base4, Mod4, State4>,
|
||||||
|
fn5: UpdateFilter<Base5, Mod5, State5>,
|
||||||
|
): UpdateFilter<
|
||||||
|
Base1 & Base2 & Base3 & Base4 & Base5,
|
||||||
|
Mod1 & Mod2 & Mod3 & Mod4 & Mod5,
|
||||||
|
State1 | State2 | State3 | State4 | State5
|
||||||
|
>
|
||||||
|
export function and<
|
||||||
|
Base1,
|
||||||
|
Mod1,
|
||||||
|
State1,
|
||||||
|
Base2,
|
||||||
|
Mod2,
|
||||||
|
State2,
|
||||||
|
Base3,
|
||||||
|
Mod3,
|
||||||
|
State3,
|
||||||
|
Base4,
|
||||||
|
Mod4,
|
||||||
|
State4,
|
||||||
|
Base5,
|
||||||
|
Mod5,
|
||||||
|
State5,
|
||||||
|
Base6,
|
||||||
|
Mod6,
|
||||||
|
State6,
|
||||||
|
>(
|
||||||
|
fn1: UpdateFilter<Base1, Mod1, State1>,
|
||||||
|
fn2: UpdateFilter<Base2, Mod2, State2>,
|
||||||
|
fn3: UpdateFilter<Base3, Mod3, State3>,
|
||||||
|
fn4: UpdateFilter<Base4, Mod4, State4>,
|
||||||
|
fn5: UpdateFilter<Base5, Mod5, State5>,
|
||||||
|
fn6: UpdateFilter<Base6, Mod6, State6>,
|
||||||
|
): UpdateFilter<
|
||||||
|
Base1 & Base2 & Base3 & Base4 & Base5 & Base6,
|
||||||
|
Mod1 & Mod2 & Mod3 & Mod4 & Mod5 & Mod6,
|
||||||
|
State1 | State2 | State3 | State4 | State5 | State6
|
||||||
|
>
|
||||||
|
export function and<Filters extends UpdateFilter<any, any>[]>(
|
||||||
|
...fns: Filters
|
||||||
|
): UpdateFilter<ExtractBaseMany<Filters>, UnionToIntersection<ExtractMod<Filters[number]>>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combine two filters by applying an AND logical operation:
|
* Combine multiple filters by applying an AND logical
|
||||||
* `and(fn1, fn2) = fn1 AND fn2`
|
* operation between every one of them:
|
||||||
|
* `and(fn1, fn2, ..., fnN) = fn1 AND fn2 AND ... AND fnN`
|
||||||
*
|
*
|
||||||
* > **Note**: This also combines type modifications, i.e.
|
* > **Note**: This also combines type modifications, i.e.
|
||||||
* > if the 1st has modification `{ field1: string }`
|
* > if the 1st has modification `{ field1: string }`
|
||||||
|
@ -45,92 +125,14 @@ export function not<Base, Mod, State>(
|
||||||
* > then the combined filter will have
|
* > then the combined filter will have
|
||||||
* > combined modification `{ field1: string, field2: number }`
|
* > combined modification `{ field1: string, field2: number }`
|
||||||
*
|
*
|
||||||
* @param fn1 First filter
|
* > **Note**: Due to TypeScript limitations (or more likely my lack of brain cells),
|
||||||
* @param fn2 Second filter
|
* > state type is only correctly inferred for up to 6 filters.
|
||||||
*/
|
* > If you need more, either provide type explicitly (e.g. `filters.state<SomeState>(...)`),
|
||||||
export function and<Base, Mod1, Mod2, State1, State2>(
|
* > or combine multiple `and` calls.
|
||||||
fn1: UpdateFilter<Base, Mod1, State1>,
|
|
||||||
fn2: UpdateFilter<Base, Mod2, State2>,
|
|
||||||
): UpdateFilter<Base, Mod1 & Mod2, State1 | State2> {
|
|
||||||
return (upd, state) => {
|
|
||||||
const res1 = fn1(upd, state as UpdateState<State1>)
|
|
||||||
|
|
||||||
if (typeof res1 === 'boolean') {
|
|
||||||
if (!res1) return false
|
|
||||||
|
|
||||||
return fn2(upd, state as UpdateState<State2>)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res1.then((r1) => {
|
|
||||||
if (!r1) return false
|
|
||||||
|
|
||||||
return fn2(upd, state as UpdateState<State2>)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combine two filters by applying an OR logical operation:
|
|
||||||
* `or(fn1, fn2) = fn1 OR fn2`
|
|
||||||
*
|
|
||||||
* > **Note**: This also combines type modifications in a union, i.e.
|
|
||||||
* > if the 1st has modification `{ field1: string }`
|
|
||||||
* > and the 2nd has modification `{ field2: number }`,
|
|
||||||
* > then the combined filter will have
|
|
||||||
* > modification `{ field1: string } | { field2: number }`.
|
|
||||||
* >
|
|
||||||
* > It is up to the compiler to handle `if`s inside
|
|
||||||
* > the handler function code, but this works with other
|
|
||||||
* > logical functions as expected.
|
|
||||||
*
|
|
||||||
* @param fn1 First filter
|
|
||||||
* @param fn2 Second filter
|
|
||||||
*/
|
|
||||||
export function or<Base, Mod1, Mod2, State1, State2>(
|
|
||||||
fn1: UpdateFilter<Base, Mod1, State1>,
|
|
||||||
fn2: UpdateFilter<Base, Mod2, State2>,
|
|
||||||
): UpdateFilter<Base, Mod1 | Mod2, State1 | State2> {
|
|
||||||
return (upd, state) => {
|
|
||||||
const res1 = fn1(upd, state as UpdateState<State1>)
|
|
||||||
|
|
||||||
if (typeof res1 === 'boolean') {
|
|
||||||
if (res1) return true
|
|
||||||
|
|
||||||
return fn2(upd, state as UpdateState<State2>)
|
|
||||||
}
|
|
||||||
|
|
||||||
return res1.then((r1) => {
|
|
||||||
if (r1) return true
|
|
||||||
|
|
||||||
return fn2(upd, state as UpdateState<State2>)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// im pretty sure it can be done simpler (return types of some and every),
|
|
||||||
// so if you know how - PRs are welcome!
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combine multiple filters by applying an AND logical
|
|
||||||
* operation between every one of them:
|
|
||||||
* `every(fn1, fn2, ..., fnN) = fn1 AND fn2 AND ... AND fnN`
|
|
||||||
*
|
|
||||||
* > **Note**: This also combines type modification in a way
|
|
||||||
* > similar to {@link and}.
|
|
||||||
* >
|
|
||||||
* > This method is less efficient than {@link and}
|
|
||||||
*
|
|
||||||
* > **Note**: This method *currently* does not propagate state
|
|
||||||
* > type. This might be fixed in the future, but for now either
|
|
||||||
* > use {@link and} or add type manually.
|
|
||||||
*
|
*
|
||||||
* @param fns Filters to combine
|
* @param fns Filters to combine
|
||||||
*/
|
*/
|
||||||
export function every<Filters extends UpdateFilter<any, any>[]>(
|
export function and(...fns: UpdateFilter<any, any, any>[]): UpdateFilter<any, any, any> {
|
||||||
...fns: Filters
|
|
||||||
): UpdateFilter<ExtractBaseMany<Filters>, UnionToIntersection<ExtractMod<Filters[number]>>> {
|
|
||||||
if (fns.length === 2) return and(fns[0], fns[1])
|
|
||||||
|
|
||||||
return (upd, state) => {
|
return (upd, state) => {
|
||||||
let i = 0
|
let i = 0
|
||||||
const max = fns.length
|
const max = fns.length
|
||||||
|
@ -157,25 +159,103 @@ export function every<Filters extends UpdateFilter<any, any>[]>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function or<Base1, Mod1, State1, Base2, Mod2, State2>(
|
||||||
|
fn1: UpdateFilter<Base1, Mod1, State1>,
|
||||||
|
fn2: UpdateFilter<Base2, Mod2, State2>,
|
||||||
|
): UpdateFilter<Base1 & Base2, Mod1 | Mod2, State1 | State2>
|
||||||
|
|
||||||
|
export function or<Base1, Mod1, State1, Base2, Mod2, State2, Base3, Mod3, State3>(
|
||||||
|
fn1: UpdateFilter<Base1, Mod1, State1>,
|
||||||
|
fn2: UpdateFilter<Base2, Mod2, State2>,
|
||||||
|
fn3: UpdateFilter<Base3, Mod3, State3>,
|
||||||
|
): UpdateFilter<Base1 & Base2 & Base3, Mod1 | Mod2 | Mod3, State1 | State2 | State3>
|
||||||
|
|
||||||
|
export function or<Base1, Mod1, State1, Base2, Mod2, State2, Base3, Mod3, State3, Base4, Mod4, State4>(
|
||||||
|
fn1: UpdateFilter<Base1, Mod1, State1>,
|
||||||
|
fn2: UpdateFilter<Base2, Mod2, State2>,
|
||||||
|
fn3: UpdateFilter<Base3, Mod3, State3>,
|
||||||
|
fn4: UpdateFilter<Base4, Mod4, State4>,
|
||||||
|
): UpdateFilter<Base1 & Base2 & Base3 & Base4, Mod1 | Mod2 | Mod3 | Mod4, State1 | State2 | State3 | State4>
|
||||||
|
|
||||||
|
export function or<
|
||||||
|
Base1,
|
||||||
|
Mod1,
|
||||||
|
State1,
|
||||||
|
Base2,
|
||||||
|
Mod2,
|
||||||
|
State2,
|
||||||
|
Base3,
|
||||||
|
Mod3,
|
||||||
|
State3,
|
||||||
|
Base4,
|
||||||
|
Mod4,
|
||||||
|
State4,
|
||||||
|
Base5,
|
||||||
|
Mod5,
|
||||||
|
State5,
|
||||||
|
>(
|
||||||
|
fn1: UpdateFilter<Base1, Mod1, State1>,
|
||||||
|
fn2: UpdateFilter<Base2, Mod2, State2>,
|
||||||
|
fn3: UpdateFilter<Base3, Mod3, State3>,
|
||||||
|
fn4: UpdateFilter<Base4, Mod4, State4>,
|
||||||
|
fn5: UpdateFilter<Base5, Mod5, State5>,
|
||||||
|
): UpdateFilter<
|
||||||
|
Base1 & Base2 & Base3 & Base4 & Base5,
|
||||||
|
Mod1 | Mod2 | Mod3 | Mod4 | Mod5,
|
||||||
|
State1 | State2 | State3 | State4 | State5
|
||||||
|
>
|
||||||
|
|
||||||
|
export function or<
|
||||||
|
Base1,
|
||||||
|
Mod1,
|
||||||
|
State1,
|
||||||
|
Base2,
|
||||||
|
Mod2,
|
||||||
|
State2,
|
||||||
|
Base3,
|
||||||
|
Mod3,
|
||||||
|
State3,
|
||||||
|
Base4,
|
||||||
|
Mod4,
|
||||||
|
State4,
|
||||||
|
Base5,
|
||||||
|
Mod5,
|
||||||
|
State5,
|
||||||
|
Base6,
|
||||||
|
Mod6,
|
||||||
|
State6,
|
||||||
|
>(
|
||||||
|
fn1: UpdateFilter<Base1, Mod1, State1>,
|
||||||
|
fn2: UpdateFilter<Base2, Mod2, State2>,
|
||||||
|
fn3: UpdateFilter<Base3, Mod3, State3>,
|
||||||
|
fn4: UpdateFilter<Base4, Mod4, State4>,
|
||||||
|
fn5: UpdateFilter<Base5, Mod5, State5>,
|
||||||
|
fn6: UpdateFilter<Base6, Mod6, State6>,
|
||||||
|
): UpdateFilter<
|
||||||
|
Base1 & Base2 & Base3 & Base4 & Base5 & Base6,
|
||||||
|
Mod1 | Mod2 | Mod3 | Mod4 | Mod5 | Mod6,
|
||||||
|
State1 | State2 | State3 | State4 | State5 | State6
|
||||||
|
>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Combine multiple filters by applying an OR logical
|
* Combine multiple filters by applying an OR logical
|
||||||
* operation between every one of them:
|
* operation between every one of them:
|
||||||
* `every(fn1, fn2, ..., fnN) = fn1 OR fn2 OR ... OR fnN`
|
* `or(fn1, fn2, ..., fnN) = fn1 OR fn2 OR ... OR fnN`
|
||||||
*
|
*
|
||||||
* > **Note**: This also combines type modification in a way
|
* > **Note**: This also combines type modifications in a union, i.e.
|
||||||
* > similar to {@link or}.
|
* > if the 1st has modification `{ field1: string }`
|
||||||
* >
|
* > and the 2nd has modification `{ field2: number }`,
|
||||||
* > This method is less efficient than {@link or}
|
* > then the combined filter will have
|
||||||
|
* > modification `{ field1: string } | { field2: number }`.
|
||||||
*
|
*
|
||||||
* > **Note**: This method *currently* does not propagate state
|
* > **Note**: Due to TypeScript limitations (or more likely my lack of brain cells),
|
||||||
* > type. This might be fixed in the future, but for now either
|
* > state type is only correctly inferred for up to 6 filters.
|
||||||
* > use {@link or} or add type manually.
|
* > If you need more, either provide type explicitly (e.g. `filters.state<SomeState>(...)`),
|
||||||
|
* > or combine multiple `and` calls.
|
||||||
*
|
*
|
||||||
* @param fns Filters to combine
|
* @param fns Filters to combine
|
||||||
*/
|
*/
|
||||||
export function some<Filters extends UpdateFilter<any, any, any>[]>(
|
export function or(...fns: UpdateFilter<any, any, any>[]): UpdateFilter<any, any, any> {
|
||||||
...fns: Filters
|
|
||||||
): UpdateFilter<ExtractBaseMany<Filters>, ExtractMod<Filters[number]>, ExtractState<Filters[number]>> {
|
|
||||||
if (fns.length === 2) return or(fns[0], fns[1])
|
if (fns.length === 2) return or(fns[0], fns[1])
|
||||||
|
|
||||||
return (upd, state) => {
|
return (upd, state) => {
|
||||||
|
@ -203,3 +283,79 @@ export function some<Filters extends UpdateFilter<any, any, any>[]>(
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For updates that contain an array of updates (e.g. `message_group`),
|
||||||
|
* apply a filter to every element of the array.
|
||||||
|
*
|
||||||
|
* Filter will match if **all** elements match.
|
||||||
|
*
|
||||||
|
* > **Note**: This also applies type modification to every element of the array.
|
||||||
|
*
|
||||||
|
* @param filter
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function every<Base, Mod, State>(filter: UpdateFilter<Base, Mod, State>): UpdateFilter<Base[], Mod, State> {
|
||||||
|
return (upds, state) => {
|
||||||
|
let i = 0
|
||||||
|
const max = upds.length
|
||||||
|
|
||||||
|
const next = (): MaybeAsync<boolean> => {
|
||||||
|
if (i === max) return true
|
||||||
|
|
||||||
|
const res = filter(upds[i++], state)
|
||||||
|
|
||||||
|
if (typeof res === 'boolean') {
|
||||||
|
if (!res) return false
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.then((r: boolean) => {
|
||||||
|
if (!r) return false
|
||||||
|
|
||||||
|
return next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For updates that contain an array of updates (e.g. `message_group`),
|
||||||
|
* apply a filter to every element of the array.
|
||||||
|
*
|
||||||
|
* Filter will match if **all** elements match.
|
||||||
|
*
|
||||||
|
* > **Note**: This *does not* apply type modification to any element of the array
|
||||||
|
*
|
||||||
|
* @param filter
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function some<Base, Mod, State>(filter: UpdateFilter<Base, Mod, State>): UpdateFilter<Base[], Mod, State> {
|
||||||
|
return (upds, state) => {
|
||||||
|
let i = 0
|
||||||
|
const max = upds.length
|
||||||
|
|
||||||
|
const next = (): MaybeAsync<boolean> => {
|
||||||
|
if (i === max) return false
|
||||||
|
|
||||||
|
const res = filter(upds[i++], state)
|
||||||
|
|
||||||
|
if (typeof res === 'boolean') {
|
||||||
|
if (res) return true
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.then((r: boolean) => {
|
||||||
|
if (r) return true
|
||||||
|
|
||||||
|
return next()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const stateEmpty: UpdateFilter<Message> = async (upd, state) => {
|
||||||
export const state = <T>(
|
export const state = <T>(
|
||||||
predicate: (state: T) => MaybeAsync<boolean>,
|
predicate: (state: T) => MaybeAsync<boolean>,
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
): UpdateFilter<Message | CallbackQuery, {}, T> => {
|
): UpdateFilter<Message | Message[] | CallbackQuery, {}, T> => {
|
||||||
return async (upd, state) => {
|
return async (upd, state) => {
|
||||||
if (!state) return false
|
if (!state) return false
|
||||||
const data = await state.get()
|
const data = await state.get()
|
||||||
|
|
|
@ -79,7 +79,7 @@ export type UpdateFilter<Base, Mod = {}, State = never> = (
|
||||||
state?: UpdateState<State>,
|
state?: UpdateState<State>,
|
||||||
) => MaybeAsync<boolean>
|
) => MaybeAsync<boolean>
|
||||||
|
|
||||||
export type Modify<Base, Mod> = Omit<Base, keyof Mod> & Mod
|
export type Modify<Base, Mod> = Base extends (infer T)[] ? Modify<T, Mod>[] : Omit<Base, keyof Mod> & Mod
|
||||||
export type Invert<Base, Mod> = {
|
export type Invert<Base, Mod> = {
|
||||||
[P in keyof Mod & keyof Base]: Exclude<Base[P], Mod[P]>
|
[P in keyof Mod & keyof Base]: Exclude<Base[P], Mod[P]>
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ export type RawUpdateHandler = BaseUpdateHandler<
|
||||||
// begin-codegen
|
// begin-codegen
|
||||||
export type NewMessageHandler<T = Message, S = never> = ParsedUpdateHandler<'new_message', T, S>
|
export type NewMessageHandler<T = Message, S = never> = ParsedUpdateHandler<'new_message', T, S>
|
||||||
export type EditMessageHandler<T = Message, S = never> = ParsedUpdateHandler<'edit_message', T, S>
|
export type EditMessageHandler<T = Message, S = never> = ParsedUpdateHandler<'edit_message', T, S>
|
||||||
|
export type MessageGroupHandler<T = Message[], S = never> = ParsedUpdateHandler<'message_group', T, S>
|
||||||
export type DeleteMessageHandler<T = DeleteMessageUpdate> = ParsedUpdateHandler<'delete_message', T>
|
export type DeleteMessageHandler<T = DeleteMessageUpdate> = ParsedUpdateHandler<'delete_message', T>
|
||||||
export type ChatMemberUpdateHandler<T = ChatMemberUpdate> = ParsedUpdateHandler<'chat_member', T>
|
export type ChatMemberUpdateHandler<T = ChatMemberUpdate> = ParsedUpdateHandler<'chat_member', T>
|
||||||
export type InlineQueryHandler<T = InlineQuery> = ParsedUpdateHandler<'inline_query', T>
|
export type InlineQueryHandler<T = InlineQuery> = ParsedUpdateHandler<'inline_query', T>
|
||||||
|
@ -71,6 +72,7 @@ export type UpdateHandler =
|
||||||
| RawUpdateHandler
|
| RawUpdateHandler
|
||||||
| NewMessageHandler
|
| NewMessageHandler
|
||||||
| EditMessageHandler
|
| EditMessageHandler
|
||||||
|
| MessageGroupHandler
|
||||||
| DeleteMessageHandler
|
| DeleteMessageHandler
|
||||||
| ChatMemberUpdateHandler
|
| ChatMemberUpdateHandler
|
||||||
| InlineQueryHandler
|
| InlineQueryHandler
|
||||||
|
|
Loading…
Reference in a new issue