feat(client): folders

This commit is contained in:
teidesu 2021-04-16 00:12:07 +03:00
parent 69270a66a2
commit d8c2ef91c4
13 changed files with 663 additions and 148 deletions

View file

@ -26,7 +26,6 @@ import { getChatMember } from './methods/chats/get-chat-member'
import { getChatMembers } from './methods/chats/get-chat-members' import { getChatMembers } from './methods/chats/get-chat-members'
import { getChatPreview } from './methods/chats/get-chat-preview' import { getChatPreview } from './methods/chats/get-chat-preview'
import { getChat } from './methods/chats/get-chat' import { getChat } from './methods/chats/get-chat'
import { getDialogs } from './methods/chats/get-dialogs'
import { getFullChat } from './methods/chats/get-full-chat' import { getFullChat } from './methods/chats/get-full-chat'
import { iterChatMembers } from './methods/chats/iter-chat-members' import { iterChatMembers } from './methods/chats/iter-chat-members'
import { joinChat } from './methods/chats/join-chat' import { joinChat } from './methods/chats/join-chat'
@ -39,6 +38,11 @@ import { setChatTitle } from './methods/chats/set-chat-title'
import { setChatUsername } from './methods/chats/set-chat-username' import { setChatUsername } from './methods/chats/set-chat-username'
import { setSlowMode } from './methods/chats/set-slow-mode' import { setSlowMode } from './methods/chats/set-slow-mode'
import { unarchiveChats } from './methods/chats/unarchive-chats' import { unarchiveChats } from './methods/chats/unarchive-chats'
import { createFolder } from './methods/dialogs/create-folder'
import { deleteFolder } from './methods/dialogs/delete-folder'
import { editFolder } from './methods/dialogs/edit-folder'
import { getDialogs } from './methods/dialogs/get-dialogs'
import { getFolders } from './methods/dialogs/get-folders'
import { downloadAsBuffer } from './methods/files/download-buffer' import { downloadAsBuffer } from './methods/files/download-buffer'
import { downloadToFile } from './methods/files/download-file' import { downloadToFile } from './methods/files/download-file'
import { downloadAsIterable } from './methods/files/download-iterable' import { downloadAsIterable } from './methods/files/download-iterable'
@ -93,6 +97,7 @@ import {
InputPeerLike, InputPeerLike,
MaybeDynamic, MaybeDynamic,
Message, Message,
PartialExcept,
PropagationSymbol, PropagationSymbol,
ReplyMarkup, ReplyMarkup,
SentCode, SentCode,
@ -563,45 +568,6 @@ export class TelegramClient extends BaseTelegramClient {
getChat(chatId: InputPeerLike): Promise<Chat> { getChat(chatId: InputPeerLike): Promise<Chat> {
return getChat.apply(this, arguments) return getChat.apply(this, arguments)
} }
/**
* Get a chunk of dialogs
*
* You can get up to 100 dialogs at once
*
* @param params Fetch parameters
*/
getDialogs(params?: {
/**
* Offset date used as an anchor for pagination.
*
* Use {@link Dialog.date} for this value.
*/
offsetDate?: Date | number
/**
* Limits the number of dialogs to be received.
*
* Defaults to 100.
*/
limit?: number
/**
* How to handle pinned dialogs?
* Whether to `include` them, `exclude`,
* or `only` return pinned dialogs.
*
* Defaults to `include`
*/
pinned?: 'include' | 'exclude' | 'only'
/**
* Whether to get dialogs from the
* archived dialogs list.
*/
archived?: boolean
}): Promise<Dialog[]> {
return getDialogs.apply(this, arguments)
}
/** /**
* Get full information about a chat. * Get full information about a chat.
* *
@ -775,6 +741,140 @@ export class TelegramClient extends BaseTelegramClient {
unarchiveChats(chats: MaybeArray<InputPeerLike>): Promise<void> { unarchiveChats(chats: MaybeArray<InputPeerLike>): Promise<void> {
return unarchiveChats.apply(this, arguments) return unarchiveChats.apply(this, arguments)
} }
/**
* Create a folder from given parameters
*
* ID for the folder is optional, if not
* provided it will be derived automatically.
*
* @param folder Parameters for the folder
* @returns Newly created folder
*/
createFolder(
folder: PartialExcept<tl.RawDialogFilter, 'title'>
): Promise<tl.RawDialogFilter> {
return createFolder.apply(this, arguments)
}
/**
* Delete a folder by its ID
*
* @param id Folder ID or folder itself
*/
deleteFolder(id: number | tl.RawDialogFilter): Promise<void> {
return deleteFolder.apply(this, arguments)
}
/**
* Edit a folder with given modification
*
* @param folder Folder or folder ID. Note that passing an ID will require re-fetching all folders
* @param modification Modification that will be applied to this folder
* @returns Modified folder
*/
editFolder(
folder: tl.RawDialogFilter | number,
modification: Partial<Omit<tl.RawDialogFilter, 'id' | '_'>>
): Promise<tl.RawDialogFilter> {
return editFolder.apply(this, arguments)
}
/**
* Iterate over dialogs
*
* @param params Fetch parameters
*/
getDialogs(params?: {
/**
* Offset message date used as an anchor for pagination.
*/
offsetDate?: Date | number
/**
* Offset message ID used as an anchor for pagination
*/
offsetId?: number
/**
* Offset peer used as an anchor for pagination
*/
offsetPeer?: tl.TypeInputPeer
/**
* Limits the number of dialogs to be received.
*
* Defaults to `Infinity`, i.e. all dialogs are fetched, ignored when `pinned=only`
*/
limit?: number
/**
* Chunk size which will be passed to `messages.getDialogs`.
* You shouldn't usually care about this.
*
* Defaults to 100.
*/
chunkSize?: number
/**
* How to handle pinned dialogs?
*
* Whether to `include` them at the start of the list,
* `exclude` them at all, or `only` return pinned dialogs.
*
* Additionally, for folders you can specify
* `keep`, which will return pinned dialogs
* ordered by date among other non-pinned dialogs.
*
* Defaults to `include`.
*
* > **Note**: fetching pinned dialogs from
* > folders is slow because of Telegram API limitations.
* > When possible, try to either `exclude` them,
* > or use `keep` and find them using {@link Dialog.findPinned},
* > passing your folder there.
* >
* > Additionally, when using `include` mode with folders,
* > folders will only be fetched if all offset parameters are unset.
*/
pinned?: 'include' | 'exclude' | 'only' | 'keep'
/**
* How to handle archived chats?
*
* Whether to `keep` them among other dialogs,
* `exclude` them from the list, or `only`
* return archived dialogs
*
* Defaults to `exclude`, ignored for folders since folders
* themselves contain information about archived chats.
*
* > **Note**: when fetching `only` pinned dialogs
* > passing `keep` will act as passing `only`
*/
archived?: 'keep' | 'exclude' | 'only'
/**
* Folder from which the dialogs will be fetched.
*
* You can pass folder object, id or title
*
* Note that passing anything except object will
* cause the list of the folders to be fetched,
* and passing a title may fetch from
* a wrong folder if you have multiple with the same title.
*
* When a folder with given ID or title is not found,
* {@link MtCuteArgumentError} is thrown
*
* By default fetches from "All" folder
*/
folder?: string | number | tl.RawDialogFilter
}): AsyncIterableIterator<Dialog> {
return getDialogs.apply(this, arguments)
}
/**
* Get list of folders.
*/
getFolders(): Promise<tl.RawDialogFilter[]> {
return getFolders.apply(this, arguments)
}
/** /**
* Download a file and return its contents as a Buffer. * Download a file and return its contents as a Buffer.
* *

View file

@ -21,6 +21,7 @@ import {
UploadedFile, UploadedFile,
UploadFileLike, UploadFileLike,
InputFileLike, InputFileLike,
PartialExcept,
FileDownloadParameters, FileDownloadParameters,
UpdateHandler, UpdateHandler,
handlers, handlers,

View file

@ -1,94 +0,0 @@
import { TelegramClient } from '../../client'
import { Dialog } from '../../types'
import { normalizeDate } from '../../utils/misc-utils'
import { createUsersChatsIndex } from '../../utils/peer-utils'
import { MtCuteTypeAssertionError } from '../../types'
import { tl } from '@mtcute/tl'
import { getMarkedPeerId } from '@mtcute/core'
/**
* Get a chunk of dialogs
*
* You can get up to 100 dialogs at once
*
* @param params Fetch parameters
* @internal
*/
export async function getDialogs(
this: TelegramClient,
params?: {
/**
* Offset date used as an anchor for pagination.
*
* Use {@link Dialog.date} for this value.
*/
offsetDate?: Date | number
/**
* Limits the number of dialogs to be received.
*
* Defaults to 100.
*/
limit?: number
/**
* How to handle pinned dialogs?
* Whether to `include` them, `exclude`,
* or `only` return pinned dialogs.
*
* Defaults to `include`
*/
pinned?: 'include' | 'exclude' | 'only'
/**
* Whether to get dialogs from the
* archived dialogs list.
*/
archived?: boolean
}
): Promise<Dialog[]> {
if (!params) params = {}
let res
if (params.pinned === 'only') {
res = await this.call({
_: 'messages.getPinnedDialogs',
folderId: params.archived ? 1 : 0,
})
} else {
res = await this.call({
_: 'messages.getDialogs',
excludePinned: params.pinned === 'exclude',
folderId: params.archived ? 1 : 0,
offsetDate: normalizeDate(params.offsetDate) ?? 0,
// offseting by id and peer is useless because when some peer sends
// a message, their dialog goes to the top and we get a cycle
offsetId: 0,
offsetPeer: { _: 'inputPeerEmpty' },
limit: params.limit ?? 100,
hash: 0,
})
}
if (res._ === 'messages.dialogsNotModified')
throw new MtCuteTypeAssertionError(
'getDialogs',
'!messages.dialogsNotModified',
'messages.dialogsNotModified'
)
const { users, chats } = createUsersChatsIndex(res)
const messages: Record<number, tl.TypeMessage> = {}
res.messages.forEach((msg) => {
if (!msg.peerId) return
messages[getMarkedPeerId(msg.peerId)] = msg
})
return res.dialogs
.filter(it => it._ === 'dialog')
.map(it => new Dialog(this, it as tl.RawDialog, users, chats, messages))
}

View file

@ -0,0 +1,49 @@
import { TelegramClient } from '../../client'
import { tl } from '@mtcute/tl'
import { PartialExcept } from '@mtcute/core'
/**
* Create a folder from given parameters
*
* ID for the folder is optional, if not
* provided it will be derived automatically.
*
* @param folder Parameters for the folder
* @returns Newly created folder
* @internal
*/
export async function createFolder(
this: TelegramClient,
folder: PartialExcept<tl.RawDialogFilter, 'title'>
): Promise<tl.RawDialogFilter> {
let id = folder.id
if (!id) {
const old = await this.getFolders()
// determine next id by finding max id
// thanks durov for awesome api
let max = 0
old.forEach((it) => {
if (it.id > max) max = it.id
})
id = max + 1
}
const filter: tl.RawDialogFilter = {
_: 'dialogFilter',
pinnedPeers: [],
includePeers: [],
excludePeers: [],
...folder,
id
}
await this.call({
_: 'messages.updateDialogFilter',
id,
filter
})
return filter
}

View file

@ -0,0 +1,18 @@
import { TelegramClient } from '../../client'
import { tl } from '@mtcute/tl'
/**
* Delete a folder by its ID
*
* @param id Folder ID or folder itself
* @internal
*/
export async function deleteFolder(
this: TelegramClient,
id: number | tl.RawDialogFilter
): Promise<void> {
await this.call({
_: 'messages.updateDialogFilter',
id: typeof id === 'number' ? id : id.id,
})
}

View file

@ -0,0 +1,38 @@
import { TelegramClient } from '../../client'
import { tl } from '@mtcute/tl'
import { MtCuteArgumentError } from '../../types'
/**
* Edit a folder with given modification
*
* @param folder Folder or folder ID. Note that passing an ID will require re-fetching all folders
* @param modification Modification that will be applied to this folder
* @returns Modified folder
* @internal
*/
export async function editFolder(
this: TelegramClient,
folder: tl.RawDialogFilter | number,
modification: Partial<Omit<tl.RawDialogFilter, 'id' | '_'>>
): Promise<tl.RawDialogFilter> {
if (typeof folder === 'number') {
const old = await this.getFolders()
const found = old.find(it => it.id === folder)
if (!found) throw new MtCuteArgumentError(`Could not find a folder with ID ${folder}`)
folder = found
}
const filter: tl.RawDialogFilter = {
...folder,
...modification
}
await this.call({
_: 'messages.updateDialogFilter',
id: folder.id,
filter
})
return filter
}

View file

@ -0,0 +1,255 @@
import { TelegramClient } from '../../client'
import {
Dialog,
MtCuteArgumentError,
MtCuteTypeAssertionError,
} from '../../types'
import { normalizeDate } from '../../utils/misc-utils'
import { createUsersChatsIndex } from '../../utils/peer-utils'
import { tl } from '@mtcute/tl'
import { getMarkedPeerId } from '@mtcute/core'
/**
* Iterate over dialogs.
*
* Note that due to Telegram limitations,
* ordering here can only be anti-chronological
* (i.e. newest - first), and draft update date
* is not considered when sorting.
*
* @param params Fetch parameters
* @internal
*/
export async function* getDialogs(
this: TelegramClient,
params?: {
/**
* Offset message date used as an anchor for pagination.
*/
offsetDate?: Date | number
/**
* Offset message ID used as an anchor for pagination
*/
offsetId?: number
/**
* Offset peer used as an anchor for pagination
*/
offsetPeer?: tl.TypeInputPeer
/**
* Limits the number of dialogs to be received.
*
* Defaults to `Infinity`, i.e. all dialogs are fetched, ignored when `pinned=only`
*/
limit?: number
/**
* Chunk size which will be passed to `messages.getDialogs`.
* You shouldn't usually care about this.
*
* Defaults to 100.
*/
chunkSize?: number
/**
* How to handle pinned dialogs?
*
* Whether to `include` them at the start of the list,
* `exclude` them at all, or `only` return pinned dialogs.
*
* Additionally, for folders you can specify
* `keep`, which will return pinned dialogs
* ordered by date among other non-pinned dialogs.
*
* Defaults to `include`.
*
* > **Note**: When using `include` mode with folders,
* > pinned dialogs will only be fetched if all offset
* > parameters are unset.
*/
pinned?: 'include' | 'exclude' | 'only' | 'keep'
/**
* How to handle archived chats?
*
* Whether to `keep` them among other dialogs,
* `exclude` them from the list, or `only`
* return archived dialogs
*
* Defaults to `exclude`, ignored for folders since folders
* themselves contain information about archived chats.
*
* > **Note**: when fetching `only` pinned dialogs
* > passing `keep` will act as passing `only`
*/
archived?: 'keep' | 'exclude' | 'only'
/**
* Folder from which the dialogs will be fetched.
*
* You can pass folder object, id or title
*
* Note that passing anything except object will
* cause the list of the folders to be fetched,
* and passing a title may fetch from
* a wrong folder if you have multiple with the same title.
*
* Also note that fetching dialogs in a folder is
* *orders of magnitudes* slower than normal because
* of Telegram API limitations - we have to fetch all dialogs
* and filter the ones we need manually. If possible,
* use {@link Dialog.filterFolder} instead.
*
* When a folder with given ID or title is not found,
* {@link MtCuteArgumentError} is thrown
*
* By default fetches from "All" folder
*/
folder?: string | number | tl.RawDialogFilter
}
): AsyncIterableIterator<Dialog> {
if (!params) params = {}
// fetch folder if needed
let filters: tl.RawDialogFilter | undefined
if (
typeof params.folder === 'string' ||
typeof params.folder === 'number'
) {
const folders = await this.getFolders()
const found = folders.find(
(it) => it.id === params!.folder || it.title === params!.folder
)
if (!found)
throw new MtCuteArgumentError(`Could not find folder ${params.folder}`)
filters = found
} else {
filters = params.folder
}
const fetchPinnedDialogsFromFolder = async (): Promise<tl.messages.RawPeerDialogs | null> => {
if (!filters || !filters.pinnedPeers.length) return null
const res = await this.call({
_: 'messages.getPeerDialogs',
peers: filters.pinnedPeers.map((peer) => ({
_: 'inputDialogPeer',
peer
}))
})
res.dialogs.forEach((dialog: tl.Mutable<tl.TypeDialog>) => dialog.pinned = true)
return res
}
const parseDialogs = (
res: tl.messages.TypeDialogs | tl.messages.TypePeerDialogs
): Dialog[] => {
if (res._ === 'messages.dialogsNotModified')
throw new MtCuteTypeAssertionError(
'getDialogs',
'!messages.dialogsNotModified',
'messages.dialogsNotModified'
)
const { users, chats } = createUsersChatsIndex(res)
const messages: Record<number, tl.TypeMessage> = {}
res.messages.forEach((msg) => {
if (!msg.peerId) return
messages[getMarkedPeerId(msg.peerId)] = msg
})
return res.dialogs
.filter((it) => it._ === 'dialog')
.map(
(it) =>
new Dialog(this, it as tl.RawDialog, users, chats, messages)
)
}
const pinned = params.pinned ?? 'include'
let archived = params.archived ?? 'exclude'
if (filters) {
archived = filters.excludeArchived ? 'exclude' : 'keep'
}
if (pinned === 'only') {
let res
if (filters) {
res = await fetchPinnedDialogsFromFolder()
} else {
res = await this.call({
_: 'messages.getPinnedDialogs',
folderId: archived === 'exclude' ? 0 : 1,
})
}
if (res) yield* parseDialogs(res)
return
}
let current = 0
const total = params.limit ?? Infinity
const chunkSize = Math.min(params.chunkSize ?? 100, total)
let offsetId = params.offsetId ?? 0
let offsetDate = normalizeDate(params.offsetDate) ?? 0
let offsetPeer = params.offsetPeer ?? { _: 'inputPeerEmpty' }
if (filters && filters.pinnedPeers.length && pinned === 'include') {
const res = await fetchPinnedDialogsFromFolder()
if (res) {
const dialogs = parseDialogs(res)
for (const d of dialogs) {
yield d
if (++current >= total) return
}
}
}
const filterFolder = filters
// if pinned is `only`, this wouldn't be reached
// if pinned is `exclude`, we want to exclude them
// if pinned is `include`, we already yielded them, so we also want to exclude them
// if pinned is `keep`, we want to keep them
? Dialog.filterFolder(filters, pinned !== 'keep')
: undefined
const folderId =
archived === 'keep' ? undefined : archived === 'only' ? 1 : 0
for (;;) {
const dialogs = parseDialogs(
await this.call({
_: 'messages.getDialogs',
excludePinned: params.pinned === 'exclude',
folderId,
offsetDate,
offsetId,
offsetPeer,
limit: chunkSize,
hash: 0,
})
)
if (!dialogs.length) return
const last = dialogs[dialogs.length - 1]
offsetPeer = last.chat.inputPeer
offsetId = last.raw.topMessage
offsetDate = normalizeDate(last.lastMessage.date)!
for (const d of dialogs) {
if (filterFolder && !filterFolder(d)) continue
yield d
if (++current >= total) return
}
}
}

View file

@ -0,0 +1,12 @@
import { TelegramClient } from '../../client'
import { tl } from '@mtcute/tl'
/**
* Get list of folders.
* @internal
*/
export async function getFolders(this: TelegramClient): Promise<tl.RawDialogFilter[]> {
return this.call({
_: 'messages.getDialogFilters'
})
}

View file

@ -8,4 +8,4 @@ export * from './updates'
export * from './errors' export * from './errors'
export { MaybeDynamic } from './utils' export { MaybeDynamic } from './utils'
export { MaybeAsync } from '@mtcute/core' export { MaybeAsync, PartialExcept } from '@mtcute/core'

View file

@ -4,6 +4,8 @@ import { Chat } from '../peers'
import { Message } from './message' import { Message } from './message'
import { DraftMessage } from './draft-message' import { DraftMessage } from './draft-message'
import { makeInspectable } from '../utils' import { makeInspectable } from '../utils'
import { getMarkedPeerId } from '@mtcute/core'
import { MtCuteEmptyError } from '../errors'
/** /**
* A dialog. * A dialog.
@ -38,6 +40,90 @@ export class Dialog {
this._messages = messages this._messages = messages
} }
/**
* Find pinned dialogs from a list of dialogs
*
* @param dialogs Dialogs list
* @param folder If passed, status of pin will be checked against this folder, and not globally
*/
static findPinned(
dialogs: Dialog[],
folder?: tl.RawDialogFilter
): Dialog[] {
if (folder) {
const index: Record<number, true> = {}
folder.pinnedPeers.forEach((peer) => {
index[getMarkedPeerId(peer)] = true
})
return dialogs.filter((i) => index[i.chat.id])
}
return dialogs.filter((i) => i.isPinned)
}
/**
* Create a filter predicate for the given Folder.
* Returned predicate can be used in `Array.filter()`
*
* @param folder Folder to filter for
* @param excludePinned Whether to exclude pinned folders
*/
static filterFolder(
folder: tl.RawDialogFilter,
excludePinned = true
): (val: Dialog) => boolean {
const pinned: Record<number, true> = {}
const include: Record<number, true> = {}
const exclude: Record<number, true> = {}
// populate indices
if (excludePinned) {
folder.pinnedPeers.forEach((peer) => {
pinned[getMarkedPeerId(peer)] = true
})
}
folder.includePeers.forEach((peer) => {
include[getMarkedPeerId(peer)] = true
})
folder.excludePeers.forEach((peer) => {
exclude[getMarkedPeerId(peer)] = true
})
return (dialog) => {
const chat = dialog.chat
// manual exclusion/inclusion and pins
if (include[chat.id]) return true
if (exclude[chat.id] || pinned[chat.id]) return false
// exclusions based on status
if (folder.excludeRead && !dialog.isUnread) return false
if (folder.excludeMuted && dialog.isMuted) return false
// even though this was handled in getDialogs, this method
// could be used outside of it, so check again
if (folder.excludeArchived && dialog.isArchived) return false
// inclusions based on chat type
if (folder.contacts && chat.type === 'private' && chat.isContact)
return true
if (
folder.nonContacts &&
chat.type === 'private' &&
!chat.isContact
)
return true
if (
folder.groups &&
(chat.type === 'group' || chat.type === 'supergroup')
)
return true
if (folder.broadcasts && chat.type === 'channel') return true
if (folder.bots && chat.type === 'bot') return true
return false
}
}
/** /**
* Whether this dialog is pinned * Whether this dialog is pinned
*/ */
@ -61,6 +147,20 @@ export class Dialog {
return this.raw.unreadMark || this.raw.unreadCount > 1 return this.raw.unreadMark || this.raw.unreadCount > 1
} }
/**
* Whether this dialog is muted
*/
get isMuted(): boolean {
return !!this.raw.notifySettings.silent
}
/**
* Whether this dialog is archived
*/
get isArchived(): boolean {
return this.raw.folderId === 1
}
private _chat?: Chat private _chat?: Chat
/** /**
* Chat that this dialog represents * Chat that this dialog represents
@ -71,7 +171,9 @@ export class Dialog {
let chat let chat
if (peer._ === 'peerChannel' || peer._ === 'peerChat') { if (peer._ === 'peerChannel' || peer._ === 'peerChat') {
chat = this._chats[peer._ === 'peerChannel' ? peer.channelId : peer.chatId] chat = this._chats[
peer._ === 'peerChannel' ? peer.channelId : peer.chatId
]
} else { } else {
chat = this._users[peer.userId] chat = this._users[peer.userId]
} }
@ -82,17 +184,22 @@ export class Dialog {
return this._chat return this._chat
} }
private _lastMessage?: Message | null private _lastMessage?: Message
/** /**
* The latest message sent in this chat * The latest message sent in this chat
*/ */
get lastMessage(): Message | null { get lastMessage(): Message {
if (this._lastMessage === undefined) { if (!this._lastMessage) {
const cid = this.chat.id const cid = this.chat.id
if (cid in this._messages) { if (cid in this._messages) {
this._lastMessage = new Message(this.client, this._messages[cid], this._users, this._chats) this._lastMessage = new Message(
this.client,
this._messages[cid],
this._users,
this._chats
)
} else { } else {
this._lastMessage = null throw new MtCuteEmptyError()
} }
} }
@ -120,7 +227,11 @@ export class Dialog {
get draftMessage(): DraftMessage | null { get draftMessage(): DraftMessage | null {
if (this._draftMessage === undefined) { if (this._draftMessage === undefined) {
if (this.raw.draft?._ === 'draftMessage') { if (this.raw.draft?._ === 'draftMessage') {
this._draftMessage = new DraftMessage(this.client, this.raw.draft, this.chat.inputPeer) this._draftMessage = new DraftMessage(
this.client,
this.raw.draft,
this.chat.inputPeer
)
} else { } else {
this._draftMessage = null this._draftMessage = null
} }

View file

@ -15,6 +15,7 @@ export namespace Chat {
* - `group`: Legacy group * - `group`: Legacy group
* - `supergroup`: Supergroup * - `supergroup`: Supergroup
* - `channel`: Broadcast channel * - `channel`: Broadcast channel
* - `gigagroup`: Gigagroup aka Broadcast group
*/ */
export type Type = export type Type =
| 'private' | 'private'
@ -22,6 +23,7 @@ export namespace Chat {
| 'group' | 'group'
| 'supergroup' | 'supergroup'
| 'channel' | 'channel'
| 'gigagroup'
} }
/** /**
@ -115,7 +117,9 @@ export class Chat {
} else if (this.peer._ === 'chat') { } else if (this.peer._ === 'chat') {
this._type = 'group' this._type = 'group'
} else if (this.peer._ === 'channel') { } else if (this.peer._ === 'channel') {
this._type = this.peer.broadcast this._type = this.peer.gigagroup
? 'gigagroup'
: this.peer.broadcast
? 'channel' ? 'channel'
: 'supergroup' : 'supergroup'
} }
@ -168,6 +172,11 @@ export class Chat {
return this.peer._ === 'user' && this.peer.self! return this.peer._ === 'user' && this.peer.self!
} }
/** Whether this peer is your contact */
get isContact(): boolean {
return this.peer._ === 'user' && this.peer.contact!
}
/** /**
* Title, for supergroups, channels and groups * Title, for supergroups, channels and groups
*/ */
@ -439,7 +448,10 @@ export class Chat {
* Number of old messages to be forwarded (0-100). * Number of old messages to be forwarded (0-100).
* Only applicable to legacy groups, ignored for supergroups and channels * Only applicable to legacy groups, ignored for supergroups and channels
*/ */
async addMembers(users: MaybeArray<InputPeerLike>, forwardCount?: number): Promise<void> { async addMembers(
users: MaybeArray<InputPeerLike>,
forwardCount?: number
): Promise<void> {
return this.client.addChatMembers(this.inputPeer, users, forwardCount) return this.client.addChatMembers(this.inputPeer, users, forwardCount)
} }

View file

@ -1,7 +1,7 @@
import { MaybeDynamic, MtCuteError } from '../types' import { MaybeDynamic, MtCuteError } from '../types'
import { BigInteger } from 'big-integer' import { BigInteger } from 'big-integer'
import { randomBytes } from '@mtcute/core/dist/utils/buffer-utils' import { randomBytes } from '@mtcute/core/src/utils/buffer-utils'
import { bufferToBigInt } from '@mtcute/core/dist/utils/bigint-utils' import { bufferToBigInt } from '@mtcute/core/src/utils/bigint-utils'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
export const EMPTY_BUFFER = Buffer.alloc(0) export const EMPTY_BUFFER = Buffer.alloc(0)

View file

@ -2,7 +2,6 @@ import { tl } from '@mtcute/tl'
export const INVITE_LINK_REGEX = /^(?:https?:\/\/)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)\/joinchat\/)([\w-]+)$/i export const INVITE_LINK_REGEX = /^(?:https?:\/\/)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)\/joinchat\/)([\w-]+)$/i
// helpers to normalize result of `resolvePeer` function // helpers to normalize result of `resolvePeer` function
export function normalizeToInputPeer( export function normalizeToInputPeer(
@ -81,7 +80,16 @@ export function inputPeerToPeer(inp: tl.TypeInputPeer): tl.TypePeer {
return inp as never return inp as never
} }
export function createUsersChatsIndex(obj: { users: tl.TypeUser[], chats: tl.TypeChat[] }): { export function createUsersChatsIndex(
obj: {
users: tl.TypeUser[]
chats: tl.TypeChat[]
},
second?: {
users: tl.TypeUser[]
chats: tl.TypeChat[]
}
): {
users: Record<number, tl.TypeUser> users: Record<number, tl.TypeUser>
chats: Record<number, tl.TypeChat> chats: Record<number, tl.TypeChat>
} { } {
@ -90,5 +98,10 @@ export function createUsersChatsIndex(obj: { users: tl.TypeUser[], chats: tl.Typ
obj.users.forEach((e) => (users[e.id] = e)) obj.users.forEach((e) => (users[e.id] = e))
obj.chats.forEach((e) => (chats[e.id] = e)) obj.chats.forEach((e) => (chats[e.id] = e))
if (second) {
second.users.forEach((e) => (users[e.id] = e))
second.chats.forEach((e) => (chats[e.id] = e))
}
return { users, chats } return { users, chats }
} }