From d36c1781bdd14855a0bf60787a7226acfe1bd2ba Mon Sep 17 00:00:00 2001 From: teidesu Date: Fri, 7 May 2021 15:37:17 +0300 Subject: [PATCH] feat(dispatcher): support poll related updates also fixed a few type and export issues, and changed poll option generation to match tdlib and others --- .../methods/files/normalize-input-media.ts | 7 +- .../client/src/types/bots/inline-query.ts | 2 +- packages/client/src/types/media/index.ts | 4 + packages/client/src/types/messages/message.ts | 12 +-- packages/dispatcher/scripts/update-types.txt | 2 + packages/dispatcher/src/builders.ts | 56 ++++++++++++ packages/dispatcher/src/dispatcher.ts | 68 ++++++++++++++ packages/dispatcher/src/filters.ts | 8 +- packages/dispatcher/src/handler.ts | 9 ++ .../dispatcher/src/updates/poll-update.ts | 73 +++++++++++++++ packages/dispatcher/src/updates/poll-vote.ts | 89 +++++++++++++++++++ 11 files changed, 316 insertions(+), 14 deletions(-) create mode 100644 packages/dispatcher/src/updates/poll-update.ts create mode 100644 packages/dispatcher/src/updates/poll-vote.ts diff --git a/packages/client/src/methods/files/normalize-input-media.ts b/packages/client/src/methods/files/normalize-input-media.ts index f9e6e798..750f520e 100644 --- a/packages/client/src/methods/files/normalize-input-media.ts +++ b/packages/client/src/methods/files/normalize-input-media.ts @@ -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, } } diff --git a/packages/client/src/types/bots/inline-query.ts b/packages/client/src/types/bots/inline-query.ts index 7ceb9427..5ca91609 100644 --- a/packages/client/src/types/bots/inline-query.ts +++ b/packages/client/src/types/bots/inline-query.ts @@ -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 diff --git a/packages/client/src/types/media/index.ts b/packages/client/src/types/media/index.ts index 8d7cb053..f35d4fa6 100644 --- a/packages/client/src/types/media/index.ts +++ b/packages/client/src/types/media/index.ts @@ -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' diff --git a/packages/client/src/types/messages/message.ts b/packages/client/src/types/messages/message.ts index f7495b18..145bb6da 100644 --- a/packages/client/src/types/messages/message.ts +++ b/packages/client/src/types/messages/message.ts @@ -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 diff --git a/packages/dispatcher/scripts/update-types.txt b/packages/dispatcher/scripts/update-types.txt index 56bb32ae..0d926905 100644 --- a/packages/dispatcher/scripts/update-types.txt +++ b/packages/dispatcher/scripts/update-types.txt @@ -7,3 +7,5 @@ chat_member: ChatMemberUpdate = ChatMemberUpdate inline_query = InlineQuery chosen_inline_result = ChosenInlineResult callback_query = CallbackQuery +poll: PollUpdate = PollUpdate +poll_vote = PollVoteUpdate diff --git a/packages/dispatcher/src/builders.ts b/packages/dispatcher/src/builders.ts index fd34fbb5..f9e6ad3d 100644 --- a/packages/dispatcher/src/builders.ts +++ b/packages/dispatcher/src/builders.ts @@ -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( 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( + filter: UpdateFilter, + handler: PollUpdateHandler>['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( + filter: UpdateFilter, + handler: PollVoteHandler< + filters.Modify + >['callback'] + ): PollVoteHandler + + /** @internal */ + export function pollVote(filter: any, handler?: any): PollVoteHandler { + return _create('poll_vote', filter, handler) + } + // end-codegen } diff --git a/packages/dispatcher/src/dispatcher.ts b/packages/dispatcher/src/dispatcher.ts index d6a681e7..1adaa8bf 100644 --- a/packages/dispatcher/src/dispatcher.ts +++ b/packages/dispatcher/src/dispatcher.ts @@ -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( + filter: UpdateFilter, + handler: PollUpdateHandler>['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( + filter: UpdateFilter, + handler: PollVoteHandler< + filters.Modify + >['callback'], + group?: number + ): void + + /** @internal */ + onPollVote(filter: any, handler?: any, group?: number): void { + this._addKnownHandler('pollVote', filter, handler, group) + } + // end-codegen } diff --git a/packages/dispatcher/src/filters.ts b/packages/dispatcher/src/filters.ts index fe79a32d..6f8b4897 100644 --- a/packages/dispatcher/src/filters.ts +++ b/packages/dispatcher/src/filters.ts @@ -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` diff --git a/packages/dispatcher/src/handler.ts b/packages/dispatcher/src/handler.ts index d669dbdc..8c1bd4ab 100644 --- a/packages/dispatcher/src/handler.ts +++ b/packages/dispatcher/src/handler.ts @@ -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: Type @@ -66,6 +68,11 @@ export type CallbackQueryHandler = ParsedUpdateHandler< 'callback_query', T > +export type PollUpdateHandler = ParsedUpdateHandler<'poll', T> +export type PollVoteHandler = ParsedUpdateHandler< + 'poll_vote', + T +> export type UpdateHandler = | RawUpdateHandler @@ -75,5 +82,7 @@ export type UpdateHandler = | InlineQueryHandler | ChosenInlineResultHandler | CallbackQueryHandler + | PollUpdateHandler + | PollVoteHandler // end-codegen diff --git a/packages/dispatcher/src/updates/poll-update.ts b/packages/dispatcher/src/updates/poll-update.ts new file mode 100644 index 00000000..62c8a481 --- /dev/null +++ b/packages/dispatcher/src/updates/poll-update.ts @@ -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 + + constructor (client: TelegramClient, raw: tl.RawUpdateMessagePoll, users: Record) { + 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) diff --git a/packages/dispatcher/src/updates/poll-vote.ts b/packages/dispatcher/src/updates/poll-vote.ts new file mode 100644 index 00000000..95c3b75f --- /dev/null +++ b/packages/dispatcher/src/updates/poll-vote.ts @@ -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 + + constructor(client: TelegramClient, raw: tl.RawUpdateMessagePollVote, users: Record) { + 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)