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 {
|
||||
_: 'pollAnswer',
|
||||
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,
|
||||
answers,
|
||||
closePeriod: media.closePeriod,
|
||||
closeDate: normalizeDate(media.closeDate)
|
||||
closeDate: normalizeDate(media.closeDate),
|
||||
},
|
||||
correctAnswers: correct,
|
||||
solution,
|
||||
solutionEntities
|
||||
solutionEntities,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ export class InlineQuery {
|
|||
if (this.raw.geo?._ !== 'geoPoint') return null
|
||||
|
||||
if (!this._location) {
|
||||
this._location = new Location(this.raw.geo)
|
||||
this._location = new Location(this.client, this.raw.geo)
|
||||
}
|
||||
|
||||
return this._location
|
||||
|
|
|
@ -10,3 +10,7 @@ export * from './voice'
|
|||
export * from './sticker'
|
||||
export * from './input-media'
|
||||
export * from './venue'
|
||||
export * from './poll'
|
||||
export * from './invoice'
|
||||
export * from './game'
|
||||
export * from './web-page'
|
||||
|
|
|
@ -21,14 +21,14 @@ import {
|
|||
LiveLocation,
|
||||
Sticker,
|
||||
Voice,
|
||||
InputMediaLike, Venue,
|
||||
InputMediaLike,
|
||||
Venue,
|
||||
Poll,
|
||||
Invoice,
|
||||
Game,
|
||||
WebPage
|
||||
} from '../media'
|
||||
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
|
||||
|
|
|
@ -7,3 +7,5 @@ chat_member: ChatMemberUpdate = ChatMemberUpdate
|
|||
inline_query = InlineQuery
|
||||
chosen_inline_result = ChosenInlineResult
|
||||
callback_query = CallbackQuery
|
||||
poll: PollUpdate = PollUpdate
|
||||
poll_vote = PollVoteUpdate
|
||||
|
|
|
@ -8,12 +8,16 @@ import {
|
|||
InlineQueryHandler,
|
||||
ChosenInlineResultHandler,
|
||||
CallbackQueryHandler,
|
||||
PollUpdateHandler,
|
||||
PollVoteHandler,
|
||||
} from './handler'
|
||||
// end-codegen-imports
|
||||
import { filters, UpdateFilter } from './filters'
|
||||
import { CallbackQuery, InlineQuery, Message } from '@mtcute/client'
|
||||
import { ChatMemberUpdate } from './updates'
|
||||
import { ChosenInlineResult } from './updates/chosen-inline-result'
|
||||
import { PollUpdate } from './updates/poll-update'
|
||||
import { PollVoteUpdate } from './updates/poll-vote'
|
||||
|
||||
function _create<T extends UpdateHandler>(
|
||||
type: T['type'],
|
||||
|
@ -235,5 +239,57 @@ export namespace handlers {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -22,12 +22,16 @@ import {
|
|||
InlineQueryHandler,
|
||||
ChosenInlineResultHandler,
|
||||
CallbackQueryHandler,
|
||||
PollUpdateHandler,
|
||||
PollVoteHandler,
|
||||
} from './handler'
|
||||
// end-codegen-imports
|
||||
import { filters, UpdateFilter } from './filters'
|
||||
import { handlers } from './builders'
|
||||
import { ChatMemberUpdate } from './updates'
|
||||
import { ChosenInlineResult } from './updates/chosen-inline-result'
|
||||
import { PollUpdate } from './updates/poll-update'
|
||||
import { PollVoteUpdate } from './updates/poll-vote'
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
|
@ -88,6 +92,14 @@ const PARSERS: Partial<
|
|||
],
|
||||
updateBotCallbackQuery: callbackQueryParser,
|
||||
updateInlineBotCallbackQuery: callbackQueryParser,
|
||||
updateMessagePoll: [
|
||||
'poll',
|
||||
(client, upd, users) => new PollUpdate(client, upd as any, users),
|
||||
],
|
||||
updateMessagePollVote: [
|
||||
'poll_vote',
|
||||
(client, upd, users) => new PollVoteUpdate(client, upd as any, users),
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -599,5 +611,61 @@ export class Dispatcher {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -17,14 +17,14 @@ import {
|
|||
User, Venue,
|
||||
Video,
|
||||
Voice,
|
||||
Poll,
|
||||
Invoice,
|
||||
Game,
|
||||
WebPage
|
||||
} 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 { ChatMemberUpdate } from './updates'
|
||||
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`
|
||||
|
|
|
@ -9,6 +9,8 @@ import { tl } from '@mtcute/tl'
|
|||
import { PropagationSymbol } from './propagation'
|
||||
import { ChatMemberUpdate } from './updates'
|
||||
import { ChosenInlineResult } from './updates/chosen-inline-result'
|
||||
import { PollUpdate } from './updates/poll-update'
|
||||
import { PollVoteUpdate } from './updates/poll-vote'
|
||||
|
||||
interface BaseUpdateHandler<Type, Handler, Checker> {
|
||||
type: Type
|
||||
|
@ -66,6 +68,11 @@ export type CallbackQueryHandler<T = CallbackQuery> = ParsedUpdateHandler<
|
|||
'callback_query',
|
||||
T
|
||||
>
|
||||
export type PollUpdateHandler<T = PollUpdate> = ParsedUpdateHandler<'poll', T>
|
||||
export type PollVoteHandler<T = PollVoteUpdate> = ParsedUpdateHandler<
|
||||
'poll_vote',
|
||||
T
|
||||
>
|
||||
|
||||
export type UpdateHandler =
|
||||
| RawUpdateHandler
|
||||
|
@ -75,5 +82,7 @@ export type UpdateHandler =
|
|||
| InlineQueryHandler
|
||||
| ChosenInlineResultHandler
|
||||
| CallbackQueryHandler
|
||||
| PollUpdateHandler
|
||||
| PollVoteHandler
|
||||
|
||||
// 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