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(
|
||||
`
|
||||
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 { TelegramClientOptions }\n')
|
||||
output.write('\nexport class TelegramClient extends BaseTelegramClient {\n')
|
||||
|
||||
state.fields.forEach(({ code }) => output.write(`protected ${code}\n`))
|
||||
|
|
|
@ -8,16 +8,12 @@ const snakeToCamel = (s) => {
|
|||
})
|
||||
}
|
||||
|
||||
const camelToPascal = (s) =>
|
||||
s[0].toUpperCase() + s.substr(1)
|
||||
const camelToPascal = (s) => s[0].toUpperCase() + s.substr(1)
|
||||
|
||||
const camelToSnake = (s) => {
|
||||
return s.replace(
|
||||
/(?<=[a-zA-Z0-9])([A-Z0-9]+(?=[A-Z]|$)|[A-Z0-9])/g,
|
||||
($1) => {
|
||||
return '_' + $1.toLowerCase()
|
||||
},
|
||||
)
|
||||
return s.replace(/(?<=[a-zA-Z0-9])([A-Z0-9]+(?=[A-Z]|$)|[A-Z0-9])/g, ($1) => {
|
||||
return '_' + $1.toLowerCase()
|
||||
})
|
||||
}
|
||||
|
||||
function parseUpdateTypes() {
|
||||
|
@ -30,15 +26,13 @@ function parseUpdateTypes() {
|
|||
const ret = []
|
||||
|
||||
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}`)
|
||||
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]),
|
||||
funcName: m[2] ? m[2][0].toLowerCase() + m[2].substr(1) : snakeToCamel(m[1]),
|
||||
state: Boolean(m[4]),
|
||||
})
|
||||
}
|
||||
|
@ -47,9 +41,7 @@ function parseUpdateTypes() {
|
|||
}
|
||||
|
||||
function replaceSections(filename, sections, dir = __dirname) {
|
||||
let lines = fs
|
||||
.readFileSync(path.join(dir, '../src', filename), 'utf-8')
|
||||
.split('\n')
|
||||
let lines = fs.readFileSync(path.join(dir, '../src', filename), 'utf-8').split('\n')
|
||||
|
||||
const findMarker = (marker) => {
|
||||
const idx = lines.findIndex((line) => line.trim() === `// ${marker}`)
|
||||
|
@ -84,9 +76,7 @@ async function formatFile(filename, dir = __dirname) {
|
|||
}
|
||||
|
||||
function toSentence(type, stype = 'inline') {
|
||||
const name = camelToSnake(type.handlerTypeName)
|
||||
.toLowerCase()
|
||||
.replace(/_/g, ' ')
|
||||
const name = camelToSnake(type.handlerTypeName).toLowerCase().replace(/_/g, ' ')
|
||||
|
||||
if (stype === 'inline') {
|
||||
return `${name[0].match(/[aeiouy]/i) ? 'an' : 'a'} ${name} handler`
|
||||
|
@ -99,7 +89,8 @@ function toSentence(type, stype = 'inline') {
|
|||
|
||||
function generateParsedUpdate() {
|
||||
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(''),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# format: type_name[: handler_type_name] = update_type[ + State]
|
||||
new_message = Message + State
|
||||
edit_message = Message + State
|
||||
message_group = Message[] + State
|
||||
delete_message = DeleteMessageUpdate
|
||||
chat_member: ChatMemberUpdate = ChatMemberUpdate
|
||||
inline_query = InlineQuery
|
||||
|
|
|
@ -329,6 +329,66 @@ import {
|
|||
} from './types'
|
||||
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
|
||||
interface PendingUpdateContainer {
|
||||
upd: tl.TypeUpdates
|
||||
|
@ -5482,45 +5542,9 @@ export interface TelegramClient extends BaseTelegramClient {
|
|||
bio?: string
|
||||
}): 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
|
||||
|
||||
/**
|
||||
* 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 { TelegramClientOptions }
|
||||
|
||||
export class TelegramClient extends BaseTelegramClient {
|
||||
protected _userId: number | null
|
||||
protected _isBot: boolean
|
||||
|
@ -5544,6 +5568,8 @@ export class TelegramClient extends BaseTelegramClient {
|
|||
protected _updLock: AsyncLock
|
||||
protected _rpsIncoming?: RpsMeter
|
||||
protected _rpsProcessing?: RpsMeter
|
||||
protected _messageGroupingInterval: number
|
||||
protected _messageGroupingPending: Map<string, [Message[], NodeJS.Timeout]>
|
||||
protected _pts?: number
|
||||
protected _qts?: number
|
||||
protected _date?: number
|
||||
|
@ -5583,6 +5609,9 @@ export class TelegramClient extends BaseTelegramClient {
|
|||
this._noDispatchPts = new Map()
|
||||
this._noDispatchQts = new Set()
|
||||
|
||||
this._messageGroupingInterval = opts.messageGroupingInterval ?? 0
|
||||
this._messageGroupingPending = new Map()
|
||||
|
||||
this._updLock = new AsyncLock()
|
||||
// we dont need to initialize state fields since
|
||||
// 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'
|
||||
|
||||
import { TelegramClient, TelegramClientOptions } from '../client'
|
||||
import { PeersIndex } from '../types'
|
||||
import { Message, PeersIndex } from '../types'
|
||||
import { _parseUpdate } from '../types/updates/parse-update'
|
||||
import { extractChannelIdFromUpdate } from '../utils/misc-utils'
|
||||
import { normalizeToInputChannel } from '../utils/peer-utils'
|
||||
|
@ -65,6 +65,9 @@ interface UpdatesState {
|
|||
_rpsIncoming?: RpsMeter
|
||||
_rpsProcessing?: RpsMeter
|
||||
|
||||
_messageGroupingInterval: number
|
||||
_messageGroupingPending: Map<string, [Message[], NodeJS.Timeout]>
|
||||
|
||||
// accessing storage every time might be expensive,
|
||||
// so store everything here, and load & save
|
||||
// every time session is loaded & saved.
|
||||
|
@ -109,6 +112,9 @@ function _initializeUpdates(this: TelegramClient, opts: TelegramClientOptions) {
|
|||
this._noDispatchPts = new Map()
|
||||
this._noDispatchQts = new Set()
|
||||
|
||||
this._messageGroupingInterval = opts.messageGroupingInterval ?? 0
|
||||
this._messageGroupingPending = new Map()
|
||||
|
||||
this._updLock = new AsyncLock()
|
||||
// we dont need to initialize state fields since
|
||||
// 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)
|
||||
|
||||
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(parsed.name, parsed.data)
|
||||
}
|
||||
|
|
|
@ -161,6 +161,15 @@ export class Message {
|
|||
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
|
||||
|
||||
/**
|
||||
|
|
|
@ -36,6 +36,7 @@ export {
|
|||
export type ParsedUpdate =
|
||||
| { name: 'new_message'; data: Message }
|
||||
| { name: 'edit_message'; data: Message }
|
||||
| { name: 'message_group'; data: Message[] }
|
||||
| { name: 'delete_message'; data: DeleteMessageUpdate }
|
||||
| { name: 'chat_member'; data: ChatMemberUpdate }
|
||||
| { name: 'inline_query'; data: InlineQuery }
|
||||
|
|
|
@ -41,6 +41,7 @@ import {
|
|||
EditMessageHandler,
|
||||
HistoryReadHandler,
|
||||
InlineQueryHandler,
|
||||
MessageGroupHandler,
|
||||
NewMessageHandler,
|
||||
PollUpdateHandler,
|
||||
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
|
||||
* FSM storage
|
||||
*/
|
||||
constructor(client: TelegramClient, ...args: State extends never ? [] : [IStateStorage, StateKeyDelegate?])
|
||||
constructor(
|
||||
client: TelegramClient,
|
||||
...args: (() => State) extends () => never ? [] : [IStateStorage, StateKeyDelegate?]
|
||||
)
|
||||
constructor(
|
||||
client?: TelegramClient | IStateStorage | StateKeyDelegate,
|
||||
storage?: IStateStorage | StateKeyDelegate,
|
||||
|
@ -290,11 +294,16 @@ export class Dispatcher<State = never, SceneName extends string = string> {
|
|||
if (
|
||||
this._storage &&
|
||||
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
|
||||
|
||||
const key = await this._stateKeyDelegate!(update.data)
|
||||
const key = await this._stateKeyDelegate!(
|
||||
update.name === 'message_group' ? update.data[0] : update.data,
|
||||
)
|
||||
|
||||
if (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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
|
||||
import { MaybeAsync } from '@mtcute/core'
|
||||
|
||||
import { UpdateState } from '../state'
|
||||
import { ExtractBaseMany, ExtractMod, ExtractState, Invert, UnionToIntersection, UpdateFilter } from './types'
|
||||
import { ExtractBaseMany, ExtractMod, Invert, UnionToIntersection, UpdateFilter } from './types'
|
||||
|
||||
/**
|
||||
* 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:
|
||||
* `and(fn1, fn2) = fn1 AND fn2`
|
||||
* Combine multiple filters by applying an AND logical
|
||||
* operation between every one of them:
|
||||
* `and(fn1, fn2, ..., fnN) = fn1 AND fn2 AND ... AND fnN`
|
||||
*
|
||||
* > **Note**: This also combines type modifications, i.e.
|
||||
* > if the 1st has modification `{ field1: string }`
|
||||
|
@ -45,92 +125,14 @@ export function not<Base, Mod, State>(
|
|||
* > then the combined filter will have
|
||||
* > combined modification `{ field1: string, field2: number }`
|
||||
*
|
||||
* @param fn1 First filter
|
||||
* @param fn2 Second filter
|
||||
*/
|
||||
export function and<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 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.
|
||||
* > **Note**: Due to TypeScript limitations (or more likely my lack of brain cells),
|
||||
* > state type is only correctly inferred for up to 6 filters.
|
||||
* > If you need more, either provide type explicitly (e.g. `filters.state<SomeState>(...)`),
|
||||
* > or combine multiple `and` calls.
|
||||
*
|
||||
* @param fns Filters to combine
|
||||
*/
|
||||
export function every<Filters extends UpdateFilter<any, any>[]>(
|
||||
...fns: Filters
|
||||
): UpdateFilter<ExtractBaseMany<Filters>, UnionToIntersection<ExtractMod<Filters[number]>>> {
|
||||
if (fns.length === 2) return and(fns[0], fns[1])
|
||||
|
||||
export function and(...fns: UpdateFilter<any, any, any>[]): UpdateFilter<any, any, any> {
|
||||
return (upd, state) => {
|
||||
let i = 0
|
||||
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
|
||||
* 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
|
||||
* > similar to {@link or}.
|
||||
* >
|
||||
* > This method is less efficient than {@link or}
|
||||
* > **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 }`.
|
||||
*
|
||||
* > **Note**: This method *currently* does not propagate state
|
||||
* > type. This might be fixed in the future, but for now either
|
||||
* > use {@link or} or add type manually.
|
||||
* > **Note**: Due to TypeScript limitations (or more likely my lack of brain cells),
|
||||
* > state type is only correctly inferred for up to 6 filters.
|
||||
* > If you need more, either provide type explicitly (e.g. `filters.state<SomeState>(...)`),
|
||||
* > or combine multiple `and` calls.
|
||||
*
|
||||
* @param fns Filters to combine
|
||||
*/
|
||||
export function some<Filters extends UpdateFilter<any, any, any>[]>(
|
||||
...fns: Filters
|
||||
): UpdateFilter<ExtractBaseMany<Filters>, ExtractMod<Filters[number]>, ExtractState<Filters[number]>> {
|
||||
export function or(...fns: UpdateFilter<any, any, any>[]): UpdateFilter<any, any, any> {
|
||||
if (fns.length === 2) return or(fns[0], fns[1])
|
||||
|
||||
return (upd, state) => {
|
||||
|
@ -203,3 +283,79 @@ export function some<Filters extends UpdateFilter<any, any, any>[]>(
|
|||
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>(
|
||||
predicate: (state: T) => MaybeAsync<boolean>,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
): UpdateFilter<Message | CallbackQuery, {}, T> => {
|
||||
): UpdateFilter<Message | Message[] | CallbackQuery, {}, T> => {
|
||||
return async (upd, state) => {
|
||||
if (!state) return false
|
||||
const data = await state.get()
|
||||
|
|
|
@ -79,7 +79,7 @@ export type UpdateFilter<Base, Mod = {}, State = never> = (
|
|||
state?: UpdateState<State>,
|
||||
) => 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> = {
|
||||
[P in keyof Mod & keyof Base]: Exclude<Base[P], Mod[P]>
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ export type RawUpdateHandler = BaseUpdateHandler<
|
|||
// begin-codegen
|
||||
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 MessageGroupHandler<T = Message[], S = never> = ParsedUpdateHandler<'message_group', T, S>
|
||||
export type DeleteMessageHandler<T = DeleteMessageUpdate> = ParsedUpdateHandler<'delete_message', T>
|
||||
export type ChatMemberUpdateHandler<T = ChatMemberUpdate> = ParsedUpdateHandler<'chat_member', T>
|
||||
export type InlineQueryHandler<T = InlineQuery> = ParsedUpdateHandler<'inline_query', T>
|
||||
|
@ -71,6 +72,7 @@ export type UpdateHandler =
|
|||
| RawUpdateHandler
|
||||
| NewMessageHandler
|
||||
| EditMessageHandler
|
||||
| MessageGroupHandler
|
||||
| DeleteMessageHandler
|
||||
| ChatMemberUpdateHandler
|
||||
| InlineQueryHandler
|
||||
|
|
Loading…
Reference in a new issue