build: updated to 179 layer #56

Merged
teidesu merged 13 commits from layer179 into master 2024-05-06 12:28:14 +03:00
21 changed files with 1124 additions and 33 deletions
Showing only changes of commit 756b99e12f - Show all commits

View file

@ -177,10 +177,15 @@ import { cancelPasswordEmail, resendPasswordEmail, verifyPasswordEmail } from '.
import { removeCloudPassword } from './methods/password/remove-cloud-password.js'
import { applyBoost } from './methods/premium/apply-boost.js'
import { canApplyBoost, CanApplyBoostResult } from './methods/premium/can-apply-boost.js'
import { createBusinessChatLink } from './methods/premium/create-business-chat-link.js'
import { deleteBusinessChatLink, editBusinessChatLink } from './methods/premium/edit-business-chat-link.js'
import { getBoostStats } from './methods/premium/get-boost-stats.js'
import { getBoosts } from './methods/premium/get-boosts.js'
import { getBusinessChatLinks } from './methods/premium/get-business-chat-links.js'
import { getMyBoostSlots } from './methods/premium/get-my-boost-slots.js'
import { iterBoosters } from './methods/premium/iter-boosters.js'
import { setBusinessIntro } from './methods/premium/set-business-intro.js'
import { setBusinessWorkHours } from './methods/premium/set-business-work-hours.js'
import { addStickerToSet } from './methods/stickers/add-sticker-to-set.js'
import { createStickerSet } from './methods/stickers/create-sticker-set.js'
import { deleteStickerFromSet } from './methods/stickers/delete-sticker-from-set.js'
@ -225,6 +230,7 @@ import { iterProfilePhotos } from './methods/users/iter-profile-photos.js'
import { resolveChannel, resolvePeer, resolveUser } from './methods/users/resolve-peer.js'
import { resolvePeerMany } from './methods/users/resolve-peer-many.js'
import { setGlobalTtl } from './methods/users/set-global-ttl.js'
import { setMyBirthday } from './methods/users/set-my-birthday.js'
import { setMyEmojiStatus } from './methods/users/set-my-emoji-status.js'
import { setMyProfilePhoto } from './methods/users/set-my-profile-photo.js'
import { setMyUsername } from './methods/users/set-my-username.js'
@ -245,6 +251,8 @@ import {
BotReactionCountUpdate,
BotReactionUpdate,
BotStoppedUpdate,
BusinessChatLink,
BusinessWorkHoursDay,
CallbackQuery,
Chat,
ChatEvent,
@ -271,6 +279,7 @@ import {
InputFileLike,
InputInlineResult,
InputMediaLike,
InputMediaSticker,
InputMessageId,
InputPeerLike,
InputPrivacyRule,
@ -4161,6 +4170,47 @@ export interface TelegramClient extends ITelegramClient {
*/
canApplyBoost(): Promise<CanApplyBoostResult>
/**
* Create a new business chat link
*
* **Available**: 👤 users only
*
* @param text Text to be inserted into the message input
*/
createBusinessChatLink(
text: InputText,
params?: {
/** Custom title for the link */
title?: string
},
): Promise<BusinessChatLink>
/**
* Edit an existing business chat link
*
* **Available**: 👤 users only
*
* @param link The link to edit
*/
editBusinessChatLink(
link: string | BusinessChatLink,
params: {
/** Text to be inserted in the message input */
text: InputText
/** Custom title for the link */
title?: string
},
): Promise<BusinessChatLink>
/**
* Delete a business chat link
*
* **Available**: 👤 users only
*
* @param link The link to delete
*/
deleteBusinessChatLink(link: string | BusinessChatLink): Promise<void>
/**
* Get information about boosts in a channel
*
@ -4190,6 +4240,13 @@ export interface TelegramClient extends ITelegramClient {
limit?: number
},
): Promise<ArrayPaginated<Boost, string>>
/**
* Get current user's business chat links
* **Available**: 👤 users only
*
*/
getBusinessChatLinks(): Promise<BusinessChatLink[]>
/**
* Get boost slots information of the current user.
*
@ -4227,6 +4284,57 @@ export interface TelegramClient extends ITelegramClient {
chunkSize?: number
},
): AsyncIterableIterator<Boost>
/**
* Set current user's business introduction.
*
* **Available**: 👤 users only
*
* @param intro Introduction parameters, or `null` to remove
*/
setBusinessIntro(
intro: {
/**
* Title of the introduction
*/
title?: string
/**
* Description of the introduction
*/
description?: string
/**
* Sticker to show beneath the introduction
*/
sticker?: InputMediaSticker | InputFileLike | tl.TypeInputDocument
} | null,
): Promise<void>
/**
* Set current user's business work hours.
* **Available**: 👤 users only
*
*/
setBusinessWorkHours(
params:
| ({
/** Timezone in which the hours are defined */
timezone: string
} & (
| {
/**
* Business work intervals, per-day (like available in {@link BusinessWorkHours.days})
*/
hours: ReadonlyArray<BusinessWorkHoursDay>
}
| {
/** Business work intervals, raw intervals */
intervals: tl.TypeBusinessWeeklyOpen[]
}
))
| null,
): Promise<void>
/**
* Add a sticker to a sticker set.
*
@ -5062,6 +5170,22 @@ export interface TelegramClient extends ITelegramClient {
* @param period New TTL period, in seconds (or 0 to disable)
*/
setGlobalTtl(period: number): Promise<void>
/**
* Set or remove current user's birthday.
* **Available**: 👤 users only
*
*/
setMyBirthday(
birthday: {
/** Birthday day */
day: number
/** Birthday month */
month: number
/** Birthday year (optional) */
year?: number
} | null,
): Promise<void>
/**
* Set an emoji status for the current user
*
@ -5742,18 +5866,36 @@ TelegramClient.prototype.applyBoost = function (...args) {
TelegramClient.prototype.canApplyBoost = function (...args) {
return canApplyBoost(this._client, ...args)
}
TelegramClient.prototype.createBusinessChatLink = function (...args) {
return createBusinessChatLink(this._client, ...args)
}
TelegramClient.prototype.editBusinessChatLink = function (...args) {
return editBusinessChatLink(this._client, ...args)
}
TelegramClient.prototype.deleteBusinessChatLink = function (...args) {
return deleteBusinessChatLink(this._client, ...args)
}
TelegramClient.prototype.getBoostStats = function (...args) {
return getBoostStats(this._client, ...args)
}
TelegramClient.prototype.getBoosts = function (...args) {
return getBoosts(this._client, ...args)
}
TelegramClient.prototype.getBusinessChatLinks = function (...args) {
return getBusinessChatLinks(this._client, ...args)
}
TelegramClient.prototype.getMyBoostSlots = function (...args) {
return getMyBoostSlots(this._client, ...args)
}
TelegramClient.prototype.iterBoosters = function (...args) {
return iterBoosters(this._client, ...args)
}
TelegramClient.prototype.setBusinessIntro = function (...args) {
return setBusinessIntro(this._client, ...args)
}
TelegramClient.prototype.setBusinessWorkHours = function (...args) {
return setBusinessWorkHours(this._client, ...args)
}
TelegramClient.prototype.addStickerToSet = function (...args) {
return addStickerToSet(this._client, ...args)
}
@ -5900,6 +6042,9 @@ TelegramClient.prototype.resolveChannel = function (...args) {
TelegramClient.prototype.setGlobalTtl = function (...args) {
return setGlobalTtl(this._client, ...args)
}
TelegramClient.prototype.setMyBirthday = function (...args) {
return setMyBirthday(this._client, ...args)
}
TelegramClient.prototype.setMyEmojiStatus = function (...args) {
return setMyEmojiStatus(this._client, ...args)
}

View file

@ -192,10 +192,16 @@ export { removeCloudPassword } from './methods/password/remove-cloud-password.js
export { applyBoost } from './methods/premium/apply-boost.js'
export type { CanApplyBoostResult } from './methods/premium/can-apply-boost.js'
export { canApplyBoost } from './methods/premium/can-apply-boost.js'
export { createBusinessChatLink } from './methods/premium/create-business-chat-link.js'
export { editBusinessChatLink } from './methods/premium/edit-business-chat-link.js'
export { deleteBusinessChatLink } from './methods/premium/edit-business-chat-link.js'
export { getBoostStats } from './methods/premium/get-boost-stats.js'
export { getBoosts } from './methods/premium/get-boosts.js'
export { getBusinessChatLinks } from './methods/premium/get-business-chat-links.js'
export { getMyBoostSlots } from './methods/premium/get-my-boost-slots.js'
export { iterBoosters } from './methods/premium/iter-boosters.js'
export { setBusinessIntro } from './methods/premium/set-business-intro.js'
export { setBusinessWorkHours } from './methods/premium/set-business-work-hours.js'
export { addStickerToSet } from './methods/stickers/add-sticker-to-set.js'
export { createStickerSet } from './methods/stickers/create-sticker-set.js'
export { deleteStickerFromSet } from './methods/stickers/delete-sticker-from-set.js'
@ -245,6 +251,7 @@ export { resolveUser } from './methods/users/resolve-peer.js'
export { resolveChannel } from './methods/users/resolve-peer.js'
export { resolvePeerMany } from './methods/users/resolve-peer-many.js'
export { setGlobalTtl } from './methods/users/set-global-ttl.js'
export { setMyBirthday } from './methods/users/set-my-birthday.js'
export { setMyEmojiStatus } from './methods/users/set-my-emoji-status.js'
export { setMyProfilePhoto } from './methods/users/set-my-profile-photo.js'
export { setMyUsername } from './methods/users/set-my-username.js'

View file

@ -26,6 +26,8 @@ import {
BotReactionCountUpdate,
BotReactionUpdate,
BotStoppedUpdate,
BusinessChatLink,
BusinessWorkHoursDay,
CallbackQuery,
Chat,
ChatEvent,
@ -52,6 +54,7 @@ import {
InputFileLike,
InputInlineResult,
InputMediaLike,
InputMediaSticker,
InputMessageId,
InputPeerLike,
InputPrivacyRule,

View file

@ -0,0 +1,32 @@
import { ITelegramClient } from '../../client.types.js'
import { InputText } from '../../types/index.js'
import { BusinessChatLink } from '../../types/premium/business-chat-link.js'
import { _normalizeInputText } from '../misc/normalize-text.js'
// @available=user
/**
* Create a new business chat link
*
* @param text Text to be inserted into the message input
*/
export async function createBusinessChatLink(
client: ITelegramClient,
text: InputText,
params?: {
/** Custom title for the link */
title?: string
},
): Promise<BusinessChatLink> {
const [message, entities] = await _normalizeInputText(client, text)
const res = await client.call({
_: 'account.createBusinessChatLink',
link: {
_: 'inputBusinessChatLink',
message,
entities,
title: params?.title,
},
})
return new BusinessChatLink(res)
}

View file

@ -0,0 +1,51 @@
import { assertTrue } from '../../../utils/type-assertions.js'
import { ITelegramClient } from '../../client.types.js'
import { InputText } from '../../types/index.js'
import { BusinessChatLink } from '../../types/premium/business-chat-link.js'
import { _normalizeInputText } from '../misc/normalize-text.js'
// @available=user
/**
* Edit an existing business chat link
*
* @param link The link to edit
*/
export async function editBusinessChatLink(
client: ITelegramClient,
link: string | BusinessChatLink,
params: {
/** Text to be inserted in the message input */
text: InputText
/** Custom title for the link */
title?: string
},
): Promise<BusinessChatLink> {
const [message, entities] = await _normalizeInputText(client, params.text)
const res = await client.call({
_: 'account.editBusinessChatLink',
slug: link instanceof BusinessChatLink ? link.link : link,
link: {
_: 'inputBusinessChatLink',
message,
entities,
title: params?.title,
},
})
return new BusinessChatLink(res)
}
// @available=user
/**
* Delete a business chat link
*
* @param link The link to delete
*/
export async function deleteBusinessChatLink(client: ITelegramClient, link: string | BusinessChatLink): Promise<void> {
const res = await client.call({
_: 'account.deleteBusinessChatLink',
slug: typeof link === 'string' ? link : link.link,
})
assertTrue('account.deleteBusinessChatLink', res)
}

View file

@ -0,0 +1,12 @@
import { ITelegramClient } from '../../client.types.js'
import { BusinessChatLink } from '../../types/premium/business-chat-link.js'
// @available=user
/**
* Get current user's business chat links
*/
export async function getBusinessChatLinks(client: ITelegramClient): Promise<BusinessChatLink[]> {
const res = await client.call({ _: 'account.getBusinessChatLinks' })
return res.links.map((x) => new BusinessChatLink(x))
}

View file

@ -0,0 +1,67 @@
import { tl } from '@mtcute/tl'
import { assertTrue, assertTypeIs } from '../../../utils/type-assertions.js'
import { ITelegramClient } from '../../client.types.js'
import { InputFileLike, InputMediaSticker } from '../../types/index.js'
import { _normalizeFileToDocument } from '../files/normalize-file-to-document.js'
import { _normalizeInputMedia } from '../files/normalize-input-media.js'
const isInputMediaSticker = (media: unknown): media is InputMediaSticker =>
typeof media === 'object' && media !== null && 'type' in media && media.type === 'sticker'
// @available=user
/**
* Set current user's business introduction.
*
* @param intro Introduction parameters, or `null` to remove
*/
export async function setBusinessIntro(
client: ITelegramClient,
intro: {
/**
* Title of the introduction
*/
title?: string
/**
* Description of the introduction
*/
description?: string
/**
* Sticker to show beneath the introduction
*/
sticker?: InputMediaSticker | InputFileLike | tl.TypeInputDocument
} | null,
): Promise<void> {
let tlIntro: tl.TypeInputBusinessIntro | undefined = undefined
if (intro) {
let sticker: tl.TypeInputDocument | undefined
if (intro.sticker) {
if (isInputMediaSticker(intro.sticker)) {
const media = await _normalizeInputMedia(client, intro.sticker, undefined, true)
assertTypeIs('_normalizeInputMedia', media, 'inputMediaDocument')
sticker = media.id
} else {
sticker = await _normalizeFileToDocument(client, intro.sticker, {})
}
}
tlIntro = {
_: 'inputBusinessIntro',
title: intro.title ?? '',
description: intro.description ?? '',
sticker,
}
}
const res = await client.call({
_: 'account.updateBusinessIntro',
intro: tlIntro,
})
assertTrue('account.updateBusinessIntro', res)
}

View file

@ -0,0 +1,55 @@
import { tl } from '@mtcute/tl'
import { assertTrue } from '../../../utils/type-assertions.js'
import { ITelegramClient } from '../../client.types.js'
import { BusinessWorkHoursDay, businessWorkHoursDaysToRaw } from '../../types/premium/business-work-hours.js'
// @available=user
/**
* Set current user's business work hours.
*/
export async function setBusinessWorkHours(
client: ITelegramClient,
params:
| ({
/** Timezone in which the hours are defined */
timezone: string
} & (
| {
/**
* Business work intervals, per-day (like available in {@link BusinessWorkHours.days})
*/
hours: ReadonlyArray<BusinessWorkHoursDay>
}
| {
/** Business work intervals, raw intervals */
intervals: tl.TypeBusinessWeeklyOpen[]
}
))
| null,
): Promise<void> {
let businessWorkHours: tl.TypeBusinessWorkHours | undefined = undefined
if (params) {
let weeklyOpen: tl.TypeBusinessWeeklyOpen[]
if ('hours' in params) {
weeklyOpen = businessWorkHoursDaysToRaw(params.hours)
} else {
weeklyOpen = params.intervals
}
businessWorkHours = {
_: 'businessWorkHours',
timezoneId: params.timezone,
weeklyOpen,
}
}
const res = await client.call({
_: 'account.updateBusinessWorkHours',
businessWorkHours,
})
assertTrue('account.updateBusinessWorkHours', res)
}

View file

@ -0,0 +1,30 @@
import { assertTrue } from '../../../utils/type-assertions.js'
import { ITelegramClient } from '../../client.types.js'
// @available=user
/**
* Set or remove current user's birthday.
*/
export async function setMyBirthday(
client: ITelegramClient,
birthday: {
/** Birthday day */
day: number
/** Birthday month */
month: number
/** Birthday year (optional) */
year?: number
} | null,
): Promise<void> {
const res = await client.call({
_: 'account.updateBirthday',
birthday: birthday ?
{
_: 'birthday',
...birthday,
} :
undefined,
})
assertTrue('account.updateBirthday', res)
}

View file

@ -372,7 +372,24 @@ export interface ActionPhotoSuggested {
photo: Photo
}
/** A peer was chosen by the user after clicking on a RequestPeer button */
/**
* A peer was chosen by the user after clicking on a RequestPeer button.
* The user-side version of {@link ActionPeerChosen}
*/
export interface ActionPeerSent {
readonly type: 'peer_sent'
/** ID of the button passed earlier by the bot */
buttonId: number
/** Brief information about the chosen peers */
peers: tl.TypeRequestedPeer[]
}
/**
* A peer was chosen by the user after clicking on a RequestPeer button
* The bot-side version of {@link ActionPeerSent}
*/
export interface ActionPeerChosen {
readonly type: 'peer_chosen'
@ -472,6 +489,7 @@ export type MessageAction =
| ActionWebviewDataReceived
| ActionPremiumGifted
| ActionPhotoSuggested
| ActionPeerSent
| ActionPeerChosen
| ActionWallpaperChanged
| ActionGiftCode
@ -696,6 +714,12 @@ export function _messageActionFromTl(this: Message, act: tl.TypeMessageAction):
type: 'photo_suggested',
photo: new Photo(act.photo as tl.RawPhoto),
}
case 'messageActionRequestedPeerSentMe':
return {
type: 'peer_sent',
buttonId: act.buttonId,
peers: act.peers,
}
case 'messageActionRequestedPeer':
return {
type: 'peer_chosen',

View file

@ -79,6 +79,14 @@ export class Message {
return this.raw._ === 'message' && this.raw.noforwards!
}
/**
* Whether the message was sent by an implicit action, for example,
* as an away or a greeting business message, or as a scheduled message
*/
get isFromOffline(): boolean {
return this.raw._ === 'message' && this.raw.offline!
}
/**
* Multiple media messages with the same grouped ID
* indicate an album or media group
@ -126,6 +134,15 @@ export class Message {
return parsePeer(from, this._peers)
}
/**
* Number of boosts applied to this {@link chat} by the sender
*/
get senderBoostCount(): number {
if (this.raw._ !== 'message') return 0
return this.raw.fromBoostsApplied ?? 0
}
/**
* Conversation the message belongs to
*/

View file

@ -138,6 +138,13 @@ export class StickerSet {
return this.brief.official!
}
/**
* Whether this sticker set was created by the current user
*/
get isCreator(): boolean {
return this.brief.creator!
}
/**
* Type of the stickers in this set
*/

View file

@ -4,8 +4,12 @@ import { MtTypeAssertionError } from '../../../types/errors.js'
import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { Photo } from '../media/photo.js'
import { StickerSet } from '../misc/sticker-set.js'
import { BusinessAccount } from '../premium/business-account.js'
import { Chat } from './chat.js'
import { ChatInviteLink } from './chat-invite-link.js'
import { ChatLocation } from './chat-location.js'
import { PeersIndex } from './peers-index.js'
/**
* Complete information about a particular chat.
@ -20,32 +24,33 @@ export class FullChat extends Chat {
/** @internal */
static _parse(full: tl.messages.RawChatFull | tl.users.TypeUserFull): FullChat {
const peers = PeersIndex.from(full)
if (full._ === 'users.userFull') {
const user = full.users.find((it) => it.id === full.fullUser.id)
const { fullUser } = full
const user = peers.user(full.fullUser.id)
if (!user || user._ === 'userEmpty') {
throw new MtTypeAssertionError('Chat._parseFull', 'user', user?._ ?? 'undefined')
}
return new FullChat(user, full.fullUser)
const ret = new FullChat(user, fullUser)
if (fullUser.personalChannelId) {
ret._linkedChat = new Chat(peers.chat(fullUser.personalChannelId))
}
return ret
}
const fullChat = full.fullChat
let chat: tl.TypeChat | undefined = undefined
let linked: tl.TypeChat | undefined = undefined
const { fullChat } = full
for (const c of full.chats) {
if (fullChat.id === c.id) {
chat = c
}
if (fullChat._ === 'channelFull' && fullChat.linkedChatId === c.id) {
linked = c
}
const ret = new FullChat(peers.chat(fullChat.id), fullChat)
if (fullChat._ === 'channelFull' && fullChat.linkedChatId) {
ret._linkedChat = new Chat(peers.chat(fullChat.linkedChatId))
}
const ret = new FullChat(chat!, fullChat)
ret._linkedChat = linked ? new Chat(linked) : undefined
return ret
}
@ -132,15 +137,15 @@ export class FullChat extends Chat {
}
/**
* Chat's permanent invite link, for groups, supergroups and channels.
* Chat's primary invite link, for groups, supergroups and channels.
*/
get inviteLink(): string | null {
get inviteLink(): ChatInviteLink | null {
if (this.fullPeer && this.fullPeer._ !== 'userFull') {
switch (this.fullPeer.exportedInvite?._) {
case 'chatInvitePublicJoinRequests':
return null
case 'chatInviteExported':
return this.fullPeer.exportedInvite.link
return new ChatInviteLink(this.fullPeer.exportedInvite)
}
}
@ -148,10 +153,21 @@ export class FullChat extends Chat {
}
/**
* For supergroups, name of the group sticker set.
* For supergroups, information about the group sticker set.
*/
get stickerSetName(): string | null {
return this.fullPeer && this.fullPeer._ === 'channelFull' ? this.fullPeer.stickerset?.shortName ?? null : null
get stickerSet(): StickerSet | null {
if (this.fullPeer?._ !== 'channelFull' || !this.fullPeer.stickerset) return null
return new StickerSet(this.fullPeer.stickerset)
}
/**
* For supergroups, information about the group emoji set.
*/
get emojiSet(): StickerSet | null {
if (this.fullPeer?._ !== 'channelFull' || !this.fullPeer.emojiset) return null
return new StickerSet(this.fullPeer.emojiset)
}
/**
@ -161,19 +177,40 @@ export class FullChat extends Chat {
return this.fullPeer && this.fullPeer._ === 'channelFull' ? this.fullPeer.canSetStickers ?? null : null
}
/**
* Number of boosts applied by the current user to this chat.
*/
get boostsApplied(): number {
if (!this.fullPeer || this.fullPeer._ !== 'channelFull') return 0
return this.fullPeer?.boostsApplied ?? 0
}
/**
* Number of boosts required for the user to be unrestricted in this chat.
*/
get boostsForUnrestrict(): number {
if (!this.fullPeer || this.fullPeer._ !== 'channelFull') return 0
return this.fullPeer?.boostsUnrestrict ?? 0
}
/**
* Chat members count, for groups, supergroups and channels only.
*/
get membersCount(): number | null {
if (this.fullPeer && this.fullPeer._ !== 'userFull') {
if (this.fullPeer._ === 'chatFull' && this.fullPeer.participants._ === 'chatParticipants') {
return this.fullPeer.participants.participants.length
} else if (this.fullPeer._ === 'channelFull') {
return this.fullPeer.participantsCount ?? null
}
}
switch (this.fullPeer._) {
case 'userFull':
return null
case 'chatFull':
if (this.fullPeer.participants._ !== 'chatParticipants') {
return null
}
return null
return this.fullPeer.participants.participants.length
case 'channelFull':
return this.fullPeer.participantsCount ?? null
}
}
/**
@ -189,8 +226,10 @@ export class FullChat extends Chat {
private _linkedChat?: Chat
/**
* The linked discussion group (in case of channels)
* or the linked channel (in case of supergroups).
* Information about a linked chat:
* - for channels: the discussion group
* - for supergroups: the linked channel
* - for users: the personal channel
*/
get linkedChat(): Chat | null {
return this._linkedChat ?? null
@ -202,7 +241,25 @@ export class FullChat extends Chat {
get ttlPeriod(): number | null {
return this.fullPeer?.ttlPeriod ?? null
}
/**
* If this is a business account, information about the business.
*/
get business(): BusinessAccount | null {
if (!this.fullPeer || this.fullPeer._ !== 'userFull') return null
return new BusinessAccount(this.fullPeer)
}
}
memoizeGetters(FullChat, ['fullPhoto', 'personalPhoto', 'realPhoto', 'publicPhoto', 'location'])
memoizeGetters(FullChat, [
'fullPhoto',
'personalPhoto',
'realPhoto',
'publicPhoto',
'location',
'stickerSet',
'emojiSet',
'business',
])
makeInspectable(FullChat)

View file

@ -108,6 +108,14 @@ export class User {
return this.raw.bot!
}
/**
* Whether this user is a bot that can be connected to a
* Telegram Business account to receive its messages
*/
get isBusinessBot(): boolean {
return this.raw.botBusiness!
}
/** Whether this user is a bot that has access to all messages */
get isBotWithHistory(): boolean {
return this.raw.botChatHistory!

View file

@ -0,0 +1,46 @@
import { tl } from '@mtcute/tl'
import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { BusinessIntro } from './business-intro.js'
import { BusinessLocation } from './business-location.js'
import { BusinessWorkHours } from './business-work-hours.js'
/** Information about a business account */
export class BusinessAccount {
constructor(readonly info: tl.RawUserFull) {}
/** Introduction of the business account */
get intro(): BusinessIntro | null {
if (!this.info.businessIntro) return null
return new BusinessIntro(this.info.businessIntro)
}
/** Work hours of the business */
get workHours(): BusinessWorkHours | null {
if (!this.info.businessWorkHours) return null
return new BusinessWorkHours(this.info.businessWorkHours)
}
/** Location of the business */
get location(): BusinessLocation | null {
if (!this.info.businessLocation) return null
return new BusinessLocation(this.info.businessLocation)
}
/** Information about a greeting message */
get greetingMessage(): tl.TypeBusinessGreetingMessage | null {
return this.info.businessGreetingMessage ?? null
}
/** Information about an "away" message */
get awayMessage(): tl.TypeBusinessAwayMessage | null {
return this.info.businessAwayMessage ?? null
}
}
memoizeGetters(BusinessAccount, ['intro', 'workHours', 'location'])
makeInspectable(BusinessAccount)

View file

@ -0,0 +1,40 @@
import { tl } from '@mtcute/tl'
import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { MessageEntity } from '../messages/message-entity.js'
/**
* A business chat link, i.e. a link to start a chat with a pre-filled message.
*/
export class BusinessChatLink {
constructor(readonly raw: tl.RawBusinessChatLink) {}
/** The link itself */
get link(): string {
return this.raw.link
}
/** Text to be inserted into the message input */
get text(): string {
return this.raw.message
}
/** Entities for the text */
get entities(): MessageEntity[] {
return this.raw.entities?.map((x) => new MessageEntity(x)) ?? []
}
/** Custom title for the link */
get title(): string | null {
return this.raw.title ?? null
}
/** Number of clicks on the link */
get clicks(): number {
return this.raw.views
}
}
makeInspectable(BusinessChatLink)
memoizeGetters(BusinessChatLink, ['entities'])

View file

@ -0,0 +1,43 @@
import { tl } from '@mtcute/tl'
import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { parseDocument } from '../media/document-utils.js'
import { Sticker } from '../media/sticker.js'
/**
* Information about a "business intro" text that is displayed
* when a user opens a chat with a business account for the first time.
*/
export class BusinessIntro {
constructor(readonly raw: tl.RawBusinessIntro) {}
/**
* Title of the intro.
*/
get title(): string {
return this.raw.title
}
/**
* Description of the intro.
*/
get description(): string {
return this.raw.description
}
/**
* Sticker of the intro.
*/
get sticker(): Sticker | null {
if (!this.raw.sticker || this.raw.sticker._ === 'documentEmpty') return null
const doc = parseDocument(this.raw.sticker)
if (doc.type !== 'sticker') return null
return doc
}
}
makeInspectable(BusinessIntro)
memoizeGetters(BusinessIntro, ['sticker'])

View file

@ -0,0 +1,24 @@
import { tl } from '@mtcute/tl'
import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js'
import { Location } from '../media/location.js'
/** Location of a business */
export class BusinessLocation {
constructor(readonly raw: tl.RawBusinessLocation) {}
/** Address of the business */
get address(): string {
return this.raw.address
}
get location(): Location | null {
if (!this.raw.geoPoint || this.raw.geoPoint._ === 'geoPointEmpty') return null
return new Location(this.raw.geoPoint)
}
}
makeInspectable(BusinessLocation)
memoizeGetters(BusinessLocation, ['location'])

View file

@ -0,0 +1,228 @@
import { describe, expect, it } from 'vitest'
import { createStub } from '@mtcute/test'
import { BusinessWorkHours, businessWorkHoursDaysToRaw } from './business-work-hours.js'
describe('BusinessWorkHours', () => {
describe('days', () => {
const mkHours = (intervals: [number, number][]) =>
createStub('businessWorkHours', {
weeklyOpen: intervals.map(([start, end]) =>
createStub('businessWeeklyOpen', {
startMinute: start,
endMinute: end,
}),
),
})
it('should handle a single interval on Monday', () => {
const it = new BusinessWorkHours(mkHours([[0, 60]]))
expect(it.days).toEqual([
{ day: 0, is24h: false, intervals: [{ startHour: 0, startMinute: 0, endHour: 1, endMinute: 0 }] },
{ day: 1, is24h: false, intervals: [] },
{ day: 2, is24h: false, intervals: [] },
{ day: 3, is24h: false, intervals: [] },
{ day: 4, is24h: false, intervals: [] },
{ day: 5, is24h: false, intervals: [] },
{ day: 6, is24h: false, intervals: [] },
])
})
it('should handle a single interval on Tuesday', () => {
const it = new BusinessWorkHours(mkHours([[1440, 1500]]))
expect(it.days).toEqual([
{ day: 0, is24h: false, intervals: [] },
{ day: 1, is24h: false, intervals: [{ startHour: 0, startMinute: 0, endHour: 1, endMinute: 0 }] },
{ day: 2, is24h: false, intervals: [] },
{ day: 3, is24h: false, intervals: [] },
{ day: 4, is24h: false, intervals: [] },
{ day: 5, is24h: false, intervals: [] },
{ day: 6, is24h: false, intervals: [] },
])
})
it('should handle multiple intervals within a day', () => {
const it = new BusinessWorkHours(
mkHours([
[0, 60],
[120, 180],
]),
)
expect(it.days).toEqual([
{
day: 0,
is24h: false,
intervals: [
{ startHour: 0, startMinute: 0, endHour: 1, endMinute: 0 },
{ startHour: 2, startMinute: 0, endHour: 3, endMinute: 0 },
],
},
{ day: 1, is24h: false, intervals: [] },
{ day: 2, is24h: false, intervals: [] },
{ day: 3, is24h: false, intervals: [] },
{ day: 4, is24h: false, intervals: [] },
{ day: 5, is24h: false, intervals: [] },
{ day: 6, is24h: false, intervals: [] },
])
})
it('should handle multiple intervals across different days', () => {
const it = new BusinessWorkHours(
mkHours([
[0, 60],
[25 * 60 + 30, 26 * 60 + 30],
]),
)
expect(it.days).toEqual([
{ day: 0, is24h: false, intervals: [{ startHour: 0, startMinute: 0, endHour: 1, endMinute: 0 }] },
{ day: 1, is24h: false, intervals: [{ startHour: 1, startMinute: 30, endHour: 2, endMinute: 30 }] },
{ day: 2, is24h: false, intervals: [] },
{ day: 3, is24h: false, intervals: [] },
{ day: 4, is24h: false, intervals: [] },
{ day: 5, is24h: false, intervals: [] },
{ day: 6, is24h: false, intervals: [] },
])
})
it('should handle a single interval spanning multiple days', () => {
const it = new BusinessWorkHours(mkHours([[0, 1500]]))
expect(it.days).toEqual([
{ day: 0, is24h: true, intervals: [{ startHour: 0, startMinute: 0, endHour: 24, endMinute: 0 }] },
{ day: 1, is24h: false, intervals: [{ startHour: 0, startMinute: 0, endHour: 1, endMinute: 0 }] },
{ day: 2, is24h: false, intervals: [] },
{ day: 3, is24h: false, intervals: [] },
{ day: 4, is24h: false, intervals: [] },
{ day: 5, is24h: false, intervals: [] },
{ day: 6, is24h: false, intervals: [] },
])
})
it('should handle a single 7-day interval', () => {
const it = new BusinessWorkHours(mkHours([[0, 7 * 24 * 60]]))
expect(it.days).toEqual([
{ day: 0, is24h: true, intervals: [{ startHour: 0, startMinute: 0, endHour: 24, endMinute: 0 }] },
{ day: 1, is24h: true, intervals: [{ startHour: 0, startMinute: 0, endHour: 24, endMinute: 0 }] },
{ day: 2, is24h: true, intervals: [{ startHour: 0, startMinute: 0, endHour: 24, endMinute: 0 }] },
{ day: 3, is24h: true, intervals: [{ startHour: 0, startMinute: 0, endHour: 24, endMinute: 0 }] },
{ day: 4, is24h: true, intervals: [{ startHour: 0, startMinute: 0, endHour: 24, endMinute: 0 }] },
{ day: 5, is24h: true, intervals: [{ startHour: 0, startMinute: 0, endHour: 24, endMinute: 0 }] },
{ day: 6, is24h: true, intervals: [{ startHour: 0, startMinute: 0, endHour: 24, endMinute: 0 }] },
])
})
it('should handle multiple intervals each spanning multiple days', () => {
const it = new BusinessWorkHours(
mkHours([
[0, 2 * 24 * 60],
[4 * 24 * 60, 6 * 24 * 60],
]),
)
expect(it.days).toEqual([
{ day: 0, is24h: true, intervals: [{ startHour: 0, startMinute: 0, endHour: 24, endMinute: 0 }] },
{ day: 1, is24h: true, intervals: [{ startHour: 0, startMinute: 0, endHour: 24, endMinute: 0 }] },
{ day: 2, is24h: false, intervals: [] },
{ day: 3, is24h: false, intervals: [] },
{ day: 4, is24h: true, intervals: [{ startHour: 0, startMinute: 0, endHour: 24, endMinute: 0 }] },
{ day: 5, is24h: true, intervals: [{ startHour: 0, startMinute: 0, endHour: 24, endMinute: 0 }] },
{ day: 6, is24h: false, intervals: [] },
])
})
it('should handle overlapping intervals', () => {
const it = new BusinessWorkHours(
mkHours([
[0, 60],
[30, 90],
]),
)
expect(it.days).toEqual([
{ day: 0, is24h: false, intervals: [{ startHour: 0, startMinute: 0, endHour: 1, endMinute: 30 }] },
{ day: 1, is24h: false, intervals: [] },
{ day: 2, is24h: false, intervals: [] },
{ day: 3, is24h: false, intervals: [] },
{ day: 4, is24h: false, intervals: [] },
{ day: 5, is24h: false, intervals: [] },
{ day: 6, is24h: false, intervals: [] },
])
})
it('should handle adjascent intervals', () => {
const it = new BusinessWorkHours(
mkHours([
[0, 60],
[60, 90],
]),
)
expect(it.days).toEqual([
{ day: 0, is24h: false, intervals: [{ startHour: 0, startMinute: 0, endHour: 1, endMinute: 30 }] },
{ day: 1, is24h: false, intervals: [] },
{ day: 2, is24h: false, intervals: [] },
{ day: 3, is24h: false, intervals: [] },
{ day: 4, is24h: false, intervals: [] },
{ day: 5, is24h: false, intervals: [] },
{ day: 6, is24h: false, intervals: [] },
])
})
it('should handle magic 8th day', () => {
const it = new BusinessWorkHours(
mkHours([
// Mon 12:00 - 14:00
[12 * 60, 14 * 60],
// Sun 0:00 - Mon (next week) 2:00
[6 * 24 * 60 + 20 * 60, 7 * 24 * 60 + 2 * 60],
// Mon (next week) 3:00 - Mon (next week) 4:00
[7 * 24 * 60 + 3 * 60, 7 * 24 * 60 + 4 * 60],
]),
)
expect(it.days).toEqual([
{
day: 0,
is24h: false,
intervals: [
{ startHour: 0, startMinute: 0, endHour: 2, endMinute: 0 },
{ startHour: 3, startMinute: 0, endHour: 4, endMinute: 0 },
{ startHour: 12, startMinute: 0, endHour: 14, endMinute: 0 },
],
},
{ day: 1, is24h: false, intervals: [] },
{ day: 2, is24h: false, intervals: [] },
{ day: 3, is24h: false, intervals: [] },
{ day: 4, is24h: false, intervals: [] },
{ day: 5, is24h: false, intervals: [] },
{ day: 6, is24h: false, intervals: [{ startHour: 20, startMinute: 0, endHour: 24, endMinute: 0 }] },
])
})
})
describe('businessWorkHoursDaysToRaw', () => {
it('should handle 24-hour days', () => {
expect(businessWorkHoursDaysToRaw([{ day: 0, is24h: true, intervals: [] }])).toEqual([
{ _: 'businessWeeklyOpen', startMinute: 0, endMinute: 1440 },
])
})
it('should handle intervals', () => {
expect(
businessWorkHoursDaysToRaw([
{ day: 0, is24h: false, intervals: [{ startHour: 0, startMinute: 0, endHour: 1, endMinute: 0 }] },
{ day: 3, is24h: false, intervals: [{ startHour: 12, startMinute: 0, endHour: 14, endMinute: 0 }] },
]),
).toEqual([
{ _: 'businessWeeklyOpen', startMinute: 0, endMinute: 60 },
{ _: 'businessWeeklyOpen', startMinute: 3 * 24 * 60 + 12 * 60, endMinute: 3 * 24 * 60 + 14 * 60 },
])
})
})
})

View file

@ -0,0 +1,191 @@
import { tl } from '@mtcute/tl'
import { MtArgumentError } from '../../../types/errors.js'
import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js'
export interface BusinessWorkHoursInterval {
/** Start hour of the interval (0-23) */
readonly startHour: number
/** Start minute of the interval (0-59) */
readonly startMinute: number
/** End hour of the interval (0-23) */
readonly endHour: number
/** End minute of the interval (0-59) */
readonly endMinute: number
}
export interface BusinessWorkHoursDay {
/** Day of the week, 0-6, where 0 is Monday and 6 is Sunday */
readonly day: number
/** Whether this day is open 24 hours */
readonly is24h: boolean
/** Open intervals for this day */
readonly intervals: BusinessWorkHoursInterval[]
}
const DAYS_IN_WEEK = 7
const MINUTES_IN_DAY = 24 * 60
export function businessWorkHoursDaysToRaw(day: ReadonlyArray<BusinessWorkHoursDay>): tl.TypeBusinessWeeklyOpen[] {
const res: tl.TypeBusinessWeeklyOpen[] = []
for (const d of day) {
const dayStart = d.day * MINUTES_IN_DAY
if (d.is24h) {
res.push({
_: 'businessWeeklyOpen',
startMinute: dayStart,
endMinute: dayStart + MINUTES_IN_DAY,
})
continue
}
for (const interval of d.intervals) {
const start = dayStart + interval.startHour * 60 + interval.startMinute
const end = dayStart + interval.endHour * 60 + interval.endMinute
if (start >= end) {
throw new MtArgumentError('startMinute >= endMinute')
}
res.push({
_: 'businessWeeklyOpen',
startMinute: start,
endMinute: end,
})
}
}
return res
}
/**
* Information about business work hours.
*/
export class BusinessWorkHours {
constructor(readonly raw: tl.RawBusinessWorkHours) {}
/** Whether the business is open right now */
get isOpenNow(): boolean {
return this.raw.openNow!
}
/**
* Identifier of the time zone in which the {@link hours} are defined,
* in the IANA format.
*/
get timezoneId(): string {
return this.raw.timezoneId
}
/** Raw "open" intervals */
get intervals(): tl.TypeBusinessWeeklyOpen[] {
return this.raw.weeklyOpen
}
/**
* Parsed business hours intervals per week day.
*
* @returns Array of 7 elements, each representing a day of the week (starting from Monday = 0)
*/
get days(): ReadonlyArray<BusinessWorkHoursDay> {
const days: BusinessWorkHoursDay[] = Array.from({ length: DAYS_IN_WEEK }, (_, i) => ({
day: i,
is24h: false,
intervals: [],
}))
// sort intervals by start time
const sorted = [...this.raw.weeklyOpen].sort((a, b) => a.startMinute - b.startMinute)
// merge overlapping/consecutive intervals
for (let i = 1; i < sorted.length; i++) {
const prev = sorted[i - 1]
const cur = sorted[i]
if (prev.endMinute >= cur.startMinute) {
prev.endMinute = cur.endMinute
sorted.splice(i, 1)
i--
}
}
const mondayPrepend: BusinessWorkHoursInterval[] = []
// process intervals
for (const interval of sorted) {
if (interval.startMinute > interval.endMinute) {
throw new MtArgumentError('startMinute is greater than endMinute')
}
const startDay = Math.floor(interval.startMinute / MINUTES_IN_DAY)
const endDay = Math.floor(interval.endMinute / MINUTES_IN_DAY)
if (endDay > DAYS_IN_WEEK + 1) {
throw new MtArgumentError('interval spans more than a week')
}
for (
let day = startDay, dayStart = startDay * MINUTES_IN_DAY;
day <= endDay;
day++, dayStart += MINUTES_IN_DAY
) {
const startWithin = Math.max(interval.startMinute, dayStart) - dayStart
const endWithin = Math.min(interval.endMinute, dayStart + MINUTES_IN_DAY) - dayStart
const startHour = Math.floor(startWithin / 60)
const startMinute = startWithin % 60
const endHour = Math.floor(endWithin / 60)
const endMinute = endWithin % 60
if (startHour === 0 && startMinute === 0 && endHour === 0 && endMinute === 0) {
continue
}
const obj: BusinessWorkHoursInterval = {
startHour,
startMinute,
endHour,
endMinute,
}
if (day === DAYS_IN_WEEK) {
// prepend to Monday
mondayPrepend.push(obj)
} else {
days[day].intervals.push(obj)
}
}
}
if (mondayPrepend.length > 0) {
// we do this like this to keep everything sorted
days[0].intervals.unshift(...mondayPrepend)
}
// set up 24h days
for (const day of days) {
if (day.intervals.length !== 1) continue
const interval = day.intervals[0]
if (
interval.startHour === 0 &&
interval.startMinute === 0 &&
((interval.endHour === 24 && interval.endMinute === 0) ||
(interval.endHour === 23 && interval.endMinute === 59))
) {
(day as tl.Mutable<BusinessWorkHoursDay>).is24h = true
}
}
return days
}
}
makeInspectable(BusinessWorkHours, undefined, ['intervals'])
memoizeGetters(BusinessWorkHours, ['days'])

View file

@ -1,3 +1,7 @@
export * from './boost.js'
export * from './boost-slot.js'
export * from './boost-stats.js'
export * from './business-account.js'
export * from './business-chat-link.js'
export * from './business-intro.js'
export * from './business-work-hours.js'