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`
This commit is contained in:
alina 🌸 2024-12-09 21:15:10 +03:00
parent 5d0cdc421a
commit b5bf02fc72
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
32 changed files with 915 additions and 462 deletions

View file

@ -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')
})
})

View file

@ -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<FullChat>
/**
* Get full information about a user.
*
* **Available**: both users and bots
*
* @param userId ID of the user or their username
*/
getFullUser(userId: InputPeerLike): Promise<FullUser>
/**
* Get nearby chats
*
@ -1678,6 +1689,15 @@ export interface TelegramClient extends ITelegramClient {
*/
getSimilarChannels(
channel: InputPeerLike): Promise<ArrayWithTotal<Chat>>
/**
* 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<User>
/**
* 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)
}

View file

@ -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'

View file

@ -52,6 +52,7 @@ import {
FileDownloadParameters,
ForumTopic,
FullChat,
FullUser,
GameHighScore,
HistoryReadUpdate,
InlineCallbackQuery,

View file

@ -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`)

View file

@ -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)
}

View file

@ -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<FullUser> {
const peer = await resolveUser(client, userId)
const res = await client.call({
_: 'users.getFullUser',
id: peer,
})
return new FullUser(res)
}

View file

@ -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<User> {
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)
}

View file

@ -75,7 +75,7 @@ export async function findDialogs(client: ITelegramClient, peers: MaybeArray<str
for await (const dialog of iterDialogs(client, {
archived: 'keep',
})) {
const chat = dialog.chat
const chat = dialog.peer
const idxById = notFoundIds.get(chat.id)

View file

@ -316,7 +316,7 @@ export async function* iterDialogs(
if (!dialogs.length) return
const last = dialogs[dialogs.length - 1]
offsetPeer = last.chat.inputPeer
offsetPeer = last.peer.inputPeer
offsetId = last.raw.topMessage
if (last.lastMessage) {

View file

@ -33,7 +33,7 @@ export async function joinChatlist(
peers = all.filter(isPresent)
} else {
const preview = await getChatlistPreview(client, link)
peers = preview.chats.filter(it => !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({

View file

@ -23,7 +23,7 @@ export function commentText(
message: Message,
...params: ParametersSkip2<typeof sendText>
): ReturnType<typeof sendText> {
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<typeof sendMedia>
): ReturnType<typeof sendMedia> {
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<typeof sendMediaGroup>
): ReturnType<typeof sendMediaGroup> {
if (message.chat.chatType !== 'channel') {
if (message.chat.type !== 'chat' || message.chat.chatType !== 'channel') {
return replyMediaGroup(client, message, ...params)
}

View file

@ -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')
})

View file

@ -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)

View file

@ -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)
}
/**

View file

@ -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}`)
}
}

View file

@ -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)
}
/**

View file

@ -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'])

View file

@ -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<tl.RawUsername> | 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<typeof this.peer.photo, tl.RawChatPhotoEmpty | tl.RawUserProfilePhotoEmpty>)
?.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<tl.RawRestrictionReason> | 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, [])

View file

@ -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))
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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'

View file

@ -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!

View file

@ -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)
}
/**

View file

@ -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)
}
/**

View file

@ -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)
}
}

View file

@ -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<Message> {
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<Message> {
*
* Learn more: [Incomplete peers](https://mtcute.dev/guide/topics/peers.html#incomplete-peers)
*/
async getCompleteChat(): Promise<Chat> {
async getCompleteChat(): Promise<Peer> {
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')

View file

@ -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<string | RegExp>, {
export const start: UpdateFilter<
MessageContext | BusinessMessageContext,
{
chat: Modify<Chat, {
chatType: 'private'
}>
chat: User
command: string[]
}
> = and(chat('private'), command('start'))
> = and(chat('user'), command('start'))
/**
* Shorthand filter that matches /start commands

View file

@ -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<T extends ChatType, Obj extends { chat: Chat }>(type: T): UpdateFilter<
export function chat<T extends ChatType | 'user', Obj extends { chat: Peer }>(type: T): UpdateFilter<
Obj,
{
chat: Modify<Chat, { chatType: T }>
} & (Obj extends Message
? T extends 'private' | 'bot' | 'group'
chat: T extends 'user'
? User
: Modify<Chat, { chatType: T }>
}
& (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)))
}

View file

@ -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!)
}

View file

@ -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}`
}