refactor: extracted dispatcher filters into multiple files
This commit is contained in:
parent
a3ebd3fc66
commit
6e8351ac01
13 changed files with 1309 additions and 1342 deletions
|
@ -40,6 +40,7 @@ export type MessageMedia =
|
|||
| Poll
|
||||
| Invoice
|
||||
| null
|
||||
export type MessageMediaType = Exclude<MessageMedia, null>['type']
|
||||
|
||||
// todo: successful_payment, connected_website
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
150
packages/dispatcher/src/filters/bots.ts
Normal file
150
packages/dispatcher/src/filters/bots.ts
Normal file
|
@ -0,0 +1,150 @@
|
|||
import { Message } from '@mtcute/client'
|
||||
import { MaybeArray, MaybeAsync } from '@mtcute/core'
|
||||
|
||||
import { chat } from './chat'
|
||||
import { and } from './logic'
|
||||
import { UpdateFilter } from './types'
|
||||
|
||||
/**
|
||||
* Filter messages that call the given command(s)..
|
||||
*
|
||||
* When a command matches, the match array is stored in a
|
||||
* type-safe extension field `.commmand` of the {@link Message} object.
|
||||
* First element is the command itself, then the arguments.
|
||||
*
|
||||
* If the matched command was a RegExp, the first element is the
|
||||
* command, then the groups from the command regex, then the arguments.
|
||||
*
|
||||
* @param commands Command(s) the filter should look for (w/out prefix)
|
||||
* @param prefixes
|
||||
* Prefix(es) the filter should look for (default: `/`).
|
||||
* Can be `null` to disable prefixes altogether
|
||||
* @param caseSensitive
|
||||
*/
|
||||
export const command = (
|
||||
commands: MaybeArray<string | RegExp>,
|
||||
prefixes: MaybeArray<string> | null = '/',
|
||||
caseSensitive = false,
|
||||
): UpdateFilter<Message, { command: string[] }> => {
|
||||
if (!Array.isArray(commands)) commands = [commands]
|
||||
|
||||
commands = commands.map((i) =>
|
||||
typeof i === 'string' ? i.toLowerCase() : i,
|
||||
)
|
||||
|
||||
const argumentsRe = /(["'])(.*?)(?<!\\)\1|(\S+)/g
|
||||
const unescapeRe = /\\(['"])/
|
||||
const commandsRe: RegExp[] = []
|
||||
commands.forEach((cmd) => {
|
||||
if (typeof cmd !== 'string') cmd = cmd.source
|
||||
|
||||
commandsRe.push(
|
||||
new RegExp(
|
||||
`^(${cmd})(?:\\s|$|@([a-zA-Z0-9_]+?bot)(?:\\s|$))`,
|
||||
caseSensitive ? '' : 'i',
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
if (prefixes === null) prefixes = []
|
||||
if (typeof prefixes === 'string') prefixes = [prefixes]
|
||||
|
||||
const _prefixes = prefixes
|
||||
|
||||
const check = (msg: Message): MaybeAsync<boolean> => {
|
||||
for (const pref of _prefixes) {
|
||||
if (!msg.text.startsWith(pref)) continue
|
||||
|
||||
const withoutPrefix = msg.text.slice(pref.length)
|
||||
|
||||
for (const regex of commandsRe) {
|
||||
const m = withoutPrefix.match(regex)
|
||||
if (!m) continue
|
||||
|
||||
const lastGroup = m[m.length - 1]
|
||||
|
||||
// eslint-disable-next-line dot-notation
|
||||
if (lastGroup && msg.client['_isBot']) {
|
||||
// check bot username
|
||||
// eslint-disable-next-line dot-notation
|
||||
if (lastGroup !== msg.client['_selfUsername']) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const match = m.slice(1, -1)
|
||||
|
||||
// we use .replace to iterate over global regex, not to replace the text
|
||||
withoutPrefix
|
||||
.slice(m[0].length)
|
||||
.replace(argumentsRe, ($0, $1, $2: string, $3: string) => {
|
||||
match.push(($2 || $3 || '').replace(unescapeRe, '$1'))
|
||||
|
||||
return ''
|
||||
})
|
||||
;(msg as Message & { command: string[] }).command = match
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return check
|
||||
}
|
||||
|
||||
/**
|
||||
* Shorthand filter that matches /start commands sent to bot's
|
||||
* private messages.
|
||||
*/
|
||||
export const start = and(chat('private'), command('start'))
|
||||
|
||||
/**
|
||||
* Filter for deep links (i.e. `/start <deeplink_parameter>`).
|
||||
*
|
||||
* If the parameter is a regex, groups are added to `msg.command`,
|
||||
* meaning that the first group is available in `msg.command[2]`.
|
||||
*/
|
||||
export const deeplink = (
|
||||
params: MaybeArray<string | RegExp>,
|
||||
): UpdateFilter<Message, { command: string[] }> => {
|
||||
if (!Array.isArray(params)) {
|
||||
return and(start, (_msg: Message) => {
|
||||
const msg = _msg as Message & { command: string[] }
|
||||
|
||||
if (msg.command.length !== 2) return false
|
||||
|
||||
const p = msg.command[1]
|
||||
if (typeof params === 'string' && p === params) return true
|
||||
|
||||
const m = p.match(params)
|
||||
if (!m) return false
|
||||
|
||||
msg.command.push(...m.slice(1))
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
return and(start, (_msg: Message) => {
|
||||
const msg = _msg as Message & { command: string[] }
|
||||
|
||||
if (msg.command.length !== 2) return false
|
||||
|
||||
const p = msg.command[1]
|
||||
|
||||
for (const param of params) {
|
||||
if (typeof param === 'string' && p === param) return true
|
||||
|
||||
const m = p.match(param)
|
||||
if (!m) continue
|
||||
|
||||
msg.command.push(...m.slice(1))
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
9
packages/dispatcher/src/filters/bundle.ts
Normal file
9
packages/dispatcher/src/filters/bundle.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
export * from './bots'
|
||||
export * from './chat'
|
||||
export * from './logic'
|
||||
export * from './message'
|
||||
export * from './state'
|
||||
export * from './text'
|
||||
export * from './types'
|
||||
export * from './updates'
|
||||
export * from './user'
|
86
packages/dispatcher/src/filters/chat.ts
Normal file
86
packages/dispatcher/src/filters/chat.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { Chat, ChatType, Message, PollVoteUpdate, User } from '@mtcute/client'
|
||||
import { MaybeArray } from '@mtcute/core'
|
||||
|
||||
import { Modify, UpdateFilter } from './types'
|
||||
|
||||
/**
|
||||
* Filter messages by chat type
|
||||
*/
|
||||
export const chat =
|
||||
<T extends ChatType>(
|
||||
type: T,
|
||||
): UpdateFilter<
|
||||
Message,
|
||||
{
|
||||
chat: Modify<Chat, { type: T }>
|
||||
sender: T extends 'private' | 'bot' | 'group' ? User : User | Chat
|
||||
}
|
||||
> =>
|
||||
(msg) =>
|
||||
msg.chat.chatType === type
|
||||
|
||||
/**
|
||||
* Filter updates by chat ID(s) or username(s)
|
||||
*/
|
||||
export const chatId = (
|
||||
id: MaybeArray<number | string>,
|
||||
): UpdateFilter<Message | PollVoteUpdate> => {
|
||||
if (Array.isArray(id)) {
|
||||
const index: Record<number | string, true> = {}
|
||||
let matchSelf = false
|
||||
id.forEach((id) => {
|
||||
if (id === 'me' || id === 'self') {
|
||||
matchSelf = true
|
||||
} else {
|
||||
index[id] = true
|
||||
}
|
||||
})
|
||||
|
||||
return (upd) => {
|
||||
if (upd.constructor === PollVoteUpdate) {
|
||||
const peer = upd.peer
|
||||
|
||||
return peer.type === 'chat' && peer.id in index
|
||||
}
|
||||
|
||||
const chat = (upd as Exclude<typeof upd, PollVoteUpdate>).chat
|
||||
|
||||
return (
|
||||
(matchSelf && chat.isSelf) ||
|
||||
chat.id in index ||
|
||||
chat.username! in index
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (id === 'me' || id === 'self') {
|
||||
return (upd) => {
|
||||
if (upd.constructor === PollVoteUpdate) {
|
||||
return upd.peer.type === 'chat' && upd.peer.isSelf
|
||||
}
|
||||
|
||||
return (upd as Exclude<typeof upd, PollVoteUpdate>).chat.isSelf
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof id === 'string') {
|
||||
return (upd) => {
|
||||
if (upd.constructor === PollVoteUpdate) {
|
||||
return upd.peer.type === 'chat' && upd.peer.username === id
|
||||
}
|
||||
|
||||
return (
|
||||
(upd as Exclude<typeof upd, PollVoteUpdate>).chat.username ===
|
||||
id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (upd) => {
|
||||
if (upd.constructor === PollVoteUpdate) {
|
||||
return upd.peer.type === 'chat' && upd.peer.id === id
|
||||
}
|
||||
|
||||
return (upd as Exclude<typeof upd, PollVoteUpdate>).chat.id === id
|
||||
}
|
||||
}
|
3
packages/dispatcher/src/filters/index.ts
Normal file
3
packages/dispatcher/src/filters/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import * as filters from './bundle'
|
||||
import UpdateFilter = filters.UpdateFilter
|
||||
export { filters, UpdateFilter }
|
219
packages/dispatcher/src/filters/logic.ts
Normal file
219
packages/dispatcher/src/filters/logic.ts
Normal file
|
@ -0,0 +1,219 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// ^^ will be looked into in MTQ-29
|
||||
|
||||
import { MaybeAsync } from '@mtcute/core'
|
||||
|
||||
import { UpdateState } from '../state'
|
||||
import {
|
||||
ExtractBaseMany,
|
||||
ExtractMod,
|
||||
ExtractState,
|
||||
Invert,
|
||||
UnionToIntersection,
|
||||
UpdateFilter,
|
||||
} from './types'
|
||||
|
||||
/**
|
||||
* Filter that matches any update
|
||||
*/
|
||||
export const any: UpdateFilter<any> = () => true
|
||||
|
||||
/**
|
||||
* Invert a filter by applying a NOT logical operation:
|
||||
* `not(fn) = NOT fn`
|
||||
*
|
||||
* > **Note**: This also inverts type modification, i.e.
|
||||
* > if the base is `{ field: string | number | null }`
|
||||
* > and the modification is `{ field: string }`,
|
||||
* > then the negated filter will have
|
||||
* > inverted modification `{ field: number | null }`
|
||||
*
|
||||
* @param fn Filter to negate
|
||||
*/
|
||||
export function not<Base, Mod, State>(
|
||||
fn: UpdateFilter<Base, Mod, State>,
|
||||
): UpdateFilter<Base, Invert<Base, Mod>, State> {
|
||||
return (upd, state) => {
|
||||
const res = fn(upd, state)
|
||||
|
||||
if (typeof res === 'boolean') return !res
|
||||
|
||||
return res.then((r) => !r)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine two filters by applying an AND logical operation:
|
||||
* `and(fn1, fn2) = fn1 AND fn2`
|
||||
*
|
||||
* > **Note**: This also combines type modifications, i.e.
|
||||
* > if the 1st has modification `{ field1: string }`
|
||||
* > and the 2nd has modification `{ field2: number }`,
|
||||
* > 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.
|
||||
*
|
||||
* @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])
|
||||
|
||||
return (upd, state) => {
|
||||
let i = 0
|
||||
const max = fns.length
|
||||
|
||||
const next = (): MaybeAsync<boolean> => {
|
||||
if (i === max) return true
|
||||
|
||||
const res = fns[i++](upd, state)
|
||||
|
||||
if (typeof res === 'boolean') {
|
||||
if (!res) return false
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
return res.then((r: boolean) => {
|
||||
if (!r) return false
|
||||
|
||||
return next()
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Combine multiple filters by applying an OR logical
|
||||
* operation between every one of them:
|
||||
* `every(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 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.
|
||||
*
|
||||
* @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]>
|
||||
> {
|
||||
if (fns.length === 2) return or(fns[0], fns[1])
|
||||
|
||||
return (upd, state) => {
|
||||
let i = 0
|
||||
const max = fns.length
|
||||
|
||||
const next = (): MaybeAsync<boolean> => {
|
||||
if (i === max) return false
|
||||
|
||||
const res = fns[i++](upd, state)
|
||||
|
||||
if (typeof res === 'boolean') {
|
||||
if (res) return true
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
return res.then((r: boolean) => {
|
||||
if (r) return true
|
||||
|
||||
return next()
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
227
packages/dispatcher/src/filters/message.ts
Normal file
227
packages/dispatcher/src/filters/message.ts
Normal file
|
@ -0,0 +1,227 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// ^^ will be looked into in MTQ-29
|
||||
import {
|
||||
Chat,
|
||||
Message,
|
||||
MessageAction,
|
||||
MessageMediaType,
|
||||
RawDocument,
|
||||
RawLocation,
|
||||
Sticker,
|
||||
StickerSourceType,
|
||||
StickerType,
|
||||
User,
|
||||
Video,
|
||||
} from '@mtcute/client'
|
||||
import { MaybeArray } from '@mtcute/core'
|
||||
|
||||
import { Modify, UpdateFilter } from './types'
|
||||
|
||||
/**
|
||||
* Filter incoming messages.
|
||||
*
|
||||
* Messages sent to yourself (i.e. Saved Messages) are also "incoming"
|
||||
*/
|
||||
export const incoming: UpdateFilter<Message, { isOutgoing: false }> = (msg) =>
|
||||
!msg.isOutgoing
|
||||
|
||||
/**
|
||||
* Filter outgoing messages.
|
||||
*
|
||||
* Messages sent to yourself (i.e. Saved Messages) are **not** "outgoing"
|
||||
*/
|
||||
export const outgoing: UpdateFilter<Message, { isOutgoing: true }> = (msg) =>
|
||||
msg.isOutgoing
|
||||
|
||||
/**
|
||||
* Filter messages that are replies to some other message
|
||||
*/
|
||||
export const reply: UpdateFilter<Message, { replyToMessageId: number }> = (
|
||||
msg,
|
||||
) => msg.replyToMessageId !== null
|
||||
|
||||
/**
|
||||
* Filter messages containing some media
|
||||
*/
|
||||
export const media: UpdateFilter<
|
||||
Message,
|
||||
{ media: Exclude<Message['media'], null> }
|
||||
> = (msg) => msg.media !== null
|
||||
|
||||
/**
|
||||
* Filter messages containing media of given type
|
||||
*/
|
||||
export const mediaOf =
|
||||
<T extends MessageMediaType>(
|
||||
type: T,
|
||||
): UpdateFilter<
|
||||
Message,
|
||||
{ media: Extract<Message['media'], { type: T }> }
|
||||
> =>
|
||||
(msg) =>
|
||||
msg.media?.type === type
|
||||
|
||||
/** Filter messages containing a photo */
|
||||
export const photo = mediaOf('photo')
|
||||
/** Filter messages containing a dice */
|
||||
export const dice = mediaOf('dice')
|
||||
/** Filter messages containing a contact */
|
||||
export const contact = mediaOf('contact')
|
||||
/** Filter messages containing an audio file */
|
||||
export const audio = mediaOf('audio')
|
||||
/** Filter messages containing a voice message (audio-only) */
|
||||
export const voice = mediaOf('voice')
|
||||
/** Filter messages containing a sticker */
|
||||
export const sticker = mediaOf('sticker')
|
||||
/** Filter messages containing a document (a file) */
|
||||
export const document = mediaOf('document')
|
||||
/** Filter messages containing any video (videos, round messages and animations) */
|
||||
export const anyVideo = mediaOf('video')
|
||||
/** Filter messages containing a static location */
|
||||
export const location = mediaOf('location')
|
||||
/** Filter messages containing a live location */
|
||||
export const liveLocation = mediaOf('live_location')
|
||||
/** Filter messages containing a game */
|
||||
export const game = mediaOf('game')
|
||||
/** Filter messages containing a web page */
|
||||
export const webpage = mediaOf('web_page')
|
||||
/** Filter messages containing a venue */
|
||||
export const venue = mediaOf('venue')
|
||||
/** Filter messages containing a poll */
|
||||
export const poll = mediaOf('poll')
|
||||
/** Filter messages containing an invoice */
|
||||
export const invoice = mediaOf('invoice')
|
||||
|
||||
/**
|
||||
* Filter messages containing any location (live or static).
|
||||
*/
|
||||
export const anyLocation: UpdateFilter<Message, { media: Location }> = (msg) =>
|
||||
msg.media instanceof RawLocation
|
||||
|
||||
/**
|
||||
* Filter messages containing a document
|
||||
*
|
||||
* This will also match media like audio, video, voice
|
||||
* that also use Documents
|
||||
*/
|
||||
export const anyDocument: UpdateFilter<Message, { media: RawDocument }> = (
|
||||
msg,
|
||||
) => msg.media instanceof RawDocument
|
||||
|
||||
/**
|
||||
* Filter messages containing a simple video.
|
||||
*
|
||||
* This does not include round messages and animations
|
||||
*/
|
||||
export const video: UpdateFilter<
|
||||
Message,
|
||||
{
|
||||
media: Modify<
|
||||
Video,
|
||||
{
|
||||
isRound: false
|
||||
isAnimation: false
|
||||
}
|
||||
>
|
||||
}
|
||||
> = (msg) =>
|
||||
msg.media?.type === 'video' && !msg.media.isAnimation && !msg.media.isRound
|
||||
|
||||
/**
|
||||
* Filter messages containing an animation.
|
||||
*
|
||||
* > **Note**: Legacy GIFs (i.e. documents with `image/gif` MIME)
|
||||
* > are also considered animations.
|
||||
*/
|
||||
export const animation: UpdateFilter<
|
||||
Message,
|
||||
{
|
||||
media: Modify<
|
||||
Video,
|
||||
{
|
||||
isRound: false
|
||||
isAnimation: true
|
||||
}
|
||||
>
|
||||
}
|
||||
> = (msg) =>
|
||||
msg.media?.type === 'video' && msg.media.isAnimation && !msg.media.isRound
|
||||
|
||||
/**
|
||||
* Filter messages containing a round message (aka video note).
|
||||
*/
|
||||
export const roundMessage: UpdateFilter<
|
||||
Message,
|
||||
{
|
||||
media: Modify<
|
||||
Video,
|
||||
{
|
||||
isRound: true
|
||||
isAnimation: false
|
||||
}
|
||||
>
|
||||
}
|
||||
> = (msg) =>
|
||||
msg.media?.type === 'video' && !msg.media.isAnimation && msg.media.isRound
|
||||
|
||||
/**
|
||||
* Filter messages containing a sticker by its type
|
||||
*/
|
||||
export const stickerByType =
|
||||
(type: StickerType): UpdateFilter<Message, { media: Sticker }> =>
|
||||
(msg) =>
|
||||
msg.media?.type === 'sticker' && msg.media.stickerType === type
|
||||
|
||||
/**
|
||||
* Filter messages containing a sticker by its source file type
|
||||
*/
|
||||
export const stickerBySourceType =
|
||||
(type: StickerSourceType): UpdateFilter<Message, { media: Sticker }> =>
|
||||
(msg) =>
|
||||
msg.media?.type === 'sticker' && msg.media.sourceType === type
|
||||
|
||||
/**
|
||||
* Filter text-only messages non-service messages
|
||||
*/
|
||||
export const text: UpdateFilter<
|
||||
Message,
|
||||
{
|
||||
media: null
|
||||
isService: false
|
||||
}
|
||||
> = (msg) => msg.media === null && !msg.isService
|
||||
|
||||
/**
|
||||
* Filter service messages
|
||||
*/
|
||||
export const service: UpdateFilter<Message, { isService: true }> = (msg) =>
|
||||
msg.isService
|
||||
|
||||
/**
|
||||
* Filter service messages by action type
|
||||
*/
|
||||
export const action = <T extends Exclude<MessageAction, null>['type']>(
|
||||
type: MaybeArray<T>,
|
||||
): UpdateFilter<
|
||||
Message,
|
||||
{
|
||||
action: Extract<MessageAction, { type: T }>
|
||||
sender: T extends
|
||||
| 'user_joined_link'
|
||||
| 'user_removed'
|
||||
| 'history_cleared'
|
||||
| 'contact_joined'
|
||||
| 'bot_allowed'
|
||||
? User
|
||||
: User | Chat
|
||||
}
|
||||
> => {
|
||||
if (Array.isArray(type)) {
|
||||
const index: Partial<Record<T, true>> = {}
|
||||
type.forEach((it) => (index[it] = true))
|
||||
|
||||
return (msg) => (msg.action?.type as any) in index
|
||||
}
|
||||
|
||||
return (msg) => msg.action?.type === type
|
||||
}
|
34
packages/dispatcher/src/filters/state.ts
Normal file
34
packages/dispatcher/src/filters/state.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { CallbackQuery, Message } from '@mtcute/client'
|
||||
import { MaybeAsync } from '@mtcute/core'
|
||||
|
||||
import { UpdateFilter } from './types'
|
||||
|
||||
/**
|
||||
* Create a filter for the cases when the state is empty
|
||||
*/
|
||||
export const stateEmpty: UpdateFilter<Message> = async (upd, state) => {
|
||||
if (!state) return false
|
||||
|
||||
return !(await state.get())
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filter based on state predicate
|
||||
*
|
||||
* If state exists and matches `predicate`, update passes
|
||||
* this filter, otherwise it doesn't
|
||||
*
|
||||
* @param predicate State predicate
|
||||
*/
|
||||
export const state = <T>(
|
||||
predicate: (state: T) => MaybeAsync<boolean>,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
): UpdateFilter<Message | CallbackQuery, {}, T> => {
|
||||
return async (upd, state) => {
|
||||
if (!state) return false
|
||||
const data = await state.get()
|
||||
if (!data) return false
|
||||
|
||||
return predicate(data)
|
||||
}
|
||||
}
|
182
packages/dispatcher/src/filters/text.ts
Normal file
182
packages/dispatcher/src/filters/text.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
// ^^ will be looked into in MTQ-29
|
||||
|
||||
import {
|
||||
CallbackQuery,
|
||||
ChosenInlineResult,
|
||||
InlineQuery,
|
||||
Message,
|
||||
} from '@mtcute/client'
|
||||
|
||||
import { UpdateFilter } from './types'
|
||||
|
||||
function extractText(
|
||||
obj: Message | InlineQuery | ChosenInlineResult | CallbackQuery,
|
||||
): string | null {
|
||||
if (obj.constructor === Message) {
|
||||
return obj.text
|
||||
} else if (obj.constructor === InlineQuery) {
|
||||
return obj.query
|
||||
} else if (obj.constructor === ChosenInlineResult) {
|
||||
return obj.id
|
||||
} else if (obj.constructor === CallbackQuery) {
|
||||
if (obj.raw.data) return obj.dataStr
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter objects that match a given regular expression
|
||||
* - for `Message`, `Message.text` is used
|
||||
* - for `InlineQuery`, `InlineQuery.query` is used
|
||||
* - for {@link ChosenInlineResult}, {@link ChosenInlineResult.id} is used
|
||||
* - for `CallbackQuery`, `CallbackQuery.dataStr` is used
|
||||
*
|
||||
* When a regex matches, the match array is stored in a
|
||||
* type-safe extension field `.match` of the object
|
||||
*
|
||||
* @param regex Regex to be matched
|
||||
*/
|
||||
export const regex =
|
||||
(
|
||||
regex: RegExp,
|
||||
): UpdateFilter<
|
||||
Message | InlineQuery | ChosenInlineResult | CallbackQuery,
|
||||
{ match: RegExpMatchArray }
|
||||
> =>
|
||||
(obj) => {
|
||||
const txt = extractText(obj)
|
||||
if (!txt) return false
|
||||
|
||||
const m = txt.match(regex)
|
||||
|
||||
if (m) {
|
||||
(obj as typeof obj & { match: RegExpMatchArray }).match = m
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter objects which contain the exact text given
|
||||
* - for `Message`, `Message.text` is used
|
||||
* - for `InlineQuery`, `InlineQuery.query` is used
|
||||
* - for {@link ChosenInlineResult}, {@link ChosenInlineResult.id} is used
|
||||
* - for `CallbackQuery`, `CallbackQuery.dataStr` is used
|
||||
*
|
||||
* @param str String to be matched
|
||||
* @param ignoreCase Whether string case should be ignored
|
||||
*/
|
||||
export const equals = (
|
||||
str: string,
|
||||
ignoreCase = false,
|
||||
): UpdateFilter<Message | InlineQuery | ChosenInlineResult | CallbackQuery> => {
|
||||
if (ignoreCase) {
|
||||
str = str.toLowerCase()
|
||||
|
||||
return (obj) => extractText(obj)?.toLowerCase() === str
|
||||
}
|
||||
|
||||
return (obj) => extractText(obj) === str
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter objects which contain the text given (as a substring)
|
||||
* - for `Message`, `Message.text` is used
|
||||
* - for `InlineQuery`, `InlineQuery.query` is used
|
||||
* - for {@link ChosenInlineResult}, {@link ChosenInlineResult.id} is used
|
||||
* - for `CallbackQuery`, `CallbackQuery.dataStr` is used
|
||||
*
|
||||
* @param str Substring to be matched
|
||||
* @param ignoreCase Whether string case should be ignored
|
||||
*/
|
||||
export const contains = (
|
||||
str: string,
|
||||
ignoreCase = false,
|
||||
): UpdateFilter<Message | InlineQuery | ChosenInlineResult | CallbackQuery> => {
|
||||
if (ignoreCase) {
|
||||
str = str.toLowerCase()
|
||||
|
||||
return (obj) => {
|
||||
const txt = extractText(obj)
|
||||
|
||||
return txt != null && txt.toLowerCase().includes(str)
|
||||
}
|
||||
}
|
||||
|
||||
return (obj) => {
|
||||
const txt = extractText(obj)
|
||||
|
||||
return txt != null && txt.includes(str)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter objects which contain the text starting with a given string
|
||||
* - for `Message`, `Message.text` is used
|
||||
* - for `InlineQuery`, `InlineQuery.query` is used
|
||||
* - for {@link ChosenInlineResult}, {@link ChosenInlineResult.id} is used
|
||||
* - for `CallbackQuery`, `CallbackQuery.dataStr` is used
|
||||
*
|
||||
* @param str Substring to be matched
|
||||
* @param ignoreCase Whether string case should be ignored
|
||||
*/
|
||||
export const startsWith = (
|
||||
str: string,
|
||||
ignoreCase = false,
|
||||
): UpdateFilter<Message | InlineQuery | ChosenInlineResult | CallbackQuery> => {
|
||||
if (ignoreCase) {
|
||||
str = str.toLowerCase()
|
||||
|
||||
return (obj) => {
|
||||
const txt = extractText(obj)
|
||||
|
||||
return (
|
||||
txt != null &&
|
||||
txt.toLowerCase().substring(0, str.length) === str
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (obj) => {
|
||||
const txt = extractText(obj)
|
||||
|
||||
return txt != null && txt.substring(0, str.length) === str
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter objects which contain the text ending with a given string
|
||||
* - for `Message`, `Message.text` is used
|
||||
* - for `InlineQuery`, `InlineQuery.query` is used
|
||||
* - for {@link ChosenInlineResult}, {@link ChosenInlineResult.id} is used
|
||||
* - for `CallbackQuery`, `CallbackQuery.dataStr` is used
|
||||
*
|
||||
* @param str Substring to be matched
|
||||
* @param ignoreCase Whether string case should be ignored
|
||||
*/
|
||||
export const endsWith = (
|
||||
str: string,
|
||||
ignoreCase = false,
|
||||
): UpdateFilter<Message | InlineQuery | ChosenInlineResult | CallbackQuery> => {
|
||||
if (ignoreCase) {
|
||||
str = str.toLowerCase()
|
||||
|
||||
return (obj) => {
|
||||
const txt = extractText(obj)
|
||||
|
||||
return (
|
||||
txt != null &&
|
||||
txt.toLowerCase().substring(0, str.length) === str
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (obj) => {
|
||||
const txt = extractText(obj)
|
||||
|
||||
return txt != null && txt.substring(0, str.length) === str
|
||||
}
|
||||
}
|
117
packages/dispatcher/src/filters/types.ts
Normal file
117
packages/dispatcher/src/filters/types.ts
Normal file
|
@ -0,0 +1,117 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
// ^^ will be looked into in MTQ-29
|
||||
|
||||
import { MaybeAsync } from '@mtcute/core'
|
||||
|
||||
import { UpdateState } from '../state'
|
||||
/**
|
||||
* Type describing a primitive filter, which is a function taking some `Base`
|
||||
* and a {@link TelegramClient}, checking it against some condition
|
||||
* and returning a boolean.
|
||||
*
|
||||
* If `true` is returned, the filter is considered
|
||||
* to be matched, and the appropriate update handler function is called,
|
||||
* otherwise next registered handler is checked.
|
||||
*
|
||||
* Additionally, filter might contain a type modification
|
||||
* to `Base` for better code insights. If it is present,
|
||||
* it is used to overwrite types (!) of some of the `Base` fields
|
||||
* to given (note that this is entirely compile-time! object is not modified)
|
||||
*
|
||||
* For parametrized filters (like {@link filters.regex}),
|
||||
* type modification can also be used to add additional fields
|
||||
* (in case of `regex`, its match array is added to `.match`)
|
||||
*
|
||||
* Example without type mod:
|
||||
* ```typescript
|
||||
*
|
||||
* const hasPhoto: UpdateFilter<Message> = msg => msg.media?.type === 'photo'
|
||||
*
|
||||
* // ..later..
|
||||
* tg.onNewMessage(hasPhoto, async (msg) => {
|
||||
* // `hasPhoto` filter matched, so we can safely assume
|
||||
* // that `msg.media` is a Photo.
|
||||
* //
|
||||
* // but it is very redundant, verbose and error-rome,
|
||||
* // wonder if we could make typescript do this automagically and safely...
|
||||
* await (msg.media as Photo).downloadToFile(`${msg.id}.jpg`)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* Example with type mod:
|
||||
* ```typescript
|
||||
*
|
||||
* const hasPhoto: UpdateFilter<Message, { media: Photo }> = msg => msg.media?.type === 'photo'
|
||||
*
|
||||
* // ..later..
|
||||
* tg.onNewMessage(hasPhoto, async (msg) => {
|
||||
* // since `hasPhoto` filter matched,
|
||||
* // we have applied the modification to `msg`,
|
||||
* // and `msg.media` now has type `Photo`
|
||||
* //
|
||||
* // no more redundancy and type casts!
|
||||
* await msg.media.downloadToFile(`${msg.id}.jpg`)
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* > **Note**: Type modification can contain anything, even totally unrelated types
|
||||
* > and it is *your* task to keep track that everything is correct.
|
||||
* >
|
||||
* > Bad example:
|
||||
* > ```typescript
|
||||
* > // we check for `Photo`, but type contains `Audio`. this will be a problem!
|
||||
* > const hasPhoto: UpdateFilter<Message, { media: Audio }> = msg => msg.media?.type === 'photo'
|
||||
* >
|
||||
* > // ..later..
|
||||
* > tg.onNewMessage(hasPhoto, async (msg) => {
|
||||
* > // oops! `msg.media` is `Audio` and does not have `.width`!
|
||||
* > console.log(msg.media.width)
|
||||
* > })
|
||||
* > ```
|
||||
*
|
||||
* > **Warning!** Do not use the generics provided in functions
|
||||
* > like `and`, `or`, etc. Those are meant to be inferred by the compiler!
|
||||
*/
|
||||
// we need the second parameter because it carries meta information
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/ban-types
|
||||
export type UpdateFilter<Base, Mod = {}, State = never> = (
|
||||
update: Base,
|
||||
state?: UpdateState<State>
|
||||
) => MaybeAsync<boolean>
|
||||
|
||||
export type Modify<Base, Mod> = Omit<Base, keyof Mod> & Mod
|
||||
export type Invert<Base, Mod> = {
|
||||
[P in keyof Mod & keyof Base]: Exclude<Base[P], Mod[P]>
|
||||
}
|
||||
|
||||
export type UnionToIntersection<U> = (
|
||||
U extends any ? (k: U) => void : never
|
||||
) extends (k: infer I) => void
|
||||
? I
|
||||
: never
|
||||
|
||||
export type ExtractBase<Filter> = Filter extends UpdateFilter<infer I, any>
|
||||
? I
|
||||
: never
|
||||
|
||||
export type ExtractMod<Filter> = Filter extends UpdateFilter<any, infer I>
|
||||
? I
|
||||
: never
|
||||
|
||||
export type ExtractState<Filter> = Filter extends UpdateFilter<
|
||||
any,
|
||||
any,
|
||||
infer I
|
||||
>
|
||||
? I
|
||||
: never
|
||||
|
||||
export type TupleKeys<T extends any[]> = Exclude<keyof T, keyof []>
|
||||
export type WrapBase<T extends any[]> = {
|
||||
[K in TupleKeys<T>]: { base: ExtractBase<T[K]> }
|
||||
}
|
||||
export type Values<T> = T[keyof T]
|
||||
export type UnwrapBase<T> = T extends { base: any } ? T['base'] : never
|
||||
export type ExtractBaseMany<Filters extends any[]> = UnwrapBase<
|
||||
UnionToIntersection<Values<WrapBase<Filters>>>
|
||||
>
|
85
packages/dispatcher/src/filters/updates.ts
Normal file
85
packages/dispatcher/src/filters/updates.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import {
|
||||
CallbackQuery,
|
||||
ChatMemberUpdate,
|
||||
ChatMemberUpdateType,
|
||||
UserStatus,
|
||||
UserStatusUpdate,
|
||||
} from '@mtcute/client'
|
||||
import { MaybeArray } from '@mtcute/core'
|
||||
|
||||
import { UpdateFilter } from './types'
|
||||
|
||||
/**
|
||||
* Create a filter for {@link ChatMemberUpdate} by update type
|
||||
*
|
||||
* @param types Update type(s)
|
||||
* @link ChatMemberUpdate.Type
|
||||
*/
|
||||
export const chatMember: {
|
||||
<T extends ChatMemberUpdateType>(type: T): UpdateFilter<
|
||||
ChatMemberUpdate,
|
||||
{ type: T }
|
||||
>
|
||||
<T extends ChatMemberUpdateType[]>(types: T): UpdateFilter<
|
||||
ChatMemberUpdate,
|
||||
{ type: T[number] }
|
||||
>
|
||||
} = (
|
||||
types: MaybeArray<ChatMemberUpdateType>,
|
||||
): UpdateFilter<ChatMemberUpdate> => {
|
||||
if (Array.isArray(types)) {
|
||||
const index: Partial<Record<ChatMemberUpdateType, true>> = {}
|
||||
types.forEach((typ) => (index[typ] = true))
|
||||
|
||||
return (upd) => upd.type in index
|
||||
}
|
||||
|
||||
return (upd) => upd.type === types
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filter for {@link UserStatusUpdate} by new user status
|
||||
*
|
||||
* @param statuses Update type(s)
|
||||
* @link User.Status
|
||||
*/
|
||||
export const userStatus: {
|
||||
<T extends UserStatus>(status: T): UpdateFilter<
|
||||
UserStatusUpdate,
|
||||
{
|
||||
type: T
|
||||
lastOnline: T extends 'offline' ? Date : null
|
||||
nextOffline: T extends 'online' ? Date : null
|
||||
}
|
||||
>
|
||||
<T extends UserStatus[]>(statuses: T): UpdateFilter<
|
||||
UserStatusUpdate,
|
||||
{ type: T[number] }
|
||||
>
|
||||
} = (statuses: MaybeArray<UserStatus>): UpdateFilter<UserStatusUpdate> => {
|
||||
if (Array.isArray(statuses)) {
|
||||
const index: Partial<Record<UserStatus, true>> = {}
|
||||
statuses.forEach((typ) => (index[typ] = true))
|
||||
|
||||
return (upd) => upd.status in index
|
||||
}
|
||||
|
||||
return (upd) => upd.status === statuses
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a filter for {@link ChatMemberUpdate} for updates
|
||||
* regarding current user
|
||||
*/
|
||||
export const chatMemberSelf: UpdateFilter<
|
||||
ChatMemberUpdate,
|
||||
{ isSelf: true }
|
||||
> = (upd) => upd.isSelf
|
||||
|
||||
/**
|
||||
* Create a filter for callback queries that
|
||||
* originated from an inline message
|
||||
*/
|
||||
export const callbackInline: UpdateFilter<CallbackQuery, { isInline: true }> = (
|
||||
q,
|
||||
) => q.isInline
|
196
packages/dispatcher/src/filters/user.ts
Normal file
196
packages/dispatcher/src/filters/user.ts
Normal file
|
@ -0,0 +1,196 @@
|
|||
import {
|
||||
BotChatJoinRequestUpdate,
|
||||
CallbackQuery,
|
||||
ChatMemberUpdate,
|
||||
ChosenInlineResult,
|
||||
InlineQuery,
|
||||
Message,
|
||||
PollVoteUpdate,
|
||||
User,
|
||||
UserStatusUpdate,
|
||||
UserTypingUpdate,
|
||||
} from '@mtcute/client'
|
||||
import { MaybeArray } from '@mtcute/core'
|
||||
|
||||
import { UpdateFilter } from './types'
|
||||
|
||||
/**
|
||||
* Filter messages generated by yourself (including Saved Messages)
|
||||
*/
|
||||
export const me: UpdateFilter<Message, { sender: User }> = (msg) =>
|
||||
(msg.sender.constructor === User && msg.sender.isSelf) || msg.isOutgoing
|
||||
|
||||
/**
|
||||
* Filter messages sent by bots
|
||||
*/
|
||||
export const bot: UpdateFilter<Message, { sender: User }> = (msg) =>
|
||||
msg.sender.constructor === User && msg.sender.isBot
|
||||
|
||||
/**
|
||||
* Filter updates by user ID(s) or username(s)
|
||||
*
|
||||
* Usernames are not supported for UserStatusUpdate
|
||||
* and UserTypingUpdate.
|
||||
*
|
||||
*
|
||||
* For chat member updates, uses `user.id`
|
||||
*/
|
||||
export const userId = (
|
||||
id: MaybeArray<number | string>,
|
||||
): UpdateFilter<
|
||||
| Message
|
||||
| UserStatusUpdate
|
||||
| UserTypingUpdate
|
||||
| InlineQuery
|
||||
| ChatMemberUpdate
|
||||
| ChosenInlineResult
|
||||
| CallbackQuery
|
||||
| PollVoteUpdate
|
||||
| BotChatJoinRequestUpdate
|
||||
> => {
|
||||
if (Array.isArray(id)) {
|
||||
const index: Record<number | string, true> = {}
|
||||
let matchSelf = false
|
||||
id.forEach((id) => {
|
||||
if (id === 'me' || id === 'self') {
|
||||
matchSelf = true
|
||||
} else {
|
||||
index[id] = true
|
||||
}
|
||||
})
|
||||
|
||||
return (upd) => {
|
||||
const ctor = upd.constructor
|
||||
|
||||
if (ctor === Message) {
|
||||
const sender = (upd as Message).sender
|
||||
|
||||
return (
|
||||
(matchSelf && sender.isSelf) ||
|
||||
sender.id in index ||
|
||||
sender.username! in index
|
||||
)
|
||||
} else if (ctor === UserStatusUpdate || ctor === UserTypingUpdate) {
|
||||
const id = (upd as UserStatusUpdate | UserTypingUpdate).userId
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line dot-notation
|
||||
(matchSelf && id === upd.client['_userId']) || id in index
|
||||
)
|
||||
} else if (ctor === PollVoteUpdate) {
|
||||
const peer = (upd as PollVoteUpdate).peer
|
||||
if (peer.type !== 'user') return false
|
||||
|
||||
return (
|
||||
(matchSelf && peer.isSelf) ||
|
||||
peer.id in index ||
|
||||
peer.username! in index
|
||||
)
|
||||
}
|
||||
|
||||
const user = (
|
||||
upd as Exclude<
|
||||
typeof upd,
|
||||
| Message
|
||||
| UserStatusUpdate
|
||||
| UserTypingUpdate
|
||||
| PollVoteUpdate
|
||||
>
|
||||
).user
|
||||
|
||||
return (
|
||||
(matchSelf && user.isSelf) ||
|
||||
user.id in index ||
|
||||
user.username! in index
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (id === 'me' || id === 'self') {
|
||||
return (upd) => {
|
||||
const ctor = upd.constructor
|
||||
|
||||
if (ctor === Message) {
|
||||
return (upd as Message).sender.isSelf
|
||||
} else if (ctor === UserStatusUpdate || ctor === UserTypingUpdate) {
|
||||
return (
|
||||
(upd as UserStatusUpdate | UserTypingUpdate).userId ===
|
||||
// eslint-disable-next-line dot-notation
|
||||
upd.client['_userId']
|
||||
)
|
||||
} else if (ctor === PollVoteUpdate) {
|
||||
const peer = (upd as PollVoteUpdate).peer
|
||||
if (peer.type !== 'user') return false
|
||||
|
||||
return peer.isSelf
|
||||
}
|
||||
|
||||
return (
|
||||
upd as Exclude<
|
||||
typeof upd,
|
||||
| Message
|
||||
| UserStatusUpdate
|
||||
| UserTypingUpdate
|
||||
| PollVoteUpdate
|
||||
>
|
||||
).user.isSelf
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof id === 'string') {
|
||||
return (upd) => {
|
||||
const ctor = upd.constructor
|
||||
|
||||
if (ctor === Message) {
|
||||
return (upd as Message).sender.username === id
|
||||
} else if (ctor === UserStatusUpdate || ctor === UserTypingUpdate) {
|
||||
// username is not available
|
||||
return false
|
||||
} else if (ctor === PollVoteUpdate) {
|
||||
const peer = (upd as PollVoteUpdate).peer
|
||||
if (peer.type !== 'user') return false
|
||||
|
||||
return peer.username === id
|
||||
}
|
||||
|
||||
return (
|
||||
(
|
||||
upd as Exclude<
|
||||
typeof upd,
|
||||
| Message
|
||||
| UserStatusUpdate
|
||||
| UserTypingUpdate
|
||||
| PollVoteUpdate
|
||||
>
|
||||
).user.username === id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (upd) => {
|
||||
const ctor = upd.constructor
|
||||
|
||||
if (ctor === Message) {
|
||||
return (upd as Message).sender.id === id
|
||||
} else if (ctor === UserStatusUpdate || ctor === UserTypingUpdate) {
|
||||
return (upd as UserStatusUpdate | UserTypingUpdate).userId === id
|
||||
} else if (ctor === PollVoteUpdate) {
|
||||
const peer = (upd as PollVoteUpdate).peer
|
||||
if (peer.type !== 'user') return false
|
||||
|
||||
return peer.id === id
|
||||
}
|
||||
|
||||
return (
|
||||
(
|
||||
upd as Exclude<
|
||||
typeof upd,
|
||||
| Message
|
||||
| UserStatusUpdate
|
||||
| UserTypingUpdate
|
||||
| PollVoteUpdate
|
||||
>
|
||||
).user.id === id
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue