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:
teidesu 2021-05-07 15:37:17 +03:00
parent 6db771e3da
commit d36c1781bd
11 changed files with 316 additions and 14 deletions

View file

@ -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,
}
}

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -7,3 +7,5 @@ chat_member: ChatMemberUpdate = ChatMemberUpdate
inline_query = InlineQuery
chosen_inline_result = ChosenInlineResult
callback_query = CallbackQuery
poll: PollUpdate = PollUpdate
poll_vote = PollVoteUpdate

View file

@ -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
}

View file

@ -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
}

View file

@ -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`

View file

@ -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

View 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)

View 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)