feat(core): stars transactions

This commit is contained in:
alina 🌸 2024-08-19 12:46:18 +03:00
parent df15f1a280
commit b1ddb8e346
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
9 changed files with 484 additions and 6 deletions

View file

@ -13,7 +13,7 @@ import { MtUnsupportedError } from '../types/index.js'
import type { BaseTelegramClientOptions } from './base.js'
import { BaseTelegramClient } from './base.js'
import type { ITelegramClient } from './client.types.js'
import type { AllStories, ArrayPaginated, ArrayWithTotal, Boost, BoostSlot, BoostStats, BotChatJoinRequestUpdate, BotCommands, BotReactionCountUpdate, BotReactionUpdate, BotStoppedUpdate, BusinessCallbackQuery, BusinessChatLink, BusinessConnection, BusinessMessage, BusinessWorkHoursDay, CallbackQuery, Chat, ChatEvent, ChatInviteLink, ChatInviteLinkMember, ChatJoinRequestUpdate, ChatMember, ChatMemberUpdate, ChatPreview, ChatlistPreview, ChosenInlineResult, CollectibleInfo, DeleteBusinessMessageUpdate, DeleteMessageUpdate, DeleteStoryUpdate, Dialog, FactCheck, FileDownloadLocation, FileDownloadParameters, ForumTopic, FullChat, GameHighScore, HistoryReadUpdate, InlineCallbackQuery, InlineQuery, InputChatEventFilters, InputDialogFolder, InputFileLike, InputInlineResult, InputMediaLike, InputMediaSticker, InputMessageId, InputPeerLike, InputPrivacyRule, InputReaction, InputStickerSet, InputStickerSetItem, InputText, MaybeDynamic, Message, MessageEffect, MessageMedia, MessageReactions, ParametersSkip2, ParsedUpdate, PeerReaction, PeerStories, PeersIndex, Photo, Poll, PollUpdate, PollVoteUpdate, PreCheckoutQuery, RawDocument, ReplyMarkup, SentCode, Sticker, StickerSet, StickerSourceType, StickerType, StoriesStealthMode, Story, StoryInteractions, StoryUpdate, StoryViewer, StoryViewersList, TakeoutSession, TextWithEntities, TypingStatus, UploadFileLike, UploadedFile, User, UserStatusUpdate, UserTypingUpdate } from './types/index.js'
import type { AllStories, ArrayPaginated, ArrayWithTotal, Boost, BoostSlot, BoostStats, BotChatJoinRequestUpdate, BotCommands, BotReactionCountUpdate, BotReactionUpdate, BotStoppedUpdate, BusinessCallbackQuery, BusinessChatLink, BusinessConnection, BusinessMessage, BusinessWorkHoursDay, CallbackQuery, Chat, ChatEvent, ChatInviteLink, ChatInviteLinkMember, ChatJoinRequestUpdate, ChatMember, ChatMemberUpdate, ChatPreview, ChatlistPreview, ChosenInlineResult, CollectibleInfo, DeleteBusinessMessageUpdate, DeleteMessageUpdate, DeleteStoryUpdate, Dialog, FactCheck, FileDownloadLocation, FileDownloadParameters, ForumTopic, FullChat, GameHighScore, HistoryReadUpdate, InlineCallbackQuery, InlineQuery, InputChatEventFilters, InputDialogFolder, InputFileLike, InputInlineResult, InputMediaLike, InputMediaSticker, InputMessageId, InputPeerLike, InputPrivacyRule, InputReaction, InputStickerSet, InputStickerSetItem, InputText, MaybeDynamic, Message, MessageEffect, MessageMedia, MessageReactions, ParametersSkip2, ParsedUpdate, PeerReaction, PeerStories, PeersIndex, Photo, Poll, PollUpdate, PollVoteUpdate, PreCheckoutQuery, RawDocument, ReplyMarkup, SentCode, StarsStatus, StarsTransaction, Sticker, StickerSet, StickerSourceType, StickerType, StoriesStealthMode, Story, StoryInteractions, StoryUpdate, StoryViewer, StoryViewersList, TakeoutSession, TextWithEntities, TypingStatus, UploadFileLike, UploadedFile, User, UserStatusUpdate, UserTypingUpdate } from './types/index.js'
import type { StringSessionData } from './utils/string-session.js'
import type { ITelegramStorageProvider } from './storage/provider.js'
import { Conversation } from './types/conversation.js'
@ -216,7 +216,9 @@ import { getBoosts } from './methods/premium/get-boosts.js'
import { getBusinessChatLinks } from './methods/premium/get-business-chat-links.js'
import { getBusinessConnection } from './methods/premium/get-business-connection.js'
import { getMyBoostSlots } from './methods/premium/get-my-boost-slots.js'
import { getStarsTransactions } from './methods/premium/get-stars-transactions.js'
import { iterBoosters } from './methods/premium/iter-boosters.js'
import { iterStarsTransactions } from './methods/premium/iter-stars-transactions.js'
import { setBusinessIntro } from './methods/premium/set-business-intro.js'
import { setBusinessWorkHours } from './methods/premium/set-business-work-hours.js'
import { addStickerToSet } from './methods/stickers/add-sticker-to-set.js'
@ -4593,13 +4595,46 @@ export interface TelegramClient extends ITelegramClient {
*/
getMyBoostSlots(): Promise<BoostSlot[]>
/**
* Iterate over boosters of a channel.
* Get Telegram Stars transactions for a given peer.
*
* Wrapper over {@link getBoosters}
* You can either pass `self` to get your own transactions,
* or a chat/bot ID to get transactions of that peer.
*
* **Available**: both users and bots
*
* @returns IDs of stories that were removed
* @param peerId Peer ID
* @param params Additional parameters
*/
getStarsTransactions(
peerId: InputPeerLike,
params?: {
/**
* If passed, only transactions of this direction will be returned
*/
direction?: 'incoming' | 'outgoing'
/**
* Direction to sort transactions date by (default: desc)
*/
sort?: 'asc' | 'desc'
/**
* If passed, will only return transactions related to this subscription ID
*/
subscriptionId?: string
/** Pagination offset */
offset?: string
/**
* Pagination limit
*
* @default 100
*/
limit?: number
}): Promise<StarsStatus>
/**
* Iterate over boosters of a channel.
*
* Wrapper over {@link getBoosters}
* **Available**: both users and bots
*
*/
iterBoosters(
peerId: InputPeerLike,
@ -4619,6 +4654,37 @@ export interface TelegramClient extends ITelegramClient {
*/
chunkSize?: number
}): AsyncIterableIterator<Boost>
/**
* Iterate over Telegram Stars transactions for a given peer.
*
* You can either pass `self` to get your own transactions,
* or a chat/bot ID to get transactions of that peer.
*
* Wrapper over {@link getStarsTransactions}
*
* **Available**: both users and bots
*
* @param peerId Peer ID
* @param params Additional parameters
*/
iterStarsTransactions(
peerId: InputPeerLike,
params?: Parameters<typeof getStarsTransactions>[2] & {
/**
* Total number of boosters to fetch
*
* @default Infinity, i.e. fetch all boosters
*/
limit?: number
/**
* Number of boosters to fetch per request
* Usually you don't need to change this
*
* @default 100
*/
chunkSize?: number
}): AsyncIterableIterator<StarsTransaction>
/**
* Set current user's business introduction.
@ -6314,9 +6380,15 @@ TelegramClient.prototype.getBusinessConnection = function (...args) {
TelegramClient.prototype.getMyBoostSlots = function (...args) {
return getMyBoostSlots(this._client, ...args)
}
TelegramClient.prototype.getStarsTransactions = function (...args) {
return getStarsTransactions(this._client, ...args)
}
TelegramClient.prototype.iterBoosters = function (...args) {
return iterBoosters(this._client, ...args)
}
TelegramClient.prototype.iterStarsTransactions = function (...args) {
return iterStarsTransactions(this._client, ...args)
}
TelegramClient.prototype.setBusinessIntro = function (...args) {
return setBusinessIntro(this._client, ...args)
}

View file

@ -213,7 +213,9 @@ export { getBoosts } from './methods/premium/get-boosts.js'
export { getBusinessChatLinks } from './methods/premium/get-business-chat-links.js'
export { getBusinessConnection } from './methods/premium/get-business-connection.js'
export { getMyBoostSlots } from './methods/premium/get-my-boost-slots.js'
export { getStarsTransactions } from './methods/premium/get-stars-transactions.js'
export { iterBoosters } from './methods/premium/iter-boosters.js'
export { iterStarsTransactions } from './methods/premium/iter-stars-transactions.js'
export { setBusinessIntro } from './methods/premium/set-business-intro.js'
export { setBusinessWorkHours } from './methods/premium/set-business-work-hours.js'
export { addStickerToSet } from './methods/stickers/add-sticker-to-set.js'

View file

@ -87,6 +87,8 @@ import {
RawDocument,
ReplyMarkup,
SentCode,
StarsStatus,
StarsTransaction,
Sticker,
StickerSet,
StickerSourceType,

View file

@ -0,0 +1,55 @@
import type { ITelegramClient } from '../../client.types.js'
import type { InputPeerLike } from '../../types/index.js'
import { StarsStatus } from '../../types/premium/stars-status.js'
import { resolvePeer } from '../users/resolve-peer.js'
/**
* Get Telegram Stars transactions for a given peer.
*
* You can either pass `self` to get your own transactions,
* or a chat/bot ID to get transactions of that peer.
*
* @param peerId Peer ID
* @param params Additional parameters
*/
export async function getStarsTransactions(
client: ITelegramClient,
peerId: InputPeerLike,
params?: {
/**
* If passed, only transactions of this direction will be returned
*/
direction?: 'incoming' | 'outgoing'
/**
* Direction to sort transactions date by (default: desc)
*/
sort?: 'asc' | 'desc'
/**
* If passed, will only return transactions related to this subscription ID
*/
subscriptionId?: string
/** Pagination offset */
offset?: string
/**
* Pagination limit
*
* @default 100
*/
limit?: number
},
): Promise<StarsStatus> {
const { direction, sort, subscriptionId, offset = '', limit = 100 } = params ?? {}
const res = await client.call({
_: 'payments.getStarsTransactions',
peer: await resolvePeer(client, peerId),
offset,
limit,
outbound: direction === 'outgoing',
inbound: direction === 'incoming',
ascending: sort === 'asc',
subscriptionId,
})
return new StarsStatus(res)
}

View file

@ -9,8 +9,6 @@ import { getBoosts } from './get-boosts.js'
* Iterate over boosters of a channel.
*
* Wrapper over {@link getBoosters}
*
* @returns IDs of stories that were removed
*/
export async function* iterBoosters(
client: ITelegramClient,

View file

@ -0,0 +1,61 @@
import type { ITelegramClient } from '../../client.types.js'
import type { InputPeerLike, StarsTransaction } from '../../types/index.js'
import { resolvePeer } from '../users/resolve-peer.js'
import { getStarsTransactions } from './get-stars-transactions.js'
/**
* Iterate over Telegram Stars transactions for a given peer.
*
* You can either pass `self` to get your own transactions,
* or a chat/bot ID to get transactions of that peer.
*
* Wrapper over {@link getStarsTransactions}
*
* @param peerId Peer ID
* @param params Additional parameters
*/
export async function* iterStarsTransactions(
client: ITelegramClient,
peerId: InputPeerLike,
params?: Parameters<typeof getStarsTransactions>[2] & {
/**
* Total number of boosters to fetch
*
* @default Infinity, i.e. fetch all boosters
*/
limit?: number
/**
* Number of boosters to fetch per request
* Usually you don't need to change this
*
* @default 100
*/
chunkSize?: number
},
): AsyncIterableIterator<StarsTransaction> {
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 getStarsTransactions(client, peer, {
offset,
limit: Math.min(limit - current, chunkSize),
})
for (const transaction of res.transactions) {
yield transaction
if (++current >= limit) return
}
if (!res.transactionsNextOffset) return
offset = res.transactionsNextOffset
}
}

View file

@ -6,3 +6,5 @@ export * from './business-chat-link.js'
export * from './business-connection.js'
export * from './business-intro.js'
export * from './business-work-hours.js'
export * from './stars-transaction.js'
export * from './stars-status.js'

View file

@ -0,0 +1,36 @@
import type { tl } from '@mtcute/tl'
import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { PeersIndex } from '../peers/peers-index.js'
import { StarsTransaction } from './stars-transaction.js'
export class StarsStatus {
readonly peers: PeersIndex
constructor(
readonly raw: tl.payments.RawStarsStatus,
) {
this.peers = PeersIndex.from(raw)
}
/** Current Telegram Stars balance */
get balance(): tl.Long {
return this.raw.balance
}
/**
* History of Telegram Stars transactions
*/
get transactions(): StarsTransaction[] {
return this.raw.history?.map(it => new StarsTransaction(it, this.peers)) ?? []
}
/** Next offset of {@link transactions} for pagination */
get transactionsNextOffset(): string | null {
return this.raw.nextOffset ?? null
}
}
makeInspectable(StarsStatus)
memoizeGetters(StarsStatus, ['transactions'])

View file

@ -0,0 +1,250 @@
import type { tl } from '@mtcute/tl'
import type { PeersIndex } from '../peers/peers-index.js'
import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js'
import type { Peer } from '../peers/peer.js'
import { parsePeer } from '../peers/peer.js'
import type { User } from '../peers/user.js'
import { type MessageMedia, _messageMediaFromTl } from '../messages/message-media.js'
import { WebDocument } from '../files/web-document.js'
// ref: https://github.com/tdlib/td/blob/master/td/telegram/StarManager.cpp#L223
/**
* Type of the transaction.
*
* - `unsupported`: This transaction is not supported by the current version of client
* - `app_store`, `play_market`, `premium_bot`, `fragment`: This transaction is a purchase
* through App Store, Play Market, Premium Bot or Fragment respectively
* - `fragment_withdraw`: This transaction is a withdrawal via Fragment
* - `ads`: This transaction is with the Telegram Ads platform
* - `reaction`: This transaction is a paid reaction in a chat
* - `gift`: This transaction is a gift from a user
* - `bot_purchase`: This transaction is a purchase at a bot-operated store
* - `channel_subscription`: This transaction is a subscription to a channel
*/
export type StarsTransactionType =
| { type: 'unsupported' }
| { type: 'app_store' }
| { type: 'play_market' }
| { type: 'premium_bot' }
| { type: 'fragment' }
| {
type: 'fragment_withdraw'
status: 'pending' | 'success' | 'failed'
/** If successful, date of the withdrawal */
date?: Date
/** If successful, URL of the withdrawal transaction */
url?: string
}
| { type: 'ads' }
| {
type: 'reaction'
/**
* Related peer
*
* - For incoming transactions - user who sent the reaction
* - For outgoing transactions - channel which received the reaction
*/
peer: Peer
/** ID of the message containing the reaction */
messageId: number
}
| {
type: 'gift'
/** User who sent the gift */
user: User
}
| {
type: 'media_purchase'
/**
* Related peer
*
* - For incoming transactions - user who bought the media
* - For outgoing transactions - seller of the media
*/
peer: Peer
/** ID of the message containing the media */
messageId: number
/** The bought media (available if not refunded) */
media?: MessageMedia[]
}
| {
type: 'bot_purchase'
/**
* Related user
*
* - For incoming transactions - user who bought the item
* - For outgoing transactions - the seller bot
*/
user: User
/** Photo of the item, if available */
photo?: WebDocument
/** Title of the item */
title: string
/** Description of the item */
description?: string
/** Custom payload of the item */
payload?: Uint8Array
}
| {
type: 'channel_subscription'
/**
* Related peer
*
* - For incoming transactions - user who subscribed to the channel
* - For outgoing transactions - channel which was subscribed to
*/
peer: Peer
/** Period of the subscription, in seconds */
period: number
}
export class StarsTransaction {
constructor(
readonly raw: tl.RawStarsTransaction,
readonly peers: PeersIndex,
) {}
/** ID of the transaction */
get id(): string {
return this.raw.id
}
/** Whether this transaction is a refund */
get isRefund(): boolean {
return this.raw.refund!
}
/**
* Whether this transaction is outgoing or incoming
*/
get direction(): 'incoming' | 'outgoing' {
let isNegative = this.raw.stars.isNegative()
if (this.raw.refund) isNegative = !isNegative
return isNegative ? 'outgoing' : 'incoming'
}
/** Absolute amount of stars in the transaction */
get amount(): tl.Long {
let res = this.raw.stars
if (res.isNegative()) {
res = res.negate()
}
return res
}
/** Date of the transaction */
get date(): Date {
return new Date(this.raw.date * 1000)
}
/** Type of this transaction */
get type(): StarsTransactionType {
switch (this.raw.peer._) {
case 'starsTransactionPeerAppStore':
return { type: 'app_store' }
case 'starsTransactionPeerPlayMarket':
return { type: 'play_market' }
case 'starsTransactionPeerPremiumBot':
return { type: 'premium_bot' }
case 'starsTransactionPeerFragment': {
if (this.raw.gift) {
return { type: 'fragment' }
}
let status
if (this.raw.pending) {
status = 'pending' as const
} else if (this.raw.failed) {
status = 'failed' as const
} else {
status = 'success' as const
}
return {
type: 'fragment_withdraw',
status,
date: this.raw.transactionDate
? new Date(this.raw.transactionDate * 1000)
: undefined,
url: this.raw.transactionUrl,
}
}
case 'starsTransactionPeerAds':
return { type: 'ads' }
case 'starsTransactionPeer': {
const peer = parsePeer(this.raw.peer.peer, this.peers)
if (this.raw.msgId) {
if (this.raw.reaction) {
return {
type: 'reaction',
peer,
messageId: this.raw.msgId,
}
}
return {
type: 'media_purchase',
peer,
messageId: this.raw.msgId,
media: this.raw.extendedMedia
? this.raw.extendedMedia.map(it => _messageMediaFromTl(this.peers, it))
: undefined,
}
}
if (this.raw.subscriptionPeriod) {
return {
type: 'channel_subscription',
peer,
period: this.raw.subscriptionPeriod,
}
}
if (peer.type === 'user') {
if (this.raw.gift && !peer.isBot) {
return { type: 'gift', user: peer }
}
if (this.raw.title
|| this.raw.description
|| this.raw.photo
|| this.raw.botPayload) {
return {
type: 'bot_purchase',
user: peer,
title: this.raw.title ?? '',
description: this.raw.description,
payload: this.raw.botPayload,
photo: this.raw.photo
? new WebDocument(this.raw.photo)
: undefined,
}
}
return { type: 'unsupported' }
}
// todo
return { type: 'unsupported' }
}
default:
return { type: 'unsupported' }
}
}
}
makeInspectable(StarsTransaction)
memoizeGetters(StarsTransaction, ['amount', 'type'])