feat(core): initial chatlist support
This commit is contained in:
parent
ce11d535cf
commit
0b57a7be51
11 changed files with 277 additions and 5 deletions
|
@ -91,9 +91,11 @@ import { deleteFolder } from './methods/dialogs/delete-folder.js'
|
|||
import { editFolder } from './methods/dialogs/edit-folder.js'
|
||||
import { findDialogs } from './methods/dialogs/find-dialogs.js'
|
||||
import { findFolder } from './methods/dialogs/find-folder.js'
|
||||
import { getChatlistPreview } from './methods/dialogs/get-chatlist-preview.js'
|
||||
import { getFolders } from './methods/dialogs/get-folders.js'
|
||||
import { getPeerDialogs } from './methods/dialogs/get-peer-dialogs.js'
|
||||
import { iterDialogs } from './methods/dialogs/iter-dialogs.js'
|
||||
import { joinChatlist } from './methods/dialogs/join-chatlist.js'
|
||||
import { setFoldersOrder } from './methods/dialogs/set-folders-order.js'
|
||||
import { downloadAsBuffer } from './methods/files/download-buffer.js'
|
||||
import { downloadAsIterable } from './methods/files/download-iterable.js'
|
||||
|
@ -270,6 +272,7 @@ import {
|
|||
ChatInviteLink,
|
||||
ChatInviteLinkMember,
|
||||
ChatJoinRequestUpdate,
|
||||
ChatlistPreview,
|
||||
ChatMember,
|
||||
ChatMemberUpdate,
|
||||
ChatPreview,
|
||||
|
@ -2205,6 +2208,14 @@ export interface TelegramClient extends ITelegramClient {
|
|||
/** Folder ID */
|
||||
id?: number
|
||||
}): Promise<tl.RawDialogFilter | null>
|
||||
/**
|
||||
* Get a preview of a chatlist by its invite link
|
||||
*
|
||||
* **Available**: ✅ both users and bots
|
||||
*
|
||||
* @param link Invite link
|
||||
*/
|
||||
getChatlistPreview(link: string): Promise<ChatlistPreview>
|
||||
/**
|
||||
* Get list of folders.
|
||||
* **Available**: 👤 users only
|
||||
|
@ -2329,6 +2340,22 @@ export interface TelegramClient extends ITelegramClient {
|
|||
*/
|
||||
filter?: Partial<Omit<tl.RawDialogFilter, '_' | 'id' | 'title'>>
|
||||
}): AsyncIterableIterator<Dialog>
|
||||
/**
|
||||
* Join a chatlist by its link
|
||||
*
|
||||
* **Available**: ✅ both users and bots
|
||||
*
|
||||
* @param link Invite link to the chatlist
|
||||
* @param params Additional parameters
|
||||
* @returns Folder representing the chatlist
|
||||
*/
|
||||
joinChatlist(
|
||||
link: string,
|
||||
params?: {
|
||||
/** Chats to join from the chatlist (all by default) */
|
||||
peers?: MaybeArray<InputPeerLike>
|
||||
},
|
||||
): Promise<tl.RawDialogFilterChatlist>
|
||||
/**
|
||||
* Reorder folders
|
||||
*
|
||||
|
@ -5796,6 +5823,9 @@ TelegramClient.prototype.findDialogs = function (...args) {
|
|||
TelegramClient.prototype.findFolder = function (...args) {
|
||||
return findFolder(this._client, ...args)
|
||||
}
|
||||
TelegramClient.prototype.getChatlistPreview = function (...args) {
|
||||
return getChatlistPreview(this._client, ...args)
|
||||
}
|
||||
TelegramClient.prototype.getFolders = function (...args) {
|
||||
return getFolders(this._client, ...args)
|
||||
}
|
||||
|
@ -5805,6 +5835,9 @@ TelegramClient.prototype.getPeerDialogs = function (...args) {
|
|||
TelegramClient.prototype.iterDialogs = function (...args) {
|
||||
return iterDialogs(this._client, ...args)
|
||||
}
|
||||
TelegramClient.prototype.joinChatlist = function (...args) {
|
||||
return joinChatlist(this._client, ...args)
|
||||
}
|
||||
TelegramClient.prototype.setFoldersOrder = function (...args) {
|
||||
return setFoldersOrder(this._client, ...args)
|
||||
}
|
||||
|
|
|
@ -83,9 +83,11 @@ export { deleteFolder } from './methods/dialogs/delete-folder.js'
|
|||
export { editFolder } from './methods/dialogs/edit-folder.js'
|
||||
export { findDialogs } from './methods/dialogs/find-dialogs.js'
|
||||
export { findFolder } from './methods/dialogs/find-folder.js'
|
||||
export { getChatlistPreview } from './methods/dialogs/get-chatlist-preview.js'
|
||||
export { getFolders } from './methods/dialogs/get-folders.js'
|
||||
export { getPeerDialogs } from './methods/dialogs/get-peer-dialogs.js'
|
||||
export { iterDialogs } from './methods/dialogs/iter-dialogs.js'
|
||||
export { joinChatlist } from './methods/dialogs/join-chatlist.js'
|
||||
export { setFoldersOrder } from './methods/dialogs/set-folders-order.js'
|
||||
export { downloadAsBuffer } from './methods/files/download-buffer.js'
|
||||
export { downloadAsIterable } from './methods/files/download-iterable.js'
|
||||
|
|
|
@ -36,6 +36,7 @@ import {
|
|||
ChatInviteLink,
|
||||
ChatInviteLinkMember,
|
||||
ChatJoinRequestUpdate,
|
||||
ChatlistPreview,
|
||||
ChatMember,
|
||||
ChatMemberUpdate,
|
||||
ChatPreview,
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { ITelegramClient } from '../../client.types.js'
|
||||
import { ChatlistPreview } from '../../types/index.js'
|
||||
|
||||
/**
|
||||
* Get a preview of a chatlist by its invite link
|
||||
*
|
||||
* @param link Invite link
|
||||
*/
|
||||
export async function getChatlistPreview(client: ITelegramClient, link: string): Promise<ChatlistPreview> {
|
||||
const res = await client.call({
|
||||
_: 'chatlists.checkChatlistInvite',
|
||||
slug: link,
|
||||
})
|
||||
|
||||
return new ChatlistPreview(res)
|
||||
}
|
|
@ -2,7 +2,7 @@ import Long from 'long'
|
|||
|
||||
import { tl } from '@mtcute/tl'
|
||||
|
||||
import { MtUnsupportedError } from '../../../types/errors.js'
|
||||
import { MtArgumentError } from '../../../types/errors.js'
|
||||
import { ITelegramClient } from '../../client.types.js'
|
||||
import { Dialog, InputDialogFolder } from '../../types/index.js'
|
||||
import { normalizeDate } from '../../utils/misc-utils.js'
|
||||
|
@ -156,12 +156,76 @@ export async function* iterDialogs(
|
|||
localFilters_ = undefined
|
||||
}
|
||||
|
||||
if (localFilters_?._ === 'dialogFilterChatlist') {
|
||||
throw new MtUnsupportedError('Shared chat folders are not supported yet')
|
||||
}
|
||||
|
||||
const localFilters = localFilters_
|
||||
|
||||
if (localFilters?._ === 'dialogFilterChatlist') {
|
||||
if (offsetId !== 0 || offsetDate !== 0 || offsetPeer._ !== 'inputPeerEmpty') {
|
||||
throw new MtArgumentError('Cannot use offset parameters with chatlist filters')
|
||||
}
|
||||
|
||||
// we only need to fetch pinnedPeers and includePeers
|
||||
// instead of fetching the entire dialog list, we can shortcut
|
||||
// and just fetch the peer dialogs
|
||||
|
||||
let remaining = Math.min(limit, localFilters.includePeers.length + localFilters.pinnedPeers.length)
|
||||
|
||||
if (pinned === 'include' || pinned === 'only') {
|
||||
// yield pinned dialogs
|
||||
|
||||
const peers: tl.TypeInputDialogPeer[] = []
|
||||
|
||||
for (const peer of localFilters.pinnedPeers) {
|
||||
if (remaining <= 0) break
|
||||
remaining--
|
||||
peers.push({
|
||||
_: 'inputDialogPeer',
|
||||
peer,
|
||||
})
|
||||
}
|
||||
|
||||
const res = await client.call({
|
||||
_: 'messages.getPeerDialogs',
|
||||
peers,
|
||||
})
|
||||
|
||||
res.dialogs.forEach((dialog: tl.Mutable<tl.TypeDialog>) => (dialog.pinned = true))
|
||||
|
||||
yield* Dialog.parseTlDialogs(res)
|
||||
}
|
||||
|
||||
if (pinned === 'only' || remaining <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// yield non-pinned dialogs
|
||||
|
||||
let offset = 0
|
||||
|
||||
while (remaining > 0) {
|
||||
const peers: tl.TypeInputDialogPeer[] = []
|
||||
|
||||
for (let i = 0; i < chunkSize; i++) {
|
||||
if (remaining <= 0) break
|
||||
remaining--
|
||||
peers.push({
|
||||
_: 'inputDialogPeer',
|
||||
peer: localFilters.includePeers[offset + i],
|
||||
})
|
||||
}
|
||||
|
||||
offset += chunkSize
|
||||
|
||||
const res = await client.call({
|
||||
_: 'messages.getPeerDialogs',
|
||||
peers,
|
||||
})
|
||||
|
||||
yield* Dialog.parseTlDialogs(res)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (localFilters) {
|
||||
archived = localFilters.excludeArchived ? 'exclude' : 'keep'
|
||||
}
|
||||
|
|
55
packages/core/src/highlevel/methods/dialogs/join-chatlist.ts
Normal file
55
packages/core/src/highlevel/methods/dialogs/join-chatlist.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { tl } from '@mtcute/tl'
|
||||
|
||||
import { MtTypeAssertionError } from '../../../types/errors.js'
|
||||
import { MaybeArray } from '../../../types/utils.js'
|
||||
import { assertTypeIs, isPresent } from '../../../utils/type-assertions.js'
|
||||
import { ITelegramClient } from '../../client.types.js'
|
||||
import { InputPeerLike } from '../../types/index.js'
|
||||
import { assertIsUpdatesGroup } from '../../updates/utils.js'
|
||||
import { resolvePeerMany } from '../users/resolve-peer-many.js'
|
||||
import { getChatlistPreview } from './get-chatlist-preview.js'
|
||||
|
||||
/**
|
||||
* Join a chatlist by its link
|
||||
*
|
||||
* @param link Invite link to the chatlist
|
||||
* @param params Additional parameters
|
||||
* @returns Folder representing the chatlist
|
||||
*/
|
||||
export async function joinChatlist(
|
||||
client: ITelegramClient,
|
||||
link: string,
|
||||
params?: {
|
||||
/** Chats to join from the chatlist (all by default) */
|
||||
peers?: MaybeArray<InputPeerLike>
|
||||
},
|
||||
): Promise<tl.RawDialogFilterChatlist> {
|
||||
let peers: tl.TypeInputPeer[]
|
||||
|
||||
if (params?.peers) {
|
||||
const inputs = Array.isArray(params.peers) ? params.peers : [params.peers]
|
||||
const all = await resolvePeerMany(client, inputs)
|
||||
peers = all.filter(isPresent)
|
||||
} else {
|
||||
const preview = await getChatlistPreview(client, link)
|
||||
peers = preview.chats.filter((it) => !it.isUnavailable).map((it) => it.inputPeer)
|
||||
}
|
||||
|
||||
const res = await client.call({
|
||||
_: 'chatlists.joinChatlistInvite',
|
||||
slug: link,
|
||||
peers,
|
||||
})
|
||||
|
||||
assertIsUpdatesGroup('joinChatlist', res)
|
||||
|
||||
const filter = res.updates.find((it) => it._ === 'updateDialogFilter') as tl.RawUpdateDialogFilter
|
||||
|
||||
if (!filter?.filter) {
|
||||
throw new MtTypeAssertionError('joinChatlist', 'updateDialogFilter', 'nothing')
|
||||
}
|
||||
|
||||
assertTypeIs('joinChatlist', filter.filter, 'dialogFilterChatlist')
|
||||
|
||||
return filter.filter
|
||||
}
|
|
@ -237,6 +237,11 @@ export class Chat {
|
|||
return this.peer._ === 'channel' && this.peer.forum!
|
||||
}
|
||||
|
||||
/** Whether the chat is not available (e.g. because the user was banned from there) */
|
||||
get isUnavailable(): boolean {
|
||||
return this.peer._ === 'chatForbidden' || this.peer._ === 'channelForbidden'
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the current user is a member of the chat.
|
||||
*
|
||||
|
|
52
packages/core/src/highlevel/types/peers/chatlist-preview.ts
Normal file
52
packages/core/src/highlevel/types/peers/chatlist-preview.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { tl } from '@mtcute/tl'
|
||||
|
||||
import { makeInspectable } from '../../utils/inspectable.js'
|
||||
import { memoizeGetters } from '../../utils/memoize.js'
|
||||
import { Chat } from './chat.js'
|
||||
import { PeersIndex } from './peers-index.js'
|
||||
|
||||
/**
|
||||
* Information about a chatlist
|
||||
*/
|
||||
export class ChatlistPreview {
|
||||
readonly peers: PeersIndex
|
||||
constructor(readonly raw: tl.chatlists.TypeChatlistInvite) {
|
||||
this.peers = PeersIndex.from(raw)
|
||||
}
|
||||
|
||||
/** Whether the current user has already joined this chatlist */
|
||||
get isJoined(): boolean {
|
||||
return this.raw._ === 'chatlists.chatlistInviteAlready'
|
||||
}
|
||||
|
||||
/** If we joined the chatlist, ID of the folder representing it */
|
||||
get folderId(): number | null {
|
||||
return this.raw._ === 'chatlists.chatlistInviteAlready' ? this.raw.filterId : null
|
||||
}
|
||||
|
||||
/** Title of the chatlist (only available for non-joined chatlists) */
|
||||
get title(): string {
|
||||
return this.raw._ === 'chatlists.chatlistInvite' ? this.raw.title : ''
|
||||
}
|
||||
|
||||
/** Emoji representing an icon of the chatlist (may only be available for non-joined chatlists) */
|
||||
get emoji(): string | null {
|
||||
return this.raw._ === 'chatlists.chatlistInvite' ? this.raw.emoticon ?? null : null
|
||||
}
|
||||
|
||||
/** List of all chats contained in the chatlist */
|
||||
get chats(): Chat[] {
|
||||
let peers
|
||||
|
||||
if (this.raw._ === 'chatlists.chatlistInvite') {
|
||||
peers = this.raw.peers
|
||||
} else {
|
||||
peers = [...this.raw.alreadyPeers, ...this.raw.missingPeers]
|
||||
}
|
||||
|
||||
return peers.map((peer) => Chat._parseFromPeer(peer, this.peers))
|
||||
}
|
||||
}
|
||||
|
||||
memoizeGetters(ChatlistPreview, ['chats'])
|
||||
makeInspectable(ChatlistPreview)
|
|
@ -7,6 +7,7 @@ export * from './chat-member.js'
|
|||
export * from './chat-permissions.js'
|
||||
export * from './chat-photo.js'
|
||||
export * from './chat-preview.js'
|
||||
export * from './chatlist-preview.js'
|
||||
export * from './forum-topic.js'
|
||||
export * from './full-chat.js'
|
||||
export * from './peer.js'
|
||||
|
|
|
@ -185,4 +185,22 @@ describe('Deep links', function () {
|
|||
expect(links.boost.parse('tg://boost?channel=123')).eql({ channelId: 123 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Shared folder links', () => {
|
||||
it('should generate tg://addlist?slug=XXX links', () => {
|
||||
expect(links.folder({ slug: 'XXX', protocol: 'tg' })).eq('tg://addlist?slug=XXX')
|
||||
})
|
||||
|
||||
it('should generate https://t.me/addlist/XXX links', () => {
|
||||
expect(links.folder({ slug: 'XXX' })).eq('https://t.me/addlist/XXX')
|
||||
})
|
||||
|
||||
it('should parse tg://addlist?slug=XXX links', () => {
|
||||
expect(links.folder.parse('tg://addlist?slug=XXX')).eql({ slug: 'XXX' })
|
||||
})
|
||||
|
||||
it('should parse https://t.me/addlist/XXX links', () => {
|
||||
expect(links.folder.parse('https://t.me/addlist/XXX')).eql({ slug: 'XXX' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -82,3 +82,28 @@ export const boost = deeplinkBuilder<{ username: string } | { channelId: number
|
|||
return { username: path }
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Link to a shared folder (chat list)
|
||||
*/
|
||||
export const folder = deeplinkBuilder<{ slug: string }>({
|
||||
// tg://addlist?slug=XXX
|
||||
internalBuild: ({ slug }) => ['addlist', { slug }],
|
||||
internalParse: (path, query) => {
|
||||
if (path !== 'addlist') return null
|
||||
|
||||
const slug = query.get('slug')
|
||||
if (!slug) return null
|
||||
|
||||
return { slug }
|
||||
},
|
||||
|
||||
// https://t.me/addlist/XXX
|
||||
externalBuild: ({ slug }) => [`addlist/${slug}`, null],
|
||||
externalParse: (path) => {
|
||||
const [prefix, slug] = path.split('/')
|
||||
if (prefix !== 'addlist') return null
|
||||
|
||||
return { slug }
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue