feat(client): folders
This commit is contained in:
parent
69270a66a2
commit
d8c2ef91c4
13 changed files with 663 additions and 148 deletions
|
@ -26,7 +26,6 @@ import { getChatMember } from './methods/chats/get-chat-member'
|
|||
import { getChatMembers } from './methods/chats/get-chat-members'
|
||||
import { getChatPreview } from './methods/chats/get-chat-preview'
|
||||
import { getChat } from './methods/chats/get-chat'
|
||||
import { getDialogs } from './methods/chats/get-dialogs'
|
||||
import { getFullChat } from './methods/chats/get-full-chat'
|
||||
import { iterChatMembers } from './methods/chats/iter-chat-members'
|
||||
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 { setSlowMode } from './methods/chats/set-slow-mode'
|
||||
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 { downloadToFile } from './methods/files/download-file'
|
||||
import { downloadAsIterable } from './methods/files/download-iterable'
|
||||
|
@ -93,6 +97,7 @@ import {
|
|||
InputPeerLike,
|
||||
MaybeDynamic,
|
||||
Message,
|
||||
PartialExcept,
|
||||
PropagationSymbol,
|
||||
ReplyMarkup,
|
||||
SentCode,
|
||||
|
@ -563,45 +568,6 @@ export class TelegramClient extends BaseTelegramClient {
|
|||
getChat(chatId: InputPeerLike): Promise<Chat> {
|
||||
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.
|
||||
*
|
||||
|
@ -775,6 +741,140 @@ export class TelegramClient extends BaseTelegramClient {
|
|||
unarchiveChats(chats: MaybeArray<InputPeerLike>): Promise<void> {
|
||||
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.
|
||||
*
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
UploadedFile,
|
||||
UploadFileLike,
|
||||
InputFileLike,
|
||||
PartialExcept,
|
||||
FileDownloadParameters,
|
||||
UpdateHandler,
|
||||
handlers,
|
||||
|
|
|
@ -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))
|
||||
}
|
49
packages/client/src/methods/dialogs/create-folder.ts
Normal file
49
packages/client/src/methods/dialogs/create-folder.ts
Normal 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
|
||||
}
|
18
packages/client/src/methods/dialogs/delete-folder.ts
Normal file
18
packages/client/src/methods/dialogs/delete-folder.ts
Normal 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,
|
||||
})
|
||||
}
|
38
packages/client/src/methods/dialogs/edit-folder.ts
Normal file
38
packages/client/src/methods/dialogs/edit-folder.ts
Normal 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
|
||||
}
|
255
packages/client/src/methods/dialogs/get-dialogs.ts
Normal file
255
packages/client/src/methods/dialogs/get-dialogs.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
12
packages/client/src/methods/dialogs/get-folders.ts
Normal file
12
packages/client/src/methods/dialogs/get-folders.ts
Normal 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'
|
||||
})
|
||||
}
|
|
@ -8,4 +8,4 @@ export * from './updates'
|
|||
|
||||
export * from './errors'
|
||||
export { MaybeDynamic } from './utils'
|
||||
export { MaybeAsync } from '@mtcute/core'
|
||||
export { MaybeAsync, PartialExcept } from '@mtcute/core'
|
||||
|
|
|
@ -4,6 +4,8 @@ import { Chat } from '../peers'
|
|||
import { Message } from './message'
|
||||
import { DraftMessage } from './draft-message'
|
||||
import { makeInspectable } from '../utils'
|
||||
import { getMarkedPeerId } from '@mtcute/core'
|
||||
import { MtCuteEmptyError } from '../errors'
|
||||
|
||||
/**
|
||||
* A dialog.
|
||||
|
@ -38,6 +40,90 @@ export class Dialog {
|
|||
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
|
||||
*/
|
||||
|
@ -61,6 +147,20 @@ export class Dialog {
|
|||
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
|
||||
/**
|
||||
* Chat that this dialog represents
|
||||
|
@ -71,7 +171,9 @@ export class Dialog {
|
|||
|
||||
let chat
|
||||
if (peer._ === 'peerChannel' || peer._ === 'peerChat') {
|
||||
chat = this._chats[peer._ === 'peerChannel' ? peer.channelId : peer.chatId]
|
||||
chat = this._chats[
|
||||
peer._ === 'peerChannel' ? peer.channelId : peer.chatId
|
||||
]
|
||||
} else {
|
||||
chat = this._users[peer.userId]
|
||||
}
|
||||
|
@ -82,17 +184,22 @@ export class Dialog {
|
|||
return this._chat
|
||||
}
|
||||
|
||||
private _lastMessage?: Message | null
|
||||
private _lastMessage?: Message
|
||||
/**
|
||||
* The latest message sent in this chat
|
||||
*/
|
||||
get lastMessage(): Message | null {
|
||||
if (this._lastMessage === undefined) {
|
||||
get lastMessage(): Message {
|
||||
if (!this._lastMessage) {
|
||||
const cid = this.chat.id
|
||||
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 {
|
||||
this._lastMessage = null
|
||||
throw new MtCuteEmptyError()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -120,7 +227,11 @@ export class Dialog {
|
|||
get draftMessage(): DraftMessage | null {
|
||||
if (this._draftMessage === undefined) {
|
||||
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 {
|
||||
this._draftMessage = null
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ export namespace Chat {
|
|||
* - `group`: Legacy group
|
||||
* - `supergroup`: Supergroup
|
||||
* - `channel`: Broadcast channel
|
||||
* - `gigagroup`: Gigagroup aka Broadcast group
|
||||
*/
|
||||
export type Type =
|
||||
| 'private'
|
||||
|
@ -22,6 +23,7 @@ export namespace Chat {
|
|||
| 'group'
|
||||
| 'supergroup'
|
||||
| 'channel'
|
||||
| 'gigagroup'
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -115,7 +117,9 @@ export class Chat {
|
|||
} else if (this.peer._ === 'chat') {
|
||||
this._type = 'group'
|
||||
} else if (this.peer._ === 'channel') {
|
||||
this._type = this.peer.broadcast
|
||||
this._type = this.peer.gigagroup
|
||||
? 'gigagroup'
|
||||
: this.peer.broadcast
|
||||
? 'channel'
|
||||
: 'supergroup'
|
||||
}
|
||||
|
@ -168,6 +172,11 @@ export class Chat {
|
|||
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
|
||||
*/
|
||||
|
@ -439,7 +448,10 @@ export class Chat {
|
|||
* Number of old messages to be forwarded (0-100).
|
||||
* 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)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { MaybeDynamic, MtCuteError } from '../types'
|
||||
import { BigInteger } from 'big-integer'
|
||||
import { randomBytes } from '@mtcute/core/dist/utils/buffer-utils'
|
||||
import { bufferToBigInt } from '@mtcute/core/dist/utils/bigint-utils'
|
||||
import { randomBytes } from '@mtcute/core/src/utils/buffer-utils'
|
||||
import { bufferToBigInt } from '@mtcute/core/src/utils/bigint-utils'
|
||||
import { tl } from '@mtcute/tl'
|
||||
|
||||
export const EMPTY_BUFFER = Buffer.alloc(0)
|
||||
|
|
|
@ -2,7 +2,6 @@ import { tl } from '@mtcute/tl'
|
|||
|
||||
export const INVITE_LINK_REGEX = /^(?:https?:\/\/)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)\/joinchat\/)([\w-]+)$/i
|
||||
|
||||
|
||||
// helpers to normalize result of `resolvePeer` function
|
||||
|
||||
export function normalizeToInputPeer(
|
||||
|
@ -81,7 +80,16 @@ export function inputPeerToPeer(inp: tl.TypeInputPeer): tl.TypePeer {
|
|||
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>
|
||||
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.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 }
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue