feat: stories and boosts

closes MTQ-51
This commit is contained in:
alina 🌸 2023-10-04 19:26:21 +03:00
parent 62815d26d7
commit 7abcc6188a
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
71 changed files with 3327 additions and 257 deletions

View file

@ -10,7 +10,8 @@
"test": "mocha -r ts-node/register \"tests/**/*.spec.ts\"",
"docs": "typedoc",
"build": "tsc",
"gen-client": "node ./scripts/generate-client.js"
"gen-client": "node ./scripts/generate-client.js",
"gen-updates": "node ./scripts/generate-updates.js"
},
"dependencies": {
"@types/node": "18.16.0",

View file

@ -15,3 +15,5 @@ bot_stopped = BotStoppedUpdate
bot_chat_join_request = BotChatJoinRequestUpdate
chat_join_request = ChatJoinRequestUpdate
pre_checkout_query = PreCheckoutQuery
story: StoryUpdate = StoryUpdate
delete_story = DeleteStoryUpdate

View file

@ -165,6 +165,7 @@ import { translateText } from './methods/messages/translate-text'
import { unpinAllMessages } from './methods/messages/unpin-all-messages'
import { unpinMessage } from './methods/messages/unpin-message'
import { initTakeoutSession } from './methods/misc/init-takeout-session'
import { _normalizePrivacyRules } from './methods/misc/normalize-privacy-rules'
import {
getParseMode,
registerParseMode,
@ -183,6 +184,33 @@ import { getInstalledStickers } from './methods/stickers/get-installed-stickers'
import { getStickerSet } from './methods/stickers/get-sticker-set'
import { moveStickerInSet } from './methods/stickers/move-sticker-in-set'
import { setStickerSetThumb } from './methods/stickers/set-sticker-set-thumb'
import { applyBoost } from './methods/stories/apply-boost'
import { canApplyBoost, CanApplyBoostResult } from './methods/stories/can-apply-boost'
import { canSendStory, CanSendStoryResult } from './methods/stories/can-send-story'
import { deleteStories } from './methods/stories/delete-stories'
import { editStory } from './methods/stories/edit-story'
import { _findStoryInUpdate } from './methods/stories/find-in-update'
import { getAllStories } from './methods/stories/get-all-stories'
import { getBoostStats } from './methods/stories/get-boost-stats'
import { getBoosters } from './methods/stories/get-boosters'
import { getPeerStories } from './methods/stories/get-peer-stories'
import { getProfileStories } from './methods/stories/get-profile-stories'
import { getStoriesById } from './methods/stories/get-stories-by-id'
import { getStoriesInteractions } from './methods/stories/get-stories-interactions'
import { getStoryLink } from './methods/stories/get-story-link'
import { getStoryViewers } from './methods/stories/get-story-viewers'
import { hideMyStoriesViews } from './methods/stories/hide-my-stories-views'
import { incrementStoriesViews } from './methods/stories/increment-stories-views'
import { iterAllStories } from './methods/stories/iter-all-stories'
import { iterBoosters } from './methods/stories/iter-boosters'
import { iterProfileStories } from './methods/stories/iter-profile-stories'
import { iterStoryViewers } from './methods/stories/iter-story-viewers'
import { readStories } from './methods/stories/read-stories'
import { reportStory } from './methods/stories/report-story'
import { sendStory } from './methods/stories/send-story'
import { sendStoryReaction } from './methods/stories/send-story-reaction'
import { togglePeerStoriesArchived } from './methods/stories/toggle-peer-stories-archived'
import { toggleStoriesPinned } from './methods/stories/toggle-stories-pinned'
import {
_dispatchUpdate,
_fetchUpdatesState,
@ -218,8 +246,11 @@ import { setUsername } from './methods/users/set-username'
import { unblockUser } from './methods/users/unblock-user'
import { updateProfile } from './methods/users/update-profile'
import {
AllStories,
ArrayPaginated,
ArrayWithTotal,
Booster,
BoostStats,
BotChatJoinRequestUpdate,
BotCommands,
BotStoppedUpdate,
@ -249,6 +280,7 @@ import {
InputInlineResult,
InputMediaLike,
InputPeerLike,
InputPrivacyRule,
InputReaction,
InputStickerSetItem,
MaybeDynamic,
@ -259,6 +291,7 @@ import {
ParsedUpdate,
PeerReaction,
PeersIndex,
PeerStories,
Photo,
Poll,
PollUpdate,
@ -271,6 +304,11 @@ import {
StickerSet,
StickerSourceType,
StickerType,
StoriesStealthMode,
Story,
StoryInteractions,
StoryViewer,
StoryViewersList,
TakeoutSession,
TermsOfService,
TypingStatus,
@ -3832,6 +3870,12 @@ export interface TelegramClient extends BaseTelegramClient {
* @param params Takeout session parameters
*/
initTakeoutSession(params: Omit<tl.account.RawInitTakeoutSessionRequest, '_'>): Promise<TakeoutSession>
/**
* Normalize {@link InputPrivacyRule}[] to `tl.TypeInputPrivacyRule`,
* resolving the peers if needed.
*
*/
_normalizePrivacyRules(rules: InputPrivacyRule[]): Promise<tl.TypeInputPrivacyRule[]>
/**
* Register a given {@link IMessageEntityParser} as a parse mode
* for messages. When this method is first called, given parse
@ -4079,6 +4123,532 @@ export interface TelegramClient extends BaseTelegramClient {
progressCallback?: (uploaded: number, total: number) => void
},
): Promise<StickerSet>
/**
* Boost a given channel
*
* @param peerId Peer ID to boost
*/
applyBoost(peerId: InputPeerLike): Promise<void>
/**
* 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
*/
canApplyBoost(peerId: InputPeerLike): Promise<CanApplyBoostResult>
/**
* Check if the current user can post stories as a given peer
*
* @param peerId Peer ID whose stories to fetch
* @returns
* - `true` if the user can post stories
* - `"need_admin"` if the user is not an admin in the chat
* - `"need_boosts"` if the channel doesn't have enough boosts
*/
canSendStory(peerId: InputPeerLike): Promise<CanSendStoryResult>
/**
* Delete a story
*
* @returns IDs of stories that were removed
*/
deleteStories(params: {
/**
* Story IDs to delete
*/
ids: MaybeArray<number>
/**
* Peer ID whose stories to delete
*
* @default `self`
*/
peer?: InputPeerLike
}): Promise<number[]>
/**
* Edit a sent story
*
* @returns Edited story
*/
editStory(params: {
/**
* Story ID to edit
*/
id: number
/**
* Peer ID to whose story to edit
*
* @default `self`
*/
peer?: InputPeerLike
/**
* Media contained in a story. Currently can only be a photo or a video.
*/
media?: InputMediaLike
/**
* Override caption for {@link media}
*/
caption?: string | FormattedString<string>
/**
* Override entities for {@link media}
*/
entities?: tl.TypeMessageEntity[]
/**
* Parse mode to use to parse entities before sending
* the message. Defaults to current default parse mode (if any).
*
* Passing `null` will explicitly disable formatting.
*/
parseMode?: string | null
/**
* Interactive elements to add to the story
*/
interactiveElements?: tl.TypeMediaArea[]
/**
* Privacy rules to apply to the story
*
* @default "Everyone"
*/
privacyRules?: InputPrivacyRule[]
}): Promise<Story>
_findStoryInUpdate(res: tl.TypeUpdates): Story
/**
* Get all stories (e.g. to load the top bar)
*
*/
getAllStories(params?: {
/**
* Offset from which to fetch stories
*/
offset?: string
/**
* Whether to fetch stories from "archived" (or "hidden") peers
*/
archived?: boolean
}): Promise<AllStories>
/**
* Get information about boosts in a channel
*
* @returns IDs of stories that were removed
*/
getBoostStats(peerId: InputPeerLike): Promise<BoostStats>
/**
* Get boosters of a channel
*
* @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
*
* @param peerId Peer ID whose stories to fetch
*/
getPeerStories(peerId: InputPeerLike): Promise<PeerStories>
/**
* Get profile stories
*
*/
getProfileStories(
peerId: InputPeerLike,
params?: {
/**
* Kind of stories to fetch
* - `pinned` - stories pinned to the profile and visible to everyone
* - `archived` - "archived" stories that can later be pinned, only visible to the owner
*
* @default `pinned`
*/
kind?: 'pinned' | 'archived'
/**
* Offset ID for pagination
*/
offsetId?: number
/**
* Maximum number of stories to fetch
*
* @default 100
*/
limit?: number
},
): Promise<ArrayPaginated<Story, number>>
/**
* Get a single story by its ID
*
* @param peerId Peer ID whose stories to fetch
* @param storyId Story ID
*/
getStoriesById(peerId: InputPeerLike, storyId: number): Promise<Story>
/**
* Get multiple stories by their IDs
*
* @param peerId Peer ID whose stories to fetch
* @param storyIds Story IDs
*/
getStoriesById(peerId: InputPeerLike, storyIds: number[]): Promise<Story[]>
/**
* Get brief information about story interactions.
*
*/
getStoriesInteractions(peerId: InputPeerLike, storyId: number): Promise<StoryInteractions>
/**
* Get brief information about stories interactions.
*
* The result will be in the same order as the input IDs
*
*/
getStoriesInteractions(peerId: InputPeerLike, storyIds: number[]): Promise<StoryInteractions[]>
/**
* Generate a link to a story.
*
* Basically the link format is `t.me/<username>/s/<story_id>`,
* and if the user doesn't have a username, `USER_PUBLIC_MISSING` is thrown.
*
* I have no idea why is this an RPC call, but whatever
*
*/
getStoryLink(peerId: InputPeerLike, storyId: number): Promise<string>
/**
* Get viewers list of a story
*
*/
getStoryViewers(
peerId: InputPeerLike,
storyId: number,
params?: {
/**
* Whether to only fetch viewers from contacts
*/
onlyContacts?: boolean
/**
* How to sort the results?
* - `reaction` - by reaction (viewers who has reacted are first), then by date (newest first)
* - `date` - by date, newest first
*
* @default `reaction`
*/
sortBy?: 'reaction' | 'date'
/**
* Search query
*/
query?: string
/**
* Offset ID for pagination
*/
offset?: string
/**
* Maximum number of viewers to fetch
*
* @default 100
*/
limit?: number
},
): Promise<StoryViewersList>
/**
* Hide own stories views (activate so called "stealth mode")
*
* Currently has a cooldown of 1 hour, and throws FLOOD_WAIT error if it is on cooldown.
*
*/
hideMyStoriesViews(params?: {
/**
* Whether to hide views from the last 5 minutes
*
* @default true
*/
past?: boolean
/**
* Whether to hide views for the next 25 minutes
*
* @default true
*/
future?: boolean
}): Promise<StoriesStealthMode>
/**
* Increment views of one or more stories.
*
* This should be used for pinned stories, as they can't
* be marked as read when the user sees them ({@link Story#isActive} == false)
*
* @param peerId Peer ID whose stories to mark as read
* @param ids ID(s) of the stories to increment views of (max 200)
*/
incrementStoriesViews(peerId: InputPeerLike, ids: MaybeArray<number>): Promise<boolean>
/**
* Iterate over all stories (e.g. to load the top bar)
*
* Wrapper over {@link getAllStories}
*
*/
iterAllStories(params?: {
/**
* Offset from which to start fetching stories
*/
offset?: string
/**
* Maximum number of stories to fetch
*
* @default Infinity
*/
limit?: number
/**
* Whether to fetch stories from "archived" (or "hidden") peers
*/
archived?: boolean
}): AsyncIterableIterator<PeerStories>
/**
* Iterate over boosters of a channel.
*
* Wrapper over {@link getBoosters}
*
* @returns IDs of stories that were removed
*/
iterBoosters(
peerId: InputPeerLike,
params?: Parameters<TelegramClient['getBoosters']>[1] & {
/**
* 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}
*
*/
iterProfileStories(
peerId: InputPeerLike,
params?: Parameters<TelegramClient['getProfileStories']>[1] & {
/**
* Total number of stories to fetch
*
* @default `Infinity`, i.e. fetch all stories
*/
limit?: number
/**
* Number of stories to fetch per request.
* Usually you shouldn't care about this.
*
* @default 100
*/
chunkSize?: number
},
): AsyncIterableIterator<Story>
/**
* Iterate over viewers list of a story.
* Wrapper over {@link getStoryViewers}
*
*/
iterStoryViewers(
peerId: InputPeerLike,
storyId: number,
params?: Parameters<TelegramClient['getStoryViewers']>[2] & {
/**
* Total number of viewers to fetch
*
* @default Infinity, i.e. fetch all viewers
*/
limit?: number
/**
* Number of viewers to fetch per request.
* Usually you don't need to change this.
*
* @default 100
*/
chunkSize?: number
},
): AsyncIterableIterator<StoryViewer>
/**
* Mark all stories up to a given ID as read
*
* This should only be used for "active" stories ({@link Story#isActive} == false)
*
* @param peerId Peer ID whose stories to mark as read
* @returns IDs of the stores that were marked as read
*/
readStories(peerId: InputPeerLike, maxId: number): Promise<number[]>
/**
* Report a story (or multiple stories) to the moderation team
*
*/
reportStory(
peerId: InputPeerLike,
storyIds: MaybeArray<number>,
params?: {
/**
* Reason for reporting
*
* @default inputReportReasonSpam
*/
reason?: tl.TypeReportReason
/**
* Additional comment to the report
*/
message?: string
},
): Promise<void>
/**
* Send (or remove) a reaction to a story
*
*/
sendStoryReaction(
peerId: InputPeerLike,
storyId: number,
reaction: InputReaction,
params?: {
/**
* Whether to add this reaction to recently used
*/
addToRecent?: boolean
},
): Promise<void>
/**
* Send a story
*
* @returns Created story
*/
sendStory(params: {
/**
* Peer ID to send story as
*
* @default `self`
*/
peer?: InputPeerLike
/**
* Media contained in a story. Currently can only be a photo or a video.
*
* You can also pass TDLib and Bot API compatible File ID,
* which will be wrapped in {@link InputMedia.auto}
*/
media: InputMediaLike | string
/**
* Override caption for {@link media}
*/
caption?: string | FormattedString<string>
/**
* Override entities for {@link media}
*/
entities?: tl.TypeMessageEntity[]
/**
* Parse mode to use to parse entities before sending
* the message. Defaults to current default parse mode (if any).
*
* Passing `null` will explicitly disable formatting.
*/
parseMode?: string | null
/**
* Whether to automatically pin this story to the profile
*/
pinned?: boolean
/**
* Whether to disallow sharing this story
*/
forbidForwards?: boolean
/**
* Interactive elements to add to the story
*/
interactiveElements?: tl.TypeMediaArea[]
/**
* Privacy rules to apply to the story
*
* @default "Everyone"
*/
privacyRules?: InputPrivacyRule[]
/**
* TTL period of the story, in seconds
*
* @default 86400
*/
period?: number
}): Promise<Story>
/**
* Toggle whether peer's stories are archived (hidden) or not.
*
* This **does not** archive the chat with that peer, only stories.
*
*/
togglePeerStoriesArchived(peerId: InputPeerLike, archived: boolean): Promise<void>
/**
* Toggle one or more stories pinned status
*
* @returns IDs of stories that were toggled
*/
toggleStoriesPinned(params: {
/**
* Story ID(s) to toggle
*/
ids: MaybeArray<number>
/**
* Whether to pin or unpin the story
*/
pinned: boolean
/**
* Peer ID whose stories to toggle
*
* @default `self`
*/
peer?: InputPeerLike
}): Promise<number[]>
/**
* Enable RPS meter.
* Only available in NodeJS v10.7.0 and newer
@ -4579,6 +5149,7 @@ export class TelegramClient extends BaseTelegramClient {
unpinAllMessages = unpinAllMessages
unpinMessage = unpinMessage
initTakeoutSession = initTakeoutSession
_normalizePrivacyRules = _normalizePrivacyRules
registerParseMode = registerParseMode
unregisterParseMode = unregisterParseMode
getParseMode = getParseMode
@ -4597,6 +5168,33 @@ export class TelegramClient extends BaseTelegramClient {
getStickerSet = getStickerSet
moveStickerInSet = moveStickerInSet
setStickerSetThumb = setStickerSetThumb
applyBoost = applyBoost
canApplyBoost = canApplyBoost
canSendStory = canSendStory
deleteStories = deleteStories
editStory = editStory
_findStoryInUpdate = _findStoryInUpdate
getAllStories = getAllStories
getBoostStats = getBoostStats
getBoosters = getBoosters
getPeerStories = getPeerStories
getProfileStories = getProfileStories
getStoriesById = getStoriesById
getStoriesInteractions = getStoriesInteractions
getStoryLink = getStoryLink
getStoryViewers = getStoryViewers
hideMyStoriesViews = hideMyStoriesViews
incrementStoriesViews = incrementStoriesViews
iterAllStories = iterAllStories
iterBoosters = iterBoosters
iterProfileStories = iterProfileStories
iterStoryViewers = iterStoryViewers
readStories = readStories
reportStory = reportStory
sendStoryReaction = sendStoryReaction
sendStory = sendStory
togglePeerStoriesArchived = togglePeerStoriesArchived
toggleStoriesPinned = toggleStoriesPinned
enableRps = enableRps
getCurrentRpsIncoming = getCurrentRpsIncoming
getCurrentRpsProcessing = getCurrentRpsProcessing

View file

@ -11,8 +11,11 @@ import { tdFileId } from '@mtcute/file-id'
// @copy
import {
AllStories,
ArrayPaginated,
ArrayWithTotal,
Booster,
BoostStats,
BotChatJoinRequestUpdate,
BotCommands,
BotStoppedUpdate,
@ -42,6 +45,7 @@ import {
InputInlineResult,
InputMediaLike,
InputPeerLike,
InputPrivacyRule,
InputReaction,
InputStickerSetItem,
MaybeDynamic,
@ -52,6 +56,7 @@ import {
ParsedUpdate,
PeerReaction,
PeersIndex,
PeerStories,
Photo,
Poll,
PollUpdate,
@ -64,6 +69,11 @@ import {
StickerSet,
StickerSourceType,
StickerType,
StoriesStealthMode,
Story,
StoryInteractions,
StoryViewer,
StoryViewersList,
TakeoutSession,
TermsOfService,
TypingStatus,

View file

@ -253,7 +253,7 @@ export async function* iterDialogs(
const last = dialogs[dialogs.length - 1]
offsetPeer = last.chat.inputPeer
offsetId = last.raw.topMessage
offsetDate = normalizeDate(last.lastMessage.date)!
offsetDate = last.lastMessage.raw.date
for (const d of dialogs) {
if (filterFolder && !filterFolder(d)) continue

View file

@ -246,14 +246,15 @@ export async function uploadFile(
lock.release()
}
if (fileSize === -1 && stream.readableEnded) {
fileSize = pos + (part?.length ?? 0)
partCount = ~~((fileSize + partSize - 1) / partSize)
this.log.debug('readable ended, file size = %d, part count = %d', fileSize, partCount)
if (!part && fileSize !== -1) {
throw new MtArgumentError(`Unexpected EOS (there were only ${idx} parts, but expected ${partCount})`)
}
if (!part) {
throw new MtArgumentError(`Unexpected EOS (there were only ${idx} parts, but expected ${partCount})`)
if (fileSize === -1 && (stream.readableEnded || !part)) {
fileSize = pos + (part?.length ?? 0)
partCount = ~~((fileSize + partSize - 1) / partSize)
if (!part) part = Buffer.alloc(0)
this.log.debug('readable ended, file size = %d, part count = %d', fileSize, partCount)
}
if (!Buffer.isBuffer(part)) {
@ -305,14 +306,10 @@ export async function uploadFile(
return uploadNextPart()
}
await Promise.all(
Array.from(
{
length: connectionPoolSize * requestsPerConnection,
},
uploadNextPart,
),
)
let poolSize = connectionPoolSize * requestsPerConnection
if (partCount !== -1 && poolSize > partCount) poolSize = partCount
await Promise.all(Array.from({ length: poolSize }, uploadNextPart))
let inputFile: tl.TypeInputFile

View file

@ -88,7 +88,9 @@ export async function editMessage(
params.media.entities,
)
}
} else if (params.text) {
}
if (params.text) {
[content, entities] = await this._parseEntities(params.text, params.parseMode, params.entities)
}

View file

@ -0,0 +1,52 @@
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { InputPrivacyRule } from '../../types'
import { normalizeToInputUser } from '../../utils'
/**
* Normalize {@link InputPrivacyRule}[] to `tl.TypeInputPrivacyRule`,
* resolving the peers if needed.
*
* @internal
*/
export async function _normalizePrivacyRules(
this: TelegramClient,
rules: InputPrivacyRule[],
): Promise<tl.TypeInputPrivacyRule[]> {
const res: tl.TypeInputPrivacyRule[] = []
for (const rule of rules) {
if ('_' in rule) {
res.push(rule)
continue
}
if ('users' in rule) {
const users = await this.resolvePeerMany(rule.users, normalizeToInputUser)
res.push({
_: rule.allow ? 'inputPrivacyValueAllowUsers' : 'inputPrivacyValueDisallowUsers',
users,
})
continue
}
if ('chats' in rule) {
const chats = await this.resolvePeerMany(rule.chats)
res.push({
_: rule.allow ? 'inputPrivacyValueAllowChatParticipants' : 'inputPrivacyValueDisallowChatParticipants',
chats: chats.map((peer) => {
if ('channelId' in peer) return peer.channelId
if ('chatId' in peer) return peer.chatId
throw new Error('UNREACHABLE')
}),
})
continue
}
}
return res
}

View file

@ -0,0 +1,15 @@
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
/**
* Boost a given channel
*
* @param peerId Peer ID to boost
* @internal
*/
export async function applyBoost(this: TelegramClient, peerId: InputPeerLike): Promise<void> {
await this.call({
_: 'stories.applyBoost',
peer: await this.resolvePeer(peerId),
})
}

View file

@ -0,0 +1,63 @@
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { Chat, InputPeerLike, PeersIndex } from '../../types'
// @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)
* @internal
*/
export async function canApplyBoost(this: TelegramClient, peerId: InputPeerLike): Promise<CanApplyBoostResult> {
try {
const res = await this.call(
{
_: 'stories.canApplyBoost',
peer: await this.resolvePeer(peerId),
},
{ floodSleepThreshold: 0 },
)
if (res._ === 'stories.canApplyBoostOk') return { can: true }
const peers = PeersIndex.from(res)
const chat = new Chat(this, 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

@ -0,0 +1,39 @@
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
// @exported
export type CanSendStoryResult = true | 'need_admin' | 'need_boosts'
/**
* Check if the current user can post stories as a given peer
*
* @param peerId Peer ID whose stories to fetch
* @returns
* - `true` if the user can post stories
* - `"need_admin"` if the user is not an admin in the chat
* - `"need_boosts"` if the channel doesn't have enough boosts
* @internal
*/
export async function canSendStory(this: TelegramClient, peerId: InputPeerLike): Promise<CanSendStoryResult> {
try {
const res = await this.call({
_: 'stories.canSendStory',
peer: await this.resolvePeer(peerId),
})
if (!res) return 'need_admin'
return true
} catch (e) {
if (tl.RpcError.is(e, 'CHAT_ADMIN_REQUIRED')) {
return 'need_admin'
}
if (tl.RpcError.is(e, 'BOOSTS_REQUIRED')) {
return 'need_boosts'
}
throw e
}
}

View file

@ -0,0 +1,35 @@
import { MaybeArray } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
/**
* Delete a story
*
* @returns IDs of stories that were removed
* @internal
*/
export async function deleteStories(
this: TelegramClient,
params: {
/**
* Story IDs to delete
*/
ids: MaybeArray<number>
/**
* Peer ID whose stories to delete
*
* @default `self`
*/
peer?: InputPeerLike
},
): Promise<number[]> {
const { ids, peer = 'me' } = params
return this.call({
_: 'stories.deleteStories',
peer: await this.resolvePeer(peer),
id: Array.isArray(ids) ? ids : [ids],
})
}

View file

@ -0,0 +1,101 @@
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { FormattedString, InputMediaLike, InputPeerLike, InputPrivacyRule, Story } from '../../types'
/**
* Edit a sent story
*
* @returns Edited story
* @internal
*/
export async function editStory(
this: TelegramClient,
params: {
/**
* Story ID to edit
*/
id: number
/**
* Peer ID to whose story to edit
*
* @default `self`
*/
peer?: InputPeerLike
/**
* Media contained in a story. Currently can only be a photo or a video.
*/
media?: InputMediaLike
/**
* Override caption for {@link media}
*/
caption?: string | FormattedString<string>
/**
* Override entities for {@link media}
*/
entities?: tl.TypeMessageEntity[]
/**
* Parse mode to use to parse entities before sending
* the message. Defaults to current default parse mode (if any).
*
* Passing `null` will explicitly disable formatting.
*/
parseMode?: string | null
/**
* Interactive elements to add to the story
*/
interactiveElements?: tl.TypeMediaArea[]
/**
* Privacy rules to apply to the story
*
* @default "Everyone"
*/
privacyRules?: InputPrivacyRule[]
},
): Promise<Story> {
const { id, peer = 'me', interactiveElements } = params
let caption: string | undefined = undefined
let entities: tl.TypeMessageEntity[] | undefined
let media: tl.TypeInputMedia | undefined = undefined
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) {
[caption, entities] = await this._parseEntities(
params.media.caption,
params.parseMode,
params.media.entities,
)
}
}
if (params.caption) {
[caption, entities] = await this._parseEntities(params.caption, params.parseMode, params.entities)
}
const privacyRules = params.privacyRules ? await this._normalizePrivacyRules(params.privacyRules) : undefined
const res = await this.call({
_: 'stories.editStory',
peer: await this.resolvePeer(peer),
id,
media,
mediaAreas: interactiveElements,
caption,
entities,
privacyRules,
})
return this._findStoryInUpdate(res)
}

View file

@ -0,0 +1,24 @@
import { MtTypeAssertionError, tl } from '@mtcute/core'
import { assertTypeIs, hasValueAtKey } from '@mtcute/core/utils'
import { TelegramClient } from '../../client'
import { PeersIndex, Story } from '../../types'
import { assertIsUpdatesGroup } from '../../utils/updates-utils'
/** @internal */
export function _findStoryInUpdate(this: TelegramClient, res: tl.TypeUpdates): Story {
assertIsUpdatesGroup('_findStoryInUpdate', res)
this._handleUpdate(res, true)
const peers = PeersIndex.from(res)
const updateStory = res.updates.find(hasValueAtKey('_', 'updateStory'))
if (!updateStory) {
throw new MtTypeAssertionError('_findStoryInUpdate (@ .updates[*])', 'updateStory', 'none')
}
assertTypeIs('updateStory.story', updateStory.story, 'storyItem')
return new Story(this, updateStory.story, peers)
}

View file

@ -0,0 +1,39 @@
import { assertTypeIsNot } from '@mtcute/core/utils'
import { TelegramClient } from '../../client'
import { AllStories } from '../../types'
/**
* Get all stories (e.g. to load the top bar)
*
* @internal
*/
export async function getAllStories(
this: TelegramClient,
params?: {
/**
* Offset from which to fetch stories
*/
offset?: string
/**
* Whether to fetch stories from "archived" (or "hidden") peers
*/
archived?: boolean
},
): Promise<AllStories> {
if (!params) params = {}
const { offset, archived } = params
const res = await this.call({
_: 'stories.getAllStories',
state: offset,
next: Boolean(offset),
hidden: archived,
})
assertTypeIsNot('getAllStories', res, 'stories.allStoriesNotModified')
return new AllStories(this, res)
}

View file

@ -0,0 +1,18 @@
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
import { BoostStats } from '../../types/stories/boost-stats'
/**
* Get information about boosts in a channel
*
* @returns IDs of stories that were removed
* @internal
*/
export async function getBoostStats(this: TelegramClient, peerId: InputPeerLike): Promise<BoostStats> {
const res = await this.call({
_: 'stories.getBoostsStatus',
peer: await this.resolvePeer(peerId),
})
return new BoostStats(res)
}

View file

@ -0,0 +1,45 @@
import { TelegramClient } from '../../client'
import { ArrayPaginated, InputPeerLike, PeersIndex } from '../../types'
import { Booster } from '../../types/stories/booster'
import { makeArrayPaginated } from '../../utils'
/**
* Get boosters of a channel
*
* @returns IDs of stories that were removed
* @internal
*/
export async function getBoosters(
this: TelegramClient,
peerId: InputPeerLike,
params?: {
/**
* Offset for pagination
*/
offset?: string
/**
* Maximum number of boosters to fetch
*
* @default 100
*/
limit?: number
},
): Promise<ArrayPaginated<Booster, string>> {
const { offset = '', limit = 100 } = params ?? {}
const res = await this.call({
_: 'stories.getBoostersList',
peer: await this.resolvePeer(peerId),
offset,
limit,
})
const peers = PeersIndex.from(res)
return makeArrayPaginated(
res.boosters.map((it) => new Booster(this, it, peers)),
res.count,
res.nextOffset,
)
}

View file

@ -0,0 +1,19 @@
import { TelegramClient } from '../../client'
import { InputPeerLike, PeersIndex, PeerStories } from '../../types'
/**
* Get stories of a given peer
*
* @param peerId Peer ID whose stories to fetch
* @internal
*/
export async function getPeerStories(this: TelegramClient, peerId: InputPeerLike): Promise<PeerStories> {
const res = await this.call({
_: 'stories.getPeerStories',
peer: await this.resolvePeer(peerId),
})
const peers = PeersIndex.from(res)
return new PeerStories(this, res.stories, peers)
}

View file

@ -0,0 +1,60 @@
import { assertTypeIs } from '@mtcute/core/utils'
import { TelegramClient } from '../../client'
import { ArrayPaginated, InputPeerLike, PeersIndex, Story } from '../../types'
import { makeArrayPaginated } from '../../utils'
/**
* Get profile stories
*
* @internal
*/
export async function getProfileStories(
this: TelegramClient,
peerId: InputPeerLike,
params?: {
/**
* Kind of stories to fetch
* - `pinned` - stories pinned to the profile and visible to everyone
* - `archived` - "archived" stories that can later be pinned, only visible to the owner
*
* @default `pinned`
*/
kind?: 'pinned' | 'archived'
/**
* Offset ID for pagination
*/
offsetId?: number
/**
* Maximum number of stories to fetch
*
* @default 100
*/
limit?: number
},
): Promise<ArrayPaginated<Story, number>> {
if (!params) params = {}
const { kind = 'pinned', offsetId = 0, limit = 100 } = params
const res = await this.call({
_: kind === 'pinned' ? 'stories.getPinnedStories' : 'stories.getStoriesArchive',
peer: await this.resolvePeer(peerId),
offsetId,
limit,
})
const peers = PeersIndex.from(res)
const stories = res.stories.map((it) => {
assertTypeIs('getProfileStories', it, 'storyItem')
return new Story(this, it, peers)
})
const last = stories[stories.length - 1]
const next = last?.id
return makeArrayPaginated(stories, res.count, next)
}

View file

@ -0,0 +1,51 @@
import { MaybeArray } from '@mtcute/core'
import { assertTypeIs } from '@mtcute/core/utils'
import { TelegramClient } from '../../client'
import { InputPeerLike, PeersIndex, Story } from '../../types'
/**
* Get a single story by its ID
*
* @param peerId Peer ID whose stories to fetch
* @param storyId Story ID
* @internal
*/
export async function getStoriesById(this: TelegramClient, peerId: InputPeerLike, storyId: number): Promise<Story>
/**
* Get multiple stories by their IDs
*
* @param peerId Peer ID whose stories to fetch
* @param storyIds Story IDs
* @internal
*/
export async function getStoriesById(this: TelegramClient, peerId: InputPeerLike, storyIds: number[]): Promise<Story[]>
/**
* @internal
*/
export async function getStoriesById(
this: TelegramClient,
peerId: InputPeerLike,
storyIds: MaybeArray<number>,
): Promise<MaybeArray<Story>> {
const single = !Array.isArray(storyIds)
if (single) storyIds = [storyIds as number]
const res = await this.call({
_: 'stories.getStoriesByID',
peer: await this.resolvePeer(peerId),
id: storyIds as number[],
})
const peers = PeersIndex.from(res)
const stories = res.stories.map((it) => {
assertTypeIs('getProfileStories', it, 'storyItem')
return new Story(this, it, peers)
})
return single ? stories[0] : stories
}

View file

@ -0,0 +1,52 @@
import { MaybeArray } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { InputPeerLike, PeersIndex, StoryInteractions } from '../../types'
/**
* Get brief information about story interactions.
*
* @internal
*/
export async function getStoriesInteractions(
this: TelegramClient,
peerId: InputPeerLike,
storyId: number,
): Promise<StoryInteractions>
/**
* Get brief information about stories interactions.
*
* The result will be in the same order as the input IDs
*
* @internal
*/
export async function getStoriesInteractions(
this: TelegramClient,
peerId: InputPeerLike,
storyIds: number[],
): Promise<StoryInteractions[]>
/**
* @internal
*/
export async function getStoriesInteractions(
this: TelegramClient,
peerId: InputPeerLike,
storyIds: MaybeArray<number>,
): Promise<MaybeArray<StoryInteractions>> {
const isSingle = !Array.isArray(storyIds)
if (isSingle) storyIds = [storyIds as number]
const res = await this.call({
_: 'stories.getStoriesViews',
peer: await this.resolvePeer(peerId),
id: storyIds as number[],
})
const peers = PeersIndex.from(res)
const infos = res.views.map((it) => new StoryInteractions(this, it, peers))
return isSingle ? infos[0] : infos
}

View file

@ -0,0 +1,20 @@
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
/**
* Generate a link to a story.
*
* Basically the link format is `t.me/<username>/s/<story_id>`,
* and if the user doesn't have a username, `USER_PUBLIC_MISSING` is thrown.
*
* I have no idea why is this an RPC call, but whatever
*
* @internal
*/
export async function getStoryLink(this: TelegramClient, peerId: InputPeerLike, storyId: number): Promise<string> {
return this.call({
_: 'stories.exportStoryLink',
peer: await this.resolvePeer(peerId),
id: storyId,
}).then((r) => r.link)
}

View file

@ -0,0 +1,62 @@
import { TelegramClient } from '../../client'
import { InputPeerLike, StoryViewersList } from '../../types'
/**
* Get viewers list of a story
*
* @internal
*/
export async function getStoryViewers(
this: TelegramClient,
peerId: InputPeerLike,
storyId: number,
params?: {
/**
* Whether to only fetch viewers from contacts
*/
onlyContacts?: boolean
/**
* How to sort the results?
* - `reaction` - by reaction (viewers who has reacted are first), then by date (newest first)
* - `date` - by date, newest first
*
* @default `reaction`
*/
sortBy?: 'reaction' | 'date'
/**
* Search query
*/
query?: string
/**
* Offset ID for pagination
*/
offset?: string
/**
* Maximum number of viewers to fetch
*
* @default 100
*/
limit?: number
},
): Promise<StoryViewersList> {
if (!params) params = {}
const { onlyContacts, sortBy = 'reaction', query, offset = '', limit = 100 } = params
const res = await this.call({
_: 'stories.getStoryViewsList',
peer: await this.resolvePeer(peerId),
justContacts: onlyContacts,
reactionsFirst: sortBy === 'reaction',
q: query,
id: storyId,
offset,
limit,
})
return new StoryViewersList(this, res)
}

View file

@ -0,0 +1,48 @@
import { MtTypeAssertionError } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { StoriesStealthMode } from '../../types/stories/stealth-mode'
import { assertIsUpdatesGroup, hasValueAtKey } from '../../utils'
/**
* Hide own stories views (activate so called "stealth mode")
*
* Currently has a cooldown of 1 hour, and throws FLOOD_WAIT error if it is on cooldown.
*
* @internal
*/
export async function hideMyStoriesViews(
this: TelegramClient,
params?: {
/**
* Whether to hide views from the last 5 minutes
*
* @default true
*/
past?: boolean
/**
* Whether to hide views for the next 25 minutes
*
* @default true
*/
future?: boolean
},
): Promise<StoriesStealthMode> {
const { past = true, future = true } = params ?? {}
const res = await this.call({
_: 'stories.activateStealthMode',
past,
future,
})
assertIsUpdatesGroup('hideMyStoriesViews', res)
this._handleUpdate(res, true)
const upd = res.updates.find(hasValueAtKey('_', 'updateStoriesStealthMode'))
if (!upd) { throw new MtTypeAssertionError('hideMyStoriesViews (@ res.updates[*])', 'updateStoriesStealthMode', 'none') }
return new StoriesStealthMode(upd.stealthMode)
}

View file

@ -0,0 +1,26 @@
import { MaybeArray } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
/**
* Increment views of one or more stories.
*
* This should be used for pinned stories, as they can't
* be marked as read when the user sees them ({@link Story#isActive} == false)
*
* @param peerId Peer ID whose stories to mark as read
* @param ids ID(s) of the stories to increment views of (max 200)
* @internal
*/
export async function incrementStoriesViews(
this: TelegramClient,
peerId: InputPeerLike,
ids: MaybeArray<number>,
): Promise<boolean> {
return this.call({
_: 'stories.incrementStoryViews',
peer: await this.resolvePeer(peerId),
id: Array.isArray(ids) ? ids : [ids],
})
}

View file

@ -0,0 +1,53 @@
import { TelegramClient } from '../../client'
import { PeerStories } from '../../types'
/**
* Iterate over all stories (e.g. to load the top bar)
*
* Wrapper over {@link getAllStories}
*
* @internal
*/
export async function* iterAllStories(
this: TelegramClient,
params?: {
/**
* Offset from which to start fetching stories
*/
offset?: string
/**
* Maximum number of stories to fetch
*
* @default Infinity
*/
limit?: number
/**
* Whether to fetch stories from "archived" (or "hidden") peers
*/
archived?: boolean
},
): AsyncIterableIterator<PeerStories> {
if (!params) params = {}
const { archived, limit = Infinity } = params
let { offset } = params
let current = 0
for (;;) {
const res = await this.getAllStories({
offset,
archived,
})
for (const peer of res.peerStories) {
yield peer
if (++current >= limit) return
}
if (!res.hasMore) return
offset = res.next
}
}

View file

@ -0,0 +1,56 @@
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
import { Booster } from '../../types/stories/booster'
/**
* Iterate over boosters of a channel.
*
* Wrapper over {@link getBoosters}
*
* @returns IDs of stories that were removed
* @internal
*/
export async function* iterBoosters(
this: TelegramClient,
peerId: InputPeerLike,
params?: Parameters<TelegramClient['getBoosters']>[1] & {
/**
* 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> {
if (!params) params = {}
const { limit = Infinity, chunkSize = 100 } = params
let { offset } = params
let current = 0
const peer = await this.resolvePeer(peerId)
for (;;) {
const res = await this.getBoosters(peer, {
offset,
limit: Math.min(limit - current, chunkSize),
})
for (const booster of res) {
yield booster
if (++current >= limit) return
}
if (!res.next) return
offset = res.next
}
}

View file

@ -0,0 +1,54 @@
import { TelegramClient } from '../../client'
import { InputPeerLike, Story } from '../../types'
/**
* Iterate over profile stories. Wrapper over {@link getProfileStories}
*
* @internal
*/
export async function* iterProfileStories(
this: TelegramClient,
peerId: InputPeerLike,
params?: Parameters<TelegramClient['getProfileStories']>[1] & {
/**
* Total number of stories to fetch
*
* @default `Infinity`, i.e. fetch all stories
*/
limit?: number
/**
* Number of stories to fetch per request.
* Usually you shouldn't care about this.
*
* @default 100
*/
chunkSize?: number
},
): AsyncIterableIterator<Story> {
if (!params) params = {}
const { kind = 'pinned', limit = Infinity, chunkSize = 100 } = params
let { offsetId } = params
let current = 0
const peer = await this.resolvePeer(peerId)
for (;;) {
const res = await this.getProfileStories(peer, {
kind,
offsetId,
limit: Math.min(limit - current, chunkSize),
})
for (const peer of res) {
yield peer
if (++current >= limit) return
}
if (!res.next) return
offsetId = res.next
}
}

View file

@ -0,0 +1,58 @@
import { TelegramClient } from '../../client'
import { InputPeerLike, StoryViewer } from '../../types'
/**
* Iterate over viewers list of a story.
* Wrapper over {@link getStoryViewers}
*
* @internal
*/
export async function* iterStoryViewers(
this: TelegramClient,
peerId: InputPeerLike,
storyId: number,
params?: Parameters<TelegramClient['getStoryViewers']>[2] & {
/**
* Total number of viewers to fetch
*
* @default Infinity, i.e. fetch all viewers
*/
limit?: number
/**
* Number of viewers to fetch per request.
* Usually you don't need to change this.
*
* @default 100
*/
chunkSize?: number
},
): AsyncIterableIterator<StoryViewer> {
if (!params) params = {}
const { onlyContacts, sortBy = 'reaction', query, limit = Infinity, chunkSize = 100 } = params
let { offset = '' } = params
let current = 0
const peer = await this.resolvePeer(peerId)
for (;;) {
const res = await this.getStoryViewers(peer, storyId, {
onlyContacts,
sortBy,
query,
offset,
limit: Math.min(limit - current, chunkSize),
})
for (const peer of res.viewers) {
yield peer
if (++current >= limit) return
}
if (!res.next) return
offset = res.next
}
}

View file

@ -0,0 +1,19 @@
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
/**
* Mark all stories up to a given ID as read
*
* This should only be used for "active" stories ({@link Story#isActive} == false)
*
* @param peerId Peer ID whose stories to mark as read
* @returns IDs of the stores that were marked as read
* @internal
*/
export async function readStories(this: TelegramClient, peerId: InputPeerLike, maxId: number): Promise<number[]> {
return this.call({
_: 'stories.readStories',
peer: await this.resolvePeer(peerId),
maxId,
})
}

View file

@ -0,0 +1,38 @@
import { MaybeArray, tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
/**
* Report a story (or multiple stories) to the moderation team
*
* @internal
*/
export async function reportStory(
this: TelegramClient,
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 ?? {}
await this.call({
_: 'stories.report',
peer: await this.resolvePeer(peerId),
id: Array.isArray(storyIds) ? storyIds : [storyIds],
message,
reason,
})
}

View file

@ -0,0 +1,32 @@
import { TelegramClient } from '../../client'
import { InputPeerLike, InputReaction, normalizeInputReaction } from '../../types'
/**
* Send (or remove) a reaction to a story
*
* @internal
*/
export async function sendStoryReaction(
this: TelegramClient,
peerId: InputPeerLike,
storyId: number,
reaction: InputReaction,
params?: {
/**
* Whether to add this reaction to recently used
*/
addToRecent?: boolean
},
): Promise<void> {
const { addToRecent } = params ?? {}
const res = await this.call({
_: 'stories.sendReaction',
peer: await this.resolvePeer(peerId),
storyId,
reaction: normalizeInputReaction(reaction),
addToRecent,
})
this._handleUpdate(res, true)
}

View file

@ -0,0 +1,118 @@
import { tl } from '@mtcute/core'
import { randomLong } from '@mtcute/core/utils'
import { TelegramClient } from '../../client'
import { FormattedString, InputMediaLike, InputPeerLike, InputPrivacyRule, Story } from '../../types'
/**
* Send a story
*
* @returns Created story
* @internal
*/
export async function sendStory(
this: TelegramClient,
params: {
/**
* Peer ID to send story as
*
* @default `self`
*/
peer?: InputPeerLike
/**
* Media contained in a story. Currently can only be a photo or a video.
*
* You can also pass TDLib and Bot API compatible File ID,
* which will be wrapped in {@link InputMedia.auto}
*/
media: InputMediaLike | string
/**
* Override caption for {@link media}
*/
caption?: string | FormattedString<string>
/**
* Override entities for {@link media}
*/
entities?: tl.TypeMessageEntity[]
/**
* Parse mode to use to parse entities before sending
* the message. Defaults to current default parse mode (if any).
*
* Passing `null` will explicitly disable formatting.
*/
parseMode?: string | null
/**
* Whether to automatically pin this story to the profile
*/
pinned?: boolean
/**
* Whether to disallow sharing this story
*/
forbidForwards?: boolean
/**
* Interactive elements to add to the story
*/
interactiveElements?: tl.TypeMediaArea[]
/**
* Privacy rules to apply to the story
*
* @default "Everyone"
*/
privacyRules?: InputPrivacyRule[]
/**
* TTL period of the story, in seconds
*
* @default 86400
*/
period?: number
},
): Promise<Story> {
const { peer = 'me', pinned, forbidForwards, interactiveElements, period } = params
let { media } = params
if (typeof media === 'string') {
media = {
type: 'auto',
file: media,
}
}
const inputMedia = await this._normalizeInputMedia(media, params)
const privacyRules = params.privacyRules ?
await this._normalizePrivacyRules(params.privacyRules) :
[{ _: 'inputPrivacyValueAllowAll' } as const]
const [caption, entities] = await this._parseEntities(
// 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
params.caption || (media as Extract<typeof media, { caption?: unknown }>).caption,
params.parseMode,
params.entities || (media as Extract<typeof media, { entities?: unknown }>).entities,
)
const res = await this.call({
_: 'stories.sendStory',
pinned,
noforwards: forbidForwards,
peer: await this.resolvePeer(peer),
media: inputMedia,
mediaAreas: interactiveElements,
caption,
entities,
privacyRules,
randomId: randomLong(),
period,
})
return this._findStoryInUpdate(res)
}

View file

@ -0,0 +1,21 @@
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
/**
* Toggle whether peer's stories are archived (hidden) or not.
*
* This **does not** archive the chat with that peer, only stories.
*
* @internal
*/
export async function togglePeerStoriesArchived(
this: TelegramClient,
peerId: InputPeerLike,
archived: boolean,
): Promise<void> {
await this.call({
_: 'stories.togglePeerStoriesHidden',
peer: await this.resolvePeer(peerId),
hidden: archived,
})
}

View file

@ -0,0 +1,41 @@
import { MaybeArray } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
/**
* Toggle one or more stories pinned status
*
* @returns IDs of stories that were toggled
* @internal
*/
export async function toggleStoriesPinned(
this: TelegramClient,
params: {
/**
* Story ID(s) to toggle
*/
ids: MaybeArray<number>
/**
* Whether to pin or unpin the story
*/
pinned: boolean
/**
* Peer ID whose stories to toggle
*
* @default `self`
*/
peer?: InputPeerLike
},
): Promise<number[]> {
const { ids, pinned, peer = 'me' } = params
return await this.call({
_: 'stories.togglePinned',
peer: await this.resolvePeer(peer),
id: Array.isArray(ids) ? ids : [ids],
pinned,
})
}

View file

@ -9,5 +9,7 @@ export * from './messages'
export * from './misc'
export * from './parser'
export * from './peers'
export * from './reactions'
export * from './stories'
export * from './updates'
export * from './utils'

View file

@ -4,5 +4,5 @@ export * from './message'
export * from './message-action'
export * from './message-entity'
export * from './message-media'
export * from './reactions'
export * from './message-reactions'
export * from './search-filters'

View file

@ -0,0 +1,55 @@
import { tl } from '@mtcute/core'
import { TelegramClient } from '../..'
import { makeInspectable } from '../../utils'
import { PeersIndex } from '../peers'
import { PeerReaction } from '../reactions/peer-reaction'
import { ReactionCount } from '../reactions/reaction-count'
/**
* Reactions on a message
*/
export class MessageReactions {
constructor(
readonly client: TelegramClient,
readonly messageId: number,
readonly chatId: number,
readonly raw: tl.RawMessageReactions,
readonly _peers: PeersIndex,
) {}
/**
* Whether you can use {@link getUsers}
* (or {@link TelegramClient.getReactionUsers})
* to get the users who reacted to this message
*/
get usersVisible(): boolean {
return this.raw.canSeeList!
}
private _reactions?: ReactionCount[]
/**
* Reactions on the message, along with their counts
*/
get reactions(): ReactionCount[] {
return (this._reactions ??= this.raw.results.map((it) => new ReactionCount(it)))
}
private _recentReactions?: PeerReaction[]
/**
* Recently reacted users.
* To get a full list of users, use {@link getUsers}
*/
get recentReactions(): PeerReaction[] {
if (!this.raw.recentReactions) {
return []
}
return (this._recentReactions ??= this.raw.recentReactions.map(
(reaction) => new PeerReaction(this.client, reaction, this._peers),
))
}
}
makeInspectable(MessageReactions)

View file

@ -17,7 +17,7 @@ import { Chat, InputPeerLike, PeersIndex, User } from '../peers'
import { _messageActionFromTl, MessageAction } from './message-action'
import { MessageEntity } from './message-entity'
import { _messageMediaFromTl, MessageMedia } from './message-media'
import { MessageReactions } from './reactions'
import { MessageReactions } from './message-reactions'
/** Information about a forward */
export interface MessageForwardInfo {

View file

@ -1,148 +0,0 @@
import Long from 'long'
import { getMarkedPeerId, tl } from '@mtcute/core'
import { assertTypeIs } from '@mtcute/core/utils'
import { TelegramClient } from '../../client'
import { makeInspectable } from '../../utils'
import { PeersIndex, User } from '../peers'
/**
* Emoji describing a reaction.
*
* Either a `string` with a unicode emoji, or a `tl.Long` for a custom emoji
*/
export type InputReaction = string | tl.Long | tl.TypeReaction
export function normalizeInputReaction(reaction?: InputReaction | null): tl.TypeReaction {
if (typeof reaction === 'string') {
return {
_: 'reactionEmoji',
emoticon: reaction,
}
} else if (Long.isLong(reaction)) {
return {
_: 'reactionCustomEmoji',
documentId: reaction,
}
} else if (reaction) {
return reaction
}
return {
_: 'reactionEmpty',
}
}
export class PeerReaction {
constructor(
readonly client: TelegramClient,
readonly raw: tl.RawMessagePeerReaction,
readonly _peers: PeersIndex,
) {}
/**
* Emoji representing the reaction
*/
get emoji(): string {
const r = this.raw.reaction
switch (r._) {
case 'reactionCustomEmoji':
return r.documentId.toString()
case 'reactionEmoji':
return r.emoticon
case 'reactionEmpty':
return ''
}
}
/**
* Whether this reaction is a custom emoji
*/
get isCustomEmoji(): boolean {
return this.raw.reaction._ === 'reactionCustomEmoji'
}
/**
* Whether this is a big reaction
*/
get big(): boolean {
return this.raw.big!
}
/**
* Whether this reaction is unread by the current user
*/
get unread(): boolean {
return this.raw.unread!
}
/**
* ID of the user who has reacted
*/
get userId(): number {
return getMarkedPeerId(this.raw.peerId)
}
private _user?: User
/**
* User who has reacted
*/
get user(): User {
if (!this._user) {
assertTypeIs('PeerReaction#user', this.raw.peerId, 'peerUser')
this._user = new User(this.client, this._peers.user(this.raw.peerId.userId))
}
return this._user
}
}
makeInspectable(PeerReaction)
export class MessageReactions {
constructor(
readonly client: TelegramClient,
readonly messageId: number,
readonly chatId: number,
readonly raw: tl.RawMessageReactions,
readonly _peers: PeersIndex,
) {}
/**
* Whether you can use {@link getUsers}
* (or {@link TelegramClient.getReactionUsers})
* to get the users who reacted to this message
*/
get usersVisible(): boolean {
return this.raw.canSeeList!
}
/**
* Reactions on the message, along with their counts
*/
get reactions(): tl.TypeReactionCount[] {
return this.raw.results
}
private _recentReactions?: PeerReaction[]
/**
* Recently reacted users.
* To get a full list of users, use {@link getUsers}
*/
get recentReactions(): PeerReaction[] {
if (!this.raw.recentReactions) {
return []
}
return (this._recentReactions ??= this.raw.recentReactions.map(
(reaction) => new PeerReaction(this.client, reaction, this._peers),
))
}
}
makeInspectable(MessageReactions)

View file

@ -1,2 +1,3 @@
export * from './input-privacy-rule'
export * from './sticker-set'
export * from './takeout-session'

View file

@ -0,0 +1,96 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { MaybeArray, tl } from '@mtcute/core'
import { InputPeerLike } from '../peers'
interface InputPrivacyRuleUsers {
allow: boolean
users: InputPeerLike[]
}
interface InputPrivacyRuleChatParticipants {
allow: boolean
chats: InputPeerLike[]
}
export type InputPrivacyRule = InputPrivacyRuleChatParticipants | InputPrivacyRuleUsers | tl.TypeInputPrivacyRule
/**
* Helpers for creating {@link InputPrivacyRule}s
*
* @example
* ```typescript
* const rules = [
* PrivacyRule.allow.all,
* PrivacyRule.disallow.users([123456789, 'username']),
* ]
* ```
*/
export namespace PrivacyRule {
export namespace allow {
/** Allow all users */
export const all: tl.RawInputPrivacyValueAllowAll = { _: 'inputPrivacyValueAllowAll' }
/** Allow only contacts */
export const contacts: tl.RawInputPrivacyValueAllowContacts = { _: 'inputPrivacyValueAllowContacts' }
/** Allow only "close friends" list */
export const closeFriends: tl.RawInputPrivacyValueAllowCloseFriends = {
_: 'inputPrivacyValueAllowCloseFriends',
}
/**
* Allow only users specified in `users`
*
* @param users Users to allow
*/
export function users(users: MaybeArray<InputPeerLike>): InputPrivacyRuleUsers {
return {
allow: true,
users: Array.isArray(users) ? users : [users],
}
}
/**
* Allow only participants of chats specified in `chats`
*
* @param chats Chats to allow
*/
export function chatParticipants(chats: MaybeArray<InputPeerLike>): InputPrivacyRuleChatParticipants {
return {
allow: true,
chats: Array.isArray(chats) ? chats : [chats],
}
}
}
export namespace disallow {
/** Disallow all users */
export const all: tl.RawInputPrivacyValueDisallowAll = { _: 'inputPrivacyValueDisallowAll' }
/** Disallow contacts */
export const contacts: tl.RawInputPrivacyValueDisallowContacts = { _: 'inputPrivacyValueDisallowContacts' }
/**
* Disallow users specified in `users`
*
* @param users Users to disallow
*/
export function users(users: MaybeArray<InputPeerLike>): InputPrivacyRuleUsers {
return {
allow: false,
users: Array.isArray(users) ? users : [users],
}
}
/**
* Disallow participants of chats specified in `chats`
*
* @param chats Chats to disallow
*/
export function chatParticipants(chats: MaybeArray<InputPeerLike>): InputPrivacyRuleChatParticipants {
return {
allow: false,
chats: Array.isArray(chats) ? chats : [chats],
}
}
}
}

View file

@ -0,0 +1,2 @@
export * from './peer-reaction'
export * from './types'

View file

@ -0,0 +1,63 @@
import { getMarkedPeerId, tl } from '@mtcute/core'
import { assertTypeIs } from '@mtcute/core/utils'
import { TelegramClient } from '../..'
import { makeInspectable } from '../../utils'
import { PeersIndex, User } from '../peers'
import { ReactionEmoji, toReactionEmoji } from './types'
/**
* Reactions of a user to a message
*/
export class PeerReaction {
constructor(
readonly client: TelegramClient,
readonly raw: tl.RawMessagePeerReaction,
readonly _peers: PeersIndex,
) {}
/**
* Emoji representing the reaction
*/
get emoji(): ReactionEmoji {
return toReactionEmoji(this.raw.reaction)
}
/**
* Whether this is a big reaction
*/
get big(): boolean {
return this.raw.big!
}
/**
* Whether this reaction is unread by the current user
*/
get unread(): boolean {
return this.raw.unread!
}
/**
* ID of the user who has reacted
*/
get userId(): number {
return getMarkedPeerId(this.raw.peerId)
}
private _user?: User
/**
* User who has reacted
*/
get user(): User {
if (!this._user) {
assertTypeIs('PeerReaction#user', this.raw.peerId, 'peerUser')
this._user = new User(this.client, this._peers.user(this.raw.peerId.userId))
}
return this._user
}
}
makeInspectable(PeerReaction)

View file

@ -0,0 +1,36 @@
import { tl } from '@mtcute/core'
import { makeInspectable } from '../../utils'
import { ReactionEmoji, toReactionEmoji } from './types'
/**
* Reaction count
*/
export class ReactionCount {
constructor(readonly raw: tl.RawReactionCount) {}
/**
* Emoji representing the reaction
*/
get emoji(): ReactionEmoji {
return toReactionEmoji(this.raw.reaction)
}
/**
* Number of users who reacted with this emoji
*/
get count(): number {
return this.raw.count
}
/**
* If the current user has reacted with this emoji,
* this field will contain the order in which the
* reaction was added.
*/
get order(): number | null {
return this.raw.chosenOrder ?? null
}
}
makeInspectable(ReactionCount)

View file

@ -0,0 +1,53 @@
import Long from 'long'
import { MtTypeAssertionError, tl } from '@mtcute/core'
/**
* Input version of {@link ReactionEmoji}, which also accepts bare TL object
*/
export type InputReaction = string | tl.Long | tl.TypeReaction
/**
* Emoji describing a reaction.
*
* Either a `string` with a unicode emoji, or a `tl.Long` for a custom emoji
*/
export type ReactionEmoji = string | tl.Long
export function normalizeInputReaction(reaction?: InputReaction | null): tl.TypeReaction {
if (typeof reaction === 'string') {
return {
_: 'reactionEmoji',
emoticon: reaction,
}
} else if (Long.isLong(reaction)) {
return {
_: 'reactionCustomEmoji',
documentId: reaction,
}
} else if (reaction) {
return reaction
}
return {
_: 'reactionEmpty',
}
}
export function toReactionEmoji(reaction: tl.TypeReaction, allowEmpty?: false): ReactionEmoji
export function toReactionEmoji(reaction: tl.TypeReaction, allowEmpty: true): ReactionEmoji | null
export function toReactionEmoji(reaction: tl.TypeReaction, allowEmpty?: boolean): ReactionEmoji | null {
switch (reaction._) {
case 'reactionEmoji':
return reaction.emoticon
case 'reactionCustomEmoji':
return reaction.documentId
case 'reactionEmpty':
if (!allowEmpty) {
throw new MtTypeAssertionError('toReactionEmoji', 'not reactionEmpty', reaction._)
}
return null
}
}

View file

@ -0,0 +1,48 @@
import { PeersIndex, TelegramClient, tl } from '../..'
import { makeInspectable } from '../../utils'
import { PeerStories } from './peer-stories'
import { StoriesStealthMode } from './stealth-mode'
/**
* All stories of the current user
*
* Returned by {@link TelegramClient.getAllStories}
*/
export class AllStories {
constructor(readonly client: TelegramClient, readonly raw: tl.stories.RawAllStories) {}
readonly _peers = PeersIndex.from(this.raw)
/** Whether there are more stories to fetch */
get hasMore(): boolean {
return this.raw.hasMore!
}
/** Next offset for pagination */
get next(): string {
return this.raw.state
}
/** Total number of {@link PeerStories} available */
get total(): number {
return this.raw.count
}
private _peerStories?: PeerStories[]
/** Peers with their stories */
get peerStories(): PeerStories[] {
if (!this._peerStories) {
this._peerStories = this.raw.peerStories.map((it) => new PeerStories(this.client, it, this._peers))
}
return this._peerStories
}
private _stealthMode?: StoriesStealthMode
/** Stealth mode info */
get stealthMode(): StoriesStealthMode | null {
return (this._stealthMode ??= new StoriesStealthMode(this.raw.stealthMode))
}
}
makeInspectable(AllStories)

View file

@ -0,0 +1,85 @@
import { tl } from '@mtcute/core'
import { makeInspectable } from '../../utils'
/**
* Information about boosts in a channel
*/
export class BoostStats {
constructor(readonly raw: tl.stories.RawBoostsStatus) {}
/** Whether this channel is being boosted by the current user */
get isBoosting(): boolean {
return this.raw.myBoost!
}
/**
* Current level of boosts in this channel.
*
* Currently this maps 1-to-1 to the number of stories
* the channel can post daily
*/
get level(): number {
return this.raw.level
}
/** Whether this channel is already at the maximum level */
get isMaxLevel(): boolean {
return this.raw.nextLevelBoosts === undefined
}
/**
* Number of boosts that were needed for the current level
*/
get currentLevelBoosts(): number {
return this.raw.currentLevelBoosts
}
/** Total number of boosts this channel has */
get currentBoosts(): number {
return this.raw.boosts
}
/**
* Number of boosts the channel must have to reach the next level
*
* `null` if the channel is already at the maximum level
*/
get nextLevelBoosts(): number | null {
return this.raw.nextLevelBoosts ?? null
}
/**
* Number of boosts the channel needs in addition to the current value
* to reach the next level
*/
get remainingBoosts(): number {
if (!this.raw.nextLevelBoosts) return 0
return this.raw.nextLevelBoosts - this.raw.boosts
}
/** If available, total number of subscribers this channel has */
get totalSubscribers(): number | null {
return this.raw.premiumAudience?.total ?? null
}
/** If available, total number of Premium subscribers this channel has */
get totalPremiumSubscribers(): number | null {
return this.raw.premiumAudience?.part ?? null
}
/** If available, percentage of this channel's subscribers that are Premium */
get premiumSubscribersPercentage(): number | null {
if (!this.raw.premiumAudience) return null
return (this.raw.premiumAudience.part / this.raw.premiumAudience.total) * 100
}
/** URL that would bring up the boost interface */
get url(): string {
return this.raw.boostUrl
}
}
makeInspectable(BoostStats)

View file

@ -0,0 +1,31 @@
import { tl } from '@mtcute/core'
import { TelegramClient } from '../..'
import { makeInspectable } from '../../utils'
import { PeersIndex, User } from '../peers'
/**
* Information about a user who is boosting a channel
*/
export class Booster {
constructor(readonly client: TelegramClient, 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)
}
private _user?: User
/**
* User who is boosting the channel
*/
get user(): User {
return (this._user ??= new User(this.client, this._peers.user(this.raw.userId)))
}
}
makeInspectable(Booster)

View file

@ -0,0 +1,9 @@
export * from './all-stories'
export * from './boost-stats'
export * from './booster'
export * from './interactive'
export * from './peer-stories'
export * from './stealth-mode'
export * from './story'
export * from './story-interactions'
export * from './story-viewer'

View file

@ -0,0 +1,36 @@
import { tl } from '@mtcute/core/src'
import { TelegramClient } from '../../../client'
export abstract class StoryInteractiveArea {
abstract type: string
constructor(readonly client: TelegramClient, readonly raw: Exclude<tl.TypeMediaArea, tl.RawInputMediaAreaVenue>) {
this.raw = raw
}
/** X coordinate of the top-left corner of the area */
get x(): number {
return this.raw.coordinates.x
}
/** Y coordinate of the top-left corner of the area */
get y(): number {
return this.raw.coordinates.y
}
/** Width of the area */
get width(): number {
return this.raw.coordinates.w
}
/** Height of the area */
get height(): number {
return this.raw.coordinates.h
}
/** Rotation of the area */
get rotation(): number {
return this.raw.coordinates.rotation
}
}

View file

@ -0,0 +1,23 @@
import { MtTypeAssertionError, tl } from '@mtcute/core'
import { TelegramClient } from '../../../client'
import { StoryInteractiveLocation } from './location'
import { StoryInteractiveReaction } from './reaction'
import { StoryInteractiveVenue } from './venue'
export * from './input'
export type StoryInteractiveElement = StoryInteractiveReaction | StoryInteractiveLocation | StoryInteractiveVenue
export function _storyInteractiveElementFromTl(client: TelegramClient, raw: tl.TypeMediaArea): StoryInteractiveElement {
switch (raw._) {
case 'mediaAreaSuggestedReaction':
return new StoryInteractiveReaction(client, raw)
case 'mediaAreaGeoPoint':
return new StoryInteractiveLocation(client, raw)
case 'mediaAreaVenue':
return new StoryInteractiveVenue(client, raw)
case 'inputMediaAreaVenue':
throw new MtTypeAssertionError('StoryInteractiveElement', '!input*', raw._)
}
}

View file

@ -0,0 +1,133 @@
import Long from 'long'
import { tl } from '@mtcute/core'
import { VenueSource } from '../../media'
import { InputReaction, normalizeInputReaction } from '../../reactions'
/**
* Constructor for interactive story elements.
*
* @example
* ```typescript
* const element = StoryElement
* .at({ x: 0, y: 0, width: 10, height: 10 })
* .reaction('👍', { dark: true })
* ```
*/
export class StoryElement {
private constructor(private _position: tl.RawMediaAreaCoordinates) {}
static at(params: { x: number; y: number; width: number; height: number; rotation?: number }) {
return new StoryElement({
_: 'mediaAreaCoordinates',
x: params.x,
y: params.y,
w: params.width,
h: params.height,
rotation: params.rotation ?? 0,
})
}
venue(params: {
/**
* Latitude of the geolocation
*/
latitude: number
/**
* Longitude of the geolocation
*/
longitude: number
/**
* Venue name
*/
title: string
/**
* Venue address
*/
address: string
/**
* Source where this venue was acquired
*/
source: VenueSource
}): tl.RawMediaAreaVenue {
return {
_: 'mediaAreaVenue',
coordinates: this._position,
geo: {
_: 'geoPoint',
lat: params.latitude,
long: params.longitude,
accessHash: Long.ZERO,
},
title: params.title,
address: params.address,
provider: params.source.provider ?? 'foursquare',
venueId: params.source.id,
venueType: params.source.type,
}
}
venueFromInline(queryId: tl.Long, resultId: string): tl.RawInputMediaAreaVenue {
return {
_: 'inputMediaAreaVenue',
coordinates: this._position,
queryId,
resultId,
}
}
location(params: {
/**
* Latitude of the geolocation
*/
latitude: number
/**
* Longitude of the geolocation
*/
longitude: number
}): tl.RawMediaAreaGeoPoint {
return {
_: 'mediaAreaGeoPoint',
coordinates: this._position,
geo: {
_: 'geoPoint',
lat: params.latitude,
long: params.longitude,
accessHash: Long.ZERO,
},
}
}
reaction(
reaction: InputReaction,
params: {
/**
* Whether this reaction is on a dark background
*/
dark?: boolean
/**
* Whether this reaction is flipped (i.e. has tail on the left)
*/
flipped?: boolean
} = {},
): tl.RawMediaAreaSuggestedReaction {
// for whatever reason, in MTProto dimensions of these are expected to be 16:9/
// we adjust them here to make it easier to work with
this._position.h *= 9 / 16
return {
_: 'mediaAreaSuggestedReaction',
coordinates: this._position,
reaction: normalizeInputReaction(reaction),
dark: params.dark,
flipped: params.flipped,
}
}
}

View file

@ -0,0 +1,33 @@
import { tl } from '@mtcute/core'
import { assertTypeIs } from '@mtcute/core/utils'
import { TelegramClient } from '../../../client'
import { makeInspectable } from '../../../utils'
import { Location } from '../../media'
import { StoryInteractiveArea } from './base'
/**
* Interactive element containing a location on the map
*/
export class StoryInteractiveLocation extends StoryInteractiveArea {
readonly type = 'location' as const
constructor(client: TelegramClient, readonly raw: tl.RawMediaAreaGeoPoint) {
super(client, raw)
}
private _location?: Location
/**
* Geolocation
*/
get location(): Location {
if (!this._location) {
assertTypeIs('StoryInteractiveLocation#location', this.raw.geo, 'geoPoint')
this._location = new Location(this.client, this.raw.geo)
}
return this._location
}
}
makeInspectable(StoryInteractiveLocation)

View file

@ -0,0 +1,38 @@
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../../client'
import { makeInspectable } from '../../../utils'
import { ReactionEmoji, toReactionEmoji } from '../../reactions'
import { StoryInteractiveArea } from './base'
/**
* Interactive element containing a reaction.
*
* Number of reactions should be taken from {@link StoryViews} by emoji ID
*
* For whatever reason, in MTProto dimensions of these are expected to be 16:9
*/
export class StoryInteractiveReaction extends StoryInteractiveArea {
readonly type = 'reaction' as const
constructor(client: TelegramClient, readonly raw: tl.RawMediaAreaSuggestedReaction) {
super(client, raw)
}
/** Whether this reaction is on a dark background */
get isDark(): boolean {
return this.raw.dark!
}
/** Whether this reaction is flipped (i.e. has tail on the left) */
get isFlipped(): boolean {
return this.raw.flipped!
}
/** Emoji representing the reaction */
get emoji(): ReactionEmoji {
return toReactionEmoji(this.raw.reaction)
}
}
makeInspectable(StoryInteractiveReaction)

View file

@ -0,0 +1,60 @@
import { tl } from '@mtcute/core'
import { assertTypeIs } from '@mtcute/core/utils'
import { TelegramClient } from '../../../client'
import { makeInspectable } from '../../../utils'
import { Location, VenueSource } from '../../media'
import { StoryInteractiveArea } from './base'
/**
* Interactive element containing a venue
*/
export class StoryInteractiveVenue extends StoryInteractiveArea {
readonly type = 'venue' as const
constructor(client: TelegramClient, readonly raw: tl.RawMediaAreaVenue) {
super(client, raw)
}
private _location?: Location
/**
* Geolocation of the venue
*/
get location(): Location {
if (!this._location) {
assertTypeIs('StoryInteractiveVenue#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(): VenueSource | null {
if (!this.raw.provider) return null
return {
provider: this.raw.provider as VenueSource['provider'],
id: this.raw.venueId,
type: this.raw.venueType,
}
}
}
makeInspectable(StoryInteractiveVenue)

View file

@ -0,0 +1,48 @@
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { assertTypeIs, makeInspectable } from '../../utils'
import { Chat, PeersIndex, User } from '../peers'
import { Story } from './story'
export class PeerStories {
constructor(readonly client: TelegramClient, readonly raw: tl.RawPeerStories, readonly _peers: PeersIndex) {}
private _peer?: User | Chat
/**
* Peer that owns these stories.
*/
get peer(): User | Chat {
if (this._peer) return this._peer
switch (this.raw.peer._) {
case 'peerUser':
return (this._peer = new User(this.client, this._peers.user(this.raw.peer.userId)))
case 'peerChat':
return (this._peer = new Chat(this.client, this._peers.chat(this.raw.peer.chatId)))
case 'peerChannel':
return (this._peer = new Chat(this.client, this._peers.chat(this.raw.peer.channelId)))
}
}
/**
* ID of the last read story of this peer.
*/
get maxReadId(): number {
return this.raw.maxReadId ?? 0
}
private _stories?: Story[]
/**
* List of peer stories.
*/
get stories(): Story[] {
return (this._stories ??= this.raw.stories.map((it) => {
assertTypeIs('PeerStories#stories', it, 'storyItem')
return new Story(this.client, it, this._peers)
}))
}
}
makeInspectable(PeerStories)

View file

@ -0,0 +1,23 @@
import { tl } from '@mtcute/core'
import { makeInspectable } from '../../utils'
export class StoriesStealthMode {
constructor(readonly raw: tl.RawStoriesStealthMode) {}
/** Stealth mode is active until this date */
get activeUntil(): Date | null {
if (!this.raw.activeUntilDate) return null
return new Date(this.raw.activeUntilDate * 1000)
}
/** Stealth mode is having a cooldown until this date */
get cooldownUntil(): Date | null {
if (!this.raw.cooldownUntilDate) return null
return new Date(this.raw.cooldownUntilDate * 1000)
}
}
makeInspectable(StoriesStealthMode)

View file

@ -0,0 +1,63 @@
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { makeInspectable } from '../../utils'
import { PeersIndex, User } from '../peers'
import { ReactionCount } from '../reactions/reaction-count'
/**
* Brief information about story views/interactions
*/
export class StoryInteractions {
constructor(readonly client: TelegramClient, readonly raw: tl.RawStoryViews, readonly _peers: PeersIndex) {}
/**
* Whether information about viewers is available.
*
* When `true`, you can use {@link TelegarmClient.getStoryViewers}
* to get the full list of viewers, and also {@link recentViewers}
* will be available.
*/
get hasViewers(): boolean {
return this.raw.hasViewers!
}
/** Number of views */
get viewsCount(): number {
return this.raw.viewsCount
}
/** Number of forwards (if available) */
get forwardsCount(): number | null {
return this.raw.forwardsCount ?? null
}
/** Total number of reactions */
get reactionsCount(): number {
return this.raw.reactionsCount ?? 0
}
private _reactions?: ReactionCount[]
/**
* Reactions on the message, along with their counts
*/
get reactions(): ReactionCount[] {
if (!this.raw.reactions) return []
return (this._reactions ??= this.raw.reactions.map((it) => new ReactionCount(it)))
}
private _recentViewers?: User[]
/**
* List of users who have recently viewed this story.
*/
get recentViewers(): User[] {
if (!this._recentViewers) {
this._recentViewers = this.raw.recentViewers?.map((it) => new User(this.client, this._peers.user(it))) ?? []
}
return this._recentViewers
}
}
makeInspectable(StoryInteractions)

View file

@ -0,0 +1,79 @@
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { makeInspectable } from '../../utils'
import { PeersIndex, User } from '../peers'
import { ReactionEmoji, toReactionEmoji } from '../reactions'
/**
* Information about a single user who has viewed a story.
*/
export class StoryViewer {
constructor(readonly client: TelegramClient, readonly raw: tl.RawStoryView, readonly _peers: PeersIndex) {}
/** Whether this user is in current user's global blacklist */
get isBlocked(): boolean {
return this.raw.blocked!
}
/** Whether current user's stories are hidden from this user */
get isStoriesBlocked(): boolean {
return this.raw.blockedMyStoriesFrom!
}
/** Date when the view has occurred */
get date(): Date {
return new Date(this.raw.date * 1000)
}
/** Reaction this user has left, if any */
get reactionEmoji(): ReactionEmoji | null {
if (!this.raw.reaction) return null
return toReactionEmoji(this.raw.reaction, true)
}
private _user?: User
/** Information about the user */
get user(): User {
return (this._user ??= new User(this.client, this._peers.user(this.raw.userId)))
}
}
makeInspectable(StoryViewer)
/**
* List of story viewers.
*/
export class StoryViewersList {
constructor(readonly client: TelegramClient, readonly raw: tl.stories.RawStoryViewsList) {}
readonly _peers = PeersIndex.from(this.raw)
/** Next offset for pagination */
get next(): string | undefined {
return this.raw.nextOffset
}
/** Total number of views this story has */
get total(): number {
return this.raw.count
}
/** Total number of reactions this story has */
get reactionsTotal(): number {
return this.raw.reactionsCount
}
private _viewers?: StoryViewer[]
/** List of viewers */
get viewers(): StoryViewer[] {
if (!this._viewers) {
this._viewers = this.raw.views.map((it) => new StoryViewer(this.client, it, this._peers))
}
return this._viewers
}
}
makeInspectable(StoryViewersList)

View file

@ -0,0 +1,180 @@
import { MtUnsupportedError, tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { makeInspectable } from '../../utils'
import { Photo, Video } from '../media'
import { _messageMediaFromTl, MessageEntity } from '../messages'
import { PeersIndex } from '../peers'
import { ReactionEmoji, toReactionEmoji } from '../reactions'
import { _storyInteractiveElementFromTl, StoryInteractiveElement } from './interactive'
import { StoryInteractions } from './story-interactions'
/**
* Information about story visibility.
*
* - `public` - story is visible to everyone
* - `contacts` - story is visible only to contacts
* - `selectedContacts` - story is visible only to some contacts
* - `closeFriends` - story is visible only to "close friends"
*/
export type StoryVisibility = 'public' | 'contacts' | 'selected_contacts' | 'close_friends'
export type StoryMedia = Photo | Video
export class Story {
constructor(readonly client: TelegramClient, readonly raw: tl.RawStoryItem, readonly _peers: PeersIndex) {}
/** Whether this story is pinned */
get isPinned(): boolean {
return this.raw.pinned!
}
/**
* Whether this object contains reduced set of fields.
*
* When `true`, these field will not contain correct data:
* {@link privacyRules}, {@link interactiveAreas}
*
*/
get isShort(): boolean {
return this.raw.min!
}
/** Whether this story is content-protected, i.e. can't be forwarded */
get isContentProtected(): boolean {
return this.raw.noforwards!
}
/** Whether this story has been edited */
get isEdited(): boolean {
return this.raw.edited!
}
/** Whether this story has been posted by the current user */
get isMy(): boolean {
return this.raw.out!
}
/** ID of the story */
get id(): number {
return this.raw.id
}
/** Date when this story was posted */
get date(): Date {
return new Date(this.raw.date * 1000)
}
/** Date when this story will expire */
get expireDate(): Date {
return new Date(this.raw.expireDate * 1000)
}
/** Whether the story is active (i.e. not expired yet) */
get isActive(): boolean {
return Date.now() < this.expireDate.getTime()
}
/** Story visibility */
get visibility(): StoryVisibility {
if (this.raw.public) return 'public'
if (this.raw.contacts) return 'contacts'
if (this.raw.closeFriends) return 'close_friends'
if (this.raw.selectedContacts) return 'selected_contacts'
throw new MtUnsupportedError('Unknown story visibility')
}
/** Caption of the story */
get caption(): string | null {
return this.raw.caption ?? null
}
private _entities?: MessageEntity[]
/**
* Caption entities (may be empty)
*/
get entities(): ReadonlyArray<MessageEntity> {
if (!this._entities) {
this._entities = []
if (this.raw.entities?.length) {
for (const ent of this.raw.entities) {
const parsed = MessageEntity._parse(ent)
if (parsed) this._entities.push(parsed)
}
}
}
return this._entities
}
private _media?: StoryMedia
/**
* Story media.
*
* Currently, can only be {@link Photo} or {@link Video}.
*/
get media(): StoryMedia {
if (this._media === undefined) {
const media = _messageMediaFromTl(this.client, this._peers, this.raw.media)
switch (media?.type) {
case 'photo':
case 'video':
this._media = media
break
default:
throw new MtUnsupportedError('Unsupported story media type')
}
}
return this._media
}
private _interactiveElements?: StoryInteractiveElement[]
/**
* Interactive elements of the story
*/
get interactiveElements() {
if (!this.raw.mediaAreas) return []
if (this._interactiveElements === undefined) {
this._interactiveElements = this.raw.mediaAreas.map((it) => _storyInteractiveElementFromTl(this.client, it))
}
return this._interactiveElements
}
/**
* Privacy rules of the story.
*
* Only available when {@link isMy} is `true`.
*/
get privacyRules(): tl.TypePrivacyRule[] | null {
if (!this.raw.privacy) return null
return this.raw.privacy
}
private _interactions?: StoryInteractions
/**
* Information about story interactions
*/
get interactions(): StoryInteractions | null {
if (!this.raw.views) return null
return (this._interactions ??= new StoryInteractions(this.client, this.raw.views, this._peers))
}
/**
* Emoji representing a reaction sent by the current user, if any
*/
get sentReactionEmoji(): ReactionEmoji | null {
if (!this.raw.sentReaction) return null
return toReactionEmoji(this.raw.sentReaction, true)
}
}
makeInspectable(Story)

View file

@ -0,0 +1,35 @@
import { Chat, PeersIndex, TelegramClient, tl, User } from '../..'
import { makeInspectable } from '../../utils'
/**
* A story was deleted
*/
export class DeleteStoryUpdate {
constructor(readonly client: TelegramClient, readonly raw: tl.RawUpdateStory, readonly _peers: PeersIndex) {}
private _peer?: User | Chat
/**
* Peer that owns these stories.
*/
get peer(): User | Chat {
if (this._peer) return this._peer
switch (this.raw.peer._) {
case 'peerUser':
return (this._peer = new User(this.client, this._peers.user(this.raw.peer.userId)))
case 'peerChat':
return (this._peer = new Chat(this.client, this._peers.chat(this.raw.peer.chatId)))
case 'peerChannel':
return (this._peer = new Chat(this.client, this._peers.chat(this.raw.peer.channelId)))
}
}
/**
* ID of the deleted story
*/
get storyId(): number {
return this.raw.story.id
}
}
makeInspectable(DeleteStoryUpdate)

View file

@ -5,10 +5,12 @@ import { ChatJoinRequestUpdate } from './chat-join-request'
import { ChatMemberUpdate, ChatMemberUpdateType } from './chat-member-update'
import { ChosenInlineResult } from './chosen-inline-result'
import { DeleteMessageUpdate } from './delete-message-update'
import { DeleteStoryUpdate } from './delete-story-update'
import { HistoryReadUpdate } from './history-read-update'
import { PollUpdate } from './poll-update'
import { PollVoteUpdate } from './poll-vote'
import { PreCheckoutQuery } from './pre-checkout-query'
import { StoryUpdate } from './story-update'
import { UserStatusUpdate } from './user-status-update'
import { UserTypingUpdate } from './user-typing-update'
@ -20,10 +22,12 @@ export {
ChatMemberUpdateType,
ChosenInlineResult,
DeleteMessageUpdate,
DeleteStoryUpdate,
HistoryReadUpdate,
PollUpdate,
PollVoteUpdate,
PreCheckoutQuery,
StoryUpdate,
UserStatusUpdate,
UserTypingUpdate,
}
@ -46,5 +50,7 @@ export type ParsedUpdate =
| { name: 'bot_chat_join_request'; data: BotChatJoinRequestUpdate }
| { name: 'chat_join_request'; data: ChatJoinRequestUpdate }
| { name: 'pre_checkout_query'; data: PreCheckoutQuery }
| { name: 'story'; data: StoryUpdate }
| { name: 'delete_story'; data: DeleteStoryUpdate }
// end-codegen

View file

@ -10,6 +10,7 @@ import {
ChatMemberUpdate,
ChosenInlineResult,
DeleteMessageUpdate,
DeleteStoryUpdate,
HistoryReadUpdate,
InlineQuery,
Message,
@ -18,103 +19,105 @@ import {
PollUpdate,
PollVoteUpdate,
PreCheckoutQuery,
StoryUpdate,
UserStatusUpdate,
UserTypingUpdate,
} from '../index'
type ParserFunction = (
client: TelegramClient,
upd: tl.TypeUpdate | tl.TypeMessage,
peers: PeersIndex,
) => ParsedUpdate['data']
type UpdateParser = [ParsedUpdate['name'], ParserFunction]
const baseMessageParser: ParserFunction = (client: TelegramClient, upd, peers) =>
new Message(
client,
tl.isAnyMessage(upd) ? upd : (upd as { message: tl.TypeMessage }).message,
peers,
upd._ === 'updateNewScheduledMessage',
)
const newMessageParser: UpdateParser = ['new_message', baseMessageParser]
const editMessageParser: UpdateParser = ['edit_message', baseMessageParser]
const chatMemberParser: UpdateParser = [
'chat_member',
(client, upd, peers) => new ChatMemberUpdate(client, upd as any, peers),
]
const callbackQueryParser: UpdateParser = [
'callback_query',
(client, upd, peers) => new CallbackQuery(client, upd as any, peers),
]
const userTypingParser: UpdateParser = ['user_typing', (client, upd) => new UserTypingUpdate(client, upd as any)]
const deleteMessageParser: UpdateParser = [
'delete_message',
(client, upd) => new DeleteMessageUpdate(client, upd as any),
]
const historyReadParser: UpdateParser = ['history_read', (client, upd) => new HistoryReadUpdate(client, upd as any)]
const PARSERS: Partial<Record<(tl.TypeUpdate | tl.TypeMessage)['_'], UpdateParser>> = {
message: newMessageParser,
messageEmpty: newMessageParser,
messageService: newMessageParser,
updateNewMessage: newMessageParser,
updateNewChannelMessage: newMessageParser,
updateNewScheduledMessage: newMessageParser,
updateEditMessage: editMessageParser,
updateEditChannelMessage: editMessageParser,
updateChatParticipant: chatMemberParser,
updateChannelParticipant: chatMemberParser,
updateBotInlineQuery: ['inline_query', (client, upd, peers) => new InlineQuery(client, upd as any, peers)],
updateBotInlineSend: [
'chosen_inline_result',
(client, upd, peers) => new ChosenInlineResult(client, upd as any, peers),
],
updateBotCallbackQuery: callbackQueryParser,
updateInlineBotCallbackQuery: callbackQueryParser,
updateMessagePoll: ['poll', (client, upd, peers) => new PollUpdate(client, upd as any, peers)],
updateMessagePollVote: ['poll_vote', (client, upd, peers) => new PollVoteUpdate(client, upd as any, peers)],
updateUserStatus: ['user_status', (client, upd) => new UserStatusUpdate(client, upd as any)],
updateChannelUserTyping: userTypingParser,
updateChatUserTyping: userTypingParser,
updateUserTyping: userTypingParser,
updateDeleteChannelMessages: deleteMessageParser,
updateDeleteMessages: deleteMessageParser,
updateReadHistoryInbox: historyReadParser,
updateReadHistoryOutbox: historyReadParser,
updateReadChannelInbox: historyReadParser,
updateReadChannelOutbox: historyReadParser,
updateReadChannelDiscussionInbox: historyReadParser,
updateReadChannelDiscussionOutbox: historyReadParser,
updateBotStopped: ['bot_stopped', (client, upd, peers) => new BotStoppedUpdate(client, upd as any, peers)],
updateBotChatInviteRequester: [
'bot_chat_join_request',
(client, upd, peers) => new BotChatJoinRequestUpdate(client, upd as any, peers),
],
updatePendingJoinRequests: [
'chat_join_request',
(client, upd, peers) => new ChatJoinRequestUpdate(client, upd as any, peers),
],
updateBotPrecheckoutQuery: [
'pre_checkout_query',
(client, upd, peers) => new PreCheckoutQuery(client, upd as any, peers),
],
}
/** @internal */
export function _parseUpdate(
client: TelegramClient,
update: tl.TypeUpdate | tl.TypeMessage,
peers: PeersIndex,
): ParsedUpdate | null {
const pair = PARSERS[update._]
switch (update._) {
case 'message':
case 'messageEmpty':
case 'messageService':
case 'updateNewMessage':
case 'updateNewChannelMessage':
case 'updateNewScheduledMessage':
return {
name: 'new_message',
data: new Message(
client,
tl.isAnyMessage(update) ? update : update.message,
peers,
update._ === 'updateNewScheduledMessage',
),
}
break
case 'updateEditMessage':
case 'updateEditChannelMessage':
return { name: 'edit_message', data: new Message(client, update.message, peers) }
break
case 'updateChatParticipant':
case 'updateChannelParticipant':
return { name: 'chat_member', data: new ChatMemberUpdate(client, update, peers) }
break
case 'updateBotInlineQuery':
return { name: 'inline_query', data: new InlineQuery(client, update, peers) }
break
case 'updateBotInlineSend':
return { name: 'chosen_inline_result', data: new ChosenInlineResult(client, update, peers) }
break
case 'updateBotCallbackQuery':
case 'updateInlineBotCallbackQuery':
return { name: 'callback_query', data: new CallbackQuery(client, update, peers) }
break
case 'updateMessagePoll':
return { name: 'poll', data: new PollUpdate(client, update, peers) }
break
case 'updateMessagePollVote':
return { name: 'poll_vote', data: new PollVoteUpdate(client, update, peers) }
break
case 'updateUserStatus':
return { name: 'user_status', data: new UserStatusUpdate(client, update) }
break
case 'updateChannelUserTyping':
case 'updateChatUserTyping':
case 'updateUserTyping':
return { name: 'user_typing', data: new UserTypingUpdate(client, update) }
break
case 'updateDeleteChannelMessages':
case 'updateDeleteMessages':
return { name: 'delete_message', data: new DeleteMessageUpdate(client, update) }
break
case 'updateReadHistoryInbox':
case 'updateReadHistoryOutbox':
case 'updateReadChannelInbox':
case 'updateReadChannelOutbox':
case 'updateReadChannelDiscussionInbox':
case 'updateReadChannelDiscussionOutbox':
return { name: 'history_read', data: new HistoryReadUpdate(client, update) }
break
case 'updateBotStopped':
return { name: 'bot_stopped', data: new BotStoppedUpdate(client, update, peers) }
break
case 'updateBotChatInviteRequester':
return { name: 'bot_chat_join_request', data: new BotChatJoinRequestUpdate(client, update, peers) }
break
case 'updatePendingJoinRequests':
return { name: 'chat_join_request', data: new ChatJoinRequestUpdate(client, update, peers) }
break
case 'updateBotPrecheckoutQuery':
return { name: 'pre_checkout_query', data: new PreCheckoutQuery(client, update, peers) }
break
case 'updateStory': {
const story = update.story
if (pair) {
return {
name: pair[0],
data: pair[1](client, update, peers),
} as ParsedUpdate
if (story._ === 'storyItemDeleted') {
return { name: 'delete_story', data: new DeleteStoryUpdate(client, update, peers) }
}
return {
name: 'story',
data: new StoryUpdate(client, update, peers),
}
break
}
default:
return null
}
return null
}

View file

@ -0,0 +1,43 @@
import { Chat, PeersIndex, Story, TelegramClient, tl, User } from '../..'
import { assertTypeIs, makeInspectable } from '../../utils'
/**
* A story was posted or edited
*
* > **Note**: Currently the only way to reliably test if this is a new story or an update
* > is to store known stories IDs and compare them to the one in the update.
*/
export class StoryUpdate {
constructor(readonly client: TelegramClient, readonly raw: tl.RawUpdateStory, readonly _peers: PeersIndex) {}
private _peer?: User | Chat
/**
* Peer that owns these stories.
*/
get peer(): User | Chat {
if (this._peer) return this._peer
switch (this.raw.peer._) {
case 'peerUser':
return (this._peer = new User(this.client, this._peers.user(this.raw.peer.userId)))
case 'peerChat':
return (this._peer = new Chat(this.client, this._peers.chat(this.raw.peer.chatId)))
case 'peerChannel':
return (this._peer = new Chat(this.client, this._peers.chat(this.raw.peer.channelId)))
}
}
private _story?: Story
/**
* Story that was posted or edited.
*/
get story(): Story {
if (this._story) return this._story
assertTypeIs('StoryUpdate.story', this.raw.story, 'storyItem')
return (this._story = new Story(this.client, this.raw.story, this._peers))
}
}
makeInspectable(StoryUpdate)

View file

@ -9,7 +9,8 @@
"scripts": {
"test": "mocha -r ts-node/register \"tests/**/*.spec.ts\"",
"docs": "typedoc",
"build": "tsc"
"build": "tsc",
"gen-updates": "node ./scripts/generate.js"
},
"dependencies": {
"@mtcute/core": "workspace:^1.0.0",

View file

@ -19,6 +19,8 @@ import {
PollUpdate,
PollVoteUpdate,
PreCheckoutQuery,
StoryUpdate,
DeleteStoryUpdate,
TelegramClient,
UserStatusUpdate,
UserTypingUpdate,
@ -35,6 +37,7 @@ import {
ChatMemberUpdateHandler,
ChosenInlineResultHandler,
DeleteMessageHandler,
DeleteStoryHandler,
EditMessageHandler,
HistoryReadHandler,
InlineQueryHandler,
@ -43,6 +46,7 @@ import {
PollVoteHandler,
PreCheckoutQueryHandler,
RawUpdateHandler,
StoryUpdateHandler,
UpdateHandler,
UserStatusUpdateHandler,
UserTypingHandler,
@ -1419,5 +1423,57 @@ export class Dispatcher<State = never, SceneName extends string = string> {
this._addKnownHandler('pre_checkout_query', filter, handler, group)
}
/**
* Register a story update handler without any filters
*
* @param handler Story update handler
* @param group Handler group index
*/
onStoryUpdate(handler: StoryUpdateHandler['callback'], group?: number): void
/**
* Register a story update handler with a filter
*
* @param filter Update filter
* @param handler Story update handler
* @param group Handler group index
*/
onStoryUpdate<Mod>(
filter: UpdateFilter<StoryUpdate, Mod>,
handler: StoryUpdateHandler<filters.Modify<StoryUpdate, Mod>>['callback'],
group?: number,
): void
/** @internal */
onStoryUpdate(filter: any, handler?: any, group?: number): void {
this._addKnownHandler('story', filter, handler, group)
}
/**
* Register a delete story handler without any filters
*
* @param handler Delete story handler
* @param group Handler group index
*/
onDeleteStory(handler: DeleteStoryHandler['callback'], group?: number): void
/**
* Register a delete story handler with a filter
*
* @param filter Update filter
* @param handler Delete story handler
* @param group Handler group index
*/
onDeleteStory<Mod>(
filter: UpdateFilter<DeleteStoryUpdate, Mod>,
handler: DeleteStoryHandler<filters.Modify<DeleteStoryUpdate, Mod>>['callback'],
group?: number,
): void
/** @internal */
onDeleteStory(filter: any, handler?: any, group?: number): void {
this._addKnownHandler('delete_story', filter, handler, group)
}
// end-codegen
}

View file

@ -6,6 +6,7 @@ import {
ChatMemberUpdate,
ChosenInlineResult,
DeleteMessageUpdate,
DeleteStoryUpdate,
HistoryReadUpdate,
InlineQuery,
MaybeAsync,
@ -14,6 +15,7 @@ import {
PollUpdate,
PollVoteUpdate,
PreCheckoutQuery,
StoryUpdate,
TelegramClient,
tl,
UserStatusUpdate,
@ -62,6 +64,8 @@ export type BotStoppedHandler<T = BotStoppedUpdate> = ParsedUpdateHandler<'bot_s
export type BotChatJoinRequestHandler<T = BotChatJoinRequestUpdate> = ParsedUpdateHandler<'bot_chat_join_request', T>
export type ChatJoinRequestHandler<T = ChatJoinRequestUpdate> = ParsedUpdateHandler<'chat_join_request', T>
export type PreCheckoutQueryHandler<T = PreCheckoutQuery> = ParsedUpdateHandler<'pre_checkout_query', T>
export type StoryUpdateHandler<T = StoryUpdate> = ParsedUpdateHandler<'story', T>
export type DeleteStoryHandler<T = DeleteStoryUpdate> = ParsedUpdateHandler<'delete_story', T>
export type UpdateHandler =
| RawUpdateHandler
@ -81,5 +85,7 @@ export type UpdateHandler =
| BotChatJoinRequestHandler
| ChatJoinRequestHandler
| PreCheckoutQueryHandler
| StoryUpdateHandler
| DeleteStoryHandler
// end-codegen

View file

@ -2,7 +2,7 @@
> TL schema and related utils used for mtcute.
Generated from TL layer **165** (last updated on 03.10.2023).
Generated from TL layer **165** (last updated on 04.10.2023).
## About

File diff suppressed because one or more lines are too long

View file

@ -10,6 +10,7 @@
"attachMenuBot": ["bot_id"],
"autoDownloadSettings": ["video_size_max", "file_size_max"],
"botInfo": ["user_id"],
"booster": ["user_id"],
"channel": ["id"],
"channelAdminLogEvent": ["user_id"],
"channelAdminLogEventActionChangeLinkedChat": ["prev_value", "new_value"],
@ -92,6 +93,7 @@
"statsGroupTopInviter": ["user_id"],
"statsGroupTopPoster": ["user_id"],
"storyView": ["user_id"],
"storyViews": ["recent_viewers"],
"updateBotCallbackQuery": ["user_id"],
"updateBotCommands": ["bot_id"],
"updateBotInlineQuery": ["user_id"],