feat!: updated to layer 169

breaking:
  - boost related methods re-done to allow for multiple boosts per user
  - `BotKeyboard.requestPeer` and `peer_chosen` changed
This commit is contained in:
alina 🌸 2023-12-24 01:43:58 +03:00
parent 9ff6a628e5
commit c175e41616
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
29 changed files with 558 additions and 250 deletions

View file

@ -178,6 +178,12 @@ import { changeCloudPassword } from './methods/password/change-cloud-password.js
import { enableCloudPassword } from './methods/password/enable-cloud-password.js'
import { cancelPasswordEmail, resendPasswordEmail, verifyPasswordEmail } from './methods/password/password-email.js'
import { removeCloudPassword } from './methods/password/remove-cloud-password.js'
import { applyBoost } from './methods/premium/apply-boost.js'
import { canApplyBoost, CanApplyBoostResult } from './methods/premium/can-apply-boost.js'
import { getBoostStats } from './methods/premium/get-boost-stats.js'
import { getBoosts } from './methods/premium/get-boosts.js'
import { getMyBoostSlots } from './methods/premium/get-my-boost-slots.js'
import { iterBoosters } from './methods/premium/iter-boosters.js'
import { addStickerToSet } from './methods/stickers/add-sticker-to-set.js'
import { createStickerSet } from './methods/stickers/create-sticker-set.js'
import { deleteStickerFromSet } from './methods/stickers/delete-sticker-from-set.js'
@ -187,14 +193,10 @@ import { getStickerSet } from './methods/stickers/get-sticker-set.js'
import { moveStickerInSet } from './methods/stickers/move-sticker-in-set.js'
import { setChatStickerSet } from './methods/stickers/set-chat-sticker-set.js'
import { setStickerSetThumb } from './methods/stickers/set-sticker-set-thumb.js'
import { applyBoost } from './methods/stories/apply-boost.js'
import { canApplyBoost, CanApplyBoostResult } from './methods/stories/can-apply-boost.js'
import { canSendStory, CanSendStoryResult } from './methods/stories/can-send-story.js'
import { deleteStories } from './methods/stories/delete-stories.js'
import { editStory } from './methods/stories/edit-story.js'
import { getAllStories } from './methods/stories/get-all-stories.js'
import { getBoostStats } from './methods/stories/get-boost-stats.js'
import { getBoosters } from './methods/stories/get-boosters.js'
import { getPeerStories } from './methods/stories/get-peer-stories.js'
import { getProfileStories } from './methods/stories/get-profile-stories.js'
import { getStoriesById } from './methods/stories/get-stories-by-id.js'
@ -204,7 +206,6 @@ import { getStoryViewers } from './methods/stories/get-story-viewers.js'
import { hideMyStoriesViews } from './methods/stories/hide-my-stories-views.js'
import { incrementStoriesViews } from './methods/stories/increment-stories-views.js'
import { iterAllStories } from './methods/stories/iter-all-stories.js'
import { iterBoosters } from './methods/stories/iter-boosters.js'
import { iterProfileStories } from './methods/stories/iter-profile-stories.js'
import { iterStoryViewers } from './methods/stories/iter-story-viewers.js'
import { readStories } from './methods/stories/read-stories.js'
@ -254,7 +255,8 @@ import {
AllStories,
ArrayPaginated,
ArrayWithTotal,
Booster,
Boost,
BoostSlot,
BoostStats,
BotChatJoinRequestUpdate,
BotCommands,
@ -4145,6 +4147,96 @@ export interface TelegramClient extends BaseTelegramClient {
* @param password 2FA password as plaintext
*/
removeCloudPassword(password: string): Promise<void>
/**
* Boost a given channel
*
* **Available**: 👤 users only
*
* @param peerId Peer ID to boost
*/
applyBoost(peerId: InputPeerLike): Promise<void>
/**
* Check if the current user can apply boost to some channel
*
* **Available**: both users and bots
*
* @returns
* - `{ can: true }` if the user can apply boost
* - `.replace` - {@link Chat}s that can be replaced with the current one.
* If the user can apply boost without replacing any chats, this field will be `undefined`.
* - `{ can: false }` if the user can't apply boost
* - `.reason == "no_slots"` if the user has no available slots
* - `.reason == "need_premium"` if the user needs Premium to boost
* - In all cases, `slots` will contain all the current user's boost slots
*/
canApplyBoost(): Promise<CanApplyBoostResult>
/**
* Get information about boosts in a channel
*
* **Available**: 👤 users only
*
* @returns IDs of stories that were removed
*/
getBoostStats(peerId: InputPeerLike): Promise<BoostStats>
/**
* Get boosts of a channel
* **Available**: 👤 users only
*
*/
getBoosts(
peerId: InputPeerLike,
params?: {
/**
* Offset for pagination
*/
offset?: string
/**
* Maximum number of boosters to fetch
*
* @default 100
*/
limit?: number
},
): Promise<ArrayPaginated<Boost, string>>
/**
* Get boost slots information of the current user.
*
* Includes information about the currently boosted channels,
* as well as the slots that can be used to boost other channels.
* **Available**: 👤 users only
*
*/
getMyBoostSlots(): Promise<BoostSlot[]>
/**
* Iterate over boosters of a channel.
*
* Wrapper over {@link getBoosters}
*
* **Available**: both users and bots
*
* @returns IDs of stories that were removed
*/
iterBoosters(
peerId: InputPeerLike,
params?: Parameters<typeof getBoosts>[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<Boost>
/**
* Add a sticker to a sticker set.
*
@ -4356,30 +4448,6 @@ export interface TelegramClient extends BaseTelegramClient {
progressCallback?: (uploaded: number, total: number) => void
},
): Promise<StickerSet>
/**
* Boost a given channel
*
* **Available**: 👤 users only
*
* @param peerId Peer ID to boost
*/
applyBoost(peerId: InputPeerLike): Promise<void>
/**
* Check if the current user can apply boost to a given channel
*
* **Available**: 👤 users only
*
* @param peerId Peer ID whose stories to fetch
* @returns
* - `{ can: true }` if the user can apply boost
* - `.current` - {@link Chat} that the current user is currently boosting, if any
* - `{ can: false }` if the user can't apply boost
* - `.reason == "already_boosting"` if the user is already boosting this channel
* - `.reason == "need_premium"` if the user needs Premium to boost this channel
* - `.reason == "timeout"` if the user has recently boosted a channel and needs to wait
* (`.until` contains the date until which the user needs to wait)
*/
canApplyBoost(peerId: InputPeerLike): Promise<CanApplyBoostResult>
/**
* Check if the current user can post stories as a given peer
*
@ -4470,38 +4538,6 @@ export interface TelegramClient extends BaseTelegramClient {
*/
archived?: boolean
}): Promise<AllStories>
/**
* Get information about boosts in a channel
*
* **Available**: 👤 users only
*
* @returns IDs of stories that were removed
*/
getBoostStats(peerId: InputPeerLike): Promise<BoostStats>
/**
* Get boosters of a channel
*
* **Available**: 👤 users only
*
* @returns IDs of stories that were removed
*/
getBoosters(
peerId: InputPeerLike,
params?: {
/**
* Offset for pagination
*/
offset?: string
/**
* Maximum number of boosters to fetch
*
* @default 100
*/
limit?: number
},
): Promise<ArrayPaginated<Booster, string>>
/**
* Get stories of a given peer
*
@ -4660,34 +4696,6 @@ export interface TelegramClient extends BaseTelegramClient {
limit?: number
},
): AsyncIterableIterator<PeerStories>
/**
* Iterate over boosters of a channel.
*
* Wrapper over {@link getBoosters}
*
* **Available**: both users and bots
*
* @returns IDs of stories that were removed
*/
iterBoosters(
peerId: InputPeerLike,
params?: Parameters<typeof getBoosters>[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<Booster>
/**
* Iterate over profile stories. Wrapper over {@link getProfileStories}
* **Available**: both users and bots
@ -5999,6 +6007,30 @@ TelegramClient.prototype.removeCloudPassword = function (...args) {
return removeCloudPassword(this, ...args)
}
TelegramClient.prototype.applyBoost = function (...args) {
return applyBoost(this, ...args)
}
TelegramClient.prototype.canApplyBoost = function (...args) {
return canApplyBoost(this, ...args)
}
TelegramClient.prototype.getBoostStats = function (...args) {
return getBoostStats(this, ...args)
}
TelegramClient.prototype.getBoosts = function (...args) {
return getBoosts(this, ...args)
}
TelegramClient.prototype.getMyBoostSlots = function (...args) {
return getMyBoostSlots(this, ...args)
}
TelegramClient.prototype.iterBoosters = function (...args) {
return iterBoosters(this, ...args)
}
TelegramClient.prototype.addStickerToSet = function (...args) {
return addStickerToSet(this, ...args)
}
@ -6039,14 +6071,6 @@ TelegramClient.prototype.setStickerSetThumb = function (...args) {
return setStickerSetThumb(this, ...args)
}
TelegramClient.prototype.applyBoost = function (...args) {
return applyBoost(this, ...args)
}
TelegramClient.prototype.canApplyBoost = function (...args) {
return canApplyBoost(this, ...args)
}
TelegramClient.prototype.canSendStory = function (...args) {
return canSendStory(this, ...args)
}
@ -6063,14 +6087,6 @@ TelegramClient.prototype.getAllStories = function (...args) {
return getAllStories(this, ...args)
}
TelegramClient.prototype.getBoostStats = function (...args) {
return getBoostStats(this, ...args)
}
TelegramClient.prototype.getBoosters = function (...args) {
return getBoosters(this, ...args)
}
TelegramClient.prototype.getPeerStories = function (...args) {
return getPeerStories(this, ...args)
}
@ -6107,10 +6123,6 @@ TelegramClient.prototype.iterAllStories = function (...args) {
return iterAllStories(this, ...args)
}
TelegramClient.prototype.iterBoosters = function (...args) {
return iterBoosters(this, ...args)
}
TelegramClient.prototype.iterProfileStories = function (...args) {
return iterProfileStories(this, ...args)
}

View file

@ -19,7 +19,8 @@ import {
AllStories,
ArrayPaginated,
ArrayWithTotal,
Booster,
Boost,
BoostSlot,
BoostStats,
BotChatJoinRequestUpdate,
BotCommands,

View file

@ -10,7 +10,7 @@ import { resolvePeer } from '../users/resolve-peer.js'
*/
export async function applyBoost(client: BaseTelegramClient, peerId: InputPeerLike): Promise<void> {
await client.call({
_: 'stories.applyBoost',
_: 'premium.applyBoost',
peer: await resolvePeer(client, peerId),
})
}

View file

@ -0,0 +1,45 @@
import { BaseTelegramClient } from '@mtcute/core'
import { Chat } from '../../types/index.js'
import { BoostSlot } from '../../types/premium/boost-slot.js'
import { getMyBoostSlots } from './get-my-boost-slots.js'
// @exported
export type CanApplyBoostResult =
| { can: true; replace?: Chat[]; slots: BoostSlot[] }
| { can: false; reason: 'need_premium'; slots: BoostSlot[] }
| { can: false; reason: 'no_slots'; slots: BoostSlot[] }
/**
* Check if the current user can apply boost to some channel
*
* @returns
* - `{ can: true }` if the user can apply boost
* - `.replace` - {@link Chat}s that can be replaced with the current one.
* If the user can apply boost without replacing any chats, this field will be `undefined`.
* - `{ can: false }` if the user can't apply boost
* - `.reason == "no_slots"` if the user has no available slots
* - `.reason == "need_premium"` if the user needs Premium to boost
* - In all cases, `slots` will contain all the current user's boost slots
*/
export async function canApplyBoost(client: BaseTelegramClient): Promise<CanApplyBoostResult> {
const myBoosts = await getMyBoostSlots(client)
if (!myBoosts.length) {
return { can: false, reason: 'need_premium', slots: myBoosts }
}
const emptySlots = myBoosts.filter((it) => !it.occupied)
if (emptySlots.length > 0) {
return { can: true, slots: myBoosts }
}
const replaceableSlots = myBoosts.filter((it) => it.cooldownUntil === null)
if (replaceableSlots.length) {
return { can: true, replace: replaceableSlots.map((it) => it.chat!), slots: myBoosts }
}
return { can: false, reason: 'no_slots', slots: myBoosts }
}

View file

@ -1,7 +1,7 @@
import { BaseTelegramClient } from '@mtcute/core'
import { InputPeerLike } from '../../types/index.js'
import { BoostStats } from '../../types/stories/boost-stats.js'
import { BoostStats } from '../../types/premium/boost-stats.js'
import { resolvePeer } from '../users/resolve-peer.js'
// @available=user

View file

@ -1,16 +1,14 @@
import { BaseTelegramClient } from '@mtcute/core'
import { ArrayPaginated, InputPeerLike, PeersIndex } from '../../types/index.js'
import { Booster } from '../../types/stories/booster.js'
import { Boost } from '../../types/premium/boost.js'
import { makeArrayPaginated } from '../../utils/index.js'
import { resolvePeer } from '../users/resolve-peer.js'
/**
* Get boosters of a channel
*
* @returns IDs of stories that were removed
* Get boosts of a channel
*/
export async function getBoosters(
export async function getBoosts(
client: BaseTelegramClient,
peerId: InputPeerLike,
params?: {
@ -26,11 +24,11 @@ export async function getBoosters(
*/
limit?: number
},
): Promise<ArrayPaginated<Booster, string>> {
): Promise<ArrayPaginated<Boost, string>> {
const { offset = '', limit = 100 } = params ?? {}
const res = await client.call({
_: 'stories.getBoostersList',
_: 'premium.getBoostsList',
peer: await resolvePeer(client, peerId),
offset,
limit,
@ -39,7 +37,7 @@ export async function getBoosters(
const peers = PeersIndex.from(res)
return makeArrayPaginated(
res.boosters.map((it) => new Booster(it, peers)),
res.boosts.map((it) => new Boost(it, peers)),
res.count,
res.nextOffset,
)

View file

@ -0,0 +1,20 @@
import { BaseTelegramClient } from '@mtcute/core'
import { PeersIndex } from '../../types/index.js'
import { BoostSlot } from '../../types/premium/boost-slot.js'
/**
* Get boost slots information of the current user.
*
* Includes information about the currently boosted channels,
* as well as the slots that can be used to boost other channels.
*/
export async function getMyBoostSlots(client: BaseTelegramClient): Promise<BoostSlot[]> {
const res = await client.call({
_: 'premium.getMyBoosts',
})
const peers = PeersIndex.from(res)
return res.myBoosts.map((it) => new BoostSlot(it, peers))
}

View file

@ -1,9 +1,9 @@
import { BaseTelegramClient } from '@mtcute/core'
import { InputPeerLike } from '../../types/index.js'
import { Booster } from '../../types/stories/booster.js'
import { Boost } from '../../types/premium/boost.js'
import { resolvePeer } from '../users/resolve-peer.js'
import { getBoosters } from './get-boosters.js'
import { getBoosts } from './get-boosts.js'
/**
* Iterate over boosters of a channel.
@ -15,7 +15,7 @@ import { getBoosters } from './get-boosters.js'
export async function* iterBoosters(
client: BaseTelegramClient,
peerId: InputPeerLike,
params?: Parameters<typeof getBoosters>[2] & {
params?: Parameters<typeof getBoosts>[2] & {
/**
* Total number of boosters to fetch
*
@ -31,7 +31,7 @@ export async function* iterBoosters(
*/
chunkSize?: number
},
): AsyncIterableIterator<Booster> {
): AsyncIterableIterator<Boost> {
if (!params) params = {}
const { limit = Infinity, chunkSize = 100 } = params
@ -41,7 +41,7 @@ export async function* iterBoosters(
const peer = await resolvePeer(client, peerId)
for (;;) {
const res = await getBoosters(client, peer, {
const res = await getBoosts(client, peer, {
offset,
limit: Math.min(limit - current, chunkSize),
})

View file

@ -1,62 +0,0 @@
import { BaseTelegramClient, tl } from '@mtcute/core'
import { Chat, InputPeerLike, PeersIndex } from '../../types/index.js'
import { resolvePeer } from '../users/resolve-peer.js'
// @exported
export type CanApplyBoostResult =
| { can: true; current?: Chat }
| { can: false; reason: 'already_boosting' | 'need_premium' }
| { can: false; reason: 'timeout'; until: Date }
/**
* Check if the current user can apply boost to a given channel
*
* @param peerId Peer ID whose stories to fetch
* @returns
* - `{ can: true }` if the user can apply boost
* - `.current` - {@link Chat} that the current user is currently boosting, if any
* - `{ can: false }` if the user can't apply boost
* - `.reason == "already_boosting"` if the user is already boosting this channel
* - `.reason == "need_premium"` if the user needs Premium to boost this channel
* - `.reason == "timeout"` if the user has recently boosted a channel and needs to wait
* (`.until` contains the date until which the user needs to wait)
*/
export async function canApplyBoost(client: BaseTelegramClient, peerId: InputPeerLike): Promise<CanApplyBoostResult> {
try {
const res = await client.call(
{
_: 'stories.canApplyBoost',
peer: await resolvePeer(client, peerId),
},
{ floodSleepThreshold: 0 },
)
if (res._ === 'stories.canApplyBoostOk') return { can: true }
const peers = PeersIndex.from(res)
const chat = new Chat(peers.get(res.currentBoost))
return { can: true, current: chat }
} catch (e) {
if (!tl.RpcError.is(e)) throw e
if (e.is('BOOST_NOT_MODIFIED')) {
return { can: false, reason: 'already_boosting' }
}
if (e.is('PREMIUM_ACCOUNT_REQUIRED')) {
return { can: false, reason: 'need_premium' }
}
if (e.is('FLOOD_WAIT_%d')) {
return {
can: false,
reason: 'timeout',
until: new Date(Date.now() + e.seconds * 1000),
}
}
throw e
}
}

View file

@ -354,18 +354,30 @@ export namespace BotKeyboard {
*
* @param text Text of the button
* @param buttonId ID of the button that will later be passed to the service message
* @param peerType Peer type, along with filters
*/
export function requestPeer(
text: string,
buttonId: number,
peerType: tl.TypeRequestPeerType,
params: {
/**
* Peer type, along with filters
*/
peerType: tl.TypeRequestPeerType
/**
* Maximum number of peers to be selected
*
* @default 1
*/
count?: number
},
): tl.RawKeyboardButtonRequestPeer {
return {
_: 'keyboardButtonRequestPeer',
text,
buttonId,
peerType,
peerType: params.peerType,
maxQuantity: params.count ?? 1,
}
}

View file

@ -8,6 +8,7 @@ export * from './media/index.js'
export * from './messages/index.js'
export * from './misc/index.js'
export * from './peers/index.js'
export * from './premium/index.js'
export * from './reactions/index.js'
export * from './stories/index.js'
export * from './updates/index.js'

View file

@ -1,7 +1,8 @@
import { getMarkedPeerId, tl } from '@mtcute/core'
import { tl } from '@mtcute/core'
import { _callDiscardReasonFromTl, CallDiscardReason } from '../calls/index.js'
import { Photo } from '../media/index.js'
import { Photo } from '../media/photo.js'
import { parsePeer, Peer } from '../peers/peer.js'
import type { Message } from './message.js'
/** Group was created */
@ -378,11 +379,8 @@ export interface ActionPeerChosen {
/** ID of the button passed earlier by the bot */
buttonId: number
/** Marked ID of the chosen peer */
peerId: number
/** Input peer of the chosen peer */
inputPeer?: tl.TypeInputPeer
/** Chosen peers */
peers: Peer[]
}
/** A wallpaper of the chathas been changed */
@ -659,8 +657,7 @@ export function _messageActionFromTl(this: Message, act: tl.TypeMessageAction):
return {
type: 'peer_chosen',
buttonId: act.buttonId,
peerId: getMarkedPeerId(act.peer),
// todo - pass the peer itself?
peers: act.peers.map((it) => parsePeer(it, this._peers)),
}
case 'messageActionSetChatWallPaper':
case 'messageActionSetSameChatWallPaper':

View file

@ -24,6 +24,7 @@ import { MaskPosition, Sticker, StickerSourceType, StickerType, Thumbnail } from
* when reacting to messages using a normal emoji without a custom animation
* - `"default_statuses"` - Default custom emoji status stickerset
* - `"default_topic_icons"` - Default custom emoji stickerset for forum topic icons
* - `"default_channel_statuses"` - Default custom emoji status stickerset for channels
*/
export type InputStickerSet =
| tl.TypeInputStickerSet
@ -36,6 +37,7 @@ export type InputStickerSet =
| 'generic_animations'
| 'default_statuses'
| 'default_topic_icons'
| 'default_channel_statuses'
}
| StickerSet
| string
@ -70,6 +72,8 @@ export function normalizeInputStickerSet(input: InputStickerSet): tl.TypeInputSt
return { _: 'inputStickerSetEmojiDefaultStatuses' }
case 'default_topic_icons':
return { _: 'inputStickerSetEmojiDefaultTopicIcons' }
case 'default_channel_statuses':
return { _: 'inputStickerSetEmojiChannelDefaultStatuses' }
}
}

View file

@ -2,7 +2,7 @@ import { tl } from '@mtcute/core'
import { makeInspectable } from '../../utils/index.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { Photo } from '../media/index.js'
import { Photo } from '../media/photo.js'
import { User } from './user.js'
/**

View file

@ -0,0 +1,70 @@
import { tl } from '@mtcute/core'
import { assertTypeIs } from '@mtcute/core/utils.js'
import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { Chat, PeersIndex } from '../peers/index.js'
/**
* Information about a boost slot
*/
export class BoostSlot {
constructor(
readonly raw: tl.RawMyBoost,
readonly _peers: PeersIndex,
) {}
/** ID of this slot */
get id(): number {
return this.raw.slot
}
/**
* Whether this slot is occupied
*/
get occupied(): boolean {
return this.raw.peer !== undefined
}
/**
* Channel that is occupying this slot, if any
*/
get chat(): Chat | null {
if (!this.raw.peer) return null
assertTypeIs('BoostSlot.chat', this.raw.peer, 'peerChannel')
return new Chat(this._peers.chat(this.raw.peer.channelId))
}
/**
* Date when we started boosting this channel
*
* If this slot is not occupied, will be `0`
*/
get date(): Date {
return new Date(this.raw.date * 1000)
}
/**
* Date when this boost will automatically expire.
*/
get expireDate(): Date {
return new Date(this.raw.expires * 1000)
}
/**
* If this slot is occupied, returns the date when
* we can reassing this slot to another channel.
*
* If `null`, we can reassign it immediately.
*/
get cooldownUntil(): Date | null {
if (!this.raw.cooldownUntilDate) return null
return new Date(this.raw.cooldownUntilDate * 1000)
}
}
memoizeGetters(BoostSlot, ['chat'])
makeInspectable(BoostSlot)

View file

@ -28,6 +28,14 @@ export class BoostStats {
return this.raw.nextLevelBoosts === undefined
}
/**
* The number of boosts acquired from created Telegram Premium
* gift codes and giveaways, only available to channel admins
*/
get gifts(): number {
return this.raw.giftBoosts ?? 0
}
/**
* Number of boosts that were needed for the current level
*/
@ -80,6 +88,14 @@ export class BoostStats {
get url(): string {
return this.raw.boostUrl
}
/**
* If {@link isBoosting}, IDs of the boost slots that are
* currently occupied by this channel
*/
get boostSlots(): number[] {
return this.raw.myBoostSlots ?? []
}
}
makeInspectable(BoostStats)

View file

@ -0,0 +1,94 @@
import { MtUnsupportedError, tl } from '@mtcute/core'
import { makeInspectable } from '../../utils/index.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { PeersIndex, User } from '../peers/index.js'
/**
* Origin of a boost
*
* - `gift` - boost applied because the channel gifted a subscription to some user
* - `unclaimed_gift` - boost applied because the channel gifted a subscription to some user,
* but the user hasn't yet claimed it
* - `giveaway` - boost applied because the user was chosen in a giveaway
* - `user` - boost applied because the user directly boosted the channel
*/
type BoostOrigin = 'gift' | 'unclaimed_gift' | 'giveaway' | 'user'
/**
* Information about a boost (one or more)
*/
export class Boost {
constructor(
readonly raw: tl.RawBoost,
readonly _peers: PeersIndex,
) {}
/** Unique ID of this boost */
get id(): string {
return this.raw.id
}
/** Number of boosts this boost is actually representing */
get count(): number {
return this.raw.multiplier ?? 1
}
/** Date when this boost was applied */
get date(): Date {
return new Date(this.raw.date * 1000)
}
/**
* Date when this boost will automatically expire.
*
* > **Note**: User can still manually cancel the boost before that date
*/
get expireDate(): Date {
return new Date(this.raw.expires * 1000)
}
/**
* Whether this boost was applied because the channel
* directly gifted a subscription to the user
*/
get origin(): BoostOrigin {
if (this.raw.unclaimed) return 'unclaimed_gift'
if (this.raw.gift) return 'gift'
if (this.raw.giveaway) return 'giveaway'
if (this.raw.userId) return 'user'
throw new MtUnsupportedError('Unknown boost origin')
}
/**
* User who is boosting the channel.
*
* Only available for some origins
*/
get user(): User | null {
if (!this.raw.userId) return null
return new User(this._peers.user(this.raw.userId))
}
/**
* ID of the message containing the giveaway where this
* user has won
*/
get giveawayMessageId(): number | null {
return this.raw.giveawayMsgId ?? null
}
/**
* The created Telegram Premium gift code, only set if `origin` is not `user`,
* AND it is either a gift code for the currently logged in user,
* or if it was already claimed
*/
get usedGiftSlug(): string | null {
return this.raw.usedGiftSlug ?? null
}
}
memoizeGetters(Boost, ['user'])
makeInspectable(Boost)

View file

@ -0,0 +1,3 @@
export * from './boost.js'
export * from './boost-slot.js'
export * from './boost-stats.js'

View file

@ -1,34 +0,0 @@
import { tl } from '@mtcute/core'
import { makeInspectable } from '../../utils/index.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { PeersIndex, User } from '../peers/index.js'
/**
* Information about a user who is boosting a channel
*/
export class Booster {
constructor(
readonly raw: tl.RawBooster,
readonly _peers: PeersIndex,
) {}
/**
* Date when this boost will automatically expire.
*
* > **Note**: User can still manually cancel the boost before that date
*/
get expireDate(): Date {
return new Date(this.raw.expires * 1000)
}
/**
* User who is boosting the channel
*/
get user(): User {
return new User(this._peers.user(this.raw.userId))
}
}
memoizeGetters(Booster, ['user'])
makeInspectable(Booster)

View file

@ -1,6 +1,4 @@
export * from './all-stories.js'
export * from './boost-stats.js'
export * from './booster.js'
export * from './interactive/index.js'
export * from './peer-stories.js'
export * from './stealth-mode.js'

View file

@ -0,0 +1,34 @@
import { tl } from '@mtcute/core'
import { makeInspectable } from '../../../utils/index.js'
import { memoizeGetters } from '../../../utils/memoize.js'
import { Chat } from '../../peers/chat.js'
import { PeersIndex } from '../../peers/peers-index.js'
import { StoryInteractiveArea } from './base.js'
/**
* Interactive element containing a channel post
*/
export class StoryInteractiveChannelPost extends StoryInteractiveArea {
readonly type = 'channel_post' as const
constructor(
readonly raw: tl.RawMediaAreaChannelPost,
readonly _peers: PeersIndex,
) {
super(raw)
}
/** Channel being mentioned */
get chat(): Chat {
return new Chat(this._peers.chat(this.raw.channelId))
}
/** ID of the message being mentioned */
get messageId(): number {
return this.raw.msgId
}
}
memoizeGetters(StoryInteractiveChannelPost, ['chat'])
makeInspectable(StoryInteractiveChannelPost)

View file

@ -1,15 +1,21 @@
import { MtTypeAssertionError, tl } from '@mtcute/core'
import { PeersIndex } from '../../peers/index.js'
import { StoryInteractiveChannelPost } from './channel-post.js'
import { StoryInteractiveLocation } from './location.js'
import { StoryInteractiveReaction } from './reaction.js'
import { StoryInteractiveVenue } from './venue.js'
export * from './input.js'
export { StoryInteractiveLocation, StoryInteractiveReaction, StoryInteractiveVenue }
export { StoryInteractiveChannelPost, StoryInteractiveLocation, StoryInteractiveReaction, StoryInteractiveVenue }
export type StoryInteractiveElement = StoryInteractiveReaction | StoryInteractiveLocation | StoryInteractiveVenue
export type StoryInteractiveElement =
| StoryInteractiveReaction
| StoryInteractiveLocation
| StoryInteractiveVenue
| StoryInteractiveChannelPost
export function _storyInteractiveElementFromTl(raw: tl.TypeMediaArea): StoryInteractiveElement {
export function _storyInteractiveElementFromTl(raw: tl.TypeMediaArea, peers: PeersIndex): StoryInteractiveElement {
switch (raw._) {
case 'mediaAreaSuggestedReaction':
return new StoryInteractiveReaction(raw)
@ -17,7 +23,10 @@ export function _storyInteractiveElementFromTl(raw: tl.TypeMediaArea): StoryInte
return new StoryInteractiveLocation(raw)
case 'mediaAreaVenue':
return new StoryInteractiveVenue(raw)
case 'mediaAreaChannelPost':
return new StoryInteractiveChannelPost(raw, peers)
case 'inputMediaAreaVenue':
case 'inputMediaAreaChannelPost':
throw new MtTypeAssertionError('StoryInteractiveElement', '!input*', raw._)
}
}

View file

@ -1,9 +1,11 @@
import { tl } from '@mtcute/core'
import { MtTypeAssertionError, tl } from '@mtcute/core'
import { makeInspectable } from '../../utils/index.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { PeersIndex, User } from '../peers/index.js'
import { Message } from '../messages/index.js'
import { parsePeer, Peer, PeersIndex, User } from '../peers/index.js'
import { ReactionEmoji, toReactionEmoji } from '../reactions/index.js'
import { Story } from './story.js'
/**
* Information about a single user who has viewed a story.
@ -45,6 +47,83 @@ export class StoryViewer {
memoizeGetters(StoryViewer, ['user'])
makeInspectable(StoryViewer)
/**
* Kind of a story repost.
* - `forward` - a story has been forwarded somewhere
* - `repost` - a story has been reposted as a story
*/
export type StoryRepostKind = 'forward' | 'repost'
/**
* Information about a single story repost.
*/
export class StoryRepost {
constructor(
readonly raw: tl.RawStoryViewPublicForward | tl.RawStoryViewPublicRepost,
readonly _peers: PeersIndex,
) {}
/** Whether this peer is in current user's global blacklist */
get isBlocked(): boolean {
return this.raw.blocked!
}
/** Whether current user's stories are hidden from this peer */
get isStoriesBlocked(): boolean {
return this.raw.blockedMyStoriesFrom!
}
/** Kind of the repost */
get kind(): StoryRepostKind {
return this.raw._ === 'storyViewPublicForward' ? 'forward' : 'repost'
}
/** Date when the repost has happened */
get date(): Date {
if (this.raw._ === 'storyViewPublicForward' && this.raw.message._ === 'message') {
return new Date(this.raw.message.date * 1000)
}
if (this.raw._ === 'storyViewPublicRepost' && this.raw.story._ === 'storyItem') {
return new Date(this.raw.story.date * 1000)
}
throw new MtTypeAssertionError('StoryRepost', 'date', 'none')
}
/**
* Message that has been forwarded/reposted.
*
* Only available if {@link kind} is `forward`.
*/
get message(): Message | null {
if (this.raw._ === 'storyViewPublicRepost') return null
return new Message(this.raw.message, this._peers)
}
/**
* Story that has been reposted.
*
* Only available if {@link kind} is `repost`.
*/
get story(): Story | null {
if (this.raw._ === 'storyViewPublicForward') return null
if (this.raw.story._ !== 'storyItem') return null
return new Story(this.raw.story, this._peers)
}
/** Information about the peer who has made the reposted */
get peer(): Peer {
if (this.raw._ === 'storyViewPublicForward') {
return this.message!.sender
}
return parsePeer(this.raw.peerId, this._peers)
}
}
/**
* List of story viewers.
*/
@ -70,7 +149,15 @@ export class StoryViewersList {
/** List of viewers */
get viewers(): StoryViewer[] {
return this.raw.views.map((it) => new StoryViewer(it, this._peers))
const res: StoryViewer[] = []
for (const view of this.raw.views) {
if (view._ === 'storyView') {
res.push(new StoryViewer(view, this._peers))
}
}
return res
}
}

View file

@ -122,7 +122,9 @@ export class Story {
return new Photo(this.raw.media.photo, this.raw.media)
case 'messageMediaDocument': {
if (this.raw.media.document?._ !== 'document') { throw new MtUnsupportedError('Unsupported story media type') }
if (this.raw.media.document?._ !== 'document') {
throw new MtUnsupportedError('Unsupported story media type')
}
const doc = parseDocument(this.raw.media.document, this.raw.media)
if (doc.type === 'video') return doc
@ -138,7 +140,7 @@ export class Story {
get interactiveElements(): StoryInteractiveElement[] {
if (!this.raw.mediaAreas) return []
return this.raw.mediaAreas.map((it) => _storyInteractiveElementFromTl(it))
return this.raw.mediaAreas.map((it) => _storyInteractiveElementFromTl(it, this._peers))
}
/**

View file

@ -2,7 +2,7 @@
TL schema and related utils used for mtcute.
Generated from TL layer **167** (last updated on 01.12.2023).
Generated from TL layer **169** (last updated on 24.12.2023).
## About

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -10,7 +10,7 @@
"attachMenuBot": ["bot_id"],
"autoDownloadSettings": ["video_size_max", "file_size_max"],
"botInfo": ["user_id"],
"booster": ["user_id"],
"boost": ["user_id"],
"channel": ["id"],
"channelAdminLogEvent": ["user_id"],
"channelAdminLogEventActionChangeLinkedChat": ["prev_value", "new_value"],
@ -56,6 +56,7 @@
"inputUser": ["user_id"],
"inputUserFromMessage": ["user_id"],
"keyboardButtonUserProfile": ["user_id"],
"mediaAreaChannelPost": ["channel_id"],
"message": ["via_bot_id"],
"messageActionChannelMigrateFrom": ["chat_id"],
"messageActionChatAddUser": ["users"],

View file

@ -1,6 +1,6 @@
{
"name": "@mtcute/tl",
"version": "167.0.1",
"version": "169.0.0",
"description": "TL schema used for mtcute",
"main": "index.js",
"author": "Alina Sireneva <alina@tei.su>",