feat(client): a lot of changes

- support web documents
 - support previews for locations
 - unify media interfaces, merge everything into sendMedia
 - support invoices, polls, venues (both sending and receiving)
This commit is contained in:
teidesu 2021-05-07 14:26:29 +03:00
parent 169d95d6ed
commit 6db771e3da
23 changed files with 1319 additions and 518 deletions

View file

@ -72,8 +72,6 @@ import { _parseEntities } from './methods/messages/parse-entities'
import { pinMessage } from './methods/messages/pin-message'
import { searchGlobal } from './methods/messages/search-global'
import { searchMessages } from './methods/messages/search-messages'
import { sendDice } from './methods/messages/send-dice'
import { sendLocation } from './methods/messages/send-location'
import { sendMediaGroup } from './methods/messages/send-media-group'
import { sendMedia } from './methods/messages/send-media'
import { sendText } from './methods/messages/send-text'
@ -1662,83 +1660,6 @@ export interface TelegramClient extends BaseTelegramClient {
chunkSize?: number
}
): AsyncIterableIterator<Message>
/**
* Send an animated dice with a random value.
*
* For convenience, known dice emojis are available
* as static members of {@link Dice}.
*
* Note that dice result value is generated randomly on the server,
* you can't influence it in any way!
*
* @param chatId ID of the chat, its username, phone or `"me"` or `"self"`
* @param emoji Emoji representing a dice
* @param params Additional sending parameters
* @link Dice
*/
sendDice(
chatId: InputPeerLike,
emoji: string,
params?: {
/**
* Message to reply to. Either a message object or message ID.
*/
replyTo?: number | Message
/**
* Whether to send this message silently.
*/
silent?: boolean
/**
* If set, the message will be scheduled to this date.
* When passing a number, a UNIX time in ms is expected.
*/
schedule?: Date | number
/**
* For bots: inline or reply markup or an instruction
* to hide a reply keyboard or to force a reply.
*/
replyMarkup?: ReplyMarkup
}
): Promise<Message>
/**
* Send a static geo location.
*
* @param chatId ID of the chat, its username, phone or `"me"` or `"self"`
* @param latitude Latitude of the location
* @param longitude Longitude of the location
* @param params Additional sending parameters
*/
sendLocation(
chatId: InputPeerLike,
latitude: number,
longitude: number,
params?: {
/**
* Message to reply to. Either a message object or message ID.
*/
replyTo?: number | Message
/**
* Whether to send this message silently.
*/
silent?: boolean
/**
* If set, the message will be scheduled to this date.
* When passing a number, a UNIX time in ms is expected.
*/
schedule?: Date | number
/**
* For bots: inline or reply markup or an instruction
* to hide a reply keyboard or to force a reply.
*/
replyMarkup?: ReplyMarkup
}
): Promise<Message>
/**
* Send a group of media.
*
@ -2322,8 +2243,6 @@ export class TelegramClient extends BaseTelegramClient {
pinMessage = pinMessage
searchGlobal = searchGlobal
searchMessages = searchMessages
sendDice = sendDice
sendLocation = sendLocation
sendMediaGroup = sendMediaGroup
sendMedia = sendMedia
sendText = sendText

View file

@ -8,7 +8,7 @@ import {
FileDownloadParameters,
FileLocation,
} from '../../types'
import { fileIdToInputFileLocation } from '@mtcute/file-id'
import { fileIdToInputFileLocation, fileIdToInputWebFileLocation, parseFileId } from '@mtcute/file-id'
/**
* Download a file and return it as an iterable, which yields file contents
@ -39,30 +39,37 @@ export async function* downloadAsIterable(
let dcId = params.dcId
let fileSize = params.fileSize
let location = params.location
if (location instanceof FileLocation) {
if (typeof location.location === 'function') {
;(location as tl.Mutable<FileLocation>).location = location.location()
const input = params.location
let location: tl.TypeInputFileLocation | tl.TypeInputWebFileLocation
if (input instanceof FileLocation) {
if (typeof input.location === 'function') {
;(input as tl.Mutable<FileLocation>).location = input.location()
}
if (location.location instanceof Buffer) {
yield location.location
if (input.location instanceof Buffer) {
yield input.location
return
}
if (!dcId) dcId = location.dcId
if (!fileSize) fileSize = location.fileSize
location = location.location as any
}
if (typeof location === 'string') {
location = fileIdToInputFileLocation(location)
if (!dcId) dcId = input.dcId
if (!fileSize) fileSize = input.fileSize
location = input.location as any
} else if (typeof input === 'string') {
const parsed = parseFileId(input)
if (parsed.location._ === 'web') {
location = fileIdToInputWebFileLocation(parsed)
} else {
location = fileIdToInputFileLocation(parsed)
}
} else location = input
const isWeb = tl.isAnyInputWebFileLocation(location)
// we will receive a FileMigrateError in case this is invalid
if (!dcId) dcId = this._primaryDc.id
const chunkSize = partSizeKb * 1024
const limit =
let limit =
params.limit ??
(fileSize
? // derive limit from chunk size, file size and offset
@ -77,13 +84,13 @@ export async function* downloadAsIterable(
}
const requestCurrent = async (): Promise<Buffer> => {
let result: tl.RpcCallReturn['upload.getFile']
let result: tl.RpcCallReturn['upload.getFile'] | tl.RpcCallReturn['upload.getWebFile']
try {
result = await connection.sendForResult({
_: 'upload.getFile',
location: location as tl.TypeInputFileLocation,
_: isWeb ? 'upload.getWebFile' : 'upload.getFile',
location: location as any,
offset,
limit: chunkSize,
limit: chunkSize
})
} catch (e) {
if (e instanceof FileMigrateError) {
@ -94,18 +101,25 @@ export async function* downloadAsIterable(
}
return requestCurrent()
} else if (e instanceof FilerefUpgradeNeededError) {
// todo: implement once messages api is ready
// todo: implement someday
// see: https://github.com/LonamiWebs/Telethon/blob/0e8bd8248cc649637b7c392616887c50986427a0/telethon/client/downloads.py#L99
throw new MtCuteUnsupportedError('File ref expired!')
} else throw e
}
if (result._ === 'upload.fileCdnRedirect') {
// we shouldnt receive them since cdnSupported is not set in the getFile request.
// also, i couldnt find any media that would be downloaded from cdn, so even if
// i implemented that, i wouldnt be able to test that, so :shrug:
throw new MtCuteUnsupportedError(
'Received CDN redirect, which is not supported (yet)'
)
}
if (result._ === 'upload.webFile' && result.size && limit === Infinity) {
limit = result.size
}
return result.bytes
}

View file

@ -14,6 +14,8 @@ import {
} from '@mtcute/file-id'
import { extractFileName } from '../../utils/file-utils'
import { assertTypeIs } from '../../utils/type-assertion'
import bigInt from 'big-integer'
import { normalizeDate } from '../../utils/misc-utils'
/**
* Normalize an {@link InputMediaLike} to `InputMedia`,
@ -25,6 +27,7 @@ export async function _normalizeInputMedia(
this: TelegramClient,
media: InputMediaLike,
params: {
parseMode?: string | null
progressCallback?: (uploaded: number, total: number) => void
},
uploadMedia = false
@ -35,6 +38,160 @@ export async function _normalizeInputMedia(
if (tl.isAnyInputMedia(media)) return media
if (media.type === 'venue') {
return {
_: 'inputMediaVenue',
geoPoint: {
_: 'inputGeoPoint',
lat: media.latitude,
long: media.longitude,
},
title: media.title,
address: media.address,
provider: media.source?.provider ?? '',
venueId: media.source?.id ?? '',
venueType: media.source?.type ?? '',
}
}
if (media.type === 'geo') {
return {
_: 'inputMediaGeoPoint',
geoPoint: {
_: 'inputGeoPoint',
lat: media.latitude,
long: media.longitude,
},
}
}
if (media.type === 'geo_live') {
return {
_: 'inputMediaGeoLive',
geoPoint: {
_: 'inputGeoPoint',
lat: media.latitude,
long: media.longitude,
},
heading: media.heading,
period: media.period,
proximityNotificationRadius: media.proximityNotificationRadius,
}
}
if (media.type === 'dice') {
return {
_: 'inputMediaDice',
emoticon: media.emoji,
}
}
if (media.type === 'contact') {
return {
_: 'inputMediaContact',
phoneNumber: media.phone,
firstName: media.firstName,
lastName: media.lastName ?? '',
vcard: media.vcard ?? '',
}
}
if (media.type === 'game') {
return {
_: 'inputMediaGame',
id:
typeof media.game === 'string'
? {
_: 'inputGameShortName',
botId: { _: 'inputUserSelf' },
shortName: media.game,
}
: media.game,
}
}
if (media.type === 'invoice') {
return {
_: 'inputMediaInvoice',
title: media.title,
description: media.description,
photo:
typeof media.photo === 'string'
? {
_: 'inputWebDocument',
url: media.photo,
mimeType: 'image/jpeg',
size: 0,
attributes: [],
}
: media.photo,
invoice: media.invoice,
payload: media.payload,
provider: media.token,
providerData: {
_: 'dataJSON',
data: JSON.stringify(media.providerData),
},
startParam: media.startParam,
}
}
if (media.type === 'poll' || media.type === 'quiz') {
const answers: tl.TypePollAnswer[] = media.answers.map((ans, idx) => {
if (typeof ans === 'string') {
return {
_: 'pollAnswer',
text: ans,
option: Buffer.from([idx]),
}
}
return ans
})
let correct: Buffer[] | undefined = undefined
let solution: string | undefined = undefined
let solutionEntities: tl.TypeMessageEntity[] | undefined = undefined
if (media.type === 'quiz') {
let input = media.correct
if (!Array.isArray(input)) input = [input]
correct = input.map((it) => {
if (typeof it === 'number') {
return answers[it].option
}
return it
})
if (media.solution) {
;[solution, solutionEntities] = await this._parseEntities(
media.solution,
params.parseMode,
media.solutionEntities
)
}
}
return {
_: 'inputMediaPoll',
poll: {
_: 'poll',
id: bigInt.zero,
publicVoters: media.public,
multipleChoice: media.multiple,
quiz: media.type === 'quiz',
question: media.question,
answers,
closePeriod: media.closePeriod,
closeDate: normalizeDate(media.closeDate)
},
correctAnswers: correct,
solution,
solutionEntities
}
}
let inputFile: tl.TypeInputFile | undefined = undefined
let thumb: tl.TypeInputFile | undefined = undefined
let mime = 'application/octet-stream'
@ -56,18 +213,29 @@ export async function _normalizeInputMedia(
mime = uploaded.mime
}
const uploadMediaIfNeeded = async (inputMedia: tl.TypeInputMedia, photo: boolean): Promise<tl.TypeInputMedia> => {
const uploadMediaIfNeeded = async (
inputMedia: tl.TypeInputMedia,
photo: boolean
): Promise<tl.TypeInputMedia> => {
if (!uploadMedia) return inputMedia
const res = await this.call({
_: 'messages.uploadMedia',
peer: { _: 'inputPeerSelf' },
media: inputMedia
media: inputMedia,
})
if (photo) {
assertTypeIs('normalizeInputMedia (@ messages.uploadMedia)', res, 'messageMediaPhoto')
assertTypeIs('normalizeInputMedia (@ messages.uploadMedia)', res.photo!, 'photo')
assertTypeIs(
'normalizeInputMedia (@ messages.uploadMedia)',
res,
'messageMediaPhoto'
)
assertTypeIs(
'normalizeInputMedia (@ messages.uploadMedia)',
res.photo!,
'photo'
)
return {
_: 'inputMediaPhoto',
@ -75,13 +243,21 @@ export async function _normalizeInputMedia(
_: 'inputPhoto',
id: res.photo.id,
accessHash: res.photo.accessHash,
fileReference: res.photo.fileReference
fileReference: res.photo.fileReference,
},
ttlSeconds: media.ttlSeconds
ttlSeconds: media.ttlSeconds,
}
} else {
assertTypeIs('normalizeInputMedia (@ messages.uploadMedia)', res, 'messageMediaDocument')
assertTypeIs('normalizeInputMedia (@ messages.uploadMedia)', res.document!, 'document')
assertTypeIs(
'normalizeInputMedia (@ messages.uploadMedia)',
res,
'messageMediaDocument'
)
assertTypeIs(
'normalizeInputMedia (@ messages.uploadMedia)',
res.document!,
'document'
)
return {
_: 'inputMediaDocument',
@ -89,9 +265,9 @@ export async function _normalizeInputMedia(
_: 'inputDocument',
id: res.document.id,
accessHash: res.document.accessHash,
fileReference: res.document.fileReference
fileReference: res.document.fileReference,
},
ttlSeconds: media.ttlSeconds
ttlSeconds: media.ttlSeconds,
}
}
}
@ -99,13 +275,16 @@ export async function _normalizeInputMedia(
const input = media.file
if (tdFileId.isFileIdLike(input)) {
if (typeof input === 'string' && input.match(/^https?:\/\//)) {
return uploadMediaIfNeeded({
return uploadMediaIfNeeded(
{
_:
media.type === 'photo'
? 'inputMediaPhotoExternal'
: 'inputMediaDocumentExternal',
url: input,
}, media.type === 'photo')
},
media.type === 'photo'
)
} else if (typeof input === 'string' && input.match(/^file:/)) {
await upload(input.substr(5))
} else {
@ -118,13 +297,16 @@ export async function _normalizeInputMedia(
id: fileIdToInputPhoto(parsed),
}
} else if (parsed.location._ === 'web') {
return uploadMediaIfNeeded({
return uploadMediaIfNeeded(
{
_:
parsed.type === tdFileId.FileType.Photo
? 'inputMediaPhotoExternal'
: 'inputMediaDocumentExternal',
url: parsed.location.url,
}, parsed.type === tdFileId.FileType.Photo)
},
parsed.type === tdFileId.FileType.Photo
)
} else {
return {
_: 'inputMediaDocument',
@ -146,11 +328,14 @@ export async function _normalizeInputMedia(
if (!inputFile) throw new Error('should not happen')
if (media.type === 'photo') {
return uploadMediaIfNeeded({
return uploadMediaIfNeeded(
{
_: 'inputMediaUploadedPhoto',
file: inputFile,
ttlSeconds: media.ttlSeconds,
}, true)
},
true
)
}
if ('thumb' in media && media.thumb) {
@ -204,7 +389,8 @@ export async function _normalizeInputMedia(
})
}
return uploadMediaIfNeeded({
return uploadMediaIfNeeded(
{
_: 'inputMediaUploadedDocument',
nosoundVideo: media.type === 'video' && media.isAnimated,
forceFile: media.type === 'document',
@ -212,6 +398,8 @@ export async function _normalizeInputMedia(
thumb,
mimeType: mime,
attributes,
ttlSeconds: media.ttlSeconds
}, false)
ttlSeconds: media.ttlSeconds,
},
false
)
}

View file

@ -97,7 +97,10 @@ export async function editInlineMessage(
if (params.media) {
media = await this._normalizeInputMedia(params.media, params, true)
if ('caption' in params.media) {
// if there's no caption in input media (i.e. not present or undefined),
// user wants to keep current caption, thus `content` needs to stay `undefined`
if ('caption' in params.media && params.media.caption !== undefined) {
;[content, entities] = await this._parseEntities(
params.media.caption,
params.parseMode,

View file

@ -84,11 +84,16 @@ export async function editMessage(
if (params.media) {
media = await this._normalizeInputMedia(params.media, params)
// if there's no caption in input media (i.e. not present or undefined),
// user wants to keep current caption, thus `content` needs to stay `undefined`
if ('caption' in params.media && params.media.caption !== undefined) {
;[content, entities] = await this._parseEntities(
params.media.caption,
params.parseMode,
params.media.entities
)
}
} else {
;[content, entities] = await this._parseEntities(
params.text,
@ -105,7 +110,7 @@ export async function editMessage(
replyMarkup: BotKeyboard._convertToTl(params.replyMarkup),
message: content,
entities,
media
media,
})
return this._findMessageInUpdate(res, true) as any

View file

@ -1,74 +0,0 @@
import { TelegramClient } from '../../client'
import { BotKeyboard, InputPeerLike, Message, ReplyMarkup } from '../../types'
import { normalizeDate, randomUlong } from '../../utils/misc-utils'
import { normalizeToInputPeer } from '../../utils/peer-utils'
/**
* Send an animated dice with a random value.
*
* For convenience, known dice emojis are available
* as static members of {@link Dice}.
*
* Note that dice result value is generated randomly on the server,
* you can't influence it in any way!
*
* @param chatId ID of the chat, its username, phone or `"me"` or `"self"`
* @param emoji Emoji representing a dice
* @param params Additional sending parameters
* @link Dice
* @internal
*/
export async function sendDice(
this: TelegramClient,
chatId: InputPeerLike,
emoji: string,
params?: {
/**
* Message to reply to. Either a message object or message ID.
*/
replyTo?: number | Message
/**
* Whether to send this message silently.
*/
silent?: boolean
/**
* If set, the message will be scheduled to this date.
* When passing a number, a UNIX time in ms is expected.
*/
schedule?: Date | number
/**
* For bots: inline or reply markup or an instruction
* to hide a reply keyboard or to force a reply.
*/
replyMarkup?: ReplyMarkup
}
): Promise<Message> {
if (!params) params = {}
const peer = normalizeToInputPeer(await this.resolvePeer(chatId))
const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup)
const res = await this.call({
_: 'messages.sendMedia',
peer,
media: {
_: 'inputMediaDice',
emoticon: emoji,
},
silent: params.silent,
replyToMsgId: params.replyTo
? typeof params.replyTo === 'number'
? params.replyTo
: params.replyTo.id
: undefined,
randomId: randomUlong(),
scheduleDate: normalizeDate(params.schedule),
replyMarkup,
message: '',
})
return this._findMessageInUpdate(res)
}

View file

@ -1,73 +0,0 @@
import { BotKeyboard, InputPeerLike, Message, ReplyMarkup } from '../../types'
import { TelegramClient } from '../../client'
import { normalizeToInputPeer } from '../../utils/peer-utils'
import { normalizeDate, randomUlong } from '../../utils/misc-utils'
/**
* Send a static geo location.
*
* @param chatId ID of the chat, its username, phone or `"me"` or `"self"`
* @param latitude Latitude of the location
* @param longitude Longitude of the location
* @param params Additional sending parameters
* @internal
*/
export async function sendLocation(
this: TelegramClient,
chatId: InputPeerLike,
latitude: number,
longitude: number,
params?: {
/**
* Message to reply to. Either a message object or message ID.
*/
replyTo?: number | Message
/**
* Whether to send this message silently.
*/
silent?: boolean
/**
* If set, the message will be scheduled to this date.
* When passing a number, a UNIX time in ms is expected.
*/
schedule?: Date | number
/**
* For bots: inline or reply markup or an instruction
* to hide a reply keyboard or to force a reply.
*/
replyMarkup?: ReplyMarkup
}
): Promise<Message> {
if (!params) params = {}
const peer = normalizeToInputPeer(await this.resolvePeer(chatId))
const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup)
const res = await this.call({
_: 'messages.sendMedia',
peer,
media: {
_: 'inputMediaGeoPoint',
geoPoint: {
_: 'inputGeoPoint',
lat: latitude,
long: longitude
}
},
silent: params.silent,
replyToMsgId: params.replyTo
? typeof params.replyTo === 'number'
? params.replyTo
: params.replyTo.id
: undefined,
randomId: randomUlong(),
scheduleDate: normalizeDate(params.schedule),
replyMarkup,
message: '',
})
return this._findMessageInUpdate(res)
}

View file

@ -63,7 +63,11 @@ export async function sendMediaGroup(
* @param uploaded Number of bytes already uploaded
* @param total Total file size
*/
progressCallback?: (index: number, uploaded: number, total: number) => void
progressCallback?: (
index: number,
uploaded: number,
total: number
) => void
/**
* Whether to clear draft after sending this message.
@ -83,13 +87,16 @@ export async function sendMediaGroup(
for (let i = 0; i < medias.length; i++) {
const media = medias[i]
const inputMedia = await this._normalizeInputMedia(media, {
progressCallback: params.progressCallback?.bind(null, i)
progressCallback: params.progressCallback?.bind(null, i),
})
const [message, entities] = await this._parseEntities(
media.caption,
// some types dont have `caption` field, and ts warns us,
// but since it's JS, they'll just be `undefined` and properly
// handled by _parseEntities method
(media as any).caption,
params.parseMode,
media.entities
(media as any).entities
)
multiMedia.push({
@ -97,7 +104,7 @@ export async function sendMediaGroup(
randomId: randomUlong(),
media: inputMedia,
message,
entities
entities,
})
}

View file

@ -86,9 +86,12 @@ export async function sendMedia(
const inputMedia = await this._normalizeInputMedia(media, params)
const [message, entities] = await this._parseEntities(
media.caption,
// some types dont have `caption` field, and ts warns us,
// but since it's JS, they'll just be `undefined` and properly
// handled by _parseEntities method
(media as any).caption,
params.parseMode,
media.entities
(media as any).entities
)
const peer = normalizeToInputPeer(await this.resolvePeer(chatId))

View file

@ -1,6 +1,12 @@
import { tl } from '@mtcute/tl'
import { BotKeyboard, ReplyMarkup } from '../keyboards'
import { TelegramClient } from '../../../client'
import {
InputMediaGeo,
InputMediaGeoLive,
InputMediaVenue,
Venue,
} from '../../media'
/**
* Inline message containing only text
@ -57,37 +63,17 @@ export interface InputInlineMessageMedia {
/**
* Inline message containing a geolocation
*/
export interface InputInlineMessageGeo {
type: 'geo'
export interface InputInlineMessageGeo extends InputMediaGeo {
/**
* Latitude of the geolocation
* Message's reply markup
*/
latitude: number
replyMarkup?: ReplyMarkup
}
/**
* Longitude of the geolocation
/**
* Inline message containing a live geolocation
*/
longitude: number
/**
* For live locations, direction in which the location
* moves, in degrees (1-360)
*/
heading?: number
/**
* For live locations, period for which this live location
* will be updated
*/
period?: number
/**
* For live locations, a maximum distance to another
* chat member for proximity alerts, in meters (0-100000)
*/
proximityNotificationRadius?: number
export interface InputInlineMessageGeoLive extends InputMediaGeoLive {
/**
* Message's reply markup
*/
@ -97,54 +83,7 @@ export interface InputInlineMessageGeo {
/**
* Inline message containing a venue
*/
export interface InputInlineMessageVenue {
type: 'venue'
/**
* Latitude of the geolocation
*/
latitude: number
/**
* Longitude of the geolocation
*/
longitude: number
/**
* Venue name
*/
title: string
/**
* Venue address
*/
address: string
/**
* When available, source from where this venue was acquired
*/
source?: {
/**
* Provider name (`foursquare` or `gplaces` for Google Places)
*/
provider?: 'foursquare' | 'gplaces'
/**
* Venue ID in the provider's DB
*/
id: string
/**
* Venue type in the provider's DB
*
* - [Supported types for Foursquare](https://developer.foursquare.com/docs/build-with-foursquare/categories/)
* (use names, lowercase them, replace spaces and " & " with `_` (underscore) and remove other symbols,
* and use `/` (slash) as hierarchy separator)
* - [Supported types for Google Places](https://developers.google.com/places/web-service/supported_types)
*/
type: string
}
export interface InputInlineMessageVenue extends InputMediaVenue {
/**
* Message's reply markup
*/
@ -167,51 +106,62 @@ export type InputInlineMessage =
| InputInlineMessageText
| InputInlineMessageMedia
| InputInlineMessageGeo
| InputInlineMessageGeoLive
| InputInlineMessageVenue
| InputInlineMessageGame
export namespace BotInlineMessage {
export function text (
export function text(
text: string,
params?: Omit<InputInlineMessageText, 'type' | 'text'>,
params?: Omit<InputInlineMessageText, 'type' | 'text'>
): InputInlineMessageText {
return {
type: 'text',
text,
...(
params || {}
),
...(params || {}),
}
}
export function media (
params?: Omit<InputInlineMessageMedia, 'type'>,
export function media(
params?: Omit<InputInlineMessageMedia, 'type'>
): InputInlineMessageMedia {
return {
type: 'media',
...(
params || {}
),
...(params || {}),
}
}
export function geo (
export function geo(
latitude: number,
longitude: number,
params?: Omit<InputInlineMessageGeo, 'type' | 'latitude' | 'longitude'>,
params?: Omit<InputInlineMessageGeo, 'type' | 'latitude' | 'longitude'>
): InputInlineMessageGeo {
return {
type: 'geo',
latitude,
longitude,
...(
params || {}
),
...(params || {}),
}
}
export function venue (
params: Omit<InputInlineMessageVenue, 'type'>,
export function geoLive(
latitude: number,
longitude: number,
params?: Omit<
InputInlineMessageGeoLive,
'type' | 'latitude' | 'longitude'
>
): InputInlineMessageGeoLive {
return {
type: 'geo_live',
latitude,
longitude,
...(params || {}),
}
}
export function venue(
params: Omit<InputInlineMessageVenue, 'type'>
): InputInlineMessageVenue {
return {
type: 'venue',
@ -219,8 +169,8 @@ export namespace BotInlineMessage {
}
}
export function game (
params: Omit<InputInlineMessageGame, 'type'>,
export function game(
params: Omit<InputInlineMessageGame, 'type'>
): InputInlineMessageGame {
return {
type: 'game',
@ -228,45 +178,55 @@ export namespace BotInlineMessage {
}
}
export async function _convertToTl (
export async function _convertToTl(
client: TelegramClient,
obj: InputInlineMessage,
parseMode?: string | null,
parseMode?: string | null
): Promise<tl.TypeInputBotInlineMessage> {
if (obj.type === 'text') {
const [message, entities] = await client['_parseEntities'](obj.text, parseMode, obj.entities)
const [message, entities] = await client['_parseEntities'](
obj.text,
parseMode,
obj.entities
)
return {
_: 'inputBotInlineMessageText',
message,
entities,
replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup)
replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup),
}
}
if (obj.type === 'media') {
const [message, entities] = await client['_parseEntities'](obj.text, parseMode, obj.entities)
const [message, entities] = await client['_parseEntities'](
obj.text,
parseMode,
obj.entities
)
return {
_: 'inputBotInlineMessageMediaAuto',
message,
entities,
replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup)
replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup),
}
}
if (obj.type === 'geo') {
if (obj.type === 'geo' || obj.type === 'geo_live') {
return {
_: 'inputBotInlineMessageMediaGeo',
geoPoint: {
_: 'inputGeoPoint',
lat: obj.latitude,
long: obj.longitude
long: obj.longitude,
},
heading: obj.heading,
period: obj.period,
proximityNotificationRadius: obj.proximityNotificationRadius,
replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup)
// fields will be `undefined` if this is a `geo`
heading: (obj as InputMediaGeoLive).heading,
period: (obj as InputMediaGeoLive).period,
proximityNotificationRadius: (obj as InputMediaGeoLive)
.proximityNotificationRadius,
replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup),
}
}
@ -276,21 +236,21 @@ export namespace BotInlineMessage {
geoPoint: {
_: 'inputGeoPoint',
lat: obj.latitude,
long: obj.longitude
long: obj.longitude,
},
title: obj.title,
address: obj.address,
provider: obj.source?.provider ?? '',
venueId: obj.source?.id ?? '',
venueType: obj.source?.type ?? '',
replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup)
replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup),
}
}
if (obj.type === 'game') {
return {
_: 'inputBotInlineMessageGame',
replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup)
replyMarkup: BotKeyboard._convertToTl(obj.replyMarkup),
}
}

View file

@ -27,8 +27,9 @@ export class FileLocation {
*/
readonly location:
| tl.TypeInputFileLocation
| tl.TypeInputWebFileLocation
| Buffer
| (() => tl.TypeInputFileLocation | Buffer)
| (() => tl.TypeInputFileLocation | tl.TypeInputWebFileLocation | Buffer)
/**
* File size in bytes, when available
@ -44,8 +45,9 @@ export class FileLocation {
client: TelegramClient,
location:
| tl.TypeInputFileLocation
| tl.TypeInputWebFileLocation
| Buffer
| (() => tl.TypeInputFileLocation | Buffer),
| (() => tl.TypeInputFileLocation | tl.TypeInputWebFileLocation | Buffer),
fileSize?: number,
dcId?: number
) {
@ -82,7 +84,7 @@ export class FileLocation {
* in chunks of a given size. Order of the chunks is guaranteed to be
* consecutive.
*
* Shorthand for `client.downloadAsStream({ location: this })`
* Shorthand for `client.downloadAsIterable({ location: this })`
*
* @link TelegramClient.downloadAsIterable
*/

View file

@ -52,7 +52,7 @@ export interface FileDownloadParameters {
* File location which should be downloaded.
* You can also provide TDLib and Bot API compatible File ID
*/
location: tl.TypeInputFileLocation | FileLocation | string
location: tl.TypeInputFileLocation | tl.TypeInputWebFileLocation | FileLocation | string
/**
* Total file size, if known.

View file

@ -0,0 +1,67 @@
import { TelegramClient } from '../../client'
import { tl } from '@mtcute/tl'
import { FileLocation } from './file-location'
import { MtCuteArgumentError } from '../errors'
import { makeInspectable } from '../utils'
const STUB_LOCATION = () => {
throw new MtCuteArgumentError(
'This web document is not downloadable through Telegram'
)
}
/**
* An external web document, that is not
* stored on Telegram severs, and is available
* by a HTTP(s) url.
*
* > **Note**: not all web documents are downloadable
* > through Telegram. Media files usually are,
* > and web pages (i.e. `mimeType = text/html`) usually aren't.
* > To be sure, check `isDownloadable` property.
*/
export class WebDocument extends FileLocation {
readonly raw: tl.TypeWebDocument
constructor(client: TelegramClient, raw: tl.TypeWebDocument) {
super(
client,
raw._ === 'webDocument'
? {
_: 'inputWebFileLocation',
url: raw.url,
accessHash: raw.accessHash,
}
: STUB_LOCATION,
raw.size
)
this.raw = raw
}
/**
* URL to the file
*/
get url(): string {
return this.raw.url
}
/**
* MIME type of the file
*/
get mimeType(): string {
return this.raw.mimeType
}
/**
* Whether this file can be downloaded through Telegram.
*
* If `false`, you should use {@link url} to manually
* fetch data via HTTP(s), and trying to use `download*` methods
* will result in an error
*/
get isDownloadable(): boolean {
return this.raw._ === 'webDocument'
}
}
makeInspectable(WebDocument)

View file

@ -9,3 +9,4 @@ export * from './location'
export * from './voice'
export * from './sticker'
export * from './input-media'
export * from './venue'

View file

@ -1,5 +1,7 @@
import { InputFileLike } from '../files'
import { tl } from '@mtcute/tl'
import { Venue } from './venue'
import { MaybeArray } from '@mtcute/core'
interface BaseInputMedia {
/**
@ -233,6 +235,303 @@ export interface InputMediaVideo extends BaseInputMedia {
isRound?: boolean
}
/**
* A geolocation to be sent
*/
export interface InputMediaGeo {
type: 'geo'
/**
* Latitude of the geolocation
*/
latitude: number
/**
* Longitude of the geolocation
*/
longitude: number
/**
* The estimated horizontal accuracy of the
* geolocation, in meters (0-1500)
*/
accuracy?: number
}
/**
* A live geolocation to be sent
*/
export interface InputMediaGeoLive extends Omit<InputMediaGeo, 'type'> {
type: 'geo_live'
/**
* Direction in which the location moves, in degrees (1-360)
*/
heading?: number
/**
* Validity period of the live location
*/
period?: number
/**
* Maximum distance to another chat member for proximity
* alerts, in meters (0-100000)
*/
proximityNotificationRadius?: number
}
/**
* An animated dice with a random value to be sent
*
* For convenience, known dice emojis are available
* as static members of {@link Dice}.
*
* Note that dice result value is generated randomly on the server,
* you can't influence it in any way!
*/
export interface InputMediaDice {
type: 'dice'
/**
* Emoji representing a dice
*/
emoji: string
}
/**
* A venue to be sent
*/
export interface InputMediaVenue {
type: 'venue'
/**
* Latitude of the geolocation
*/
latitude: number
/**
* Longitude of the geolocation
*/
longitude: number
/**
* Venue name
*/
title: string
/**
* Venue address
*/
address: string
/**
* When available, source from where this venue was acquired
*/
source?: Venue.VenueSource
}
/**
* A contact to be sent
*/
export interface InputMediaContact {
type: 'contact'
/**
* Contact's phone number
*/
phone: string
/**
* Contact's first name
*/
firstName: string
/**
* Contact's last name
*/
lastName?: string
/**
* Additional data about the contact
* as a vCard (0-2048 bytes)
*/
vcard?: string
}
/**
* A game to be sent
*/
export interface InputMediaGame {
type: 'game'
/**
* Game's short name, or a TL object with an input game
*/
game: string | tl.TypeInputGame
}
/**
* An invoice to be sent (see https://core.telegram.org/bots/payments)
*/
export interface InputMediaInvoice {
type: 'invoice'
/**
* Product name (1-32 chars)
*/
title: string
/**
* Product description (1-255 chars)
*/
description: string
/**
* The invoice itself
*/
invoice: tl.TypeInvoice
/**
* Bot-defined invoice payload (1-128 bytes).
*
* Will not be displayed to the user and can be used
* for internal processes
*/
payload: Buffer
/**
* Payments provider token, obtained from
* [@BotFather](//t.me/botfather)
*/
token: string
/**
* Data about the invoice as a plain JS object, which
* will be shared with the payment provider. A detailed
* description of required fields should be provided by
* the payment provider.
*/
providerData: any
/**
* Start parameter for the bot
*/
startParam: string
/**
* Product photo. Can be a photo of the goods or a marketing image for a service.
*
* Can be a URL, or a TL object with input web document
*/
photo?: string | tl.TypeInputWebDocument
}
/**
* A simple poll to be sent
*/
export interface InputMediaPoll {
type: 'poll'
/**
* Question of the poll (1-255 chars for users, 1-300 chars for bots)
*/
question: string
/**
* Answers of the poll.
*
* You can either provide a string, or a
* TL object representing an answer.
* Strings will be transformed to TL
* objects, with a single=byte incrementing
* `option` value.
*/
answers: (string | tl.TypePollAnswer)[]
/**
* Whether this is a public poll
* (i.e. users who have voted are visible to everyone)
*/
public?: boolean
/**
* Whether users can select multiple answers
* as an answer
*/
multiple?: boolean
/**
* Amount of time in seconds the poll will be active after creation (5-600).
*
* Can't be used together with `closeDate`.
*/
closePeriod?: number
/**
* Point in time when the poll will be automatically closed.
*
* Must be at least 5 and no more than 600 seconds in the future,
* can't be used together with `closePeriod`.
*
* When `number` is used, UNIX time in ms is expected
*/
closeDate?: number | Date
}
/**
* A quiz to be sent.
*
* Quiz is an extended version of a poll, but quizzes have
* correct answers, and votes can't be retracted from them
*/
export interface InputMediaQuiz extends Omit<InputMediaPoll, 'type'> {
type: 'quiz'
/**
* Correct answer ID(s) or index(es).
*
* > **Note**: even though quizzes can actually
* > only have exactly one correct answer,
* > the API itself has the possibility to pass
* > multiple or zero correct answers,
* > but that would result in `QUIZ_CORRECT_ANSWERS_TOO_MUCH`
* > and `QUIZ_CORRECT_ANSWERS_EMPTY` errors respectively.
* >
* > But since the API has that option, we also provide it,
* > maybe to future-proof this :shrug:
*/
correct: MaybeArray<number | Buffer>
/**
* Explanation of the quiz solution
*/
solution?: string
/**
* Format entities for `solution`.
* If used, parse mode is ignored.
*/
solutionEntities?: tl.TypeMessageEntity[]
}
/**
* Input media that can have a caption.
*
* Note that meta-fields (like `duration`) are only
* applicable if `file` is {@link UploadFileLike},
* otherwise they are ignored.
*
* A subset of {@link InputMediaLike}
*/
export type InputMediaWithCaption =
| InputMediaAudio
| InputMediaVoice
| InputMediaDocument
| InputMediaPhoto
| InputMediaVideo
| InputMediaAuto
/**
* Input media that can be sent somewhere.
*
@ -243,17 +542,24 @@ export interface InputMediaVideo extends BaseInputMedia {
* @link InputMedia
*/
export type InputMediaLike =
| InputMediaAudio
| InputMediaVoice
| InputMediaDocument
| InputMediaPhoto
| InputMediaVideo
| InputMediaAuto
| InputMediaWithCaption
| InputMediaSticker
| InputMediaVenue
| InputMediaGeo
| InputMediaGeoLive
| InputMediaDice
| InputMediaContact
| InputMediaGame
| InputMediaInvoice
| InputMediaPoll
| InputMediaQuiz
| tl.TypeInputMedia
export namespace InputMedia {
type OmitTypeAndFile<T extends InputMediaLike> = Omit<T, 'type' | 'file'>
type OmitTypeAndFile<
T extends InputMediaLike,
K extends keyof T = never
> = Omit<T, 'type' | 'file' | K>
/**
* Create an animation to be sent
@ -354,6 +660,121 @@ export namespace InputMedia {
}
}
/**
* Create a venue to be sent
*/
export function venue(
params: OmitTypeAndFile<InputMediaVenue>
): InputMediaVenue {
return {
type: 'venue',
...params,
}
}
/**
* Create a geolocation to be sent
*/
export function geo(
latitude: number,
longitude: number,
params?: OmitTypeAndFile<InputMediaGeo, 'latitude' | 'longitude'>
): InputMediaGeo {
return {
type: 'geo',
latitude,
longitude,
...(params || {}),
}
}
/**
* Create a live geolocation to be sent
*/
export function geoLive(
latitude: number,
longitude: number,
params?: OmitTypeAndFile<InputMediaGeoLive, 'latitude' | 'longitude'>
): InputMediaGeoLive {
return {
type: 'geo_live',
latitude,
longitude,
...(params || {}),
}
}
/**
* Create a dice to be sent
*
* For convenience, known dice emojis are available
* as static members of {@link Dice}.
*/
export function dice(emoji: string): InputMediaDice {
return {
type: 'dice',
emoji,
}
}
/**
* Create a contact to be sent
*/
export function contact(
params: OmitTypeAndFile<InputMediaContact>
): InputMediaContact {
return {
type: 'contact',
...params,
}
}
/**
* Create a game to be sent
*/
export function game(game: string | tl.TypeInputGame): InputMediaGame {
return {
type: 'game',
game,
}
}
/**
* Create an invoice to be sent
*/
export function invoice(
params: OmitTypeAndFile<InputMediaInvoice>
): InputMediaInvoice {
return {
type: 'invoice',
...params,
}
}
/**
* Create a poll to be sent
*/
export function poll(
params: OmitTypeAndFile<InputMediaPoll>
): InputMediaPoll {
return {
type: 'poll',
...params,
}
}
/**
* Create a quiz to be sent
*/
export function quiz(
params: OmitTypeAndFile<InputMediaQuiz>
): InputMediaQuiz {
return {
type: 'quiz',
...params,
}
}
/**
* Create a document to be sent, which subtype
* is inferred automatically by file contents.

View file

@ -0,0 +1,95 @@
import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client'
import { makeInspectable } from '../utils'
import { WebDocument } from '../files/web-document'
/**
* An invoice
*/
export class Invoice {
readonly client: TelegramClient
readonly raw: tl.RawMessageMediaInvoice
constructor (client: TelegramClient, raw: tl.RawMessageMediaInvoice) {
this.client = client
this.raw = raw
}
/**
* Whether the shipping address was requested
*/
isShippingAddressRequested(): boolean {
return !!this.raw.shippingAddressRequested
}
/**
* Whether this is an example (test) invoice
*/
isTest(): boolean {
return !!this.raw.test
}
/**
* Product name, 1-32 characters
*/
get title(): string {
return this.raw.title
}
/**
* Product description, 1-255 characters
*/
get description(): string {
return this.raw.description
}
private _photo?: WebDocument
/**
* URL of the product photo for the invoice
*/
get photo(): WebDocument | null {
if (!this.raw.photo) return null
if (!this._photo) {
this._photo = new WebDocument(this.client, this.raw.photo)
}
return this._photo
}
/**
* Message ID of receipt
*/
get receiptMessageId(): number | null {
return this.raw.receiptMsgId ?? null
}
/**
* Three-letter ISO 4217 currency code
*/
get currency(): string {
return this.raw.currency
}
/**
* Total price in the smallest units of the currency
* (integer, not float/double). For example, for a price
* of `US$ 1.45` `amount = 145`.
*
* See the exp parameter in [currencies.json](https://core.telegram.org/bots/payments/currencies.json),
* it shows the number of digits past the decimal point
* for each currency (2 for the majority of currencies).
*/
get amount(): tl.Long {
return this.raw.totalAmount
}
/**
* Unique bot deep-linking parameter that can be used to generate this invoice
*/
get startParam(): string {
return this.raw.startParam
}
}
makeInspectable(Invoice)

View file

@ -1,13 +1,17 @@
import { makeInspectable } from '../utils'
import { tl } from '@mtcute/tl'
import { FileLocation } from '../files'
import { TelegramClient } from '../../client'
/**
* A point on the map
*/
export class Location {
readonly client: TelegramClient
readonly geo: tl.RawGeoPoint
constructor(geo: tl.RawGeoPoint) {
constructor(client: TelegramClient, geo: tl.RawGeoPoint) {
this.client = client
this.geo = geo
}
@ -31,13 +35,62 @@ export class Location {
get radius(): number {
return this.geo.accuracyRadius ?? 0
}
/**
* Create {@link FileLocation} containing
* server-generated image with the map preview
*/
preview(params: {
/**
* Map width in pixels before applying scale (16-1024)
*
* Defaults to `128`
*/
width?: number
/**
* Map height in pixels before applying scale (16-1024)
*
* Defaults to `128`
*/
height?: number
/**
* Map zoom level (13-20)
*
* Defaults to `15`
*/
zoom?: number
/**
* Map scale (1-3)
*
* Defaults to `1`
*/
scale?: number
} = {}): FileLocation {
return new FileLocation(this.client, {
_: 'inputWebFileGeoPointLocation',
geoPoint: {
_: 'inputGeoPoint',
lat: this.geo.lat,
long: this.geo.long,
accuracyRadius: this.geo.accuracyRadius
},
accessHash: this.geo.accessHash,
w: params.width ?? 128,
h: params.height ?? 128,
zoom: params.zoom ?? 15,
scale: params.scale ?? 1,
})
}
}
export class LiveLocation extends Location {
readonly live: tl.RawMessageMediaGeoLive
constructor(live: tl.RawMessageMediaGeoLive) {
super(live.geo as tl.RawGeoPoint)
constructor(client: TelegramClient, live: tl.RawMessageMediaGeoLive) {
super(client, live.geo as tl.RawGeoPoint)
this.live = live
}

View file

@ -0,0 +1,185 @@
import { makeInspectable } from '../utils'
import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client'
import { MessageEntity } from '../messages'
export namespace Poll {
export interface PollAnswer {
/**
* Answer text
*/
text: string
/**
* Answer data, to be passed to
* {@link TelegramClient.votePoll}
*/
data: Buffer
/**
* Number of people who has chosen this result.
* If not available (i.e. not voted yet), defaults to `0`
*/
voters: number
/**
* Whether this answer was chosen by the current user
*/
chosen: boolean
/**
* Whether this answer is correct (for quizzes).
* Not available before choosing an answer, and defaults to `false`
*/
correct: boolean
}
}
export class Poll {
readonly client: TelegramClient
readonly raw: tl.TypePoll
readonly results?: tl.TypePollResults
readonly _users: Record<number, tl.TypeUser>
constructor(
client: TelegramClient,
raw: tl.TypePoll,
users: Record<number, tl.TypeUser>,
results?: tl.TypePollResults
) {
this.client = client
this.raw = raw
this._users = users
this.results = results
}
/**
* Unique identifier of the poll
*/
get id(): tl.Long {
return this.raw.id
}
/**
* Poll question
*/
get question(): string {
return this.raw.question
}
private _answers?: Poll.PollAnswer[]
/**
* List of answers in this poll
*/
get answers(): Poll.PollAnswer[] {
if (!this._answers) {
const results = this.results?.results
this._answers = this.raw.answers.map((ans, idx) => {
if (results) {
const res = results[idx]
return {
text: ans.text,
data: ans.option,
voters: res.voters,
chosen: !!res.chosen,
correct: !!res.correct
}
} else {
return {
text: ans.text,
data: ans.option,
voters: 0,
chosen: false,
correct: false
}
}
})
}
return this._answers
}
/**
* Total number of voters in this poll, if available
*/
get voters(): number {
return this.results?.totalVoters ?? 0
}
/**
* Whether this poll is closed, i.e. does not
* accept votes anymore
*/
get isClosed(): boolean {
return !!this.raw.closed
}
/**
* Whether this poll is public, i.e. you
* list of voters is publicly available
*/
get isPublic(): boolean {
return !!this.raw.publicVoters
}
/**
* Whether this is a quiz
*/
get isQuiz(): boolean {
return !!this.raw.quiz
}
/**
* Whether this poll accepts multiple answers
*/
get isMultiple(): boolean {
return !!this.raw.multipleChoice
}
/**
* Solution for the quiz, only available
* in case you have already answered
*/
get solution(): string | null {
return this.results?.solution ?? null
}
private _entities?: MessageEntity[]
/**
* Format entities for {@link solution}, only available
* in case you have already answered
*/
get solutionEntities(): MessageEntity[] | null {
if (!this.results) return null
if (!this._entities) {
this._entities = []
if (this.results.solutionEntities?.length) {
for (const ent of this.results.solutionEntities) {
const parsed = MessageEntity._parse(ent)
if (parsed) this._entities.push(parsed)
}
}
}
return this._entities
}
/**
* Get the solution text formatted with a given parse mode.
* Returns `null` if solution is not available
*
* @param parseMode Parse mode to use (`null` for default)
*/
unparseSolution(parseMode?: string | null): string | null {
if (!this.solution) return null
return this.client
.getParseMode(parseMode)
.unparse(this.solution, this.solutionEntities!)
}
}
makeInspectable(Poll)

View file

@ -0,0 +1,81 @@
import { tl } from '@mtcute/tl'
import { Location } from './location'
import { assertTypeIs } from '../../utils/type-assertion'
import { makeInspectable } from '../utils'
import { TelegramClient } from '../../client'
export namespace Venue {
export interface VenueSource {
/**
* Provider name (`foursquare` or `gplaces` for Google Places)
*/
provider?: 'foursquare' | 'gplaces'
/**
* Venue ID in the provider's DB
*/
id: string
/**
* Venue type in the provider's DB
*
* - [Supported types for Foursquare](https://developer.foursquare.com/docs/build-with-foursquare/categories/)
* (use names, lowercase them, replace spaces and " & " with `_` (underscore) and remove other symbols,
* and use `/` (slash) as hierarchy separator)
* - [Supported types for Google Places](https://developers.google.com/places/web-service/supported_types)
*/
type: string
}
}
export class Venue {
readonly client: TelegramClient
readonly raw: tl.RawMessageMediaVenue
constructor (client: TelegramClient, raw: tl.RawMessageMediaVenue) {
this.client = client
this.raw = raw
}
private _location: Location
/**
* Geolocation of the venue
*/
get location(): Location {
if (!this._location) {
assertTypeIs('Venue#location', this.raw.geo, 'geoPoint')
this._location = new Location(this.client, this.raw.geo)
}
return this._location
}
/**
* Venue name
*/
get title(): string {
return this.raw.title
}
/**
* Venue address
*/
get address(): string {
return this.raw.address
}
/**
* When available, source from where this venue was acquired
*/
get source(): Venue.VenueSource | null {
if (!this.raw.provider) return null
return {
provider: this.raw.provider as Venue.VenueSource['provider'],
id: this.raw.venueId,
type: this.raw.venueType,
}
}
}
makeInspectable(Venue)

View file

@ -7,7 +7,7 @@ import { MessageEntity } from './message-entity'
import { Message } from './message'
import { InputPeerLike } from '../peers'
import { makeInspectable } from '../utils'
import { InputMediaLike } from '../media'
import { InputMediaWithCaption } from '../media'
export class DraftMessage {
readonly client: TelegramClient
@ -101,7 +101,7 @@ export class DraftMessage {
* @link TelegramClient.sendMedia
*/
sendWithMedia(
media: InputMediaLike,
media: InputMediaWithCaption,
params?: Parameters<TelegramClient['sendMedia']>[2]
): Promise<Message> {
if (!media.caption) {

View file

@ -21,12 +21,14 @@ import {
LiveLocation,
Sticker,
Voice,
InputMediaLike,
InputMediaLike, Venue,
} 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
@ -208,6 +210,9 @@ export namespace Message {
| LiveLocation
| Game
| WebPage
| Venue
| Poll
| Invoice
| null
}
@ -655,12 +660,12 @@ export class Message {
m._ === 'messageMediaGeo' &&
m.geo._ === 'geoPoint'
) {
media = new Location(m.geo)
media = new Location(this.client, m.geo)
} else if (
m._ === 'messageMediaGeoLive' &&
m.geo._ === 'geoPoint'
) {
media = new LiveLocation(m)
media = new LiveLocation(this.client, m)
} else if (m._ === 'messageMediaGame') {
media = new Game(this.client, m.game)
} else if (
@ -668,6 +673,12 @@ export class Message {
m.webpage._ === 'webPage'
) {
media = new WebPage(this.client, m.webpage)
} else if (m._ === 'messageMediaVenue') {
media = new Venue(this.client, m)
} else if (m._ === 'messageMediaPoll') {
media = new Poll(this.client, m.poll, this._users, m.results)
} else if (m._ === 'messageMediaInvoice') {
media = new Invoice(this.client, m)
} else {
media = null
}
@ -757,7 +768,6 @@ export class Message {
.unparse(this.text, this.entities)
}
// todo: bound methods https://github.com/pyrogram/pyrogram/blob/701c1cde07af779ab18dbf79a3e626f04fa5d5d2/pyrogram/types/messages_and_media/message.py#L737
/**
* For replies, fetch the message that is being replied.
*
@ -794,30 +804,6 @@ export class Message {
return this.client.sendText(this.chat.inputPeer, text, params)
}
/**
* Send a photo in reply to this message.
*
* By default just sends a message to the same chat,
* to make the reply a "real" reply, pass `visible=true`
*
* @param photo Photo to send
* @param visible Whether the reply should be visible
* @param params
*/
replyPhoto(
photo: InputFileLike,
visible = false,
params?: Parameters<TelegramClient['sendPhoto']>[2]
): ReturnType<TelegramClient['sendPhoto']> {
if (visible) {
return this.client.sendPhoto(this.chat.inputPeer, photo, {
...(params || {}),
replyTo: this.id,
})
}
return this.client.sendPhoto(this.chat.inputPeer, photo, params)
}
/**
* Send a media in reply to this message.
*
@ -842,66 +828,6 @@ export class Message {
return this.client.sendMedia(this.chat.inputPeer, media, params)
}
/**
* Send a dice in reply to this message.
*
* By default just sends a message to the same chat,
* to make the reply a "real" reply, pass `visible=true`
*
* @param emoji Emoji representing a dice to send
* @param visible Whether the reply should be visible
* @param params
*/
replyDice(
emoji: string,
visible = false,
params?: Parameters<TelegramClient['sendDice']>[2]
): ReturnType<TelegramClient['sendDice']> {
if (visible) {
return this.client.sendDice(this.chat.inputPeer, emoji, {
...(params || {}),
replyTo: this.id,
})
}
return this.client.sendDice(this.chat.inputPeer, emoji, params)
}
/**
* Send a static geo location in reply to this message.
*
* By default just sends a message to the same chat,
* to make the reply a "real" reply, pass `visible=true`
*
* @param latitude Latitude of the location
* @param longitude Longitude of the location
* @param visible Whether the reply should be visible
* @param params
*/
replyLocation(
latitude: number,
longitude: number,
visible = false,
params?: Parameters<TelegramClient['sendLocation']>[3]
): ReturnType<TelegramClient['sendLocation']> {
if (visible) {
return this.client.sendLocation(
this.chat.inputPeer,
latitude,
longitude,
{
...(params || {}),
replyTo: this.id,
}
)
}
return this.client.sendLocation(
this.chat.inputPeer,
latitude,
longitude,
params
)
}
/**
* Delete this message.
*

View file

@ -14,7 +14,7 @@ import {
RawDocument,
Sticker,
TelegramClient,
User,
User, Venue,
Video,
Voice,
} from '@mtcute/client'
@ -23,6 +23,8 @@ 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`
@ -482,7 +484,23 @@ export namespace filters {
export const webpage: UpdateFilter<Message, { media: WebPage }> = (msg) =>
msg.media instanceof WebPage
// todo: more filters, see https://github.com/pyrogram/pyrogram/blob/701c1cde07af779ab18dbf79a3e626f04fa5d5d2/pyrogram/filters.py#L191
/**
* Filter messages containing a venue.
*/
export const venue: UpdateFilter<Message, { media: Venue }> = (msg) =>
msg.media instanceof Venue
/**
* Filter messages containing a poll.
*/
export const poll: UpdateFilter<Message, { media: Poll }> = (msg) =>
msg.media instanceof Poll
/**
* Filter messages containing an invoice.
*/
export const invoice: UpdateFilter<Message, { media: Invoice }> = (msg) =>
msg.media instanceof Invoice
/**
* Filter objects that match a given regular expression

View file

@ -66,7 +66,7 @@ export class ChosenInlineResult {
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