From b5bf02fc728b1473a06a427c0f437a6f1bb1cf30 Mon Sep 17 00:00:00 2001 From: alina sireneva Date: Mon, 9 Dec 2024 21:15:10 +0300 Subject: [PATCH] chore(core)!: extract user-specific fields from (Full)Chat to (Full)User breaking: - `getChat`, `getFullChat` now only work for chats (channels/supergroups/basic groups) - for users, use `getUser` and `getFullUser` - many fields that previously had type `Chat` now have type `User | Chat` --- e2e/tests/02.methods.e2e.ts | 4 +- packages/core/src/highlevel/client.ts | 28 +- packages/core/src/highlevel/methods.ts | 2 + .../core/src/highlevel/methods/_imports.ts | 1 + .../src/highlevel/methods/chats/get-chat.ts | 15 +- .../highlevel/methods/chats/get-full-chat.ts | 15 +- .../highlevel/methods/chats/get-full-user.ts | 22 + .../src/highlevel/methods/chats/get-user.ts | 22 + .../highlevel/methods/dialogs/find-dialogs.ts | 2 +- .../highlevel/methods/dialogs/iter-dialogs.ts | 2 +- .../methods/dialogs/join-chatlist.ts | 2 +- .../methods/messages/send-comment.ts | 6 +- .../methods/messages/send-text.test.ts | 10 +- .../src/highlevel/types/messages/dialog.ts | 42 +- .../types/messages/message-forward.ts | 7 +- .../src/highlevel/types/messages/message.ts | 17 +- .../types/messages/replied-message.ts | 6 +- .../src/highlevel/types/peers/bot-info.ts | 59 +++ .../core/src/highlevel/types/peers/chat.ts | 372 ++++++++--------- .../highlevel/types/peers/chatlist-preview.ts | 9 +- .../src/highlevel/types/peers/full-chat.ts | 381 +++++++++++------- .../src/highlevel/types/peers/full-user.ts | 264 ++++++++++++ .../core/src/highlevel/types/peers/index.ts | 2 + .../core/src/highlevel/types/peers/user.ts | 10 + .../highlevel/types/updates/bot-reaction.ts | 9 +- .../highlevel/types/updates/callback-query.ts | 9 +- .../updates/delete-business-message-update.ts | 7 +- packages/dispatcher/src/context/message.ts | 13 +- packages/dispatcher/src/filters/bots.ts | 8 +- packages/dispatcher/src/filters/chat.ts | 23 +- packages/dispatcher/src/filters/user.ts | 2 +- packages/dispatcher/src/state/key.ts | 6 +- 32 files changed, 915 insertions(+), 462 deletions(-) create mode 100644 packages/core/src/highlevel/methods/chats/get-full-user.ts create mode 100644 packages/core/src/highlevel/methods/chats/get-user.ts create mode 100644 packages/core/src/highlevel/types/peers/bot-info.ts create mode 100644 packages/core/src/highlevel/types/peers/full-user.ts diff --git a/e2e/tests/02.methods.e2e.ts b/e2e/tests/02.methods.e2e.ts index a3cf0680..e7237dcc 100644 --- a/e2e/tests/02.methods.e2e.ts +++ b/e2e/tests/02.methods.e2e.ts @@ -29,8 +29,8 @@ describe('2. calling methods', () => { const history = await tg.getHistory(777000, { limit: 5 }) - expect(history[0].chat.chatType).to.equal('private') + expect(history[0].chat.type).to.equal('user') expect(history[0].chat.id).to.equal(777000) - expect(history[0].chat.firstName).to.equal('Telegram') + expect(history[0].chat.displayName).to.equal('Telegram') }) }) diff --git a/packages/core/src/highlevel/client.ts b/packages/core/src/highlevel/client.ts index ffbd52fb..0cfbbb13 100644 --- a/packages/core/src/highlevel/client.ts +++ b/packages/core/src/highlevel/client.ts @@ -24,7 +24,7 @@ import type { QuoteParamsFrom } from './methods/messages/send-quote.js' import type { CanApplyBoostResult } from './methods/premium/can-apply-boost.js' import type { CanSendStoryResult } from './methods/stories/can-send-story.js' import type { ITelegramStorageProvider } from './storage/provider.js' -import type { AllStories, ArrayPaginated, ArrayWithTotal, Boost, BoostSlot, BoostStats, BotChatJoinRequestUpdate, BotCommands, BotReactionCountUpdate, BotReactionUpdate, BotStoppedUpdate, BusinessCallbackQuery, BusinessChatLink, BusinessConnection, BusinessMessage, BusinessWorkHoursDay, CallbackQuery, Chat, ChatEvent, ChatInviteLink, ChatInviteLinkMember, ChatJoinRequestUpdate, ChatlistPreview, ChatMember, ChatMemberUpdate, ChatPreview, ChosenInlineResult, CollectibleInfo, DeleteBusinessMessageUpdate, DeleteMessageUpdate, DeleteStoryUpdate, Dialog, FactCheck, FileDownloadLocation, FileDownloadParameters, ForumTopic, FullChat, GameHighScore, HistoryReadUpdate, InlineCallbackQuery, InlineQuery, InputChatEventFilters, InputDialogFolder, InputFileLike, InputInlineResult, InputMediaLike, InputMediaSticker, InputMessageId, InputPeerLike, InputPrivacyRule, InputReaction, InputStickerSet, InputStickerSetItem, InputText, InputWebview, MaybeDynamic, Message, MessageEffect, MessageMedia, MessageReactions, ParametersSkip2, ParsedUpdate, PeerReaction, PeerStories, Photo, Poll, PollUpdate, PollVoteUpdate, PreCheckoutQuery, RawDocument, ReplyMarkup, SentCode, StarGift, StarsStatus, StarsTransaction, Sticker, StickerSet, StickerType, StoriesStealthMode, Story, StoryInteractions, StoryUpdate, StoryViewer, StoryViewersList, TakeoutSession, TextWithEntities, TypingStatus, UploadedFile, UploadFileLike, User, UserStarGift, UserStatusUpdate, UserTypingUpdate, WebviewResult } from './types/index.js' +import type { AllStories, ArrayPaginated, ArrayWithTotal, Boost, BoostSlot, BoostStats, BotChatJoinRequestUpdate, BotCommands, BotReactionCountUpdate, BotReactionUpdate, BotStoppedUpdate, BusinessCallbackQuery, BusinessChatLink, BusinessConnection, BusinessMessage, BusinessWorkHoursDay, CallbackQuery, Chat, ChatEvent, ChatInviteLink, ChatInviteLinkMember, ChatJoinRequestUpdate, ChatlistPreview, ChatMember, ChatMemberUpdate, ChatPreview, ChosenInlineResult, CollectibleInfo, DeleteBusinessMessageUpdate, DeleteMessageUpdate, DeleteStoryUpdate, Dialog, FactCheck, FileDownloadLocation, FileDownloadParameters, ForumTopic, FullChat, FullUser, GameHighScore, HistoryReadUpdate, InlineCallbackQuery, InlineQuery, InputChatEventFilters, InputDialogFolder, InputFileLike, InputInlineResult, InputMediaLike, InputMediaSticker, InputMessageId, InputPeerLike, InputPrivacyRule, InputReaction, InputStickerSet, InputStickerSetItem, InputText, InputWebview, MaybeDynamic, Message, MessageEffect, MessageMedia, MessageReactions, ParametersSkip2, ParsedUpdate, PeerReaction, PeerStories, Photo, Poll, PollUpdate, PollVoteUpdate, PreCheckoutQuery, RawDocument, ReplyMarkup, SentCode, StarGift, StarsStatus, StarsTransaction, Sticker, StickerSet, StickerType, StoriesStealthMode, Story, StoryInteractions, StoryUpdate, StoryViewer, StoryViewersList, TakeoutSession, TextWithEntities, TypingStatus, UploadedFile, UploadFileLike, User, UserStarGift, UserStatusUpdate, UserTypingUpdate, WebviewResult } from './types/index.js' import type { ParsedUpdateHandlerParams } from './updates/parsed.js' import type { RawUpdateInfo } from './updates/types.js' import type { InputStringSessionData } from './utils/string-session.js' @@ -81,8 +81,10 @@ import { getChatMembers } from './methods/chats/get-chat-members.js' import { getChatPreview } from './methods/chats/get-chat-preview.js' import { getChat } from './methods/chats/get-chat.js' import { getFullChat } from './methods/chats/get-full-chat.js' +import { getFullUser } from './methods/chats/get-full-user.js' import { getNearbyChats } from './methods/chats/get-nearby-chats.js' import { getSimilarChannels } from './methods/chats/get-similar-channels.js' +import { getUser } from './methods/chats/get-user.js' import { iterChatEventLog } from './methods/chats/iter-chat-event-log.js' import { iterChatMembers } from './methods/chats/iter-chat-members.js' import { joinChat } from './methods/chats/join-chat.js' @@ -1655,6 +1657,15 @@ export interface TelegramClient extends ITelegramClient { * Use {@link getChatPreview} instead. */ getFullChat(chatId: InputPeerLike): Promise + + /** + * Get full information about a user. + * + * **Available**: ✅ both users and bots + * + * @param userId ID of the user or their username + */ + getFullUser(userId: InputPeerLike): Promise /** * Get nearby chats * @@ -1678,6 +1689,15 @@ export interface TelegramClient extends ITelegramClient { */ getSimilarChannels( channel: InputPeerLike): Promise> + + /** + * Get basic information about a user. + * + * **Available**: ✅ both users and bots + * + * @param userId ID of the user or their username or invite link + */ + getUser(userId: InputPeerLike): Promise /** * Iterate over chat event log. * @@ -6216,12 +6236,18 @@ TelegramClient.prototype.getChat = function (...args) { TelegramClient.prototype.getFullChat = function (...args) { return getFullChat(this._client, ...args) } +TelegramClient.prototype.getFullUser = function (...args) { + return getFullUser(this._client, ...args) +} TelegramClient.prototype.getNearbyChats = function (...args) { return getNearbyChats(this._client, ...args) } TelegramClient.prototype.getSimilarChannels = function (...args) { return getSimilarChannels(this._client, ...args) } +TelegramClient.prototype.getUser = function (...args) { + return getUser(this._client, ...args) +} TelegramClient.prototype.iterChatEventLog = function (...args) { return iterChatEventLog(this._client, ...args) } diff --git a/packages/core/src/highlevel/methods.ts b/packages/core/src/highlevel/methods.ts index 88ca02bc..fe8d9f69 100644 --- a/packages/core/src/highlevel/methods.ts +++ b/packages/core/src/highlevel/methods.ts @@ -53,8 +53,10 @@ export { getChatMembers } from './methods/chats/get-chat-members.js' export { getChatPreview } from './methods/chats/get-chat-preview.js' export { getChat } from './methods/chats/get-chat.js' export { getFullChat } from './methods/chats/get-full-chat.js' +export { getFullUser } from './methods/chats/get-full-user.js' export { getNearbyChats } from './methods/chats/get-nearby-chats.js' export { getSimilarChannels } from './methods/chats/get-similar-channels.js' +export { getUser } from './methods/chats/get-user.js' export { iterChatEventLog } from './methods/chats/iter-chat-event-log.js' export { iterChatMembers } from './methods/chats/iter-chat-members.js' export { joinChat } from './methods/chats/join-chat.js' diff --git a/packages/core/src/highlevel/methods/_imports.ts b/packages/core/src/highlevel/methods/_imports.ts index f75bbcea..ca2154f7 100644 --- a/packages/core/src/highlevel/methods/_imports.ts +++ b/packages/core/src/highlevel/methods/_imports.ts @@ -52,6 +52,7 @@ import { FileDownloadParameters, ForumTopic, FullChat, + FullUser, GameHighScore, HistoryReadUpdate, InlineCallbackQuery, diff --git a/packages/core/src/highlevel/methods/chats/get-chat.ts b/packages/core/src/highlevel/methods/chats/get-chat.ts index 7ec8db9b..568fe6f6 100644 --- a/packages/core/src/highlevel/methods/chats/get-chat.ts +++ b/packages/core/src/highlevel/methods/chats/get-chat.ts @@ -1,11 +1,11 @@ import type { ITelegramClient } from '../../client.types.js' import type { InputPeerLike } from '../../types/index.js' import { MtArgumentError } from '../../../types/errors.js' -import { Chat, MtPeerNotFoundError } from '../../types/index.js' -import { INVITE_LINK_REGEX } from '../../utils/peer-utils.js' +import { Chat, MtInvalidPeerTypeError, MtPeerNotFoundError } from '../../types/index.js' +import { INVITE_LINK_REGEX, isInputPeerChannel, isInputPeerChat, toInputChannel } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' -import { _getRawPeerBatched } from './batched-queries.js' +import { _getChannelsBatched, _getChatsBatched } from './batched-queries.js' // @available=both /** @@ -36,7 +36,14 @@ export async function getChat(client: ITelegramClient, chatId: InputPeerLike): P const peer = await resolvePeer(client, chatId) - const res = await _getRawPeerBatched(client, peer) + let res + if (isInputPeerChannel(peer)) { + res = await _getChannelsBatched(client, toInputChannel(peer)) + } else if (isInputPeerChat(peer)) { + res = await _getChatsBatched(client, peer.chatId) + } else { + throw new MtInvalidPeerTypeError(chatId, 'chat or channel') + } if (!res) throw new MtPeerNotFoundError(`Chat ${JSON.stringify(chatId)} was not found`) diff --git a/packages/core/src/highlevel/methods/chats/get-full-chat.ts b/packages/core/src/highlevel/methods/chats/get-full-chat.ts index bd398c05..7b11d035 100644 --- a/packages/core/src/highlevel/methods/chats/get-full-chat.ts +++ b/packages/core/src/highlevel/methods/chats/get-full-chat.ts @@ -3,14 +3,12 @@ import type { tl } from '@mtcute/tl' import type { ITelegramClient } from '../../client.types.js' import type { InputPeerLike } from '../../types/index.js' import { MtArgumentError } from '../../../types/errors.js' -import { FullChat } from '../../types/index.js' +import { FullChat, MtInvalidPeerTypeError } from '../../types/index.js' import { INVITE_LINK_REGEX, isInputPeerChannel, isInputPeerChat, - isInputPeerUser, toInputChannel, - toInputUser, } from '../../utils/peer-utils.js' import { resolvePeer } from '../users/resolve-peer.js' @@ -44,25 +42,20 @@ export async function getFullChat(client: ITelegramClient, chatId: InputPeerLike const peer = await resolvePeer(client, chatId) - let res: tl.messages.TypeChatFull | tl.users.TypeUserFull + let res: tl.messages.TypeChatFull if (isInputPeerChannel(peer)) { res = await client.call({ _: 'channels.getFullChannel', channel: toInputChannel(peer), }) - } else if (isInputPeerUser(peer)) { - res = await client.call({ - _: 'users.getFullUser', - id: toInputUser(peer), - }) } else if (isInputPeerChat(peer)) { res = await client.call({ _: 'messages.getFullChat', chatId: peer.chatId, }) } else { - throw new Error('should not happen') + throw new MtInvalidPeerTypeError(chatId, 'chat or channel') } - return FullChat._parse(res) + return new FullChat(res) } diff --git a/packages/core/src/highlevel/methods/chats/get-full-user.ts b/packages/core/src/highlevel/methods/chats/get-full-user.ts new file mode 100644 index 00000000..18e9aebc --- /dev/null +++ b/packages/core/src/highlevel/methods/chats/get-full-user.ts @@ -0,0 +1,22 @@ +import type { ITelegramClient } from '../../client.types.js' +import type { InputPeerLike } from '../../types/index.js' +import { FullUser } from '../../types/index.js' + +import { resolveUser } from '../users/resolve-peer.js' + +// @available=both +/** + * Get full information about a user. + * + * @param userId ID of the user or their username + */ +export async function getFullUser(client: ITelegramClient, userId: InputPeerLike): Promise { + const peer = await resolveUser(client, userId) + + const res = await client.call({ + _: 'users.getFullUser', + id: peer, + }) + + return new FullUser(res) +} diff --git a/packages/core/src/highlevel/methods/chats/get-user.ts b/packages/core/src/highlevel/methods/chats/get-user.ts new file mode 100644 index 00000000..0bd89a71 --- /dev/null +++ b/packages/core/src/highlevel/methods/chats/get-user.ts @@ -0,0 +1,22 @@ +import type { ITelegramClient } from '../../client.types.js' +import type { InputPeerLike } from '../../types/index.js' +import { MtPeerNotFoundError, User } from '../../types/index.js' + +import { resolveUser } from '../users/resolve-peer.js' +import { _getUsersBatched } from './batched-queries.js' + +// @available=both +/** + * Get basic information about a user. + * + * @param userId ID of the user or their username or invite link + */ +export async function getUser(client: ITelegramClient, userId: InputPeerLike): Promise { + const peer = await resolveUser(client, userId) + + const res = await _getUsersBatched(client, peer) + + if (!res) throw new MtPeerNotFoundError(`User ${JSON.stringify(userId)} was not found`) + + return new User(res) +} diff --git a/packages/core/src/highlevel/methods/dialogs/find-dialogs.ts b/packages/core/src/highlevel/methods/dialogs/find-dialogs.ts index 9d42a0fe..110898c4 100644 --- a/packages/core/src/highlevel/methods/dialogs/find-dialogs.ts +++ b/packages/core/src/highlevel/methods/dialogs/find-dialogs.ts @@ -75,7 +75,7 @@ export async function findDialogs(client: ITelegramClient, peers: MaybeArray !it.isUnavailable).map(it => it.inputPeer) + peers = preview.chats.filter(it => !(it.type === 'chat' && it.isBanned)).map(it => it.inputPeer) } const res = await client.call({ diff --git a/packages/core/src/highlevel/methods/messages/send-comment.ts b/packages/core/src/highlevel/methods/messages/send-comment.ts index 74e792ba..0677d82a 100644 --- a/packages/core/src/highlevel/methods/messages/send-comment.ts +++ b/packages/core/src/highlevel/methods/messages/send-comment.ts @@ -23,7 +23,7 @@ export function commentText( message: Message, ...params: ParametersSkip2 ): ReturnType { - if (message.chat.chatType !== 'channel') { + if (message.chat.type !== 'chat' || message.chat.chatType !== 'channel') { return replyText(client, message, ...params) } @@ -52,7 +52,7 @@ export function commentMedia( message: Message, ...params: ParametersSkip2 ): ReturnType { - if (message.chat.chatType !== 'channel') { + if (message.chat.type !== 'chat' || message.chat.chatType !== 'channel') { return replyMedia(client, message, ...params) } @@ -81,7 +81,7 @@ export function commentMediaGroup( message: Message, ...params: ParametersSkip2 ): ReturnType { - if (message.chat.chatType !== 'channel') { + if (message.chat.type !== 'chat' || message.chat.chatType !== 'channel') { return replyMediaGroup(client, message, ...params) } diff --git a/packages/core/src/highlevel/methods/messages/send-text.test.ts b/packages/core/src/highlevel/methods/messages/send-text.test.ts index 501cc691..17d71ee7 100644 --- a/packages/core/src/highlevel/methods/messages/send-text.test.ts +++ b/packages/core/src/highlevel/methods/messages/send-text.test.ts @@ -1,9 +1,10 @@ +import type { Chat } from '../../types/index.js' import { createStub, StubTelegramClient } from '@mtcute/test' import Long from 'long' + import { describe, expect, it } from 'vitest' import { toggleChannelIdMark } from '../../../utils/peer-utils.js' - import { sendText } from './send-text.js' const stubUser = createStub('user', { @@ -52,7 +53,7 @@ describe('sendText', () => { expect(msg).toBeDefined() expect(msg.id).toEqual(123) - expect(msg.chat.chatType).toEqual('private') + expect(msg.chat.type).toEqual('user') expect(msg.chat.id).toEqual(stubUser.id) expect(msg.text).toEqual('test') }) @@ -96,7 +97,8 @@ describe('sendText', () => { expect(msg).toBeDefined() expect(msg.id).toEqual(123) - expect(msg.chat.chatType).toEqual('supergroup') + expect(msg.chat.type).toEqual('chat') + expect((msg.chat as Chat).chatType).toEqual('supergroup') expect(msg.chat.id).toEqual(markedChannelId) expect(msg.text).toEqual('test') }) @@ -124,7 +126,7 @@ describe('sendText', () => { expect(msg).toBeDefined() expect(msg.id).toEqual(123) - expect(msg.chat.chatType).toEqual('private') + expect(msg.chat.type).toEqual('user') expect(msg.chat.id).toEqual(stubUser.id) expect(msg.text).toEqual('test') }) diff --git a/packages/core/src/highlevel/types/messages/dialog.ts b/packages/core/src/highlevel/types/messages/dialog.ts index f852c718..1867cc55 100644 --- a/packages/core/src/highlevel/types/messages/dialog.ts +++ b/packages/core/src/highlevel/types/messages/dialog.ts @@ -1,12 +1,13 @@ import type { tl } from '@mtcute/tl' +import type { Peer } from '../peers/peer.js' import { getMarkedPeerId } from '../../../utils/peer-utils.js' import { assertTypeIsNot, hasValueAtKey } from '../../../utils/type-assertions.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' -import { Chat } from '../peers/chat.js' -import { PeersIndex } from '../peers/peers-index.js' +import { parsePeer } from '../peers/peer.js' +import { PeersIndex } from '../peers/peers-index.js' import { DraftMessage } from './draft-message.js' import { Message } from './message.js' @@ -71,7 +72,7 @@ export class Dialog { index[getMarkedPeerId(peer)] = true }) - return dialogs.filter(i => index[i.chat.id]) + return dialogs.filter(i => index[i.peer.id]) } return dialogs.filter(i => i.isPinned) @@ -105,11 +106,11 @@ export class Dialog { if (folder._ === 'dialogFilterChatlist') { return (dialog) => { - const chatId = dialog.chat.id + const peerId = dialog.peer.id - if (excludePinned && pinned[chatId]) return false + if (excludePinned && pinned[peerId]) return false - return include[chatId] || pinned[chatId] + return include[peerId] || pinned[peerId] } } @@ -118,14 +119,13 @@ export class Dialog { }) return (dialog) => { - const chat = dialog.chat - const chatId = dialog.chat.id - const chatType = dialog.chat.chatType + const peer = dialog.peer + const peerId = dialog.peer.id // manual exclusion/inclusion and pins - if (include[chatId]) return true + if (include[peerId]) return true - if (exclude[chatId] || (excludePinned && pinned[chatId])) { + if (exclude[peerId] || (excludePinned && pinned[peerId])) { return false } @@ -137,17 +137,17 @@ export class Dialog { if (folder.excludeArchived && dialog.isArchived) return false // inclusions based on chat type - if (folder.contacts && chatType === 'private' && chat.isContact) { + if (folder.contacts && peer.type === 'user' && peer.isContact) { return true } - if (folder.nonContacts && chatType === 'private' && !chat.isContact) { + if (folder.nonContacts && peer.type === 'user' && !peer.isContact) { return true } - if (folder.groups && (chatType === 'group' || chatType === 'supergroup')) { + if (folder.groups && peer.type === 'chat' && peer.isGroup) { return true } - if (folder.broadcasts && chatType === 'channel') return true - if (folder.bots && chatType === 'bot') return true + if (folder.broadcasts && peer.type === 'chat' && peer.chatType === 'channel') return true + if (folder.bots && peer.type === 'user' && peer.isBot) return true return false } @@ -193,17 +193,17 @@ export class Dialog { } /** - * Chat that this dialog represents + * Peer that this dialog represents */ - get chat(): Chat { - return Chat._parseFromPeer(this.raw.peer, this._peers) + get peer(): Peer { + return parsePeer(this.raw.peer, this._peers) } /** * The latest message sent in this chat (if any) */ get lastMessage(): Message | null { - const cid = this.chat.id + const cid = this.peer.id if (this._messages.has(cid)) { return new Message(this._messages.get(cid)!, this._peers) @@ -273,5 +273,5 @@ export class Dialog { } } -memoizeGetters(Dialog, ['chat', 'lastMessage', 'draftMessage']) +memoizeGetters(Dialog, ['peer', 'lastMessage', 'draftMessage']) makeInspectable(Dialog) diff --git a/packages/core/src/highlevel/types/messages/message-forward.ts b/packages/core/src/highlevel/types/messages/message-forward.ts index 592a5570..8b105994 100644 --- a/packages/core/src/highlevel/types/messages/message-forward.ts +++ b/packages/core/src/highlevel/types/messages/message-forward.ts @@ -1,11 +1,10 @@ import type { tl } from '@mtcute/tl' -import type { PeerSender } from '../peers/peer.js' +import type { Peer, PeerSender } from '../peers/peer.js' import type { PeersIndex } from '../peers/peers-index.js' import { MtTypeAssertionError } from '../../../types/errors.js' import { makeInspectable } from '../../utils/inspectable.js' import { memoizeGetters } from '../../utils/memoize.js' -import { Chat } from '../peers/chat.js' import { parsePeer } from '../peers/peer.js' /** @@ -49,10 +48,10 @@ export class MessageForwardInfo { * * `null` for other messages, you might want to use {@link sender} instead */ - fromChat(): Chat | null { + fromChat(): Peer | null { if (!this.raw.savedFromPeer) return null - return Chat._parseFromPeer(this.raw.savedFromPeer, this._peers) + return parsePeer(this.raw.savedFromPeer, this._peers) } /** diff --git a/packages/core/src/highlevel/types/messages/message.ts b/packages/core/src/highlevel/types/messages/message.ts index 42bd3389..b9e772f2 100644 --- a/packages/core/src/highlevel/types/messages/message.ts +++ b/packages/core/src/highlevel/types/messages/message.ts @@ -2,6 +2,7 @@ import type { tl } from '@mtcute/tl' import type { ReplyMarkup } from '../bots/keyboards/index.js' import type { TextWithEntities } from '../misc/index.js' +import type { Chat } from '../peers/chat.js' import type { Peer } from '../peers/peer.js' import type { PeersIndex } from '../peers/peers-index.js' import type { MessageAction } from './message-action.js' @@ -12,9 +13,8 @@ import { getMarkedPeerId, toggleChannelIdMark } from '../../../utils/peer-utils. import { assertTypeIsNot } from '../../../utils/type-assertions.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' -import { BotKeyboard } from '../bots/keyboards/index.js' -import { Chat } from '../peers/chat.js' +import { BotKeyboard } from '../bots/keyboards/index.js' import { parsePeer } from '../peers/peer.js' import { User } from '../peers/user.js' import { FactCheck } from './fact-check.js' @@ -174,7 +174,7 @@ export class Message { /** * Message sender. * - * Usually is a {@link User}, but can be a {@link Chat} + * Usually is a {@link User}, but can be a {@link Peer} * in case the message was sent by an anonymous admin, anonymous premium user, * or if the message is a forwarded channel post. * @@ -211,8 +211,8 @@ export class Message { /** * Conversation the message belongs to */ - get chat(): Chat { - return Chat._parseFromMessage(this.raw, this._peers) + get chat(): Peer { + return parsePeer(this.raw.peerId, this._peers) } /** @@ -252,7 +252,8 @@ export class Message { const fwd = this.raw.fwdFrom return Boolean( - this.chat.chatType === 'supergroup' + this.chat.type === 'chat' + && this.chat.chatType === 'supergroup' && fwd.savedFromMsgId && fwd.savedFromPeer?._ === 'peerChannel' && getMarkedPeerId(fwd.savedFromPeer) !== getMarkedPeerId(this.raw.peerId), @@ -509,7 +510,7 @@ export class Message { * @throws MtArgumentError In case the chat does not support message links */ get link(): string { - if (this.chat.chatType === 'supergroup' || this.chat.chatType === 'channel') { + if (this.chat.type === 'chat' && (this.chat.chatType === 'supergroup' || this.chat.chatType === 'channel')) { if (this.chat.username) { return `https://t.me/${this.chat.username}/${this.id}` } @@ -517,7 +518,7 @@ export class Message { return `https://t.me/c/${toggleChannelIdMark(this.chat.id)}/${this.id}` } - throw new MtArgumentError(`Cannot generate message link for ${this.chat.chatType}`) + throw new MtArgumentError(`Cannot generate message link for ${(this.chat as Chat).chatType ?? this.chat.type}`) } } diff --git a/packages/core/src/highlevel/types/messages/replied-message.ts b/packages/core/src/highlevel/types/messages/replied-message.ts index cc2bd2d1..29590a89 100644 --- a/packages/core/src/highlevel/types/messages/replied-message.ts +++ b/packages/core/src/highlevel/types/messages/replied-message.ts @@ -1,12 +1,12 @@ import type { tl } from '@mtcute/tl' -import type { PeerSender } from '../peers/peer.js' import type { PeersIndex } from '../peers/peers-index.js' import type { MessageMedia } from './message-media.js' import { MtTypeAssertionError } from '../../../types/errors.js' import { makeInspectable } from '../../utils/inspectable.js' import { memoizeGetters } from '../../utils/memoize.js' import { Chat } from '../peers/chat.js' +import { parsePeer, type Peer, type PeerSender } from '../peers/peer.js' import { User } from '../peers/user.js' import { MessageEntity } from './message-entity.js' @@ -114,14 +114,14 @@ export class RepliedMessageInfo { * * If `null`, the message was sent in the same chat. */ - get chat(): Chat | null { + get chat(): Peer | null { if (!this.raw.replyToPeerId || !this.raw.replyFrom) { // same chat or private. even if `replyToPeerId` is available, // without `replyFrom` it would contain the sender, not the chat return null } - return Chat._parseFromPeer(this.raw.replyToPeerId, this._peers) + return parsePeer(this.raw.replyToPeerId, this._peers) } /** diff --git a/packages/core/src/highlevel/types/peers/bot-info.ts b/packages/core/src/highlevel/types/peers/bot-info.ts new file mode 100644 index 00000000..ad5cb4ae --- /dev/null +++ b/packages/core/src/highlevel/types/peers/bot-info.ts @@ -0,0 +1,59 @@ +import type { tl } from '@mtcute/tl' +import type { Video } from '../media/video.js' +import { asNonNull } from '@fuman/utils' +import { makeInspectable } from '../../utils/inspectable.js' +import { memoizeGetters } from '../../utils/memoize.js' +import { parseDocument } from '../media/document-utils.js' +import { Photo } from '../media/photo.js' + +/** Information about a bot */ +export class BotInfo { + constructor(readonly raw: tl.RawBotInfo) {} + + /** Whether the bot has preview medias available */ + get hasPreviewMedia(): boolean { + return this.raw.hasPreviewMedias! + } + + /** ID of the bot */ + get id(): number { + return asNonNull(this.raw.userId) + } + + get description(): string { + return this.raw.description ?? '' + } + + get descriptionPhoto(): Photo | null { + if (!this.raw.descriptionPhoto || this.raw.descriptionPhoto._ !== 'photo') return null + + return new Photo(this.raw.descriptionPhoto) + } + + get descriptionVideo(): Video | null { + if (!this.raw.descriptionDocument || this.raw.descriptionDocument._ !== 'document') return null + + const parsed = parseDocument(this.raw.descriptionDocument) + if (parsed.type !== 'video') return null + + return parsed + } + + /** List of the bot's registered commands */ + get commands(): tl.TypeBotCommand[] { + return this.raw.commands ?? [] + } + + /** Action to be performed when the bot's menu button is pressed */ + get menuButton(): tl.TypeBotMenuButton | null { + return this.raw.menuButton ?? null + } + + /** URL of the bot's privacy policy */ + get privacyPolicyUrl(): string | null { + return this.raw.privacyPolicyUrl ?? null + } +} + +makeInspectable(BotInfo) +memoizeGetters(BotInfo, ['descriptionPhoto', 'descriptionVideo']) diff --git a/packages/core/src/highlevel/types/peers/chat.ts b/packages/core/src/highlevel/types/peers/chat.ts index a3badd2f..17d32b37 100644 --- a/packages/core/src/highlevel/types/peers/chat.ts +++ b/packages/core/src/highlevel/types/peers/chat.ts @@ -1,6 +1,5 @@ import type { tl } from '@mtcute/tl' -import type { PeersIndex } from './peers-index.js' import { MtArgumentError, MtTypeAssertionError } from '../../../types/errors.js' import { getMarkedPeerId } from '../../../utils/peer-utils.js' import { makeInspectable } from '../../utils/index.js' @@ -11,18 +10,15 @@ import { EmojiStatus } from '../reactions/emoji-status.js' import { ChatColors } from './chat-colors.js' import { ChatPermissions } from './chat-permissions.js' import { ChatPhoto } from './chat-photo.js' -import { User } from './user.js' /** * Chat type. Can be: - * - `private`: PM with other users or yourself (Saved Messages) - * - `bot`: PM with a bot - * - `group`: Legacy group + * - `group`: Legacy/basic group * - `supergroup`: Supergroup * - `channel`: Broadcast channel * - `gigagroup`: Gigagroup aka Broadcast group */ -export type ChatType = 'private' | 'bot' | 'group' | 'supergroup' | 'channel' | 'gigagroup' +export type ChatType = 'group' | 'supergroup' | 'channel' | 'gigagroup' /** * A chat. @@ -33,13 +29,10 @@ export class Chat { /** * Raw peer object that this {@link Chat} represents. */ - readonly peer: tl.RawUser | tl.RawChat | tl.RawChannel | tl.RawChatForbidden | tl.RawChannelForbidden - - constructor(peer: tl.TypeUser | tl.TypeChat) { - if (!peer) throw new MtArgumentError('peer is not available') + readonly raw: tl.RawChat | tl.RawChannel | tl.RawChatForbidden | tl.RawChannelForbidden + constructor(peer: tl.TypeChat) { switch (peer._) { - case 'user': case 'chat': case 'channel': case 'chatForbidden': @@ -49,7 +42,7 @@ export class Chat { throw new MtTypeAssertionError('peer', 'user | chat | channel', peer._) } - this.peer = peer + this.raw = peer } /** Marked ID of this chat */ @@ -73,7 +66,7 @@ export class Chat { * This currently only ever happens for non-bot users, so if you are building * a normal bot, you can safely ignore this field. * - * To fetch the "complete" user information, use one of these methods: + * To fetch the "complete" chat information, use one of these methods: * - {@link TelegramClient.getChat} * - {@link TelegramClient.getFullChat}. * @@ -81,7 +74,7 @@ export class Chat { */ get isMin(): boolean { // avoid additional runtime checks - return Boolean((this.peer as { min?: boolean }).min) + return Boolean((this.raw as { min?: boolean }).min) } /** @@ -96,84 +89,59 @@ export class Chat { * so prefer using it whenever you need an input peer. */ get inputPeer(): tl.TypeInputPeer { - switch (this.peer._) { - case 'user': - if (this.peer.min) { - return { - _: 'mtcute.dummyInputPeerMinUser', - userId: this.peer.id, - } - } - - if (!this.peer.accessHash) { - throw new MtArgumentError("Peer's access hash is not available!") - } - - return { - _: 'inputPeerUser', - userId: this.peer.id, - accessHash: this.peer.accessHash, - } + switch (this.raw._) { case 'chat': case 'chatForbidden': return { _: 'inputPeerChat', - chatId: this.peer.id, + chatId: this.raw.id, } case 'channel': case 'channelForbidden': - if ((this.peer as tl.RawChannel).min) { + if ((this.raw as tl.RawChannel).min) { return { _: 'mtcute.dummyInputPeerMinChannel', - channelId: this.peer.id, + channelId: this.raw.id, } } - if (!this.peer.accessHash) { + if (!this.raw.accessHash) { throw new MtArgumentError("Peer's access hash is not available!") } return { _: 'inputPeerChannel', - channelId: this.peer.id, - accessHash: this.peer.accessHash, + channelId: this.raw.id, + accessHash: this.raw.accessHash, } } } /** Type of chat */ get chatType(): ChatType { - switch (this.peer._) { - case 'user': - return this.peer.bot ? 'bot' : 'private' + switch (this.raw._) { case 'chat': case 'chatForbidden': return 'group' case 'channel': case 'channelForbidden': - if (this.peer._ === 'channel' && this.peer.gigagroup) { + if (this.raw._ === 'channel' && this.raw.gigagroup) { return 'gigagroup' - } else if (this.peer.broadcast) { + } else if (this.raw.broadcast) { return 'channel' + } else if (this.raw.megagroup) { + return 'supergroup' } - return 'supergroup' + throw new MtArgumentError('Unknown chat type') } } /** - * Whether this chat is a group chat - * (i.e. not a channel and not PM) + * Whether this chat is a group chat (i.e. not a channel) */ get isGroup(): boolean { - switch (this.chatType) { - case 'group': - case 'supergroup': - case 'gigagroup': - return true - } - - return false + return this.chatType !== 'channel' } /** @@ -181,7 +149,7 @@ export class Chat { * Supergroups, channels and groups only */ get isVerified(): boolean { - return 'verified' in this.peer ? this.peer.verified! : false + return 'verified' in this.raw ? this.raw.verified! : false } /** @@ -189,7 +157,7 @@ export class Chat { * See {@link restrictions} for details */ get isRestricted(): boolean { - return 'restricted' in this.peer ? this.peer.restricted! : false + return 'restricted' in this.raw ? this.raw.restricted! : false } /** @@ -197,7 +165,7 @@ export class Chat { * Supergroups, channels and groups only */ get isCreator(): boolean { - return 'creator' in this.peer ? this.peer.creator! : false + return 'creator' in this.raw ? this.raw.creator! : false } /** @@ -205,42 +173,62 @@ export class Chat { * Supergroups, channels and groups only. */ get isAdmin(): boolean { - return 'adminRights' in this.peer && Boolean(this.peer.adminRights) + return 'adminRights' in this.raw && Boolean(this.raw.adminRights) } /** Whether this chat has been flagged for scam */ get isScam(): boolean { - return 'scam' in this.peer ? this.peer.scam! : false + return 'scam' in this.raw ? this.raw.scam! : false } /** Whether this chat has been flagged for impersonation */ get isFake(): boolean { - return 'fake' in this.peer ? this.peer.fake! : false - } - - /** Whether this chat is part of the Telegram support team. Users and bots only */ - get isSupport(): boolean { - return this.peer._ === 'user' && this.peer.support! - } - - /** Whether this chat is chat with yourself (i.e. Saved Messages) */ - get isSelf(): boolean { - return this.peer._ === 'user' && this.peer.self! - } - - /** Whether this peer is your contact */ - get isContact(): boolean { - return this.peer._ === 'user' && this.peer.contact! + return 'fake' in this.raw ? this.raw.fake! : false } /** Whether this peer is a forum supergroup */ get isForum(): boolean { - return this.peer._ === 'channel' && this.peer.forum! + return this.raw._ === 'channel' && this.raw.forum! } - /** Whether the chat is not available (e.g. because the user was banned from there) */ - get isUnavailable(): boolean { - return this.peer._ === 'chatForbidden' || this.peer._ === 'channelForbidden' + /** + * Whether the chat is not available (e.g. because the user was banned from there). + * + * **Note**: This method checks if the underlying peer is [`chatForbidden`](https://core.telegram.org/constructor/chatForbidden) + * or [`channelForbidden`](https://core.telegram.org/constructor/channelForbidden). + * In some cases this field might be `false` *even if* the user is not a member of the chat, + * and calling `.getChat()` will throw `CHANNEL_PRIVATE`. + * In particular, this seems to be the case for `.forward.sender` of {@link Message} objects. + * + * Consider also checking for {@link isLikelyUnavailable}. + */ + get isBanned(): boolean { + return this.raw._ === 'chatForbidden' || this.raw._ === 'channelForbidden' + } + + /** + * Whether the chat is likely not available (e.g. because the user was banned from there), + * or the channel is private and the user is not a member of it. + */ + get isLikelyUnavailable(): boolean { + switch (this.raw._) { + case 'chatForbidden': + case 'channelForbidden': + return true + case 'chat': + return this.raw.left! || this.raw.deactivated! + case 'channel': + // left = true, meaning we are not a member of it + // no usernames => likely private + // for megagroups it might be linked to a public channel + return this.raw.left! + && this.raw.username === undefined + && this.raw.usernames === undefined + && ( + this.raw.broadcast! + || (this.raw.megagroup! && !this.raw.hasLink!) + ) + } } /** @@ -249,108 +237,90 @@ export class Chat { * For users, this is always `true`. */ get isMember(): boolean { - switch (this.peer._) { - case 'user': - return true + switch (this.raw._) { case 'channel': case 'chat': - return !this.peer.left + return !this.raw.left default: return false } } + /** Whether this chat has a call/livestrean active */ + get hasCall(): boolean { + return 'callActive' in this.raw ? this.raw.callActive! : false + } + + /** Whether this chat has a call/livestream, and there's at least one member in it */ + get hasCallMembers(): boolean { + return 'callNotEmpty' in this.raw ? this.raw.callNotEmpty! : false + } + /** Whether you have hidden (arhived) this chat's stories */ get storiesHidden(): boolean { - return 'storiesHidden' in this.peer ? this.peer.storiesHidden! : false + return 'storiesHidden' in this.raw ? this.raw.storiesHidden! : false } get storiesUnavailable(): boolean { - return 'storiesUnavailable' in this.peer ? this.peer.storiesUnavailable! : false + return 'storiesUnavailable' in this.raw ? this.raw.storiesUnavailable! : false } /** Whether this group is a channel/supergroup with join requests enabled */ get hasJoinRequests(): boolean { - return this.peer._ === 'channel' && this.peer.joinRequest! + return this.raw._ === 'channel' && this.raw.joinRequest! } /** Whether this group is a supergroup with join-to-send rule enabled */ get hasJoinToSend(): boolean { - return this.peer._ === 'channel' && this.peer.joinToSend! + return this.raw._ === 'channel' && this.raw.joinToSend! } /** Whether this group has content protection (i.e. disabled forwards) */ get hasContentProtection(): boolean { - return (this.peer._ === 'channel' || this.peer._ === 'chat') && this.peer.noforwards! + return (this.raw._ === 'channel' || this.raw._ === 'chat') && this.raw.noforwards! } /** Whether this channel has profile signatures (i.e. "Super Channel") */ get hasProfileSignatures(): boolean { - return this.peer._ === 'channel' && this.peer.signatureProfiles! + return this.raw._ === 'channel' && this.raw.signatureProfiles! } - /** - * Title, for supergroups, channels and groups - * (`null` for private chats) - */ - get title(): string | null { - return this.peer._ !== 'user' ? this.peer.title ?? null : null + /** Whether this channel has author signatures enabled under posts */ + get hasSignatures(): boolean { + return this.raw._ === 'channel' && this.raw.signatures! } - /** - * Username, for private chats, bots, supergroups and channels (if available) - */ + /** Chat title */ + get title(): string { + return this.raw.title + } + + /** Chat username (if available) */ get username(): string | null { - if (!('username' in this.peer)) return null + if (!('username' in this.raw)) return null - return this.peer.username ?? this.peer.usernames?.[0].username ?? null + return this.raw.username ?? this.raw.usernames?.[0].username ?? null } /** - * Usernames (inclufing collectibles), for private chats, bots, supergroups and channels if available + * Usernames (including collectibles), for private chats, bots, supergroups and channels if available */ get usernames(): ReadonlyArray | null { - if (!('usernames' in this.peer)) return null + if (!('usernames' in this.raw)) return null return ( - this.peer.usernames - ?? (this.peer.username ? [{ _: 'username', username: this.peer.username, active: true }] : null) + this.raw.usernames + ?? (this.raw.username ? [{ _: 'username', username: this.raw.username, active: true }] : null) ) } - /** - * First name of the other party in a private chat, - * for private chats and bots - * (`null` for supergroups and channels) - */ - get firstName(): string | null { - return this.peer._ === 'user' ? this.peer.firstName ?? null : null - } - - /** - * Last name of the other party in a private chat, for private chats - * (`null` for supergroups and channels) - */ - get lastName(): string | null { - return this.peer._ === 'user' ? this.peer.lastName ?? null : null - } - /** * Get the display name of the chat. * - * Title for groups and channels, - * name (and last name if available) for users + * Basically an alias to {@link title}, exists for consistency with {@link User}. */ get displayName(): string { - if (this.peer._ === 'user') { - if (this.peer.lastName) { - return `${this.peer.firstName} ${this.peer.lastName}` - } - - return this.peer.firstName ?? 'Deleted Account' - } - - return this.peer.title + return this.raw.title } /** @@ -361,32 +331,13 @@ export class Chat { */ get photo(): ChatPhoto | null { if ( - !('photo' in this.peer) - || !this.peer.photo - || (this.peer.photo._ !== 'userProfilePhoto' && this.peer.photo._ !== 'chatPhoto') + !('photo' in this.raw) + || this.raw.photo?._ !== 'chatPhoto' ) { return null } - return new ChatPhoto(this.inputPeer, this.peer.photo) - } - - /** - * User's or bot's assigned DC (data center). - * Available only in case the user has set a public profile photo. - * - * **Note**: this information is approximate; it is based on where - * Telegram stores the current chat photo. It is accurate only in case - * the owner has set the chat photo, otherwise it will be the DC assigned - * to the administrator who set the current profile photo. - */ - get dcId(): number | null { - if (!('photo' in this.peer)) return null - - return ( - (this.peer.photo as Exclude) - ?.dcId ?? null - ) + return new ChatPhoto(this.inputPeer, this.raw.photo) } /** @@ -394,37 +345,36 @@ export class Chat { * This field is available only in case {@link isRestricted} is `true` */ get restrictions(): ReadonlyArray | null { - return 'restrictionReason' in this.peer ? this.peer.restrictionReason ?? null : null + return 'restrictionReason' in this.raw ? this.raw.restrictionReason ?? null : null } /** * Current user's permissions, for supergroups. */ get permissions(): ChatPermissions | null { - if (!('bannedRights' in this.peer && this.peer.bannedRights)) { + if (!('bannedRights' in this.raw && this.raw.bannedRights)) { return null } - return new ChatPermissions(this.peer.bannedRights) + return new ChatPermissions(this.raw.bannedRights) } /** * Default chat member permissions, for groups and supergroups. */ get defaultPermissions(): ChatPermissions | null { - if (!('defaultBannedRights' in this.peer) || !this.peer.defaultBannedRights) { + if (!('defaultBannedRights' in this.raw) || !this.raw.defaultBannedRights) { return null } - return new ChatPermissions(this.peer.defaultBannedRights) + return new ChatPermissions(this.raw.defaultBannedRights) } /** - * Admin rights of the current user in this chat. - * `null` for PMs and non-administered chats + * Admin rights of the current user in this chat, if any.` */ get adminRights(): tl.RawChatAdminRights | null { - return 'adminRights' in this.peer ? this.peer.adminRights ?? null : null + return 'adminRights' in this.raw ? this.raw.adminRights ?? null : null } /** @@ -437,13 +387,7 @@ export class Chat { * Maximum ID of stories this chat has (or 0 if none) */ get storiesMaxId(): number { - switch (this.peer._) { - case 'channel': - case 'user': - return this.peer.storiesMaxId ?? 0 - } - - return 0 + return this.raw._ === 'channel' ? this.raw.storiesMaxId ?? 0 : 0 } /** @@ -452,47 +396,30 @@ export class Chat { * as well as to render the chat title */ get color(): ChatColors { - const color = this.peer._ === 'user' || this.peer._ === 'channel' ? this.peer.color : undefined - - return new ChatColors(this.peer.id, color) + return new ChatColors(this.raw.id, this.raw._ === 'channel' ? this.raw.color : undefined) } /** * Chat's emoji status, if any. */ get emojiStatus(): EmojiStatus | null { - if (this.peer._ !== 'user' && this.peer._ !== 'channel') return null - if (!this.peer.emojiStatus || this.peer.emojiStatus._ === 'emojiStatusEmpty') return null + if (this.raw._ !== 'channel') return null + if (!this.raw.emojiStatus || this.raw.emojiStatus._ === 'emojiStatusEmpty') return null - return new EmojiStatus(this.peer.emojiStatus) + return new EmojiStatus(this.raw.emojiStatus) } /** * Color that should be used when rendering the header of * the user's profile - * - * If `null`, a generic header should be used instead */ get profileColors(): ChatColors { - const color = this.peer._ === 'user' || this.peer._ === 'channel' ? this.peer.profileColor : undefined - - return new ChatColors(this.peer.id, color) + return new ChatColors(this.raw.id, this.raw._ === 'channel' ? this.raw.profileColor : undefined) } /** Boosts level this chat has (0 if none or is not a channel) */ get boostsLevel(): number { - return this.peer._ === 'channel' ? this.peer.level ?? 0 : 0 - } - - /** - * Get a {@link User} from this chat. - * - * Returns `null` if this is not a chat with user - */ - get user(): User | null { - if (this.peer._ !== 'user') return null - - return new User(this.peer) + return this.raw._ === 'channel' ? this.raw.level ?? 0 : 0 } /** @@ -500,26 +427,54 @@ export class Chat { * this field will contain the date when the subscription will expire. */ get subscriptionUntilDate(): Date | null { - if (this.peer._ !== 'channel' || !this.peer.subscriptionUntilDate) return null + if (this.raw._ !== 'channel' || !this.raw.subscriptionUntilDate) return null - return new Date(this.peer.subscriptionUntilDate * 1000) + return new Date(this.raw.subscriptionUntilDate * 1000) } - /** @internal */ - static _parseFromMessage(message: tl.RawMessage | tl.RawMessageService, peers: PeersIndex): Chat { - return Chat._parseFromPeer(message.peerId, peers) + /** Date when the current user joined this chat (if available) */ + get joinDate(): Date | null { + return this.isMember && ('date' in this.raw) ? new Date(this.raw.date * 1000) : null } - /** @internal */ - static _parseFromPeer(peer: tl.TypePeer, peers: PeersIndex): Chat { - switch (peer._) { - case 'peerUser': - return new Chat(peers.user(peer.userId)) - case 'peerChat': - return new Chat(peers.chat(peer.chatId)) - } + /** Date when the chat was created (if available) */ + get creationDate(): Date | null { + return !this.isMember && ('date' in this.raw) ? new Date(this.raw.date * 1000) : null + } - return new Chat(peers.chat(peer.channelId)) + /** + * Date when the current user will be unbanned (if available) + * + * Returns `null` if the user is not banned, or if the ban is permanent + */ + get bannedUntilDate(): Date | null { + if (this.raw._ !== 'channelForbidden' || !this.raw.untilDate) return null + + return new Date(this.raw.untilDate * 1000) + } + + /** Number of members in this chat (if available) */ + get membersCount(): number | null { + return 'participantsCount' in this.raw ? this.raw.participantsCount ?? null : null + } + + /** + * If this chat is a basic group that has been migrated to a supergroup, + * this field will contain the input peer of that supergroup. + */ + get migratedTo(): tl.TypeInputChannel | null { + return this.raw._ === 'chat' ? this.raw.migratedTo ?? null : null + } + + /** + * If this chat is a basic group that has been migrated to a supergroup, + * this field will contain the marked ID of that supergroup. + */ + get migratedToId(): number | null { + if (this.raw._ !== 'chat') return null + if (!this.raw.migratedTo) return null + + return getMarkedPeerId(this.raw.migratedTo) } /** @@ -547,8 +502,6 @@ export class Chat { * ``` */ mention(text?: string | null): string | MessageEntity { - if (this.user) return this.user.mention(text) - if (text === undefined && this.username) { return `@${this.username}` } @@ -575,7 +528,6 @@ memoizeGetters(Chat, [ 'photo', 'permissions', 'defaultPermissions', - 'user', 'color', ]) -makeInspectable(Chat, [], ['user']) +makeInspectable(Chat, []) diff --git a/packages/core/src/highlevel/types/peers/chatlist-preview.ts b/packages/core/src/highlevel/types/peers/chatlist-preview.ts index d45b3ef6..fc58c12b 100644 --- a/packages/core/src/highlevel/types/peers/chatlist-preview.ts +++ b/packages/core/src/highlevel/types/peers/chatlist-preview.ts @@ -1,9 +1,10 @@ import type { tl } from '@mtcute/tl' +import type { Peer } from './peer.js' import { makeInspectable } from '../../utils/inspectable.js' -import { memoizeGetters } from '../../utils/memoize.js' -import { Chat } from './chat.js' +import { memoizeGetters } from '../../utils/memoize.js' +import { parsePeer } from './peer.js' import { PeersIndex } from './peers-index.js' /** @@ -36,7 +37,7 @@ export class ChatlistPreview { } /** List of all chats contained in the chatlist */ - get chats(): Chat[] { + get chats(): Peer[] { let peers if (this.raw._ === 'chatlists.chatlistInvite') { @@ -45,7 +46,7 @@ export class ChatlistPreview { peers = [...this.raw.alreadyPeers, ...this.raw.missingPeers] } - return peers.map(peer => Chat._parseFromPeer(peer, this.peers)) + return peers.map(peer => parsePeer(peer, this.peers)) } } diff --git a/packages/core/src/highlevel/types/peers/full-chat.ts b/packages/core/src/highlevel/types/peers/full-chat.ts index a84a6485..52a7291c 100644 --- a/packages/core/src/highlevel/types/peers/full-chat.ts +++ b/packages/core/src/highlevel/types/peers/full-chat.ts @@ -1,70 +1,122 @@ import type { tl } from '@mtcute/tl' -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 { PeerStories } from '../stories/peer-stories.js' +import { BotInfo } from './bot-info.js' import { ChatInviteLink } from './chat-invite-link.js' import { ChatLocation } from './chat-location.js' import { Chat } from './chat.js' import { PeersIndex } from './peers-index.js' +import { User } from './user.js' /** - * Complete information about a particular chat. + * Full information about a particular chat. */ export class FullChat extends Chat { - constructor( - peer: tl.TypeUser | tl.TypeChat, - readonly fullPeer: tl.TypeUserFull | tl.TypeChatFull, - ) { - super(peer) + readonly peers: PeersIndex + readonly full: tl.TypeChatFull + + constructor(obj: tl.messages.RawChatFull) { + const peers = PeersIndex.from(obj) + super(peers.chat(obj.fullChat.id)) + + this.peers = peers + this.full = obj.fullChat } - /** @internal */ - static _parse(full: tl.messages.RawChatFull | tl.users.TypeUserFull): FullChat { - const peers = PeersIndex.from(full) - - if (full._ === 'users.userFull') { - const { fullUser } = full - const user = peers.user(full.fullUser.id) - - if (!user || user._ === 'userEmpty') { - throw new MtTypeAssertionError('Chat._parseFull', 'user', user?._ ?? 'undefined') - } - - const ret = new FullChat(user, fullUser) - - if (fullUser.personalChannelId) { - ret._linkedChat = new Chat(peers.chat(fullUser.personalChannelId)) - } - - return ret - } - - const { fullChat } = full - - const ret = new FullChat(peers.chat(fullChat.id), fullChat) - - if (fullChat._ === 'channelFull' && fullChat.linkedChatId) { - ret._linkedChat = new Chat(peers.chat(fullChat.linkedChatId)) - } - - return ret - } - - /** - * Whether this chat (user) has restricted sending them voice/video messages. - */ - get hasBlockedVoices(): boolean { - return this.fullPeer?._ === 'userFull' && this.fullPeer.voiceMessagesForbidden! + /** Whether this chat is archived */ + get isArchived(): boolean { + return this.full._ === 'channelFull' && this.full.folderId === 1 } /** Whether paid reactions are enabled for this channel */ get hasPaidReactions(): boolean { - return this.fullPeer?._ === 'channelFull' && this.fullPeer.paidReactionsAvailable! + return this.full?._ === 'channelFull' && this.full.paidReactionsAvailable! + } + + /** Whether this channel has hidden participants */ + get hasHiddenParticipants(): boolean { + return this.full._ === 'channelFull' && !this.full.participantsHidden! + } + + /** Whether the current user can change the chat's username */ + get canSetUsername(): boolean { + return this.full.canSetUsername! + } + + /** Whether the current user can change the chat's sticker set */ + get canSetStickers(): boolean { + return this.full._ === 'channelFull' && this.full.canSetStickers! + } + + /** Whether the history before we joined the chat is hidden */ + get hasHiddenHistory(): boolean { + return this.full._ === 'channelFull' && this.full.hiddenPrehistory! + } + + /** Whether there are scheduled messages in this chat */ + get hasScheduledMessages(): boolean { + return this.full._ === 'channelFull' && this.full.hasScheduled! + } + + /** Whether the current user can view the chat's statistics */ + get canViewStats(): boolean { + return this.full._ === 'channelFull' && this.full.canViewStats! + } + + /** Whether the current user can view the chat's participants list */ + get canViewParticipants(): boolean { + return this.full._ === 'channelFull' && this.full.canViewParticipants! + } + + /** Whether the current user can delete the chat */ + get canDeleteChat(): boolean { + switch (this.full._) { + case 'channelFull': + return this.full.canDeleteChannel! + case 'chatFull': + return this.raw._ === 'chat' && this.raw.creator! + } + } + + /** + * Whether the current user has blocked any anonumous admin of the supergroup. + * If set, you won't receive mentions from them, nor their replies in `@replies` + */ + get isBlocked(): boolean { + return this.full._ === 'channelFull' && this.full.blocked! + } + + /** Whether the native antispam is enabled for this channel */ + get hasNativeAntispam(): boolean { + return this.full._ === 'channelFull' && this.full.antispam! + } + + /** Whether the real-time translations popup should not be shown for this channel */ + get hasTranslationsDisabled(): boolean { + return this.full._ === 'channelFull' && this.full.translationsDisabled! + } + + /** Whether this chat has pinned stories */ + get hasPinnedStories(): boolean { + return this.full._ === 'channelFull' && this.full.storiesPinnedAvailable! + } + + /** + * Whether ads on this channels were disabled + * (this flag is only visible to channel owner) + */ + get hasAdsDisabled(): boolean { + return this.full._ === 'channelFull' && this.full.restrictedSponsored! + } + + /** Whether sending paid media is available in this channel */ + get isPaidMediaAvailable(): boolean { + return this.full._ === 'channelFull' && this.full.paidMediaAllowed! } /** @@ -78,81 +130,32 @@ export class FullChat extends Chat { * that the user may have set */ get fullPhoto(): Photo | null { - if (!this.fullPeer) return null + if (!this.full) return null - let photo: tl.TypePhoto | undefined - - switch (this.fullPeer._) { - case 'userFull': - photo = this.fullPeer.personalPhoto ?? this.fullPeer.profilePhoto ?? this.fullPeer.fallbackPhoto - break - case 'chatFull': - case 'channelFull': - photo = this.fullPeer.chatPhoto - } + const photo = this.full.chatPhoto if (photo?._ !== 'photo') return null return new Photo(photo) } - /** - * A custom photo (set by the current user) that should be displayed - * instead of the actual chat photo. - * - * Currently only available for users. - */ - get personalPhoto(): Photo | null { - if (!this.fullPeer || this.fullPeer._ !== 'userFull') return null - if (this.fullPeer.personalPhoto?._ !== 'photo') return null - - return new Photo(this.fullPeer.personalPhoto) - } - - /** - * Actual profile photo of the user, bypassing the custom one. - */ - get realPhoto(): Photo | null { - if (!this.fullPeer) return null - if (this.fullPeer._ !== 'userFull') return this.fullPhoto - if (this.fullPeer.personalPhoto?._ !== 'photo') return null - - return new Photo(this.fullPeer.personalPhoto) - } - - /** - * A photo that the user has set to be shown - * in case their actual profile photo is not available - * due to privacy settings. - * - * Currently only available for users. - */ - get publicPhoto(): Photo | null { - if (!this.fullPeer || this.fullPeer._ !== 'userFull') return null - if (this.fullPeer.fallbackPhoto?._ !== 'photo') return null - - return new Photo(this.fullPeer.fallbackPhoto) - } - /** * Bio of the other party in a private chat, or description of a * group, supergroup or channel. */ get bio(): string { - return this.fullPeer.about ?? '' + return this.full.about } /** - * Chat's primary invite link, for groups, supergroups and channels. + * Chat's primary invite link, if available */ get inviteLink(): ChatInviteLink | null { - if (this.fullPeer && this.fullPeer._ !== 'userFull') { - switch (this.fullPeer.exportedInvite?._) { - case 'chatInvitePublicJoinRequests': - return null - case 'chatInviteExported': - return new ChatInviteLink(this.fullPeer.exportedInvite) - } + switch (this.full.exportedInvite?._) { + case 'chatInvitePublicJoinRequests': + return null + case 'chatInviteExported': + return new ChatInviteLink(this.full.exportedInvite) } return null @@ -162,132 +165,208 @@ export class FullChat extends Chat { * For supergroups, information about the group sticker set. */ get stickerSet(): StickerSet | null { - if (this.fullPeer?._ !== 'channelFull' || !this.fullPeer.stickerset) return null + if (this.full?._ !== 'channelFull' || !this.full.stickerset) return null - return new StickerSet(this.fullPeer.stickerset) + return new StickerSet(this.full.stickerset) } /** * For supergroups, information about the group emoji set. */ get emojiSet(): StickerSet | null { - if (this.fullPeer?._ !== 'channelFull' || !this.fullPeer.emojiset) return null + if (this.full?._ !== 'channelFull' || !this.full.emojiset) return null - return new StickerSet(this.fullPeer.emojiset) + return new StickerSet(this.full.emojiset) } /** * Whether the group sticker set can be changed by you. */ get canSetStickerSet(): boolean | null { - return this.fullPeer && this.fullPeer._ === 'channelFull' ? this.fullPeer.canSetStickers ?? null : null + return this.full && this.full._ === 'channelFull' ? this.full.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 + if (!this.full || this.full._ !== 'channelFull') return 0 - return this.fullPeer?.boostsApplied ?? 0 + return this.full?.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 + if (!this.full || this.full._ !== 'channelFull') return 0 - return this.fullPeer?.boostsUnrestrict ?? 0 - } - - /** Number of star gifts the user has chosen to display on their profile */ - get starGiftsCount(): number { - if (this.fullPeer._ !== 'userFull') return 0 - - return this.fullPeer?.stargiftsCount ?? 0 + return this.full?.boostsUnrestrict ?? 0 } /** Whether the current user can view Telegram Stars revenue for this chat */ get canViewStarsRevenue(): boolean { - return this.fullPeer._ === 'channelFull' && this.fullPeer.canViewStarsRevenue! + return this.full._ === 'channelFull' && this.full.canViewStarsRevenue! } /** Whether the current user can view ad revenue for this chat */ get canViewAdRevenue(): boolean { - return this.fullPeer._ === 'channelFull' && this.fullPeer.canViewRevenue! + return this.full._ === 'channelFull' && this.full.canViewRevenue! } - /** If this chat is a bot, whether the current user can manage its emoji status */ - get canManageBotEmojiStatus(): boolean { - return this.fullPeer._ === 'userFull' && this.fullPeer.botCanManageEmojiStatus! + /** Number of admins in the chat (0 if not available) */ + get adminsCount(): number { + return this.full._ === 'channelFull' ? this.full.adminsCount ?? 0 : 0 + } + + /** Number of users kicked from the chat (if available) */ + get kickedCount(): number | null { + return this.full._ === 'channelFull' ? this.full.kickedCount ?? null : null + } + + /** Number of users kicked from the chat (if available) */ + get bannedCount(): number | null { + return this.full._ === 'channelFull' ? this.full.bannedCount ?? null : null + } + + /** Number of members of the chat that are currently online */ + get onlineCount(): number { + return this.full._ === 'channelFull' ? this.full.onlineCount ?? 0 : 0 + } + + /** ID of the last read incoming message in the chat */ + get readInboxMaxId(): number { + return this.full._ === 'channelFull' ? this.full.readInboxMaxId : 0 + } + + /** ID of the last read outgoing message in the chat */ + get readOutboxMaxId(): number { + return this.full._ === 'channelFull' ? this.full.readOutboxMaxId : 0 + } + + /** Number of unread messages in the chat */ + get unreadCount(): number { + return this.full._ === 'channelFull' ? this.full.unreadCount : 0 + } + + /** Notification settings for this chat */ + get notificationsSettings(): tl.TypePeerNotifySettings | null { + return this.full._ === 'channelFull' ? this.full.notifySettings ?? null : null + } + + /** Information about bots in this chat */ + get botInfo(): BotInfo[] { + return this.full._ === 'channelFull' ? this.full.botInfo.map(it => new BotInfo(it)) : [] } /** - * Chat members count, for groups, supergroups and channels only. + * For supergroups, ID of the basic group from which this supergroup was upgraded, + * and the identifier of the last message from the original group. */ - get membersCount(): number | null { - switch (this.fullPeer._) { - case 'userFull': - return null - case 'chatFull': - if (this.fullPeer.participants._ !== 'chatParticipants') { - return null - } + get migratedFrom(): { chatId: number, msgId: number } | null { + if (this.full._ !== 'channelFull') return null + if (!this.full.migratedFromChatId || !this.full.migratedFromMaxId) return null - return this.fullPeer.participants.participants.length - case 'channelFull': - return this.fullPeer.participantsCount ?? null + return { + chatId: this.full.migratedFromChatId.toNumber(), + msgId: this.full.migratedFromMaxId, } } + /** For supergroups with hidden history, ID of the first message visible to the current user */ + get historyMinId(): number { + return this.full._ === 'channelFull' ? this.full.availableMinId ?? 0 : 0 + } + + /** ID of the last pinned message in the chat */ + get pinnedMsgId(): number | null { + return this.full._ === 'channelFull' ? this.full.pinnedMsgId ?? null : null + } + /** * Location of the chat. */ get location(): ChatLocation | null { - if (!this.fullPeer || this.fullPeer._ !== 'channelFull' || this.fullPeer.location?._ !== 'channelLocation') { + if (!this.full || this.full._ !== 'channelFull' || this.full.location?._ !== 'channelLocation') { return null } - return new ChatLocation(this.fullPeer.location) + return new ChatLocation(this.full.location) } - private _linkedChat?: Chat /** * 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 + if (this.full._ !== 'channelFull') return null + if (!this.full.linkedChatId) return null + + return new Chat(this.peers.chat(this.full.linkedChatId)) } /** * TTL of all messages in this chat, in seconds */ get ttlPeriod(): number | null { - return this.fullPeer?.ttlPeriod ?? null + return this.full?.ttlPeriod ?? null + } + + /** Slowmode delay for this chat, if any */ + get slowmodeSeconds(): number | null { + return this.full._ === 'channelFull' ? this.full.slowmodeSeconds ?? null : null } /** - * If this is a business account, information about the business. + * For supergroups with slow mode, date of the next time the current user + * will be able to send a message. `null` if they can send messages now. */ - get business(): BusinessAccount | null { - if (!this.fullPeer || this.fullPeer._ !== 'userFull') return null + get slowmodeNextSendDate(): Date | null { + if (this.full._ !== 'channelFull' || !this.full.slowmodeNextSendDate) return null - return new BusinessAccount(this.fullPeer) + return new Date(this.full.slowmodeNextSendDate * 1000) + } + + /** Number of pending join requests in the chat (only visible to admins) */ + get pendingJoinRequests(): number { + return this.full._ === 'channelFull' ? this.full.requestsPending ?? 0 : 0 + } + + /** Users who have recently requested to join the chat (only visible to admins) */ + get recentRequesters(): User[] { + if (this.full._ !== 'channelFull' || !this.full.recentRequesters) return [] + + return this.full.recentRequesters.map(it => new User(this.peers.user(it.toNumber()))) + } + + /** Reactions available in this chat */ + get availableReactions(): tl.TypeChatReactions | null { + return this.full._ === 'channelFull' ? this.full.availableReactions ?? null : null + } + + /** Maximum number of unique reactions on a single message in this chat, if any */ + get maxReactions(): number | null { + return this.full._ === 'channelFull' ? this.full.reactionsLimit ?? null : null + } + + /** Channel stories */ + get stories(): PeerStories | null { + if (this.full._ !== 'channelFull' || !this.full.stories) return null + + return new PeerStories(this.full.stories, this.peers) } } memoizeGetters(FullChat, [ 'fullPhoto', - 'personalPhoto', - 'realPhoto', - 'publicPhoto', + 'inviteLink', 'location', 'stickerSet', 'emojiSet', - 'business', + 'botInfo', + 'linkedChat', + 'recentRequesters', + 'stories', ]) makeInspectable(FullChat) diff --git a/packages/core/src/highlevel/types/peers/full-user.ts b/packages/core/src/highlevel/types/peers/full-user.ts new file mode 100644 index 00000000..76b6d388 --- /dev/null +++ b/packages/core/src/highlevel/types/peers/full-user.ts @@ -0,0 +1,264 @@ +import type { tl } from '@mtcute/tl' + +import { makeInspectable } from '../../utils/inspectable.js' +import { memoizeGetters } from '../../utils/memoize.js' +import { Photo } from '../media/photo.js' + +import { BusinessAccount } from '../premium/business-account.js' + +import { Chat } from './chat.js' +import { PeersIndex } from './peers-index.js' +import { User } from './user.js' + +/** + * Full information about a particular user. + */ +export class FullUser extends User { + readonly peers: PeersIndex + readonly full: tl.TypeUserFull + + constructor(obj: tl.users.TypeUserFull) { + const peers = PeersIndex.from(obj) + super(peers.user(obj.fullUser.id)) + + this.peers = peers + this.full = obj.fullUser + } + + /** Whether this chat is archived */ + get isArchived(): boolean { + return this.full.folderId === 1 + } + + /** + * Whether this user has restricted sending them voice/video messages. + */ + get hasBlockedVoices(): boolean { + return this.full.voiceMessagesForbidden! + } + + /** Whether the user is blocked by the current user */ + get isBlocked(): boolean { + return this.full.blocked! + } + + /** Whether the user can make/accept calls */ + get hasCallsAvailable(): boolean { + return this.full.phoneCallsAvailable! + } + + /** Whether the user can make/accept video calls */ + get hasVideoCallsAvailable(): boolean { + return this.full.videoCallsAvailable! + } + + /** Whether the current user can call this user */ + get canCall(): boolean { + return !this.full.phoneCallsPrivate! + } + + /** Whether the chat with this user has some scheduled messages */ + get hasScheduled(): boolean { + return this.full.hasScheduled! + } + + /** Whether the real-time translations popup should not be shown for this channel */ + get hasTranslationsDisabled(): boolean { + return this.full.translationsDisabled! + } + + /** Whether this chat has pinned stories */ + get hasPinnedStories(): boolean { + return this.full.storiesPinnedAvailable! + } + + /** Whether the current user has blocked this user from seeing our stories */ + get blockedMyStoriesFrom(): boolean { + return this.full.blockedMyStoriesFrom! + } + + /** Whether the chat with this user has a custom wallpaper */ + get hasCustomWallpaper(): boolean { + return this.full.wallpaperOverridden! + } + + /** Whether this user has hidden the exact read dates */ + get hasHiddenReadDate(): boolean { + return this.full.readDatesPrivate! + } + + /** + * (Only for current user) Whether we have re-enabled sponsored messages, + * despite having Telegram Premium + */ + get hasSponsoredEnabled(): boolean { + return this.full.sponsoredEnabled! + } + + /** Whether the current user can view ad revenue for this bot */ + get canViewAdRevenue(): boolean { + return this.full.canViewRevenue! + } + + /** + * Full information about this chat's photo, if any. + * + * Unlike {@link Chat.photo}, this field contains additional information + * about the photo, such as its date, more sizes, and is the only + * way to get the animated profile photo. + * + * This field takes into account any personal/fallback photo + * that the user may have set + */ + get fullPhoto(): Photo | null { + if (!this.full) return null + + const photo = this.full.personalPhoto ?? this.full.profilePhoto ?? this.full.fallbackPhoto + + if (photo?._ !== 'photo') return null + + return new Photo(photo) + } + + /** + * A custom photo (set by the current user) that should be displayed + * instead of the actual chat photo. + * + * Currently only available for users. + */ + get personalPhoto(): Photo | null { + if (!this.full || this.full._ !== 'userFull') return null + if (this.full.personalPhoto?._ !== 'photo') return null + + return new Photo(this.full.personalPhoto) + } + + /** + * Actual profile photo of the user, bypassing the custom one. + */ + get realPhoto(): Photo | null { + if (!this.full) return null + if (this.full._ !== 'userFull') return this.fullPhoto + if (this.full.personalPhoto?._ !== 'photo') return null + + return new Photo(this.full.personalPhoto) + } + + /** + * A photo that the user has set to be shown + * in case their actual profile photo is not available + * due to privacy settings. + * + * Currently only available for users. + */ + get publicPhoto(): Photo | null { + if (!this.full || this.full._ !== 'userFull') return null + if (this.full.fallbackPhoto?._ !== 'photo') return null + + return new Photo(this.full.fallbackPhoto) + } + + /** + * Bio of the other party in a private chat, or description of a + * group, supergroup or channel. + */ + get bio(): string { + return this.full.about ?? '' + } + + /** Number of star gifts the user has chosen to display on their profile */ + get starGiftsCount(): number { + return this.full.stargiftsCount ?? 0 + } + + /** If this chat is a bot, whether the current user can manage its emoji status */ + get canManageBotEmojiStatus(): boolean { + return this.full.botCanManageEmojiStatus! + } + + get peerSettings(): tl.TypePeerSettings | null { + return this.full.settings ?? null + } + + /** Notification settings for this chat */ + get notificationsSettings(): tl.TypePeerNotifySettings | null { + return this.full.notifySettings ?? null + } + + /** ID of the last pinned message in the chat with this user */ + get pinnedMsgId(): number | null { + return this.full.pinnedMsgId ?? null + } + + /** Number of common chats with this user */ + get commonChatsCount(): number { + return this.full.commonChatsCount + } + + /** Anonymized text to be shown instead of the user's name when forwarding their messages */ + get privateForwardName(): string | null { + return this.full.privateForwardName ?? null + } + + /** Suggested admin rights for groups for this bot */ + get botGroupAdminRights(): tl.TypeChatAdminRights | null { + return this.full.botGroupAdminRights ?? null + } + + /** Suggested admin rights for channels for this bot */ + get botBroadcastAdminRights(): tl.TypeChatAdminRights | null { + return this.full.botBroadcastAdminRights ?? null + } + + /** Information about the user's birthday */ + get birthday(): tl.RawBirthday | null { + return this.full.birthday ?? null + } + + /** + * Information about the user's personal channel. + */ + get personalChannel(): Chat | null { + if (this.full.personalChannelId == null) return null + + return new Chat(this.peers.chat(this.full.personalChannelId)) + } + + /** ID of the last message in {@link personalChannel} */ + get personalChannelMessageId(): number | null { + return this.full.personalChannelMessage ?? null + } + + /** + * TTL of all messages in this chat, in seconds + */ + get ttlPeriod(): number | null { + return this.full?.ttlPeriod ?? null + } + + /** + * If this is a business account, information about the business. + */ + get business(): BusinessAccount | null { + if ( + this.full.businessWorkHours != null + || this.full.businessLocation != null + || this.full.businessGreetingMessage != null + || this.full.businessAwayMessage != null + || this.full.businessIntro != null + ) { + return new BusinessAccount(this.full) + } + return null + } +} + +memoizeGetters(FullUser, [ + 'fullPhoto', + 'personalPhoto', + 'realPhoto', + 'publicPhoto', + 'personalChannel', + 'business', +]) +makeInspectable(FullUser) diff --git a/packages/core/src/highlevel/types/peers/index.ts b/packages/core/src/highlevel/types/peers/index.ts index cd409acf..34e327c5 100644 --- a/packages/core/src/highlevel/types/peers/index.ts +++ b/packages/core/src/highlevel/types/peers/index.ts @@ -1,3 +1,4 @@ +export * from './bot-info.js' export * from './chat-event/index.js' export * from './chat-invite-link-member.js' export * from './chat-invite-link.js' @@ -10,6 +11,7 @@ export * from './chat.js' export * from './chatlist-preview.js' export * from './forum-topic.js' export * from './full-chat.js' +export * from './full-user.js' export * from './peer.js' export * from './peers-index.js' export * from './typing-status.js' diff --git a/packages/core/src/highlevel/types/peers/user.ts b/packages/core/src/highlevel/types/peers/user.ts index 67f70e44..bee12c7e 100644 --- a/packages/core/src/highlevel/types/peers/user.ts +++ b/packages/core/src/highlevel/types/peers/user.ts @@ -128,6 +128,16 @@ export class User { return this.raw.botAttachMenu! } + /** Whether this bot offers an attachment menu web app, and we have it enabled */ + get isBotAttachmentMenuEnabled(): boolean { + return this.raw.attachMenuEnabled! + } + + /** Whether this bot can request geolocation when used in inline mode */ + get isBotWithInlineGeo(): boolean { + return this.raw.botInlineGeo! + } + /** Whether this bot can be edited by the current user */ get isBotEditable(): boolean { return this.raw.botCanEdit! diff --git a/packages/core/src/highlevel/types/updates/bot-reaction.ts b/packages/core/src/highlevel/types/updates/bot-reaction.ts index eda06f05..8adf41cc 100644 --- a/packages/core/src/highlevel/types/updates/bot-reaction.ts +++ b/packages/core/src/highlevel/types/updates/bot-reaction.ts @@ -5,7 +5,6 @@ import type { PeersIndex } from '../peers/peers-index.js' import type { InputReaction } from '../reactions/types.js' import { makeInspectable } from '../../utils/inspectable.js' import { memoizeGetters } from '../../utils/memoize.js' -import { Chat } from '../peers/chat.js' import { parsePeer } from '../peers/peer.js' import { ReactionCount } from '../reactions/reaction-count.js' import { toReactionEmoji } from '../reactions/types.js' @@ -27,8 +26,8 @@ export class BotReactionUpdate { /** * Chat where the reaction has been changed */ - get chat(): Chat { - return Chat._parseFromPeer(this.raw.peer, this._peers) + get chat(): Peer { + return parsePeer(this.raw.peer, this._peers) } /** @@ -87,8 +86,8 @@ export class BotReactionCountUpdate { /** * Chat where the reaction has been changed */ - get chat(): Chat { - return Chat._parseFromPeer(this.raw.peer, this._peers) + get chat(): Peer { + return parsePeer(this.raw.peer, this._peers) } /** diff --git a/packages/core/src/highlevel/types/updates/callback-query.ts b/packages/core/src/highlevel/types/updates/callback-query.ts index c469a0b3..c1d7c1d5 100644 --- a/packages/core/src/highlevel/types/updates/callback-query.ts +++ b/packages/core/src/highlevel/types/updates/callback-query.ts @@ -1,13 +1,14 @@ import type { tl } from '@mtcute/tl' -import type { PeersIndex } from '../peers/peers-index.js' +import type { Peer } from '../peers/peer.js' +import type { PeersIndex } from '../peers/peers-index.js' import { utf8 } from '@fuman/utils' import { MtArgumentError } from '../../../types/errors.js' import { makeInspectable } from '../../utils/index.js' import { encodeInlineMessageId } from '../../utils/inline-utils.js' import { memoizeGetters } from '../../utils/memoize.js' import { Message } from '../messages/message.js' -import { Chat } from '../peers/chat.js' +import { parsePeer } from '../peers/peer.js' import { User } from '../peers/user.js' /** Base class for callback queries */ @@ -91,12 +92,12 @@ export class CallbackQuery extends BaseCallbackQuery { /** * Chat where the originating message was sent */ - get chat(): Chat { + get chat(): Peer { if (this.raw._ !== 'updateBotCallbackQuery') { throw new MtArgumentError('Cannot get message id for inline callback') } - return new Chat(this._peers.get(this.raw.peer)) + return parsePeer(this.raw.peer, this._peers) } /** diff --git a/packages/core/src/highlevel/types/updates/delete-business-message-update.ts b/packages/core/src/highlevel/types/updates/delete-business-message-update.ts index 52f3ff31..bf7240e1 100644 --- a/packages/core/src/highlevel/types/updates/delete-business-message-update.ts +++ b/packages/core/src/highlevel/types/updates/delete-business-message-update.ts @@ -1,9 +1,10 @@ import type { tl } from '@mtcute/tl' +import type { Peer } from '../peers/peer.js' import type { PeersIndex } from '../peers/peers-index.js' import { makeInspectable } from '../../utils/index.js' import { memoizeGetters } from '../../utils/memoize.js' -import { Chat } from '../peers/chat.js' +import { parsePeer } from '../peers/peer.js' /** * One or more messages were deleted from a connected business account @@ -29,8 +30,8 @@ export class DeleteBusinessMessageUpdate { /** * Chat where the messages were deleted */ - get chat(): Chat { - return Chat._parseFromPeer(this.raw.peer, this._peers) + get chat(): Peer { + return parsePeer(this.raw.peer, this._peers) } } diff --git a/packages/dispatcher/src/context/message.ts b/packages/dispatcher/src/context/message.ts index 58f2d3cb..bce6fc19 100644 --- a/packages/dispatcher/src/context/message.ts +++ b/packages/dispatcher/src/context/message.ts @@ -1,4 +1,4 @@ -import type { Chat, OmitInputMessageId, ParametersSkip1, Peer, Sticker } from '@mtcute/core' +import type { OmitInputMessageId, ParametersSkip1, Peer, Sticker } from '@mtcute/core' import type { TelegramClient } from '@mtcute/core/client.js' import type { DeleteMessagesParams, @@ -55,7 +55,7 @@ export class MessageContext extends Message implements UpdateContext { let res if (this.sender.type === 'user') { - [res] = await this.client.getUsers(this.sender) + res = await this.client.getUser(this.sender) } else { res = await this.client.getChat(this.sender) } @@ -72,10 +72,15 @@ export class MessageContext extends Message implements UpdateContext { * * Learn more: [Incomplete peers](https://mtcute.dev/guide/topics/peers.html#incomplete-peers) */ - async getCompleteChat(): Promise { + async getCompleteChat(): Promise { if (!this.chat.isMin) return this.chat - const res = await this.client.getChat(this.chat) + let res: Peer + if (this.chat.type === 'user') { + res = await this.client.getUser(this.chat) + } else { + res = await this.client.getChat(this.chat) + } if (!res) throw new MtPeerNotFoundError('Failed to fetch chat') diff --git a/packages/dispatcher/src/filters/bots.ts b/packages/dispatcher/src/filters/bots.ts index ff7d0053..cc63736f 100644 --- a/packages/dispatcher/src/filters/bots.ts +++ b/packages/dispatcher/src/filters/bots.ts @@ -1,4 +1,4 @@ -import type { Chat, MaybeArray, MaybePromise, Message } from '@mtcute/core' +import type { Chat, MaybeArray, MaybePromise, Message, User } from '@mtcute/core' import type { BusinessMessageContext } from '../context/business-message.js' import type { MessageContext } from '../context/message.js' @@ -100,12 +100,10 @@ export function command(commands: MaybeArray, { export const start: UpdateFilter< MessageContext | BusinessMessageContext, { - chat: Modify + chat: User command: string[] } -> = and(chat('private'), command('start')) +> = and(chat('user'), command('start')) /** * Shorthand filter that matches /start commands diff --git a/packages/dispatcher/src/filters/chat.ts b/packages/dispatcher/src/filters/chat.ts index 6da26bbc..670d8dac 100644 --- a/packages/dispatcher/src/filters/chat.ts +++ b/packages/dispatcher/src/filters/chat.ts @@ -8,6 +8,7 @@ import type { HistoryReadUpdate, MaybeArray, Message, + Peer, PollVoteUpdate, User, UserTypingUpdate, @@ -20,20 +21,26 @@ import type { EmptyObject, Modify, UpdateFilter } from './types.js' /** * Filter updates by type of the chat where they happened */ -export function chat(type: T): UpdateFilter< +export function chat(type: T): UpdateFilter< Obj, { - chat: Modify - } & (Obj extends Message - ? T extends 'private' | 'bot' | 'group' + chat: T extends 'user' + ? User + : Modify + } + & (Obj extends Message + ? T extends 'user' | 'group' ? { sender: User } - : EmptyObject + : { + sender: Chat + } : EmptyObject) > { - return msg => - msg.chat.chatType === type + if (type === 'user') return msg => msg.chat.type === 'user' + + return msg => msg.chat.type === 'chat' && msg.chat.chatType === type } /** @@ -98,7 +105,7 @@ export const chatId: { const chat = upd.chat - return (matchSelf && chat.isSelf) + return (matchSelf && chat.type === 'user' && chat.isSelf) || indexId.has(chat.id) || Boolean(chat.usernames?.some(u => indexUsername.has(u.username))) } diff --git a/packages/dispatcher/src/filters/user.ts b/packages/dispatcher/src/filters/user.ts index a0100fd6..3cda43dd 100644 --- a/packages/dispatcher/src/filters/user.ts +++ b/packages/dispatcher/src/filters/user.ts @@ -96,7 +96,7 @@ export const userId: { case 'edit_business_message': { const sender = upd.sender - return (matchSelf && sender.isSelf) + return (matchSelf && sender.type === 'user' && sender.isSelf) || indexId.has(sender.id) || indexUsername.has(sender.username!) } diff --git a/packages/dispatcher/src/state/key.ts b/packages/dispatcher/src/state/key.ts index 2683c9c1..98f8d272 100644 --- a/packages/dispatcher/src/state/key.ts +++ b/packages/dispatcher/src/state/key.ts @@ -34,9 +34,9 @@ export const defaultStateKeyDelegate: StateKeyDelegate = (upd): string | null => } if (upd._name === 'new_message' || upd._name === 'new_business_message') { + if (upd.chat.type === 'user') return String(upd.chat.id) + switch (upd.chat.chatType) { - case 'private': - case 'bot': case 'channel': return String(upd.chat.id) case 'group': @@ -49,7 +49,7 @@ export const defaultStateKeyDelegate: StateKeyDelegate = (upd): string | null => } if (upd._name === 'callback_query') { - if (upd.chat.chatType === 'private') return `${upd.user.id}` + if (upd.chat.type === 'user') return `${upd.user.id}` return `${upd.chat.id}_${upd.user.id}` }