Merge branch 'master' of github.com:mtcute/mtcute into fuman-net

This commit is contained in:
alina 🌸 2024-11-20 19:59:48 +03:00
commit 439f50bc50
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
31 changed files with 499 additions and 97 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "mtcute-workspace", "name": "mtcute-workspace",
"type": "module", "type": "module",
"version": "0.17.0", "version": "0.17.2",
"private": true, "private": true,
"packageManager": "pnpm@9.0.6", "packageManager": "pnpm@9.0.6",
"description": "Type-safe library for MTProto (Telegram API) for browser and NodeJS", "description": "Type-safe library for MTProto (Telegram API) for browser and NodeJS",

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/core", "name": "@mtcute/core",
"type": "module", "type": "module",
"version": "0.17.0", "version": "0.17.1",
"private": true, "private": true,
"description": "Type-safe library for MTProto (Telegram API)", "description": "Type-safe library for MTProto (Telegram API)",
"author": "alina sireneva <alina@tei.su>", "author": "alina sireneva <alina@tei.su>",

View file

@ -202,6 +202,7 @@ export { verifyPasswordEmail } from './methods/password/password-email.js'
export { resendPasswordEmail } from './methods/password/password-email.js' export { resendPasswordEmail } from './methods/password/password-email.js'
export { cancelPasswordEmail } from './methods/password/password-email.js' export { cancelPasswordEmail } from './methods/password/password-email.js'
export { removeCloudPassword } from './methods/password/remove-cloud-password.js' export { removeCloudPassword } from './methods/password/remove-cloud-password.js'
export { acceptStarGift } from './methods/premium/accept-star-gift.js'
export { applyBoost } from './methods/premium/apply-boost.js' export { applyBoost } from './methods/premium/apply-boost.js'
export { canApplyBoost } from './methods/premium/can-apply-boost.js' export { canApplyBoost } from './methods/premium/can-apply-boost.js'
export type { CanApplyBoostResult } from './methods/premium/can-apply-boost.js' export type { CanApplyBoostResult } from './methods/premium/can-apply-boost.js'
@ -213,9 +214,13 @@ export { getBoosts } from './methods/premium/get-boosts.js'
export { getBusinessChatLinks } from './methods/premium/get-business-chat-links.js' export { getBusinessChatLinks } from './methods/premium/get-business-chat-links.js'
export { getBusinessConnection } from './methods/premium/get-business-connection.js' export { getBusinessConnection } from './methods/premium/get-business-connection.js'
export { getMyBoostSlots } from './methods/premium/get-my-boost-slots.js' export { getMyBoostSlots } from './methods/premium/get-my-boost-slots.js'
export { getStarGiftOptions } from './methods/premium/get-star-gift-options.js'
export { getStarGifts } from './methods/premium/get-star-gifts.js'
export { getStarsTransactions } from './methods/premium/get-stars-transactions.js' export { getStarsTransactions } from './methods/premium/get-stars-transactions.js'
export { iterBoosters } from './methods/premium/iter-boosters.js' export { iterBoosters } from './methods/premium/iter-boosters.js'
export { iterStarGifts } from './methods/premium/iter-star-gifts.js'
export { iterStarsTransactions } from './methods/premium/iter-stars-transactions.js' export { iterStarsTransactions } from './methods/premium/iter-stars-transactions.js'
export { sendStarGift } from './methods/premium/send-star-gift.js'
export { setBusinessIntro } from './methods/premium/set-business-intro.js' export { setBusinessIntro } from './methods/premium/set-business-intro.js'
export { setBusinessWorkHours } from './methods/premium/set-business-work-hours.js' export { setBusinessWorkHours } from './methods/premium/set-business-work-hours.js'
export { addStickerToSet } from './methods/stickers/add-sticker-to-set.js' export { addStickerToSet } from './methods/stickers/add-sticker-to-set.js'
@ -247,7 +252,6 @@ export { iterAllStories } from './methods/stories/iter-all-stories.js'
export { iterProfileStories } from './methods/stories/iter-profile-stories.js' export { iterProfileStories } from './methods/stories/iter-profile-stories.js'
export { iterStoryViewers } from './methods/stories/iter-story-viewers.js' export { iterStoryViewers } from './methods/stories/iter-story-viewers.js'
export { readStories } from './methods/stories/read-stories.js' export { readStories } from './methods/stories/read-stories.js'
export { reportStory } from './methods/stories/report-story.js'
export { sendStoryReaction } from './methods/stories/send-story-reaction.js' export { sendStoryReaction } from './methods/stories/send-story-reaction.js'
export { sendStory } from './methods/stories/send-story.js' export { sendStory } from './methods/stories/send-story.js'
export { togglePeerStoriesArchived } from './methods/stories/toggle-peer-stories-archived.js' export { togglePeerStoriesArchived } from './methods/stories/toggle-peer-stories-archived.js'

View file

@ -89,6 +89,7 @@ import {
RawDocument, RawDocument,
ReplyMarkup, ReplyMarkup,
SentCode, SentCode,
StarGift,
StarsStatus, StarsStatus,
StarsTransaction, StarsTransaction,
Sticker, Sticker,
@ -107,6 +108,7 @@ import {
UploadFileLike, UploadFileLike,
UploadedFile, UploadedFile,
User, User,
UserStarGift,
UserStatusUpdate, UserStatusUpdate,
UserTypingUpdate, UserTypingUpdate,
} from '../types/index.js' } from '../types/index.js'

View file

@ -55,22 +55,20 @@ export async function findDialogs(client: ITelegramClient, peers: MaybeArray<str
foundIdxToOriginalIdx.set(foundInputPeers.length - 1, i) foundIdxToOriginalIdx.set(foundInputPeers.length - 1, i)
} }
if (foundInputPeers.length === 0) {
// we didn't find anything, we can't even fetch the dialogs
throw new MtPeerNotFoundError(`Could not find dialogs with peers: ${peers.join(', ')}`)
}
const dialogs = await getPeerDialogs(client, foundInputPeers)
if (foundInputPeers.length === peers.length) {
return dialogs
}
const ret: Dialog[] = Array.from({ length: peers.length }) const ret: Dialog[] = Array.from({ length: peers.length })
// populate found dialogs if (foundInputPeers.length !== 0) {
for (const [idx, origIdx] of foundIdxToOriginalIdx) { // we have some input peers, try to fetch them
ret[origIdx] = dialogs[idx] const dialogs = await getPeerDialogs(client, foundInputPeers)
if (foundInputPeers.length === peers.length) {
return dialogs
}
// populate found dialogs
for (const [idx, origIdx] of foundIdxToOriginalIdx) {
ret[origIdx] = dialogs[idx]
}
} }
// now we need to iterate over all dialogs and try to find the rest // now we need to iterate over all dialogs and try to find the rest

View file

@ -257,6 +257,7 @@ export async function _normalizeInputMedia(
fileMime: sendMime, fileMime: sendMime,
fileSize: media.fileSize, fileSize: media.fileSize,
requireFileSize: media.type === 'photo', requireFileSize: media.type === 'photo',
requireExtension: media.type === 'photo',
}) })
inputFile = uploaded.inputFile inputFile = uploaded.inputFile
mime = uploaded.mime mime = uploaded.mime

View file

@ -102,6 +102,15 @@ export async function uploadFile(
* the stream will be buffered in memory and the file size will be inferred from the buffer. * the stream will be buffered in memory and the file size will be inferred from the buffer.
*/ */
requireFileSize?: boolean requireFileSize?: boolean
/**
* When using `inputMediaUploadedPhoto` (e.g. when sending an uploaded photo) require
* the file extension to be known beforehand.
*
* This will make the library try to guess the file extension from the file mime type,
* or throw an error if it cannot be guessed.
*/
requireExtension?: boolean
}, },
): Promise<UploadedFile> { ): Promise<UploadedFile> {
// normalize params // normalize params
@ -128,6 +137,7 @@ export async function uploadFile(
if (HAS_FILE && file instanceof File) { if (HAS_FILE && file instanceof File) {
fileName = file.name fileName = file.name
fileSize = file.size fileSize = file.size
fileMime = file.type
file = file.stream() file = file.stream()
} }
@ -312,6 +322,21 @@ export async function uploadFile(
// telegram requires us to specify the file extension // telegram requires us to specify the file extension
const ext = MIME_TO_EXTENSION[fileMime!] const ext = MIME_TO_EXTENSION[fileMime!]
fileName = ext ? `${DEFAULT_FILE_NAME}.${ext}` : DEFAULT_FILE_NAME fileName = ext ? `${DEFAULT_FILE_NAME}.${ext}` : DEFAULT_FILE_NAME
} else if (params.requireExtension) {
const extFromMime = MIME_TO_EXTENSION[fileMime!]
const idx = fileName.lastIndexOf('.')
const extFromName = idx === -1 ? undefined : fileName.slice(idx + 1)
if (!extFromName) {
if (!extFromMime) {
throw new MtArgumentError(`File name does not have an extension, and we cannot guess it from the mime type (${fileMime})`)
}
fileName = `${fileName}.${extFromMime}`
} else if (extFromMime && extFromName !== extFromMime) {
throw new MtArgumentError(`File name has ${extFromName} extension (${fileName}), but the mime type (${fileMime}) expects it to be ${extFromMime}`)
}
} }
let inputFile: tl.TypeInputFile let inputFile: tl.TypeInputFile

View file

@ -0,0 +1,41 @@
import type { ITelegramClient } from '../../client.types.js'
import { type InputMessageId, normalizeInputMessageId } from '../../types/messages/input-message-id.js'
import { resolveUser } from '../users/resolve-peer.js'
// @available=user
/**
* Accept, hide or convert a star gift.
*
* @returns Whether the action was successful
*/
export async function acceptStarGift(
client: ITelegramClient,
params: InputMessageId & {
/**
* Action to perform on the gift.
* - `save` - save the gift to your profile
* - `hide` - hide the gift from your profile
* - `convert` - convert the gift to stars (can't be undone)
*/
action: 'save' | 'hide' | 'convert'
},
): Promise<boolean> {
const { action } = params
const { chatId, message } = normalizeInputMessageId(params)
const userId = await resolveUser(client, chatId)
return client.call(
action === 'convert'
? {
_: 'payments.convertStarGift',
userId,
msgId: message,
}
: {
_: 'payments.saveStarGift',
unsave: action === 'hide',
userId,
msgId: message,
},
)
}

View file

@ -0,0 +1,18 @@
import { assertTypeIsNot } from '../../../utils/type-assertions.js'
import type { ITelegramClient } from '../../client.types.js'
import { StarGift } from '../../types/premium/stars-gift.js'
// @available=user
/**
* Get the list of available star gifts.
*/
export async function getStarGiftOptions(client: ITelegramClient): Promise<StarGift[]> {
const res = await client.call({
_: 'payments.getStarGifts',
hash: 0,
})
assertTypeIsNot('payments.getStarGifts', res, 'payments.starGiftsNotModified')
return res.gifts.map(gift => new StarGift(gift))
}

View file

@ -0,0 +1,46 @@
import type { ITelegramClient } from '../../client.types.js'
import type { InputPeerLike } from '../../types/peers/peer.js'
import { PeersIndex } from '../../types/peers/peers-index.js'
import { UserStarGift } from '../../types/premium/stars-gift.js'
import type { ArrayPaginated } from '../../types/utils.js'
import { makeArrayPaginated } from '../../utils/misc-utils.js'
import { resolveUser } from '../users/resolve-peer.js'
// @available=user
/**
* Get a list of gifts sent to a user.
*
* @param userId User whose gifts to fetch
* @returns Gifts sent to the user
*/
export async function getStarGifts(
client: ITelegramClient,
userId: InputPeerLike,
params?: {
/**
* Offset for pagination.
*/
offset?: string
/**
* Maximum number of gifts to fetch.
*
* @default 100
*/
limit?: number
},
): Promise<ArrayPaginated<UserStarGift, string>> {
const { offset = '', limit = 100 } = params ?? {}
const res = await client.call({
_: 'payments.getUserStarGifts',
userId: await resolveUser(client, userId),
offset,
limit,
})
const peers = PeersIndex.from(res)
const gifts = res.gifts.map(gift => new UserStarGift(gift, peers))
return makeArrayPaginated(gifts, res.count, offset)
}

View file

@ -0,0 +1,59 @@
import type { ITelegramClient } from '../../client.types.js'
import type { InputPeerLike, UserStarGift } from '../../types/index.js'
import { resolvePeer } from '../users/resolve-peer.js'
import { getStarGifts } from './get-star-gifts.js'
// @available=user
/**
* Iterate over gifts sent to a given user.
*
* Wrapper over {@link getStarGifts}
*
* @param peerId Peer ID
* @param params Additional parameters
*/
export async function* iterStarGifts(
client: ITelegramClient,
peerId: InputPeerLike,
params?: Parameters<typeof getStarGifts>[2] & {
/**
* Total number of gifts to fetch
*
* @default Infinity, i.e. fetch all gifts
*/
limit?: number
/**
* Number of gifts to fetch per request
* Usually you don't need to change this
*
* @default 100
*/
chunkSize?: number
},
): AsyncIterableIterator<UserStarGift> {
if (!params) params = {}
const { limit = Infinity, chunkSize = 100 } = params
let { offset } = params
let current = 0
const peer = await resolvePeer(client, peerId)
for (;;) {
const res = await getStarGifts(client, peer, {
offset,
limit: Math.min(limit - current, chunkSize),
})
for (const gift of res) {
yield gift
if (++current >= limit) return
}
if (!res.next) return
offset = res.next
}
}

View file

@ -0,0 +1,73 @@
import Long from 'long'
import type { tl } from '@mtcute/tl'
import type { InputPeerLike } from '../../types/peers/peer.js'
import type { StarGift } from '../../types/premium/stars-gift.js'
import { type InputText, inputTextToTl } from '../../types/misc/entities.js'
import type { ITelegramClient } from '../../client.types.js'
import { resolveUser } from '../users/resolve-peer.js'
import { assertTypeIs } from '../../../utils/type-assertions.js'
import { _findMessageInUpdate } from '../messages/find-in-update.js'
import type { Message } from '../../types/messages/message.js'
/**
* Send a star gift to a user.
*
* > **Note**: this method is not indended to be used by full-fledged clients,
* > as this method hides the actual invoice and payment form from the user.
* > For GUI clients, you should refer to the method's source code and
* > present the payment form to the user.
*
* @returns Service message about the sent gift
*/
export async function sendStarGift(
client: ITelegramClient,
params: {
/** ID of the user to send the gift to */
userId: InputPeerLike
/** ID of the gift to send */
gift: Long | StarGift
/**
* Whether to send the gift anonymously
* (i.e. if the recipient chooses to display the gift
* on their profile, your name won't be visible)
*/
anonymous?: boolean
/** Message to send along with the gift */
message?: InputText
/**
* Whether to dispatch the new message event
* to the client's update handler.
*/
shouldDispatch?: true
},
): Promise<Message> {
const { userId, gift, anonymous, message, shouldDispatch } = params
const invoice: tl.TypeInputInvoice = {
_: 'inputInvoiceStarGift',
hideName: anonymous,
userId: await resolveUser(client, userId),
giftId: Long.isLong(gift) ? gift : gift.id,
message: message ? inputTextToTl(message) : undefined,
}
const form = await client.call({
_: 'payments.getPaymentForm',
invoice,
})
const res = await client.call({
_: 'payments.sendStarsForm',
invoice,
formId: form.formId,
})
assertTypeIs('payments.sendStarsForm', res, 'payments.paymentResult')
return _findMessageInUpdate(client, res.updates, false, !shouldDispatch)
}

View file

@ -5,7 +5,6 @@ import type {
InputFileLike, InputFileLike,
InputPeerLike, InputPeerLike,
InputStickerSetItem, InputStickerSetItem,
StickerSourceType,
StickerType, StickerType,
} from '../../types/index.js' } from '../../types/index.js'
import { import {
@ -54,13 +53,6 @@ export async function createStickerSet(
*/ */
type?: StickerType type?: StickerType
/**
* File source type for the stickers in this set.
*
* @default `static`, i.e. regular WEBP stickers.
*/
sourceType?: StickerSourceType
/** /**
* Whether to create "adaptive" emoji set. * Whether to create "adaptive" emoji set.
* *
@ -113,8 +105,6 @@ export async function createStickerSet(
const res = await client.call({ const res = await client.call({
_: 'stickers.createStickerSet', _: 'stickers.createStickerSet',
animated: params.sourceType === 'animated',
videos: params.sourceType === 'video',
masks: params.type === 'mask', masks: params.type === 'mask',
emojis: params.type === 'emoji', emojis: params.type === 'emoji',
textColor: params.adaptive, textColor: params.adaptive,

View file

@ -1,41 +0,0 @@
import type { tl } from '@mtcute/tl'
import type { MaybeArray } from '../../../types/utils.js'
import { assertTrue } from '../../../utils/type-assertions.js'
import type { ITelegramClient } from '../../client.types.js'
import type { InputPeerLike } from '../../types/index.js'
import { resolvePeer } from '../users/resolve-peer.js'
/**
* Report a story (or multiple stories) to the moderation team
*/
export async function reportStory(
client: ITelegramClient,
peerId: InputPeerLike,
storyIds: MaybeArray<number>,
params?: {
/**
* Reason for reporting
*
* @default inputReportReasonSpam
*/
reason?: tl.TypeReportReason
/**
* Additional comment to the report
*/
message?: string
},
): Promise<void> {
const { reason = { _: 'inputReportReasonSpam' }, message = '' } = params ?? {}
const r = await client.call({
_: 'stories.report',
peer: await resolvePeer(client, peerId),
id: Array.isArray(storyIds) ? storyIds : [storyIds],
message,
reason,
})
assertTrue('stories.report', r)
}

View file

@ -333,6 +333,20 @@ export function requestPeer(
} }
} }
/**
* Button to copy text to the user's clipboard
*/
export function copy(params: {
text: string
copyText?: string
}): tl.RawKeyboardButtonCopy {
return {
_: 'keyboardButtonCopy',
text: params.text,
copyText: params?.copyText ?? params.text,
}
}
/** /**
* Find a button in the keyboard by its text or by predicate * Find a button in the keyboard by its text or by predicate
* *

View file

@ -90,6 +90,10 @@ export class Video extends RawDocument {
get videoStartTs(): number | null { get videoStartTs(): number | null {
return this.attr._ === 'documentAttributeVideo' ? this.attr.videoStartTs ?? null : null return this.attr._ === 'documentAttributeVideo' ? this.attr.videoStartTs ?? null : null
} }
get codec(): string | null {
return this.attr._ === 'documentAttributeVideo' ? this.attr.videoCodec ?? null : null
}
} }
memoizeGetters(Video, ['fileName', 'thumbnails', 'fileId', 'uniqueFileId', 'isAnimation']) memoizeGetters(Video, ['fileName', 'thumbnails', 'fileId', 'uniqueFileId', 'isAnimation'])

View file

@ -6,6 +6,8 @@ import { _callDiscardReasonFromTl } from '../calls/index.js'
import { Photo } from '../media/photo.js' import { Photo } from '../media/photo.js'
import type { Peer } from '../peers/peer.js' import type { Peer } from '../peers/peer.js'
import { parsePeer } from '../peers/peer.js' import { parsePeer } from '../peers/peer.js'
import { StarGift } from '../premium/stars-gift.js'
import type { TextWithEntities } from '../misc/entities.js'
import type { Message } from './message.js' import type { Message } from './message.js'
@ -478,7 +480,7 @@ export interface ActionPaymentRefunded {
} }
/** Telegram Stars were gifted by the other chat participant */ /** Telegram Stars were gifted by the other chat participant */
export interface ActionStarsGifted { export interface ActionStarGifted {
readonly type: 'stars_gifted' readonly type: 'stars_gifted'
/** Whether `currency` is a cryptocurrency */ /** Whether `currency` is a cryptocurrency */
@ -512,12 +514,30 @@ export interface ActionStarsPrize {
/** Transaction ID */ /** Transaction ID */
transactionId: string transactionId: string
// todo: what does this mean?
boostPeer: Peer boostPeer: Peer
/** Message ID containing the giveaway */ /** Message ID containing the giveaway */
giveawayMessageId: number giveawayMessageId: number
} }
export interface ActionStarGift {
readonly type: 'stars_gift'
/** Whether the sender chose to send the gift anonymously */
nameHidden: boolean
/** Whether this gift was saved to the recipient's profile */
saved: boolean
/** Whether this gift was converted to stars */
converted: boolean
/** Amount of stars the gift can be converted to by the recipient */
convertStars: tl.Long
/** The gift itself */
gift: StarGift
/** Message attached to the gift */
message: TextWithEntities | null
}
export type MessageAction = export type MessageAction =
| ActionChatCreated | ActionChatCreated
| ActionChannelCreated | ActionChannelCreated
@ -562,8 +582,9 @@ export type MessageAction =
| ActionGiveawayEnded | ActionGiveawayEnded
| ActionBoostApply | ActionBoostApply
| ActionPaymentRefunded | ActionPaymentRefunded
| ActionStarsGifted | ActionStarGifted
| ActionStarsPrize | ActionStarsPrize
| ActionStarGift
| null | null
/** @internal */ /** @internal */
@ -848,6 +869,16 @@ export function _messageActionFromTl(this: Message, act: tl.TypeMessageAction):
boostPeer: parsePeer(act.boostPeer, this._peers), boostPeer: parsePeer(act.boostPeer, this._peers),
giveawayMessageId: act.giveawayMsgId, giveawayMessageId: act.giveawayMsgId,
} }
case 'messageActionStarGift':
return {
type: 'stars_gift',
nameHidden: act.nameHidden!,
saved: act.saved!,
converted: act.converted!,
convertStars: act.convertStars,
gift: new StarGift(act.gift),
message: act.message ?? null,
}
default: default:
return null return null
} }

View file

@ -7,7 +7,7 @@ import { memoizeGetters } from '../../utils/memoize.js'
import { MtEmptyError } from '../errors.js' import { MtEmptyError } from '../errors.js'
import type { InputFileLike } from '../files/index.js' import type { InputFileLike } from '../files/index.js'
import { parseSticker } from '../media/document-utils.js' import { parseSticker } from '../media/document-utils.js'
import type { MaskPosition, Sticker, StickerSourceType, StickerType } from '../media/index.js' import type { MaskPosition, Sticker, StickerType } from '../media/index.js'
import { Thumbnail } from '../media/index.js' import { Thumbnail } from '../media/index.js'
/** /**
@ -175,21 +175,6 @@ export class StickerSet {
return 'sticker' return 'sticker'
} }
/**
* Source file type of the stickers in this set
*/
get sourceType(): StickerSourceType {
if (this.brief.animated) {
return 'animated'
}
if (this.brief.videos) {
return 'video'
}
return 'static'
}
/** /**
* Date when this sticker set was installed * Date when this sticker set was installed
*/ */

View file

@ -8,3 +8,4 @@ export * from './business-intro.js'
export * from './business-work-hours.js' export * from './business-work-hours.js'
export * from './stars-transaction.js' export * from './stars-transaction.js'
export * from './stars-status.js' export * from './stars-status.js'
export * from './stars-gift.js'

View file

@ -0,0 +1,123 @@
import type { tl } from '@mtcute/tl'
import type { Sticker } from '../media/sticker.js'
import { parseDocument } from '../media/document-utils.js'
import { assertTypeIs } from '../../../utils/type-assertions.js'
import { MtTypeAssertionError } from '../../../types/errors.js'
import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js'
import type { PeersIndex } from '../peers/peers-index.js'
import { User } from '../peers/user.js'
import type { TextWithEntities } from '../misc/entities.js'
/**
* A gift with stars attached to it.
*/
export class StarGift {
constructor(
readonly raw: tl.TypeStarGift,
) {}
/** ID of the gift */
get id(): tl.Long {
return this.raw.id
}
/** Sticker associated with the gift */
get sticker(): Sticker {
assertTypeIs('StarGift#sticker', this.raw.sticker, 'document')
const parsed = parseDocument(this.raw.sticker)
if (parsed.type !== 'sticker') {
throw new MtTypeAssertionError('StarGift#sticker', 'sticker', parsed.type)
}
return parsed
}
/** Amount of stars the gift was purchased for */
get purchaseStars(): tl.Long {
return this.raw.stars
}
/**
* Amount of stars the gift can be converted to by the recipient
*/
get convertStars(): tl.Long {
return this.raw.convertStars
}
/**
* For limited availability gifts,
* the number of remaining and total gifts available
*/
get availability(): { remains: number, total: number } | null {
if (!this.raw.availabilityRemains || !this.raw.availabilityTotal) {
return null
}
return {
remains: this.raw.availabilityRemains,
total: this.raw.availabilityTotal,
}
}
}
makeInspectable(StarGift)
memoizeGetters(StarGift, ['sticker'])
/**
* Information about a certain user's {@link StarGift}.
*/
export class UserStarGift {
constructor(
readonly raw: tl.RawUserStarGift,
readonly peers: PeersIndex,
) {}
/** Whether the sender chose to appear anonymously */
get nameHidden(): boolean {
return this.raw.nameHidden!
}
/** Whether this gift is not visible on the recipient's profile */
get hidden(): boolean {
return this.raw.unsaved!
}
/** Sender of the gift, if available */
get sender(): User | null {
return this.raw.fromId ? new User(this.peers.user(this.raw.fromId)) : null
}
/** Message ID where the gift was sent, if available */
get messageId(): number | null {
return this.raw.msgId ?? null
}
/** Date the gift was sent */
get date(): Date {
return new Date(this.raw.date * 1000)
}
/** The gift itself */
get gift(): StarGift {
return new StarGift(this.raw.gift)
}
/** Text attached to the gift */
get text(): TextWithEntities | null {
return this.raw.message ?? null
}
/**
* If the gift was converted to stars, the amount of stars
* it was converted to
*/
get convertStars(): tl.Long | null {
return this.raw.convertStars ?? null
}
}
makeInspectable(UserStarGift)
memoizeGetters(UserStarGift, ['sender', 'gift'])

View file

@ -9,6 +9,8 @@ import type { User } from '../peers/user.js'
import { type MessageMedia, _messageMediaFromTl } from '../messages/message-media.js' import { type MessageMedia, _messageMediaFromTl } from '../messages/message-media.js'
import { WebDocument } from '../files/web-document.js' import { WebDocument } from '../files/web-document.js'
import { StarGift } from './stars-gift.js'
// ref: https://github.com/tdlib/td/blob/master/td/telegram/StarManager.cpp#L223 // ref: https://github.com/tdlib/td/blob/master/td/telegram/StarManager.cpp#L223
/** /**
@ -23,6 +25,7 @@ import { WebDocument } from '../files/web-document.js'
* - `gift`: This transaction is a gift from a user * - `gift`: This transaction is a gift from a user
* - `bot_purchase`: This transaction is a purchase at a bot-operated store * - `bot_purchase`: This transaction is a purchase at a bot-operated store
* - `channel_subscription`: This transaction is a subscription to a channel * - `channel_subscription`: This transaction is a subscription to a channel
* - `star_gift`: This transaction is either a star gift to a user (if outgoing), or converting a star gift to stars (if incoming)
*/ */
export type StarsTransactionType = export type StarsTransactionType =
| { type: 'unsupported' } | { type: 'unsupported' }
@ -113,6 +116,13 @@ export type StarsTransactionType =
/** ID of the message containing the giveaway where the stars were given */ /** ID of the message containing the giveaway where the stars were given */
messageId: number messageId: number
} }
| {
type: 'star_gift'
/** Related peer */
peer: Peer
/** The gift */
gift: StarGift
}
export class StarsTransaction { export class StarsTransaction {
constructor( constructor(
@ -201,6 +211,14 @@ export class StarsTransaction {
} }
} }
if (this.raw.stargift) {
return {
type: 'star_gift',
peer,
gift: new StarGift(this.raw.stargift),
}
}
if (this.raw.msgId) { if (this.raw.msgId) {
if (this.raw.reaction) { if (this.raw.reaction) {
return { return {

View file

@ -5,10 +5,10 @@ import type { Logger } from './logger.js'
export function reportUnknownError(log: Logger, error: tl.RpcError, method: string): void { export function reportUnknownError(log: Logger, error: tl.RpcError, method: string): void {
if (typeof fetch !== 'function') return if (typeof fetch !== 'function') return
fetch(`https://rpc.pwrtelegram.xyz/?code=${error.code}&method=${method}&error=${error.text}`) fetch(`https://report-rpc-error.madelineproto.xyz/?code=${error.code}&method=${method}&error=${error.text}`)
.then(r => r.json()) .then(r => r.json())
.then((r) => { .then((r) => {
if (r.ok) { if (r.result) {
log.info('telerpc responded with error info for %s: %s', error.text, r.result) log.info('telerpc responded with error info for %s: %s', error.text, r.result)
} else { } else {
log.info( log.info(

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/dispatcher", "name": "@mtcute/dispatcher",
"type": "module", "type": "module",
"version": "0.17.0", "version": "0.17.2",
"private": true, "private": true,
"description": "Updates dispatcher and bot framework for @mtcute/client", "description": "Updates dispatcher and bot framework for @mtcute/client",
"author": "alina sireneva <alina@tei.su>", "author": "alina sireneva <alina@tei.su>",

View file

@ -66,7 +66,7 @@ export class WizardScene<State extends object> extends Dispatcher<State & Wizard
if (step >= this._steps) { if (step >= this._steps) {
await state.exit() await state.exit()
} else { } else {
await state.merge({ $step: step }, this._defaultState) await state.merge({ $step: step }, { fallback: this._defaultState })
} }
} }

View file

@ -1,7 +1,7 @@
{ {
"name": "@mtcute/html-parser", "name": "@mtcute/html-parser",
"type": "module", "type": "module",
"version": "0.17.0", "version": "0.17.1",
"private": true, "private": true,
"description": "HTML entities parser for mtcute", "description": "HTML entities parser for mtcute",
"author": "alina sireneva <alina@tei.su>", "author": "alina sireneva <alina@tei.su>",

View file

@ -361,6 +361,14 @@ describe('HtmlMessageEntityParser', () => {
test(htm`hewwo ${htm`<br>`} world`, [], 'hewwo \nworld') test(htm`hewwo ${htm`<br>`} world`, [], 'hewwo \nworld')
}) })
it('should keep whitespaces in raw strings', () => {
const dot = ' ∙ '
const lf = '\n'
test(htm`this is${dot}some text${lf}xd`, [], 'this is ∙ some text\nxd')
test(htm`hewwo ${htm`<br>`} world`, [], 'hewwo \nworld')
})
it('should not ignore newlines and indentation in pre', () => { it('should not ignore newlines and indentation in pre', () => {
test( test(
htm`<pre>this is some text\n\nwith newlines</pre>`, htm`<pre>this is some text\n\nwith newlines</pre>`,

View file

@ -262,9 +262,11 @@ function parse(
return return
} }
if (typeof it === 'string' || typeof it === 'number') { if (typeof it === 'string') {
processPendingText()
pendingText += it pendingText += it
} else if (Long.isLong(it)) { processPendingText(false, true)
} else if (Long.isLong(it) || typeof it === 'number') {
pendingText += it.toString(10) pendingText += it.toString(10)
} else { } else {
// TextWithEntities or MessageEntity // TextWithEntities or MessageEntity

View file

@ -2,7 +2,7 @@
TL schema and related utils used for mtcute. TL schema and related utils used for mtcute.
Generated from TL layer **187** (last updated on 07.09.2024). Generated from TL layer **189** (last updated on 05.10.2024).
## About ## About

File diff suppressed because one or more lines are too long

View file

@ -138,7 +138,6 @@
"updateChatUserTyping": ["chat_id"], "updateChatUserTyping": ["chat_id"],
"updateDeleteChannelMessages": ["channel_id"], "updateDeleteChannelMessages": ["channel_id"],
"updateGroupCall": ["chat_id"], "updateGroupCall": ["chat_id"],
"updateGroupInvitePrivacyForbidden": ["user_id"],
"updateInlineBotCallbackQuery": ["user_id"], "updateInlineBotCallbackQuery": ["user_id"],
"updatePinnedChannelMessages": ["channel_id"], "updatePinnedChannelMessages": ["channel_id"],
"updateReadChannelDiscussionInbox": ["channel_id", "broadcast_id"], "updateReadChannelDiscussionInbox": ["channel_id", "broadcast_id"],
@ -157,6 +156,7 @@
"user": ["id"], "user": ["id"],
"userEmpty": ["id"], "userEmpty": ["id"],
"userFull": ["id", "personal_channel_id"], "userFull": ["id", "personal_channel_id"],
"userStarGift": ["from_id"],
"userStories": ["user_id"], "userStories": ["user_id"],
"webAuthorization": ["bot_id"], "webAuthorization": ["bot_id"],
"_": "Dummy line teehee~" "_": "Dummy line teehee~"

View file

@ -1,6 +1,6 @@
{ {
"name": "@mtcute/tl", "name": "@mtcute/tl",
"version": "187.0.0", "version": "189.0.0",
"description": "TL schema used for mtcute", "description": "TL schema used for mtcute",
"author": "alina sireneva <alina@tei.su>", "author": "alina sireneva <alina@tei.su>",
"license": "MIT", "license": "MIT",