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 }) 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.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 { CanApplyBoostResult } from './methods/premium/can-apply-boost.js'
import type { CanSendStoryResult } from './methods/stories/can-send-story.js' import type { CanSendStoryResult } from './methods/stories/can-send-story.js'
import type { ITelegramStorageProvider } from './storage/provider.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 { ParsedUpdateHandlerParams } from './updates/parsed.js'
import type { RawUpdateInfo } from './updates/types.js' import type { RawUpdateInfo } from './updates/types.js'
import type { InputStringSessionData } from './utils/string-session.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 { getChatPreview } from './methods/chats/get-chat-preview.js'
import { getChat } from './methods/chats/get-chat.js' import { getChat } from './methods/chats/get-chat.js'
import { getFullChat } from './methods/chats/get-full-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 { getNearbyChats } from './methods/chats/get-nearby-chats.js'
import { getSimilarChannels } from './methods/chats/get-similar-channels.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 { iterChatEventLog } from './methods/chats/iter-chat-event-log.js'
import { iterChatMembers } from './methods/chats/iter-chat-members.js' import { iterChatMembers } from './methods/chats/iter-chat-members.js'
import { joinChat } from './methods/chats/join-chat.js' import { joinChat } from './methods/chats/join-chat.js'
@ -1655,6 +1657,15 @@ export interface TelegramClient extends ITelegramClient {
* Use {@link getChatPreview} instead. * Use {@link getChatPreview} instead.
*/ */
getFullChat(chatId: InputPeerLike): Promise<FullChat> 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 * Get nearby chats
* *
@ -1678,6 +1689,15 @@ export interface TelegramClient extends ITelegramClient {
*/ */
getSimilarChannels( getSimilarChannels(
channel: InputPeerLike): Promise<ArrayWithTotal<Chat>> 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. * Iterate over chat event log.
* *
@ -6216,12 +6236,18 @@ TelegramClient.prototype.getChat = function (...args) {
TelegramClient.prototype.getFullChat = function (...args) { TelegramClient.prototype.getFullChat = function (...args) {
return getFullChat(this._client, ...args) return getFullChat(this._client, ...args)
} }
TelegramClient.prototype.getFullUser = function (...args) {
return getFullUser(this._client, ...args)
}
TelegramClient.prototype.getNearbyChats = function (...args) { TelegramClient.prototype.getNearbyChats = function (...args) {
return getNearbyChats(this._client, ...args) return getNearbyChats(this._client, ...args)
} }
TelegramClient.prototype.getSimilarChannels = function (...args) { TelegramClient.prototype.getSimilarChannels = function (...args) {
return getSimilarChannels(this._client, ...args) return getSimilarChannels(this._client, ...args)
} }
TelegramClient.prototype.getUser = function (...args) {
return getUser(this._client, ...args)
}
TelegramClient.prototype.iterChatEventLog = function (...args) { TelegramClient.prototype.iterChatEventLog = function (...args) {
return iterChatEventLog(this._client, ...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 { getChatPreview } from './methods/chats/get-chat-preview.js'
export { getChat } from './methods/chats/get-chat.js' export { getChat } from './methods/chats/get-chat.js'
export { getFullChat } from './methods/chats/get-full-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 { getNearbyChats } from './methods/chats/get-nearby-chats.js'
export { getSimilarChannels } from './methods/chats/get-similar-channels.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 { iterChatEventLog } from './methods/chats/iter-chat-event-log.js'
export { iterChatMembers } from './methods/chats/iter-chat-members.js' export { iterChatMembers } from './methods/chats/iter-chat-members.js'
export { joinChat } from './methods/chats/join-chat.js' export { joinChat } from './methods/chats/join-chat.js'

View file

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

View file

@ -1,11 +1,11 @@
import type { ITelegramClient } from '../../client.types.js' import type { ITelegramClient } from '../../client.types.js'
import type { InputPeerLike } from '../../types/index.js' import type { InputPeerLike } from '../../types/index.js'
import { MtArgumentError } from '../../../types/errors.js' import { MtArgumentError } from '../../../types/errors.js'
import { Chat, MtPeerNotFoundError } from '../../types/index.js' import { Chat, MtInvalidPeerTypeError, MtPeerNotFoundError } from '../../types/index.js'
import { INVITE_LINK_REGEX } from '../../utils/peer-utils.js' import { INVITE_LINK_REGEX, isInputPeerChannel, isInputPeerChat, toInputChannel } from '../../utils/peer-utils.js'
import { resolvePeer } from '../users/resolve-peer.js' import { resolvePeer } from '../users/resolve-peer.js'
import { _getRawPeerBatched } from './batched-queries.js' import { _getChannelsBatched, _getChatsBatched } from './batched-queries.js'
// @available=both // @available=both
/** /**
@ -36,7 +36,14 @@ export async function getChat(client: ITelegramClient, chatId: InputPeerLike): P
const peer = await resolvePeer(client, chatId) 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`) 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 { ITelegramClient } from '../../client.types.js'
import type { InputPeerLike } from '../../types/index.js' import type { InputPeerLike } from '../../types/index.js'
import { MtArgumentError } from '../../../types/errors.js' import { MtArgumentError } from '../../../types/errors.js'
import { FullChat } from '../../types/index.js' import { FullChat, MtInvalidPeerTypeError } from '../../types/index.js'
import { import {
INVITE_LINK_REGEX, INVITE_LINK_REGEX,
isInputPeerChannel, isInputPeerChannel,
isInputPeerChat, isInputPeerChat,
isInputPeerUser,
toInputChannel, toInputChannel,
toInputUser,
} from '../../utils/peer-utils.js' } from '../../utils/peer-utils.js'
import { resolvePeer } from '../users/resolve-peer.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) const peer = await resolvePeer(client, chatId)
let res: tl.messages.TypeChatFull | tl.users.TypeUserFull let res: tl.messages.TypeChatFull
if (isInputPeerChannel(peer)) { if (isInputPeerChannel(peer)) {
res = await client.call({ res = await client.call({
_: 'channels.getFullChannel', _: 'channels.getFullChannel',
channel: toInputChannel(peer), channel: toInputChannel(peer),
}) })
} else if (isInputPeerUser(peer)) {
res = await client.call({
_: 'users.getFullUser',
id: toInputUser(peer),
})
} else if (isInputPeerChat(peer)) { } else if (isInputPeerChat(peer)) {
res = await client.call({ res = await client.call({
_: 'messages.getFullChat', _: 'messages.getFullChat',
chatId: peer.chatId, chatId: peer.chatId,
}) })
} else { } 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, { for await (const dialog of iterDialogs(client, {
archived: 'keep', archived: 'keep',
})) { })) {
const chat = dialog.chat const chat = dialog.peer
const idxById = notFoundIds.get(chat.id) const idxById = notFoundIds.get(chat.id)

View file

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

View file

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

View file

@ -23,7 +23,7 @@ export function commentText(
message: Message, message: Message,
...params: ParametersSkip2<typeof sendText> ...params: ParametersSkip2<typeof sendText>
): ReturnType<typeof sendText> { ): ReturnType<typeof sendText> {
if (message.chat.chatType !== 'channel') { if (message.chat.type !== 'chat' || message.chat.chatType !== 'channel') {
return replyText(client, message, ...params) return replyText(client, message, ...params)
} }
@ -52,7 +52,7 @@ export function commentMedia(
message: Message, message: Message,
...params: ParametersSkip2<typeof sendMedia> ...params: ParametersSkip2<typeof sendMedia>
): ReturnType<typeof sendMedia> { ): ReturnType<typeof sendMedia> {
if (message.chat.chatType !== 'channel') { if (message.chat.type !== 'chat' || message.chat.chatType !== 'channel') {
return replyMedia(client, message, ...params) return replyMedia(client, message, ...params)
} }
@ -81,7 +81,7 @@ export function commentMediaGroup(
message: Message, message: Message,
...params: ParametersSkip2<typeof sendMediaGroup> ...params: ParametersSkip2<typeof sendMediaGroup>
): ReturnType<typeof sendMediaGroup> { ): ReturnType<typeof sendMediaGroup> {
if (message.chat.chatType !== 'channel') { if (message.chat.type !== 'chat' || message.chat.chatType !== 'channel') {
return replyMediaGroup(client, message, ...params) 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 { createStub, StubTelegramClient } from '@mtcute/test'
import Long from 'long' import Long from 'long'
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { toggleChannelIdMark } from '../../../utils/peer-utils.js' import { toggleChannelIdMark } from '../../../utils/peer-utils.js'
import { sendText } from './send-text.js' import { sendText } from './send-text.js'
const stubUser = createStub('user', { const stubUser = createStub('user', {
@ -52,7 +53,7 @@ describe('sendText', () => {
expect(msg).toBeDefined() expect(msg).toBeDefined()
expect(msg.id).toEqual(123) 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.chat.id).toEqual(stubUser.id)
expect(msg.text).toEqual('test') expect(msg.text).toEqual('test')
}) })
@ -96,7 +97,8 @@ describe('sendText', () => {
expect(msg).toBeDefined() expect(msg).toBeDefined()
expect(msg.id).toEqual(123) 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.chat.id).toEqual(markedChannelId)
expect(msg.text).toEqual('test') expect(msg.text).toEqual('test')
}) })
@ -124,7 +126,7 @@ describe('sendText', () => {
expect(msg).toBeDefined() expect(msg).toBeDefined()
expect(msg.id).toEqual(123) 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.chat.id).toEqual(stubUser.id)
expect(msg.text).toEqual('test') expect(msg.text).toEqual('test')
}) })

View file

@ -1,12 +1,13 @@
import type { tl } from '@mtcute/tl' import type { tl } from '@mtcute/tl'
import type { Peer } from '../peers/peer.js'
import { getMarkedPeerId } from '../../../utils/peer-utils.js' import { getMarkedPeerId } from '../../../utils/peer-utils.js'
import { assertTypeIsNot, hasValueAtKey } from '../../../utils/type-assertions.js' import { assertTypeIsNot, hasValueAtKey } from '../../../utils/type-assertions.js'
import { makeInspectable } from '../../utils/index.js' import { makeInspectable } from '../../utils/index.js'
import { memoizeGetters } from '../../utils/memoize.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 { DraftMessage } from './draft-message.js'
import { Message } from './message.js' import { Message } from './message.js'
@ -71,7 +72,7 @@ export class Dialog {
index[getMarkedPeerId(peer)] = true 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) return dialogs.filter(i => i.isPinned)
@ -105,11 +106,11 @@ export class Dialog {
if (folder._ === 'dialogFilterChatlist') { if (folder._ === 'dialogFilterChatlist') {
return (dialog) => { 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) => { return (dialog) => {
const chat = dialog.chat const peer = dialog.peer
const chatId = dialog.chat.id const peerId = dialog.peer.id
const chatType = dialog.chat.chatType
// manual exclusion/inclusion and pins // 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 return false
} }
@ -137,17 +137,17 @@ export class Dialog {
if (folder.excludeArchived && dialog.isArchived) return false if (folder.excludeArchived && dialog.isArchived) return false
// inclusions based on chat type // inclusions based on chat type
if (folder.contacts && chatType === 'private' && chat.isContact) { if (folder.contacts && peer.type === 'user' && peer.isContact) {
return true return true
} }
if (folder.nonContacts && chatType === 'private' && !chat.isContact) { if (folder.nonContacts && peer.type === 'user' && !peer.isContact) {
return true return true
} }
if (folder.groups && (chatType === 'group' || chatType === 'supergroup')) { if (folder.groups && peer.type === 'chat' && peer.isGroup) {
return true return true
} }
if (folder.broadcasts && chatType === 'channel') return true if (folder.broadcasts && peer.type === 'chat' && peer.chatType === 'channel') return true
if (folder.bots && chatType === 'bot') return true if (folder.bots && peer.type === 'user' && peer.isBot) return true
return false return false
} }
@ -193,17 +193,17 @@ export class Dialog {
} }
/** /**
* Chat that this dialog represents * Peer that this dialog represents
*/ */
get chat(): Chat { get peer(): Peer {
return Chat._parseFromPeer(this.raw.peer, this._peers) return parsePeer(this.raw.peer, this._peers)
} }
/** /**
* The latest message sent in this chat (if any) * The latest message sent in this chat (if any)
*/ */
get lastMessage(): Message | null { get lastMessage(): Message | null {
const cid = this.chat.id const cid = this.peer.id
if (this._messages.has(cid)) { if (this._messages.has(cid)) {
return new Message(this._messages.get(cid)!, this._peers) 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) makeInspectable(Dialog)

View file

@ -1,11 +1,10 @@
import type { tl } from '@mtcute/tl' 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 type { PeersIndex } from '../peers/peers-index.js'
import { MtTypeAssertionError } from '../../../types/errors.js' import { MtTypeAssertionError } from '../../../types/errors.js'
import { makeInspectable } from '../../utils/inspectable.js' import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js' import { memoizeGetters } from '../../utils/memoize.js'
import { Chat } from '../peers/chat.js'
import { parsePeer } from '../peers/peer.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 * `null` for other messages, you might want to use {@link sender} instead
*/ */
fromChat(): Chat | null { fromChat(): Peer | null {
if (!this.raw.savedFromPeer) return 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 { ReplyMarkup } from '../bots/keyboards/index.js'
import type { TextWithEntities } from '../misc/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 { Peer } from '../peers/peer.js'
import type { PeersIndex } from '../peers/peers-index.js' import type { PeersIndex } from '../peers/peers-index.js'
import type { MessageAction } from './message-action.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 { assertTypeIsNot } from '../../../utils/type-assertions.js'
import { makeInspectable } from '../../utils/index.js' import { makeInspectable } from '../../utils/index.js'
import { memoizeGetters } from '../../utils/memoize.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 { parsePeer } from '../peers/peer.js'
import { User } from '../peers/user.js' import { User } from '../peers/user.js'
import { FactCheck } from './fact-check.js' import { FactCheck } from './fact-check.js'
@ -174,7 +174,7 @@ export class Message {
/** /**
* Message sender. * 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, * in case the message was sent by an anonymous admin, anonymous premium user,
* or if the message is a forwarded channel post. * or if the message is a forwarded channel post.
* *
@ -211,8 +211,8 @@ export class Message {
/** /**
* Conversation the message belongs to * Conversation the message belongs to
*/ */
get chat(): Chat { get chat(): Peer {
return Chat._parseFromMessage(this.raw, this._peers) return parsePeer(this.raw.peerId, this._peers)
} }
/** /**
@ -252,7 +252,8 @@ export class Message {
const fwd = this.raw.fwdFrom const fwd = this.raw.fwdFrom
return Boolean( return Boolean(
this.chat.chatType === 'supergroup' this.chat.type === 'chat'
&& this.chat.chatType === 'supergroup'
&& fwd.savedFromMsgId && fwd.savedFromMsgId
&& fwd.savedFromPeer?._ === 'peerChannel' && fwd.savedFromPeer?._ === 'peerChannel'
&& getMarkedPeerId(fwd.savedFromPeer) !== getMarkedPeerId(this.raw.peerId), && 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 * @throws MtArgumentError In case the chat does not support message links
*/ */
get link(): string { 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) { if (this.chat.username) {
return `https://t.me/${this.chat.username}/${this.id}` 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}` 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 { tl } from '@mtcute/tl'
import type { PeerSender } from '../peers/peer.js'
import type { PeersIndex } from '../peers/peers-index.js' import type { PeersIndex } from '../peers/peers-index.js'
import type { MessageMedia } from './message-media.js' import type { MessageMedia } from './message-media.js'
import { MtTypeAssertionError } from '../../../types/errors.js' import { MtTypeAssertionError } from '../../../types/errors.js'
import { makeInspectable } from '../../utils/inspectable.js' import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js' import { memoizeGetters } from '../../utils/memoize.js'
import { Chat } from '../peers/chat.js' import { Chat } from '../peers/chat.js'
import { parsePeer, type Peer, type PeerSender } from '../peers/peer.js'
import { User } from '../peers/user.js' import { User } from '../peers/user.js'
import { MessageEntity } from './message-entity.js' import { MessageEntity } from './message-entity.js'
@ -114,14 +114,14 @@ export class RepliedMessageInfo {
* *
* If `null`, the message was sent in the same chat. * 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) { if (!this.raw.replyToPeerId || !this.raw.replyFrom) {
// same chat or private. even if `replyToPeerId` is available, // same chat or private. even if `replyToPeerId` is available,
// without `replyFrom` it would contain the sender, not the chat // without `replyFrom` it would contain the sender, not the chat
return null 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 { tl } from '@mtcute/tl'
import type { PeersIndex } from './peers-index.js'
import { MtArgumentError, MtTypeAssertionError } from '../../../types/errors.js' import { MtArgumentError, MtTypeAssertionError } from '../../../types/errors.js'
import { getMarkedPeerId } from '../../../utils/peer-utils.js' import { getMarkedPeerId } from '../../../utils/peer-utils.js'
import { makeInspectable } from '../../utils/index.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 { ChatColors } from './chat-colors.js'
import { ChatPermissions } from './chat-permissions.js' import { ChatPermissions } from './chat-permissions.js'
import { ChatPhoto } from './chat-photo.js' import { ChatPhoto } from './chat-photo.js'
import { User } from './user.js'
/** /**
* Chat type. Can be: * Chat type. Can be:
* - `private`: PM with other users or yourself (Saved Messages) * - `group`: Legacy/basic group
* - `bot`: PM with a bot
* - `group`: Legacy group
* - `supergroup`: Supergroup * - `supergroup`: Supergroup
* - `channel`: Broadcast channel * - `channel`: Broadcast channel
* - `gigagroup`: Gigagroup aka Broadcast group * - `gigagroup`: Gigagroup aka Broadcast group
*/ */
export type ChatType = 'private' | 'bot' | 'group' | 'supergroup' | 'channel' | 'gigagroup' export type ChatType = 'group' | 'supergroup' | 'channel' | 'gigagroup'
/** /**
* A chat. * A chat.
@ -33,13 +29,10 @@ export class Chat {
/** /**
* Raw peer object that this {@link Chat} represents. * Raw peer object that this {@link Chat} represents.
*/ */
readonly peer: tl.RawUser | tl.RawChat | tl.RawChannel | tl.RawChatForbidden | tl.RawChannelForbidden readonly raw: tl.RawChat | tl.RawChannel | tl.RawChatForbidden | tl.RawChannelForbidden
constructor(peer: tl.TypeUser | tl.TypeChat) {
if (!peer) throw new MtArgumentError('peer is not available')
constructor(peer: tl.TypeChat) {
switch (peer._) { switch (peer._) {
case 'user':
case 'chat': case 'chat':
case 'channel': case 'channel':
case 'chatForbidden': case 'chatForbidden':
@ -49,7 +42,7 @@ export class Chat {
throw new MtTypeAssertionError('peer', 'user | chat | channel', peer._) throw new MtTypeAssertionError('peer', 'user | chat | channel', peer._)
} }
this.peer = peer this.raw = peer
} }
/** Marked ID of this chat */ /** 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 * This currently only ever happens for non-bot users, so if you are building
* a normal bot, you can safely ignore this field. * 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.getChat}
* - {@link TelegramClient.getFullChat}. * - {@link TelegramClient.getFullChat}.
* *
@ -81,7 +74,7 @@ export class Chat {
*/ */
get isMin(): boolean { get isMin(): boolean {
// avoid additional runtime checks // 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. * so prefer using it whenever you need an input peer.
*/ */
get inputPeer(): tl.TypeInputPeer { get inputPeer(): tl.TypeInputPeer {
switch (this.peer._) { switch (this.raw._) {
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,
}
case 'chat': case 'chat':
case 'chatForbidden': case 'chatForbidden':
return { return {
_: 'inputPeerChat', _: 'inputPeerChat',
chatId: this.peer.id, chatId: this.raw.id,
} }
case 'channel': case 'channel':
case 'channelForbidden': case 'channelForbidden':
if ((this.peer as tl.RawChannel).min) { if ((this.raw as tl.RawChannel).min) {
return { return {
_: 'mtcute.dummyInputPeerMinChannel', _: '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!") throw new MtArgumentError("Peer's access hash is not available!")
} }
return { return {
_: 'inputPeerChannel', _: 'inputPeerChannel',
channelId: this.peer.id, channelId: this.raw.id,
accessHash: this.peer.accessHash, accessHash: this.raw.accessHash,
} }
} }
} }
/** Type of chat */ /** Type of chat */
get chatType(): ChatType { get chatType(): ChatType {
switch (this.peer._) { switch (this.raw._) {
case 'user':
return this.peer.bot ? 'bot' : 'private'
case 'chat': case 'chat':
case 'chatForbidden': case 'chatForbidden':
return 'group' return 'group'
case 'channel': case 'channel':
case 'channelForbidden': case 'channelForbidden':
if (this.peer._ === 'channel' && this.peer.gigagroup) { if (this.raw._ === 'channel' && this.raw.gigagroup) {
return 'gigagroup' return 'gigagroup'
} else if (this.peer.broadcast) { } else if (this.raw.broadcast) {
return 'channel' return 'channel'
} else if (this.raw.megagroup) {
return 'supergroup'
} }
return 'supergroup' throw new MtArgumentError('Unknown chat type')
} }
} }
/** /**
* Whether this chat is a group chat * Whether this chat is a group chat (i.e. not a channel)
* (i.e. not a channel and not PM)
*/ */
get isGroup(): boolean { get isGroup(): boolean {
switch (this.chatType) { return this.chatType !== 'channel'
case 'group':
case 'supergroup':
case 'gigagroup':
return true
}
return false
} }
/** /**
@ -181,7 +149,7 @@ export class Chat {
* Supergroups, channels and groups only * Supergroups, channels and groups only
*/ */
get isVerified(): boolean { 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 * See {@link restrictions} for details
*/ */
get isRestricted(): boolean { 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 * Supergroups, channels and groups only
*/ */
get isCreator(): boolean { 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. * Supergroups, channels and groups only.
*/ */
get isAdmin(): boolean { 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 */ /** Whether this chat has been flagged for scam */
get isScam(): boolean { 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 */ /** Whether this chat has been flagged for impersonation */
get isFake(): boolean { get isFake(): boolean {
return 'fake' in this.peer ? this.peer.fake! : false return 'fake' in this.raw ? this.raw.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!
} }
/** Whether this peer is a forum supergroup */ /** Whether this peer is a forum supergroup */
get isForum(): boolean { 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 { * Whether the chat is not available (e.g. because the user was banned from there).
return this.peer._ === 'chatForbidden' || this.peer._ === 'channelForbidden' *
* **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`. * For users, this is always `true`.
*/ */
get isMember(): boolean { get isMember(): boolean {
switch (this.peer._) { switch (this.raw._) {
case 'user':
return true
case 'channel': case 'channel':
case 'chat': case 'chat':
return !this.peer.left return !this.raw.left
default: default:
return false 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 */ /** Whether you have hidden (arhived) this chat's stories */
get storiesHidden(): boolean { get storiesHidden(): boolean {
return 'storiesHidden' in this.peer ? this.peer.storiesHidden! : false return 'storiesHidden' in this.raw ? this.raw.storiesHidden! : false
} }
get storiesUnavailable(): boolean { 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 */ /** Whether this group is a channel/supergroup with join requests enabled */
get hasJoinRequests(): boolean { 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 */ /** Whether this group is a supergroup with join-to-send rule enabled */
get hasJoinToSend(): boolean { 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) */ /** Whether this group has content protection (i.e. disabled forwards) */
get hasContentProtection(): boolean { 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") */ /** Whether this channel has profile signatures (i.e. "Super Channel") */
get hasProfileSignatures(): boolean { get hasProfileSignatures(): boolean {
return this.peer._ === 'channel' && this.peer.signatureProfiles! return this.raw._ === 'channel' && this.raw.signatureProfiles!
} }
/** /** Whether this channel has author signatures enabled under posts */
* Title, for supergroups, channels and groups get hasSignatures(): boolean {
* (`null` for private chats) return this.raw._ === 'channel' && this.raw.signatures!
*/
get title(): string | null {
return this.peer._ !== 'user' ? this.peer.title ?? null : null
} }
/** /** Chat title */
* Username, for private chats, bots, supergroups and channels (if available) get title(): string {
*/ return this.raw.title
}
/** Chat username (if available) */
get username(): string | null { 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 { get usernames(): ReadonlyArray<tl.RawUsername> | null {
if (!('usernames' in this.peer)) return null if (!('usernames' in this.raw)) return null
return ( return (
this.peer.usernames this.raw.usernames
?? (this.peer.username ? [{ _: 'username', username: this.peer.username, active: true }] : null) ?? (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. * Get the display name of the chat.
* *
* Title for groups and channels, * Basically an alias to {@link title}, exists for consistency with {@link User}.
* name (and last name if available) for users
*/ */
get displayName(): string { get displayName(): string {
if (this.peer._ === 'user') { return this.raw.title
if (this.peer.lastName) {
return `${this.peer.firstName} ${this.peer.lastName}`
}
return this.peer.firstName ?? 'Deleted Account'
}
return this.peer.title
} }
/** /**
@ -361,32 +331,13 @@ export class Chat {
*/ */
get photo(): ChatPhoto | null { get photo(): ChatPhoto | null {
if ( if (
!('photo' in this.peer) !('photo' in this.raw)
|| !this.peer.photo || this.raw.photo?._ !== 'chatPhoto'
|| (this.peer.photo._ !== 'userProfilePhoto' && this.peer.photo._ !== 'chatPhoto')
) { ) {
return null return null
} }
return new ChatPhoto(this.inputPeer, this.peer.photo) return new ChatPhoto(this.inputPeer, this.raw.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
)
} }
/** /**
@ -394,37 +345,36 @@ export class Chat {
* This field is available only in case {@link isRestricted} is `true` * This field is available only in case {@link isRestricted} is `true`
*/ */
get restrictions(): ReadonlyArray<tl.RawRestrictionReason> | null { 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. * Current user's permissions, for supergroups.
*/ */
get permissions(): ChatPermissions | null { get permissions(): ChatPermissions | null {
if (!('bannedRights' in this.peer && this.peer.bannedRights)) { if (!('bannedRights' in this.raw && this.raw.bannedRights)) {
return null return null
} }
return new ChatPermissions(this.peer.bannedRights) return new ChatPermissions(this.raw.bannedRights)
} }
/** /**
* Default chat member permissions, for groups and supergroups. * Default chat member permissions, for groups and supergroups.
*/ */
get defaultPermissions(): ChatPermissions | null { get defaultPermissions(): ChatPermissions | null {
if (!('defaultBannedRights' in this.peer) || !this.peer.defaultBannedRights) { if (!('defaultBannedRights' in this.raw) || !this.raw.defaultBannedRights) {
return null return null
} }
return new ChatPermissions(this.peer.defaultBannedRights) return new ChatPermissions(this.raw.defaultBannedRights)
} }
/** /**
* Admin rights of the current user in this chat. * Admin rights of the current user in this chat, if any.`
* `null` for PMs and non-administered chats
*/ */
get adminRights(): tl.RawChatAdminRights | null { 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) * Maximum ID of stories this chat has (or 0 if none)
*/ */
get storiesMaxId(): number { get storiesMaxId(): number {
switch (this.peer._) { return this.raw._ === 'channel' ? this.raw.storiesMaxId ?? 0 : 0
case 'channel':
case 'user':
return this.peer.storiesMaxId ?? 0
}
return 0
} }
/** /**
@ -452,47 +396,30 @@ export class Chat {
* as well as to render the chat title * as well as to render the chat title
*/ */
get color(): ChatColors { get color(): ChatColors {
const color = this.peer._ === 'user' || this.peer._ === 'channel' ? this.peer.color : undefined return new ChatColors(this.raw.id, this.raw._ === 'channel' ? this.raw.color : undefined)
return new ChatColors(this.peer.id, color)
} }
/** /**
* Chat's emoji status, if any. * Chat's emoji status, if any.
*/ */
get emojiStatus(): EmojiStatus | null { get emojiStatus(): EmojiStatus | null {
if (this.peer._ !== 'user' && this.peer._ !== 'channel') return null if (this.raw._ !== 'channel') return null
if (!this.peer.emojiStatus || this.peer.emojiStatus._ === 'emojiStatusEmpty') 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 * Color that should be used when rendering the header of
* the user's profile * the user's profile
*
* If `null`, a generic header should be used instead
*/ */
get profileColors(): ChatColors { get profileColors(): ChatColors {
const color = this.peer._ === 'user' || this.peer._ === 'channel' ? this.peer.profileColor : undefined return new ChatColors(this.raw.id, this.raw._ === 'channel' ? this.raw.profileColor : undefined)
return new ChatColors(this.peer.id, color)
} }
/** Boosts level this chat has (0 if none or is not a channel) */ /** Boosts level this chat has (0 if none or is not a channel) */
get boostsLevel(): number { get boostsLevel(): number {
return this.peer._ === 'channel' ? this.peer.level ?? 0 : 0 return this.raw._ === 'channel' ? this.raw.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)
} }
/** /**
@ -500,26 +427,54 @@ export class Chat {
* this field will contain the date when the subscription will expire. * this field will contain the date when the subscription will expire.
*/ */
get subscriptionUntilDate(): Date | null { 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 */ /** Date when the current user joined this chat (if available) */
static _parseFromMessage(message: tl.RawMessage | tl.RawMessageService, peers: PeersIndex): Chat { get joinDate(): Date | null {
return Chat._parseFromPeer(message.peerId, peers) return this.isMember && ('date' in this.raw) ? new Date(this.raw.date * 1000) : null
} }
/** @internal */ /** Date when the chat was created (if available) */
static _parseFromPeer(peer: tl.TypePeer, peers: PeersIndex): Chat { get creationDate(): Date | null {
switch (peer._) { return !this.isMember && ('date' in this.raw) ? new Date(this.raw.date * 1000) : null
case 'peerUser': }
return new Chat(peers.user(peer.userId))
case 'peerChat':
return new Chat(peers.chat(peer.chatId))
}
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 { mention(text?: string | null): string | MessageEntity {
if (this.user) return this.user.mention(text)
if (text === undefined && this.username) { if (text === undefined && this.username) {
return `@${this.username}` return `@${this.username}`
} }
@ -575,7 +528,6 @@ memoizeGetters(Chat, [
'photo', 'photo',
'permissions', 'permissions',
'defaultPermissions', 'defaultPermissions',
'user',
'color', 'color',
]) ])
makeInspectable(Chat, [], ['user']) makeInspectable(Chat, [])

View file

@ -1,9 +1,10 @@
import type { tl } from '@mtcute/tl' import type { tl } from '@mtcute/tl'
import type { Peer } from './peer.js'
import { makeInspectable } from '../../utils/inspectable.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' import { PeersIndex } from './peers-index.js'
/** /**
@ -36,7 +37,7 @@ export class ChatlistPreview {
} }
/** List of all chats contained in the chatlist */ /** List of all chats contained in the chatlist */
get chats(): Chat[] { get chats(): Peer[] {
let peers let peers
if (this.raw._ === 'chatlists.chatlistInvite') { if (this.raw._ === 'chatlists.chatlistInvite') {
@ -45,7 +46,7 @@ export class ChatlistPreview {
peers = [...this.raw.alreadyPeers, ...this.raw.missingPeers] 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 type { tl } from '@mtcute/tl'
import { MtTypeAssertionError } from '../../../types/errors.js'
import { makeInspectable } from '../../utils/inspectable.js' import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js' import { memoizeGetters } from '../../utils/memoize.js'
import { Photo } from '../media/photo.js' import { Photo } from '../media/photo.js'
import { StickerSet } from '../misc/sticker-set.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 { ChatInviteLink } from './chat-invite-link.js'
import { ChatLocation } from './chat-location.js' import { ChatLocation } from './chat-location.js'
import { Chat } from './chat.js' import { Chat } from './chat.js'
import { PeersIndex } from './peers-index.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 { export class FullChat extends Chat {
constructor( readonly peers: PeersIndex
peer: tl.TypeUser | tl.TypeChat, readonly full: tl.TypeChatFull
readonly fullPeer: tl.TypeUserFull | tl.TypeChatFull,
) { constructor(obj: tl.messages.RawChatFull) {
super(peer) const peers = PeersIndex.from(obj)
super(peers.chat(obj.fullChat.id))
this.peers = peers
this.full = obj.fullChat
} }
/** @internal */ /** Whether this chat is archived */
static _parse(full: tl.messages.RawChatFull | tl.users.TypeUserFull): FullChat { get isArchived(): boolean {
const peers = PeersIndex.from(full) return this.full._ === 'channelFull' && this.full.folderId === 1
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 paid reactions are enabled for this channel */ /** Whether paid reactions are enabled for this channel */
get hasPaidReactions(): boolean { 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 * that the user may have set
*/ */
get fullPhoto(): Photo | null { get fullPhoto(): Photo | null {
if (!this.fullPeer) return null if (!this.full) return null
let photo: tl.TypePhoto | undefined const photo = this.full.chatPhoto
switch (this.fullPeer._) {
case 'userFull':
photo = this.fullPeer.personalPhoto ?? this.fullPeer.profilePhoto ?? this.fullPeer.fallbackPhoto
break
case 'chatFull':
case 'channelFull':
photo = this.fullPeer.chatPhoto
}
if (photo?._ !== 'photo') return null if (photo?._ !== 'photo') return null
return new Photo(photo) 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 * Bio of the other party in a private chat, or description of a
* group, supergroup or channel. * group, supergroup or channel.
*/ */
get bio(): string { 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 { get inviteLink(): ChatInviteLink | null {
if (this.fullPeer && this.fullPeer._ !== 'userFull') { switch (this.full.exportedInvite?._) {
switch (this.fullPeer.exportedInvite?._) { case 'chatInvitePublicJoinRequests':
case 'chatInvitePublicJoinRequests': return null
return null case 'chatInviteExported':
case 'chatInviteExported': return new ChatInviteLink(this.full.exportedInvite)
return new ChatInviteLink(this.fullPeer.exportedInvite)
}
} }
return null return null
@ -162,132 +165,208 @@ export class FullChat extends Chat {
* For supergroups, information about the group sticker set. * For supergroups, information about the group sticker set.
*/ */
get stickerSet(): StickerSet | null { 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. * For supergroups, information about the group emoji set.
*/ */
get emojiSet(): StickerSet | null { 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. * Whether the group sticker set can be changed by you.
*/ */
get canSetStickerSet(): boolean | null { 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. * Number of boosts applied by the current user to this chat.
*/ */
get boostsApplied(): number { 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. * Number of boosts required for the user to be unrestricted in this chat.
*/ */
get boostsForUnrestrict(): number { get boostsForUnrestrict(): number {
if (!this.fullPeer || this.fullPeer._ !== 'channelFull') return 0 if (!this.full || this.full._ !== 'channelFull') return 0
return this.fullPeer?.boostsUnrestrict ?? 0 return this.full?.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
} }
/** Whether the current user can view Telegram Stars revenue for this chat */ /** Whether the current user can view Telegram Stars revenue for this chat */
get canViewStarsRevenue(): boolean { 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 */ /** Whether the current user can view ad revenue for this chat */
get canViewAdRevenue(): boolean { 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 */ /** Number of admins in the chat (0 if not available) */
get canManageBotEmojiStatus(): boolean { get adminsCount(): number {
return this.fullPeer._ === 'userFull' && this.fullPeer.botCanManageEmojiStatus! 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 { get migratedFrom(): { chatId: number, msgId: number } | null {
switch (this.fullPeer._) { if (this.full._ !== 'channelFull') return null
case 'userFull': if (!this.full.migratedFromChatId || !this.full.migratedFromMaxId) return null
return null
case 'chatFull':
if (this.fullPeer.participants._ !== 'chatParticipants') {
return null
}
return this.fullPeer.participants.participants.length return {
case 'channelFull': chatId: this.full.migratedFromChatId.toNumber(),
return this.fullPeer.participantsCount ?? null 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. * Location of the chat.
*/ */
get location(): ChatLocation | null { 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 null
} }
return new ChatLocation(this.fullPeer.location) return new ChatLocation(this.full.location)
} }
private _linkedChat?: Chat
/** /**
* Information about a linked chat: * Information about a linked chat:
* - for channels: the discussion group * - for channels: the discussion group
* - for supergroups: the linked channel * - for supergroups: the linked channel
* - for users: the personal channel
*/ */
get linkedChat(): Chat | null { 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 * TTL of all messages in this chat, in seconds
*/ */
get ttlPeriod(): number | null { 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 { get slowmodeNextSendDate(): Date | null {
if (!this.fullPeer || this.fullPeer._ !== 'userFull') return 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, [ memoizeGetters(FullChat, [
'fullPhoto', 'fullPhoto',
'personalPhoto', 'inviteLink',
'realPhoto',
'publicPhoto',
'location', 'location',
'stickerSet', 'stickerSet',
'emojiSet', 'emojiSet',
'business', 'botInfo',
'linkedChat',
'recentRequesters',
'stories',
]) ])
makeInspectable(FullChat) 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-event/index.js'
export * from './chat-invite-link-member.js' export * from './chat-invite-link-member.js'
export * from './chat-invite-link.js' export * from './chat-invite-link.js'
@ -10,6 +11,7 @@ export * from './chat.js'
export * from './chatlist-preview.js' export * from './chatlist-preview.js'
export * from './forum-topic.js' export * from './forum-topic.js'
export * from './full-chat.js' export * from './full-chat.js'
export * from './full-user.js'
export * from './peer.js' export * from './peer.js'
export * from './peers-index.js' export * from './peers-index.js'
export * from './typing-status.js' export * from './typing-status.js'

View file

@ -128,6 +128,16 @@ export class User {
return this.raw.botAttachMenu! 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 */ /** Whether this bot can be edited by the current user */
get isBotEditable(): boolean { get isBotEditable(): boolean {
return this.raw.botCanEdit! 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 type { InputReaction } from '../reactions/types.js'
import { makeInspectable } from '../../utils/inspectable.js' import { makeInspectable } from '../../utils/inspectable.js'
import { memoizeGetters } from '../../utils/memoize.js' import { memoizeGetters } from '../../utils/memoize.js'
import { Chat } from '../peers/chat.js'
import { parsePeer } from '../peers/peer.js' import { parsePeer } from '../peers/peer.js'
import { ReactionCount } from '../reactions/reaction-count.js' import { ReactionCount } from '../reactions/reaction-count.js'
import { toReactionEmoji } from '../reactions/types.js' import { toReactionEmoji } from '../reactions/types.js'
@ -27,8 +26,8 @@ export class BotReactionUpdate {
/** /**
* Chat where the reaction has been changed * Chat where the reaction has been changed
*/ */
get chat(): Chat { get chat(): Peer {
return Chat._parseFromPeer(this.raw.peer, this._peers) return parsePeer(this.raw.peer, this._peers)
} }
/** /**
@ -87,8 +86,8 @@ export class BotReactionCountUpdate {
/** /**
* Chat where the reaction has been changed * Chat where the reaction has been changed
*/ */
get chat(): Chat { get chat(): Peer {
return Chat._parseFromPeer(this.raw.peer, this._peers) return parsePeer(this.raw.peer, this._peers)
} }
/** /**

View file

@ -1,13 +1,14 @@
import type { tl } from '@mtcute/tl' 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 { utf8 } from '@fuman/utils'
import { MtArgumentError } from '../../../types/errors.js' import { MtArgumentError } from '../../../types/errors.js'
import { makeInspectable } from '../../utils/index.js' import { makeInspectable } from '../../utils/index.js'
import { encodeInlineMessageId } from '../../utils/inline-utils.js' import { encodeInlineMessageId } from '../../utils/inline-utils.js'
import { memoizeGetters } from '../../utils/memoize.js' import { memoizeGetters } from '../../utils/memoize.js'
import { Message } from '../messages/message.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' import { User } from '../peers/user.js'
/** Base class for callback queries */ /** Base class for callback queries */
@ -91,12 +92,12 @@ export class CallbackQuery extends BaseCallbackQuery {
/** /**
* Chat where the originating message was sent * Chat where the originating message was sent
*/ */
get chat(): Chat { get chat(): Peer {
if (this.raw._ !== 'updateBotCallbackQuery') { if (this.raw._ !== 'updateBotCallbackQuery') {
throw new MtArgumentError('Cannot get message id for inline callback') 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 { tl } from '@mtcute/tl'
import type { Peer } from '../peers/peer.js'
import type { PeersIndex } from '../peers/peers-index.js' import type { PeersIndex } from '../peers/peers-index.js'
import { makeInspectable } from '../../utils/index.js' import { makeInspectable } from '../../utils/index.js'
import { memoizeGetters } from '../../utils/memoize.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 * One or more messages were deleted from a connected business account
@ -29,8 +30,8 @@ export class DeleteBusinessMessageUpdate {
/** /**
* Chat where the messages were deleted * Chat where the messages were deleted
*/ */
get chat(): Chat { get chat(): Peer {
return Chat._parseFromPeer(this.raw.peer, this._peers) 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 { TelegramClient } from '@mtcute/core/client.js'
import type { import type {
DeleteMessagesParams, DeleteMessagesParams,
@ -55,7 +55,7 @@ export class MessageContext extends Message implements UpdateContext<Message> {
let res let res
if (this.sender.type === 'user') { if (this.sender.type === 'user') {
[res] = await this.client.getUsers(this.sender) res = await this.client.getUser(this.sender)
} else { } else {
res = await this.client.getChat(this.sender) 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) * 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 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') 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 { BusinessMessageContext } from '../context/business-message.js'
import type { MessageContext } from '../context/message.js' import type { MessageContext } from '../context/message.js'
@ -100,12 +100,10 @@ export function command(commands: MaybeArray<string | RegExp>, {
export const start: UpdateFilter< export const start: UpdateFilter<
MessageContext | BusinessMessageContext, MessageContext | BusinessMessageContext,
{ {
chat: Modify<Chat, { chat: User
chatType: 'private'
}>
command: string[] command: string[]
} }
> = and(chat('private'), command('start')) > = and(chat('user'), command('start'))
/** /**
* Shorthand filter that matches /start commands * Shorthand filter that matches /start commands

View file

@ -8,6 +8,7 @@ import type {
HistoryReadUpdate, HistoryReadUpdate,
MaybeArray, MaybeArray,
Message, Message,
Peer,
PollVoteUpdate, PollVoteUpdate,
User, User,
UserTypingUpdate, UserTypingUpdate,
@ -20,20 +21,26 @@ import type { EmptyObject, Modify, UpdateFilter } from './types.js'
/** /**
* Filter updates by type of the chat where they happened * 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, Obj,
{ {
chat: Modify<Chat, { chatType: T }> chat: T extends 'user'
} & (Obj extends Message ? User
? T extends 'private' | 'bot' | 'group' : Modify<Chat, { chatType: T }>
}
& (Obj extends Message
? T extends 'user' | 'group'
? { ? {
sender: User sender: User
} }
: EmptyObject : {
sender: Chat
}
: EmptyObject) : EmptyObject)
> { > {
return msg => if (type === 'user') return msg => msg.chat.type === 'user'
msg.chat.chatType === type
return msg => msg.chat.type === 'chat' && msg.chat.chatType === type
} }
/** /**
@ -98,7 +105,7 @@ export const chatId: {
const chat = upd.chat const chat = upd.chat
return (matchSelf && chat.isSelf) return (matchSelf && chat.type === 'user' && chat.isSelf)
|| indexId.has(chat.id) || indexId.has(chat.id)
|| Boolean(chat.usernames?.some(u => indexUsername.has(u.username))) || Boolean(chat.usernames?.some(u => indexUsername.has(u.username)))
} }

View file

@ -96,7 +96,7 @@ export const userId: {
case 'edit_business_message': { case 'edit_business_message': {
const sender = upd.sender const sender = upd.sender
return (matchSelf && sender.isSelf) return (matchSelf && sender.type === 'user' && sender.isSelf)
|| indexId.has(sender.id) || indexId.has(sender.id)
|| indexUsername.has(sender.username!) || 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._name === 'new_message' || upd._name === 'new_business_message') {
if (upd.chat.type === 'user') return String(upd.chat.id)
switch (upd.chat.chatType) { switch (upd.chat.chatType) {
case 'private':
case 'bot':
case 'channel': case 'channel':
return String(upd.chat.id) return String(upd.chat.id)
case 'group': case 'group':
@ -49,7 +49,7 @@ export const defaultStateKeyDelegate: StateKeyDelegate = (upd): string | null =>
} }
if (upd._name === 'callback_query') { 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}` return `${upd.chat.id}_${upd.user.id}`
} }