feat(dispatcher): support poll related updates
also fixed a few type and export issues, and changed poll option generation to match tdlib and others
This commit is contained in:
parent
6db771e3da
commit
d36c1781bd
11 changed files with 316 additions and 14 deletions
|
@ -142,7 +142,8 @@ export async function _normalizeInputMedia(
|
||||||
return {
|
return {
|
||||||
_: 'pollAnswer',
|
_: 'pollAnswer',
|
||||||
text: ans,
|
text: ans,
|
||||||
option: Buffer.from([idx]),
|
// emulate the behaviour of most implementations
|
||||||
|
option: Buffer.from([48 /* '0' */ + idx]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,11 +185,11 @@ export async function _normalizeInputMedia(
|
||||||
question: media.question,
|
question: media.question,
|
||||||
answers,
|
answers,
|
||||||
closePeriod: media.closePeriod,
|
closePeriod: media.closePeriod,
|
||||||
closeDate: normalizeDate(media.closeDate)
|
closeDate: normalizeDate(media.closeDate),
|
||||||
},
|
},
|
||||||
correctAnswers: correct,
|
correctAnswers: correct,
|
||||||
solution,
|
solution,
|
||||||
solutionEntities
|
solutionEntities,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,7 +66,7 @@ export class InlineQuery {
|
||||||
if (this.raw.geo?._ !== 'geoPoint') return null
|
if (this.raw.geo?._ !== 'geoPoint') return null
|
||||||
|
|
||||||
if (!this._location) {
|
if (!this._location) {
|
||||||
this._location = new Location(this.raw.geo)
|
this._location = new Location(this.client, this.raw.geo)
|
||||||
}
|
}
|
||||||
|
|
||||||
return this._location
|
return this._location
|
||||||
|
|
|
@ -10,3 +10,7 @@ export * from './voice'
|
||||||
export * from './sticker'
|
export * from './sticker'
|
||||||
export * from './input-media'
|
export * from './input-media'
|
||||||
export * from './venue'
|
export * from './venue'
|
||||||
|
export * from './poll'
|
||||||
|
export * from './invoice'
|
||||||
|
export * from './game'
|
||||||
|
export * from './web-page'
|
||||||
|
|
|
@ -21,14 +21,14 @@ import {
|
||||||
LiveLocation,
|
LiveLocation,
|
||||||
Sticker,
|
Sticker,
|
||||||
Voice,
|
Voice,
|
||||||
InputMediaLike, Venue,
|
InputMediaLike,
|
||||||
|
Venue,
|
||||||
|
Poll,
|
||||||
|
Invoice,
|
||||||
|
Game,
|
||||||
|
WebPage
|
||||||
} from '../media'
|
} from '../media'
|
||||||
import { parseDocument } from '../media/document-utils'
|
import { parseDocument } from '../media/document-utils'
|
||||||
import { Game } from '../media/game'
|
|
||||||
import { WebPage } from '../media/web-page'
|
|
||||||
import { InputFileLike } from '../files'
|
|
||||||
import { Poll } from '../media/poll'
|
|
||||||
import { Invoice } from '../media/invoice'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A message or a service message
|
* A message or a service message
|
||||||
|
|
|
@ -7,3 +7,5 @@ chat_member: ChatMemberUpdate = ChatMemberUpdate
|
||||||
inline_query = InlineQuery
|
inline_query = InlineQuery
|
||||||
chosen_inline_result = ChosenInlineResult
|
chosen_inline_result = ChosenInlineResult
|
||||||
callback_query = CallbackQuery
|
callback_query = CallbackQuery
|
||||||
|
poll: PollUpdate = PollUpdate
|
||||||
|
poll_vote = PollVoteUpdate
|
||||||
|
|
|
@ -8,12 +8,16 @@ import {
|
||||||
InlineQueryHandler,
|
InlineQueryHandler,
|
||||||
ChosenInlineResultHandler,
|
ChosenInlineResultHandler,
|
||||||
CallbackQueryHandler,
|
CallbackQueryHandler,
|
||||||
|
PollUpdateHandler,
|
||||||
|
PollVoteHandler,
|
||||||
} from './handler'
|
} from './handler'
|
||||||
// end-codegen-imports
|
// end-codegen-imports
|
||||||
import { filters, UpdateFilter } from './filters'
|
import { filters, UpdateFilter } from './filters'
|
||||||
import { CallbackQuery, InlineQuery, Message } from '@mtcute/client'
|
import { CallbackQuery, InlineQuery, Message } from '@mtcute/client'
|
||||||
import { ChatMemberUpdate } from './updates'
|
import { ChatMemberUpdate } from './updates'
|
||||||
import { ChosenInlineResult } from './updates/chosen-inline-result'
|
import { ChosenInlineResult } from './updates/chosen-inline-result'
|
||||||
|
import { PollUpdate } from './updates/poll-update'
|
||||||
|
import { PollVoteUpdate } from './updates/poll-vote'
|
||||||
|
|
||||||
function _create<T extends UpdateHandler>(
|
function _create<T extends UpdateHandler>(
|
||||||
type: T['type'],
|
type: T['type'],
|
||||||
|
@ -235,5 +239,57 @@ export namespace handlers {
|
||||||
return _create('callback_query', filter, handler)
|
return _create('callback_query', filter, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a poll update handler
|
||||||
|
*
|
||||||
|
* @param handler Poll update handler
|
||||||
|
*/
|
||||||
|
export function pollUpdate(
|
||||||
|
handler: PollUpdateHandler['callback']
|
||||||
|
): PollUpdateHandler
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a poll update handler with a filter
|
||||||
|
*
|
||||||
|
* @param filter Update filter
|
||||||
|
* @param handler Poll update handler
|
||||||
|
*/
|
||||||
|
export function pollUpdate<Mod>(
|
||||||
|
filter: UpdateFilter<PollUpdate, Mod>,
|
||||||
|
handler: PollUpdateHandler<filters.Modify<PollUpdate, Mod>>['callback']
|
||||||
|
): PollUpdateHandler
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export function pollUpdate(filter: any, handler?: any): PollUpdateHandler {
|
||||||
|
return _create('poll', filter, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a poll vote handler
|
||||||
|
*
|
||||||
|
* @param handler Poll vote handler
|
||||||
|
*/
|
||||||
|
export function pollVote(
|
||||||
|
handler: PollVoteHandler['callback']
|
||||||
|
): PollVoteHandler
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a poll vote handler with a filter
|
||||||
|
*
|
||||||
|
* @param filter Update filter
|
||||||
|
* @param handler Poll vote handler
|
||||||
|
*/
|
||||||
|
export function pollVote<Mod>(
|
||||||
|
filter: UpdateFilter<PollVoteUpdate, Mod>,
|
||||||
|
handler: PollVoteHandler<
|
||||||
|
filters.Modify<PollVoteUpdate, Mod>
|
||||||
|
>['callback']
|
||||||
|
): PollVoteHandler
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export function pollVote(filter: any, handler?: any): PollVoteHandler {
|
||||||
|
return _create('poll_vote', filter, handler)
|
||||||
|
}
|
||||||
|
|
||||||
// end-codegen
|
// end-codegen
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,16 @@ import {
|
||||||
InlineQueryHandler,
|
InlineQueryHandler,
|
||||||
ChosenInlineResultHandler,
|
ChosenInlineResultHandler,
|
||||||
CallbackQueryHandler,
|
CallbackQueryHandler,
|
||||||
|
PollUpdateHandler,
|
||||||
|
PollVoteHandler,
|
||||||
} from './handler'
|
} from './handler'
|
||||||
// end-codegen-imports
|
// end-codegen-imports
|
||||||
import { filters, UpdateFilter } from './filters'
|
import { filters, UpdateFilter } from './filters'
|
||||||
import { handlers } from './builders'
|
import { handlers } from './builders'
|
||||||
import { ChatMemberUpdate } from './updates'
|
import { ChatMemberUpdate } from './updates'
|
||||||
import { ChosenInlineResult } from './updates/chosen-inline-result'
|
import { ChosenInlineResult } from './updates/chosen-inline-result'
|
||||||
|
import { PollUpdate } from './updates/poll-update'
|
||||||
|
import { PollVoteUpdate } from './updates/poll-vote'
|
||||||
|
|
||||||
const noop = () => {}
|
const noop = () => {}
|
||||||
|
|
||||||
|
@ -88,6 +92,14 @@ const PARSERS: Partial<
|
||||||
],
|
],
|
||||||
updateBotCallbackQuery: callbackQueryParser,
|
updateBotCallbackQuery: callbackQueryParser,
|
||||||
updateInlineBotCallbackQuery: callbackQueryParser,
|
updateInlineBotCallbackQuery: callbackQueryParser,
|
||||||
|
updateMessagePoll: [
|
||||||
|
'poll',
|
||||||
|
(client, upd, users) => new PollUpdate(client, upd as any, users),
|
||||||
|
],
|
||||||
|
updateMessagePollVote: [
|
||||||
|
'poll_vote',
|
||||||
|
(client, upd, users) => new PollVoteUpdate(client, upd as any, users),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -599,5 +611,61 @@ export class Dispatcher {
|
||||||
this._addKnownHandler('callbackQuery', filter, handler, group)
|
this._addKnownHandler('callbackQuery', filter, handler, group)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a poll update handler without any filters
|
||||||
|
*
|
||||||
|
* @param handler Poll update handler
|
||||||
|
* @param group Handler group index
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
onPollUpdate(handler: PollUpdateHandler['callback'], group?: number): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a poll update handler with a filter
|
||||||
|
*
|
||||||
|
* @param filter Update filter
|
||||||
|
* @param handler Poll update handler
|
||||||
|
* @param group Handler group index
|
||||||
|
*/
|
||||||
|
onPollUpdate<Mod>(
|
||||||
|
filter: UpdateFilter<PollUpdate, Mod>,
|
||||||
|
handler: PollUpdateHandler<filters.Modify<PollUpdate, Mod>>['callback'],
|
||||||
|
group?: number
|
||||||
|
): void
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
onPollUpdate(filter: any, handler?: any, group?: number): void {
|
||||||
|
this._addKnownHandler('pollUpdate', filter, handler, group)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a poll vote handler without any filters
|
||||||
|
*
|
||||||
|
* @param handler Poll vote handler
|
||||||
|
* @param group Handler group index
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
onPollVote(handler: PollVoteHandler['callback'], group?: number): void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a poll vote handler with a filter
|
||||||
|
*
|
||||||
|
* @param filter Update filter
|
||||||
|
* @param handler Poll vote handler
|
||||||
|
* @param group Handler group index
|
||||||
|
*/
|
||||||
|
onPollVote<Mod>(
|
||||||
|
filter: UpdateFilter<PollVoteUpdate, Mod>,
|
||||||
|
handler: PollVoteHandler<
|
||||||
|
filters.Modify<PollVoteUpdate, Mod>
|
||||||
|
>['callback'],
|
||||||
|
group?: number
|
||||||
|
): void
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
onPollVote(filter: any, handler?: any, group?: number): void {
|
||||||
|
this._addKnownHandler('pollVote', filter, handler, group)
|
||||||
|
}
|
||||||
|
|
||||||
// end-codegen
|
// end-codegen
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,14 +17,14 @@ import {
|
||||||
User, Venue,
|
User, Venue,
|
||||||
Video,
|
Video,
|
||||||
Voice,
|
Voice,
|
||||||
|
Poll,
|
||||||
|
Invoice,
|
||||||
|
Game,
|
||||||
|
WebPage
|
||||||
} from '@mtcute/client'
|
} from '@mtcute/client'
|
||||||
import { Game } from '@mtcute/client/src/types/media/game'
|
|
||||||
import { WebPage } from '@mtcute/client/src/types/media/web-page'
|
|
||||||
import { MaybeArray } from '@mtcute/core'
|
import { MaybeArray } from '@mtcute/core'
|
||||||
import { ChatMemberUpdate } from './updates'
|
import { ChatMemberUpdate } from './updates'
|
||||||
import { ChosenInlineResult } from './updates/chosen-inline-result'
|
import { ChosenInlineResult } from './updates/chosen-inline-result'
|
||||||
import { Poll } from '@mtcute/client/src/types/media/poll'
|
|
||||||
import { Invoice } from '@mtcute/client/src/types/media/invoice'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type describing a primitive filter, which is a function taking some `Base`
|
* Type describing a primitive filter, which is a function taking some `Base`
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { tl } from '@mtcute/tl'
|
||||||
import { PropagationSymbol } from './propagation'
|
import { PropagationSymbol } from './propagation'
|
||||||
import { ChatMemberUpdate } from './updates'
|
import { ChatMemberUpdate } from './updates'
|
||||||
import { ChosenInlineResult } from './updates/chosen-inline-result'
|
import { ChosenInlineResult } from './updates/chosen-inline-result'
|
||||||
|
import { PollUpdate } from './updates/poll-update'
|
||||||
|
import { PollVoteUpdate } from './updates/poll-vote'
|
||||||
|
|
||||||
interface BaseUpdateHandler<Type, Handler, Checker> {
|
interface BaseUpdateHandler<Type, Handler, Checker> {
|
||||||
type: Type
|
type: Type
|
||||||
|
@ -66,6 +68,11 @@ export type CallbackQueryHandler<T = CallbackQuery> = ParsedUpdateHandler<
|
||||||
'callback_query',
|
'callback_query',
|
||||||
T
|
T
|
||||||
>
|
>
|
||||||
|
export type PollUpdateHandler<T = PollUpdate> = ParsedUpdateHandler<'poll', T>
|
||||||
|
export type PollVoteHandler<T = PollVoteUpdate> = ParsedUpdateHandler<
|
||||||
|
'poll_vote',
|
||||||
|
T
|
||||||
|
>
|
||||||
|
|
||||||
export type UpdateHandler =
|
export type UpdateHandler =
|
||||||
| RawUpdateHandler
|
| RawUpdateHandler
|
||||||
|
@ -75,5 +82,7 @@ export type UpdateHandler =
|
||||||
| InlineQueryHandler
|
| InlineQueryHandler
|
||||||
| ChosenInlineResultHandler
|
| ChosenInlineResultHandler
|
||||||
| CallbackQueryHandler
|
| CallbackQueryHandler
|
||||||
|
| PollUpdateHandler
|
||||||
|
| PollVoteHandler
|
||||||
|
|
||||||
// end-codegen
|
// end-codegen
|
||||||
|
|
73
packages/dispatcher/src/updates/poll-update.ts
Normal file
73
packages/dispatcher/src/updates/poll-update.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import { makeInspectable } from '@mtcute/client/src/types/utils'
|
||||||
|
import { TelegramClient, Poll } from '@mtcute/client'
|
||||||
|
import { tl } from '@mtcute/tl'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll state has changed (stopped, somebody
|
||||||
|
* has voted in an anonymous poll, etc.)
|
||||||
|
*
|
||||||
|
* Bots only receive updates about
|
||||||
|
* polls which were sent by this bot
|
||||||
|
*/
|
||||||
|
export class PollUpdate {
|
||||||
|
readonly client: TelegramClient
|
||||||
|
readonly raw: tl.RawUpdateMessagePoll
|
||||||
|
|
||||||
|
readonly _users: Record<number, tl.TypeUser>
|
||||||
|
|
||||||
|
constructor (client: TelegramClient, raw: tl.RawUpdateMessagePoll, users: Record<number, tl.TypeUser>) {
|
||||||
|
this.client = client
|
||||||
|
this.raw = raw
|
||||||
|
this._users = users
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique poll ID
|
||||||
|
*/
|
||||||
|
get pollId(): tl.Long {
|
||||||
|
return this.raw.pollId
|
||||||
|
}
|
||||||
|
|
||||||
|
private _poll: Poll
|
||||||
|
/**
|
||||||
|
* The poll.
|
||||||
|
*
|
||||||
|
* Note that sometimes the update does not have the poll
|
||||||
|
* (Telegram limitation), and MTCute creates a stub poll
|
||||||
|
* with empty question, answers and flags
|
||||||
|
* (like `quiz`, `public`, etc.)
|
||||||
|
*
|
||||||
|
* If you need access to them, you should
|
||||||
|
* map the {@link pollId} with full poll on your side
|
||||||
|
* (e.g. in a database) and fetch from there.
|
||||||
|
*
|
||||||
|
* Bot API and TDLib do basically the same internally,
|
||||||
|
* and thus are able to always provide them,
|
||||||
|
* but MTCute tries to keep it simple in terms of local
|
||||||
|
* storage and only stores the necessary information.
|
||||||
|
*/
|
||||||
|
get poll(): Poll {
|
||||||
|
if (!this._poll) {
|
||||||
|
let poll = this.raw.poll
|
||||||
|
if (!poll) {
|
||||||
|
// create stub poll
|
||||||
|
poll = {
|
||||||
|
_: 'poll',
|
||||||
|
id: this.raw.pollId,
|
||||||
|
question: '',
|
||||||
|
answers: this.raw.results.results?.map((res) => ({
|
||||||
|
_: 'pollAnswer',
|
||||||
|
text: '',
|
||||||
|
option: res.option
|
||||||
|
})) ?? []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._poll = new Poll(this.client, poll, this._users, this.raw.results)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._poll
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
makeInspectable(PollUpdate)
|
89
packages/dispatcher/src/updates/poll-vote.ts
Normal file
89
packages/dispatcher/src/updates/poll-vote.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import { MtCuteUnsupportedError, TelegramClient, User } from '@mtcute/client'
|
||||||
|
import { tl } from '@mtcute/tl'
|
||||||
|
import { makeInspectable } from '@mtcute/client/src/types/utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some user has voted in a public poll.
|
||||||
|
*
|
||||||
|
* Bots only receive new votes in polls
|
||||||
|
* that were sent by this bot.
|
||||||
|
*/
|
||||||
|
export class PollVoteUpdate {
|
||||||
|
readonly client: TelegramClient
|
||||||
|
readonly raw: tl.RawUpdateMessagePollVote
|
||||||
|
|
||||||
|
readonly _users: Record<number, tl.TypeUser>
|
||||||
|
|
||||||
|
constructor(client: TelegramClient, raw: tl.RawUpdateMessagePollVote, users: Record<number, tl.TypeUser>) {
|
||||||
|
this.client = client
|
||||||
|
this.raw = raw
|
||||||
|
this._users = users
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unique poll ID
|
||||||
|
*/
|
||||||
|
get pollId(): tl.Long {
|
||||||
|
return this.raw.pollId
|
||||||
|
}
|
||||||
|
|
||||||
|
private _user?: User
|
||||||
|
/**
|
||||||
|
* User who has voted
|
||||||
|
*/
|
||||||
|
get user(): User {
|
||||||
|
if (!this._user) {
|
||||||
|
this._user = new User(this.client, this._users[this.raw.userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._user
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Answers that the user has chosen.
|
||||||
|
*
|
||||||
|
* Note that due to incredible Telegram APIs, you
|
||||||
|
* have to have the poll cached to be able to properly
|
||||||
|
* tell which answers were chosen, since in the API
|
||||||
|
* there are just arbitrary `Buffer`s, which are
|
||||||
|
* defined by the client.
|
||||||
|
*
|
||||||
|
* However, most of the major implementations
|
||||||
|
* (tested with TDLib and Bot API, official apps
|
||||||
|
* for Android, Desktop, iOS/macOS) and MTCute
|
||||||
|
* (by default) create `option` as a one-byte `Buffer`,
|
||||||
|
* incrementing from `48` (ASCII `0`) up to `57` (ASCII `9`),
|
||||||
|
* and ASCII representation would define index in the array.
|
||||||
|
* Meaning, if `chosen[0][0] === 48` or `chosen[0].toString() === '0'`,
|
||||||
|
* then the first answer (indexed with `0`) was chosen. To get the index,
|
||||||
|
* you simply subtract `48` from the first byte.
|
||||||
|
*
|
||||||
|
* This might break at any time, but seems to be consistent for now.
|
||||||
|
* To get chosen answer indexes derived as before, use {@link chosenIndexesAuto}.
|
||||||
|
*/
|
||||||
|
get chosen(): Buffer[] {
|
||||||
|
return this.raw.options
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indexes of the chosen answers, derived based on observations
|
||||||
|
* described in {@link chosen}.
|
||||||
|
* This might break at any time, but seems to be consistent for now.
|
||||||
|
*
|
||||||
|
* If something does not add up, {@link MtCuteUnsupportedError} is thrown
|
||||||
|
*/
|
||||||
|
get chosenIndexesAuto(): number[] {
|
||||||
|
return this.raw.options.map((buf) => {
|
||||||
|
if (buf.length > 1)
|
||||||
|
throw new MtCuteUnsupportedError('option had >1 byte')
|
||||||
|
if (buf[0] < 48 || buf[0] > 57)
|
||||||
|
throw new MtCuteUnsupportedError(
|
||||||
|
'option had first byte out of 0-9 range'
|
||||||
|
)
|
||||||
|
|
||||||
|
return buf[0] - 48
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
makeInspectable(PollVoteUpdate)
|
Loading…
Reference in a new issue