From 756b99e12f0fedd446cd7f91f2d8de468e22f1d4 Mon Sep 17 00:00:00 2001 From: alina sireneva Date: Sun, 5 May 2024 19:38:51 +0300 Subject: [PATCH] feat(core): support Telegram Business and some other new features --- packages/core/src/highlevel/client.ts | 145 +++++++++++ packages/core/src/highlevel/methods.ts | 7 + .../core/src/highlevel/methods/_imports.ts | 3 + .../premium/create-business-chat-link.ts | 32 +++ .../premium/edit-business-chat-link.ts | 51 ++++ .../premium/get-business-chat-links.ts | 12 + .../methods/premium/set-business-intro.ts | 67 +++++ .../premium/set-business-work-hours.ts | 55 +++++ .../methods/users/set-my-birthday.ts | 30 +++ .../types/messages/message-action.ts | 26 +- .../src/highlevel/types/messages/message.ts | 17 ++ .../src/highlevel/types/misc/sticker-set.ts | 7 + .../src/highlevel/types/peers/full-chat.ts | 121 +++++++--- .../core/src/highlevel/types/peers/user.ts | 8 + .../types/premium/business-account.ts | 46 ++++ .../types/premium/business-chat-link.ts | 40 +++ .../highlevel/types/premium/business-intro.ts | 43 ++++ .../types/premium/business-location.ts | 24 ++ .../types/premium/business-work-hours.test.ts | 228 ++++++++++++++++++ .../types/premium/business-work-hours.ts | 191 +++++++++++++++ .../core/src/highlevel/types/premium/index.ts | 4 + 21 files changed, 1124 insertions(+), 33 deletions(-) create mode 100644 packages/core/src/highlevel/methods/premium/create-business-chat-link.ts create mode 100644 packages/core/src/highlevel/methods/premium/edit-business-chat-link.ts create mode 100644 packages/core/src/highlevel/methods/premium/get-business-chat-links.ts create mode 100644 packages/core/src/highlevel/methods/premium/set-business-intro.ts create mode 100644 packages/core/src/highlevel/methods/premium/set-business-work-hours.ts create mode 100644 packages/core/src/highlevel/methods/users/set-my-birthday.ts create mode 100644 packages/core/src/highlevel/types/premium/business-account.ts create mode 100644 packages/core/src/highlevel/types/premium/business-chat-link.ts create mode 100644 packages/core/src/highlevel/types/premium/business-intro.ts create mode 100644 packages/core/src/highlevel/types/premium/business-location.ts create mode 100644 packages/core/src/highlevel/types/premium/business-work-hours.test.ts create mode 100644 packages/core/src/highlevel/types/premium/business-work-hours.ts diff --git a/packages/core/src/highlevel/client.ts b/packages/core/src/highlevel/client.ts index 9445d442..254ae746 100644 --- a/packages/core/src/highlevel/client.ts +++ b/packages/core/src/highlevel/client.ts @@ -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 + /** + * 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 + + /** + * 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 + + /** + * Delete a business chat link + * + * **Available**: 👤 users only + * + * @param link The link to delete + */ + deleteBusinessChatLink(link: string | BusinessChatLink): Promise + /** * Get information about boosts in a channel * @@ -4190,6 +4240,13 @@ export interface TelegramClient extends ITelegramClient { limit?: number }, ): Promise> + + /** + * Get current user's business chat links + * **Available**: 👤 users only + * + */ + getBusinessChatLinks(): Promise /** * Get boost slots information of the current user. * @@ -4227,6 +4284,57 @@ export interface TelegramClient extends ITelegramClient { chunkSize?: number }, ): AsyncIterableIterator + + /** + * 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 + + /** + * 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 + } + | { + /** Business work intervals, raw intervals */ + intervals: tl.TypeBusinessWeeklyOpen[] + } + )) + | null, + ): Promise /** * 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 + + /** + * 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 /** * 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) } diff --git a/packages/core/src/highlevel/methods.ts b/packages/core/src/highlevel/methods.ts index 5dc33932..070738f5 100644 --- a/packages/core/src/highlevel/methods.ts +++ b/packages/core/src/highlevel/methods.ts @@ -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' diff --git a/packages/core/src/highlevel/methods/_imports.ts b/packages/core/src/highlevel/methods/_imports.ts index ce798592..be4722f1 100644 --- a/packages/core/src/highlevel/methods/_imports.ts +++ b/packages/core/src/highlevel/methods/_imports.ts @@ -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, diff --git a/packages/core/src/highlevel/methods/premium/create-business-chat-link.ts b/packages/core/src/highlevel/methods/premium/create-business-chat-link.ts new file mode 100644 index 00000000..dfb7ad5d --- /dev/null +++ b/packages/core/src/highlevel/methods/premium/create-business-chat-link.ts @@ -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 { + 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) +} diff --git a/packages/core/src/highlevel/methods/premium/edit-business-chat-link.ts b/packages/core/src/highlevel/methods/premium/edit-business-chat-link.ts new file mode 100644 index 00000000..3ef74722 --- /dev/null +++ b/packages/core/src/highlevel/methods/premium/edit-business-chat-link.ts @@ -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 { + 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 { + const res = await client.call({ + _: 'account.deleteBusinessChatLink', + slug: typeof link === 'string' ? link : link.link, + }) + + assertTrue('account.deleteBusinessChatLink', res) +} diff --git a/packages/core/src/highlevel/methods/premium/get-business-chat-links.ts b/packages/core/src/highlevel/methods/premium/get-business-chat-links.ts new file mode 100644 index 00000000..039481fe --- /dev/null +++ b/packages/core/src/highlevel/methods/premium/get-business-chat-links.ts @@ -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 { + const res = await client.call({ _: 'account.getBusinessChatLinks' }) + + return res.links.map((x) => new BusinessChatLink(x)) +} diff --git a/packages/core/src/highlevel/methods/premium/set-business-intro.ts b/packages/core/src/highlevel/methods/premium/set-business-intro.ts new file mode 100644 index 00000000..1bd8a830 --- /dev/null +++ b/packages/core/src/highlevel/methods/premium/set-business-intro.ts @@ -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 { + 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) +} diff --git a/packages/core/src/highlevel/methods/premium/set-business-work-hours.ts b/packages/core/src/highlevel/methods/premium/set-business-work-hours.ts new file mode 100644 index 00000000..8474e428 --- /dev/null +++ b/packages/core/src/highlevel/methods/premium/set-business-work-hours.ts @@ -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 + } + | { + /** Business work intervals, raw intervals */ + intervals: tl.TypeBusinessWeeklyOpen[] + } + )) + | null, +): Promise { + 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) +} diff --git a/packages/core/src/highlevel/methods/users/set-my-birthday.ts b/packages/core/src/highlevel/methods/users/set-my-birthday.ts new file mode 100644 index 00000000..2154dec5 --- /dev/null +++ b/packages/core/src/highlevel/methods/users/set-my-birthday.ts @@ -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 { + const res = await client.call({ + _: 'account.updateBirthday', + birthday: birthday ? + { + _: 'birthday', + ...birthday, + } : + undefined, + }) + + assertTrue('account.updateBirthday', res) +} diff --git a/packages/core/src/highlevel/types/messages/message-action.ts b/packages/core/src/highlevel/types/messages/message-action.ts index f858f143..af456642 100644 --- a/packages/core/src/highlevel/types/messages/message-action.ts +++ b/packages/core/src/highlevel/types/messages/message-action.ts @@ -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', diff --git a/packages/core/src/highlevel/types/messages/message.ts b/packages/core/src/highlevel/types/messages/message.ts index 77829d51..18b29157 100644 --- a/packages/core/src/highlevel/types/messages/message.ts +++ b/packages/core/src/highlevel/types/messages/message.ts @@ -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 */ diff --git a/packages/core/src/highlevel/types/misc/sticker-set.ts b/packages/core/src/highlevel/types/misc/sticker-set.ts index 4b249dff..fcb7a922 100644 --- a/packages/core/src/highlevel/types/misc/sticker-set.ts +++ b/packages/core/src/highlevel/types/misc/sticker-set.ts @@ -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 */ diff --git a/packages/core/src/highlevel/types/peers/full-chat.ts b/packages/core/src/highlevel/types/peers/full-chat.ts index 20ae49aa..2a171d49 100644 --- a/packages/core/src/highlevel/types/peers/full-chat.ts +++ b/packages/core/src/highlevel/types/peers/full-chat.ts @@ -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) diff --git a/packages/core/src/highlevel/types/peers/user.ts b/packages/core/src/highlevel/types/peers/user.ts index 6f26ed7a..80e4fc10 100644 --- a/packages/core/src/highlevel/types/peers/user.ts +++ b/packages/core/src/highlevel/types/peers/user.ts @@ -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! diff --git a/packages/core/src/highlevel/types/premium/business-account.ts b/packages/core/src/highlevel/types/premium/business-account.ts new file mode 100644 index 00000000..17ba38eb --- /dev/null +++ b/packages/core/src/highlevel/types/premium/business-account.ts @@ -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) diff --git a/packages/core/src/highlevel/types/premium/business-chat-link.ts b/packages/core/src/highlevel/types/premium/business-chat-link.ts new file mode 100644 index 00000000..8a7135d7 --- /dev/null +++ b/packages/core/src/highlevel/types/premium/business-chat-link.ts @@ -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']) diff --git a/packages/core/src/highlevel/types/premium/business-intro.ts b/packages/core/src/highlevel/types/premium/business-intro.ts new file mode 100644 index 00000000..2a6bea59 --- /dev/null +++ b/packages/core/src/highlevel/types/premium/business-intro.ts @@ -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']) diff --git a/packages/core/src/highlevel/types/premium/business-location.ts b/packages/core/src/highlevel/types/premium/business-location.ts new file mode 100644 index 00000000..54f0ca53 --- /dev/null +++ b/packages/core/src/highlevel/types/premium/business-location.ts @@ -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']) diff --git a/packages/core/src/highlevel/types/premium/business-work-hours.test.ts b/packages/core/src/highlevel/types/premium/business-work-hours.test.ts new file mode 100644 index 00000000..9da6a993 --- /dev/null +++ b/packages/core/src/highlevel/types/premium/business-work-hours.test.ts @@ -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 }, + ]) + }) + }) +}) diff --git a/packages/core/src/highlevel/types/premium/business-work-hours.ts b/packages/core/src/highlevel/types/premium/business-work-hours.ts new file mode 100644 index 00000000..7e599fbb --- /dev/null +++ b/packages/core/src/highlevel/types/premium/business-work-hours.ts @@ -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): 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 { + 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).is24h = true + } + } + + return days + } +} + +makeInspectable(BusinessWorkHours, undefined, ['intervals']) +memoizeGetters(BusinessWorkHours, ['days']) diff --git a/packages/core/src/highlevel/types/premium/index.ts b/packages/core/src/highlevel/types/premium/index.ts index ca783abf..7dead005 100644 --- a/packages/core/src/highlevel/types/premium/index.ts +++ b/packages/core/src/highlevel/types/premium/index.ts @@ -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'