feat: refactor some iterable methods to have non-iterable counterpart

This commit is contained in:
alina 🌸 2023-09-29 17:02:36 +03:00
parent 59ac74f300
commit 82994408a2
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
22 changed files with 925 additions and 508 deletions

View file

@ -60,6 +60,7 @@ import { getChatMembers } from './methods/chats/get-chat-members'
import { getChatPreview } from './methods/chats/get-chat-preview'
import { getFullChat } from './methods/chats/get-full-chat'
import { getNearbyChats } from './methods/chats/get-nearby-chats'
import { iterChatEventLog } from './methods/chats/iter-chat-event-log'
import { iterChatMembers } from './methods/chats/iter-chat-members'
import { joinChat } from './methods/chats/join-chat'
import { kickChatMember } from './methods/chats/kick-chat-member'
@ -84,10 +85,9 @@ import { createFolder } from './methods/dialogs/create-folder'
import { deleteFolder } from './methods/dialogs/delete-folder'
import { editFolder } from './methods/dialogs/edit-folder'
import { findFolder } from './methods/dialogs/find-folder'
import { getDialogs } from './methods/dialogs/get-dialogs'
import { getFolders } from './methods/dialogs/get-folders'
import { _normalizeInputFolder, getFolders } from './methods/dialogs/get-folders'
import { getPeerDialogs } from './methods/dialogs/get-peer-dialogs'
import { _parseDialogs } from './methods/dialogs/parse-dialogs'
import { iterDialogs } from './methods/dialogs/iter-dialogs'
import { setFoldersOrder } from './methods/dialogs/set-folders'
import { downloadAsBuffer } from './methods/files/download-buffer'
import { downloadToFile } from './methods/files/download-file'
@ -107,6 +107,8 @@ import { getInviteLinks } from './methods/invite-links/get-invite-links'
import { getPrimaryInviteLink } from './methods/invite-links/get-primary-invite-link'
import { hideAllJoinRequests } from './methods/invite-links/hide-all-join-requests'
import { hideJoinRequest } from './methods/invite-links/hide-join-request'
import { iterInviteLinkMembers } from './methods/invite-links/iter-invite-link-members'
import { iterInviteLinks } from './methods/invite-links/iter-invite-links'
import { revokeInviteLink } from './methods/invite-links/revoke-invite-link'
import { closePoll } from './methods/messages/close-poll'
import { deleteMessages } from './methods/messages/delete-messages'
@ -198,10 +200,9 @@ import {
BotStoppedUpdate,
CallbackQuery,
Chat,
ChatAction,
ChatEvent,
ChatInviteLink,
ChatInviteLinkJoinedMember,
ChatInviteLinkMember,
ChatJoinRequestUpdate,
ChatMember,
ChatMemberUpdate,
@ -216,6 +217,8 @@ import {
HistoryReadUpdate,
IMessageEntityParser,
InlineQuery,
InputChatEventFilters,
InputDialogFolder,
InputFileLike,
InputInlineResult,
InputMediaLike,
@ -1123,8 +1126,7 @@ export interface TelegramClient extends BaseTelegramClient {
rank?: string,
): Promise<void>
/**
* Get chat event log ("Recent actions" in official
* clients).
* Get chat event log ("Recent actions" in official clients).
*
* Only available for supergroups and channels, and
* requires (any) administrator rights.
@ -1171,23 +1173,24 @@ export interface TelegramClient extends BaseTelegramClient {
* and when passing one or more action types,
* they will be filtered locally.
*/
filters?: tl.TypeChannelAdminLogEventsFilter | MaybeArray<Exclude<ChatAction, null>['type']>
filters?: InputChatEventFilters
/**
* Limit the number of events returned.
*
* Defaults to `Infinity`, i.e. all events are returned
* > Note: when using filters, there will likely be
* > less events returned than specified here.
* > This limit is only used to limit the number of
* > events to fetch from the server.
* >
* > If you need to limit the number of events
* > returned, use {@link iterChatEventLog} instead.
*
* @default 100
*/
limit?: number
/**
* Chunk size, usually not needed.
*
* Defaults to `100`
*/
chunkSize?: number
},
): AsyncIterableIterator<ChatEvent>
): Promise<ChatEvent[]>
/**
* Get information about a single chat member
*
@ -1277,6 +1280,33 @@ export interface TelegramClient extends BaseTelegramClient {
* @param longitude Longitude of the location
*/
getNearbyChats(latitude: number, longitude: number): Promise<Chat[]>
/**
* Iterate over chat event log.
*
* Small wrapper over {@link getChatEventLog}
*
* @param chatId Chat ID
* @param params
*/
iterChatEventLog(
chatId: InputPeerLike,
params?: Parameters<TelegramClient['getChatEventLog']>[1] & {
/**
* Total number of events to return.
*
* @default Infinity
*/
limit?: number
/**
* Chunk size, passed as `limit` to {@link getChatEventLog}.
* Usually you don't need to touch this.
*
* @default 100
*/
chunkSize?: number
},
): AsyncIterableIterator<ChatEvent>
/**
* Iterate through chat members
*
@ -1570,17 +1600,35 @@ export interface TelegramClient extends BaseTelegramClient {
* @param params Search parameters. At least one must be set.
*/
findFolder(params: { title?: string; emoji?: string; id?: number }): Promise<tl.RawDialogFilter | null>
/**
* Get list of folders.
*/
getFolders(): Promise<tl.TypeDialogFilter[]>
_normalizeInputFolder(folder: InputDialogFolder): Promise<tl.TypeDialogFilter>
/**
* Get dialogs with certain peers.
*
* @param peers Peers for which to fetch dialogs.
*/
getPeerDialogs(peers: InputPeerLike): Promise<Dialog>
/**
* Get dialogs with certain peers.
*
* @param peers Peers for which to fetch dialogs.
*/
getPeerDialogs(peers: InputPeerLike[]): Promise<Dialog[]>
/**
* Iterate over dialogs.
*
* Note that due to Telegram limitations,
* Note that due to Telegram API 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
*/
getDialogs(params?: {
iterDialogs(params?: {
/**
* Offset message date used as an anchor for pagination.
*/
@ -1599,7 +1647,7 @@ export interface TelegramClient extends BaseTelegramClient {
/**
* Limits the number of dialogs to be received.
*
* Defaults to `Infinity`, i.e. all dialogs are fetched, ignored when `pinned=only`
* @default `Infinity`, i.e. all dialogs are fetched
*/
limit?: number
@ -1607,7 +1655,7 @@ export interface TelegramClient extends BaseTelegramClient {
* Chunk size which will be passed to `messages.getDialogs`.
* You shouldn't usually care about this.
*
* Defaults to 100.
* @default 100.
*/
chunkSize?: number
@ -1621,11 +1669,11 @@ export interface TelegramClient extends BaseTelegramClient {
* `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.
*
* @default `include`.
*/
pinned?: 'include' | 'exclude' | 'only' | 'keep'
@ -1636,11 +1684,13 @@ export interface TelegramClient extends BaseTelegramClient {
* `exclude` them from the list, or `only`
* return archived dialogs
*
* Defaults to `exclude`, ignored for folders since folders
* Ignored for folders, since folders
* themselves contain information about archived chats.
*
* > **Note**: when fetching `only` pinned dialogs
* > passing `keep` will act as passing `only`
* > **Note**: when `pinned=only`, `archived=keep` will act as `only`
* > because of Telegram API limitations.
*
* @default `exclude`
*/
archived?: 'keep' | 'exclude' | 'only'
@ -1663,9 +1713,9 @@ export interface TelegramClient extends BaseTelegramClient {
* When a folder with given ID or title is not found,
* {@link MtArgumentError} is thrown
*
* By default fetches from "All" folder
* @default <empty> (fetches from "All" folder)
*/
folder?: string | number | tl.RawDialogFilter
folder?: InputDialogFolder
/**
* Additional filtering for the dialogs.
@ -1676,24 +1726,6 @@ export interface TelegramClient extends BaseTelegramClient {
*/
filter?: Partial<Omit<tl.RawDialogFilter, '_' | 'id' | 'title'>>
}): AsyncIterableIterator<Dialog>
/**
* Get list of folders.
*/
getFolders(): Promise<tl.TypeDialogFilter[]>
/**
* Get dialogs with certain peers.
*
* @param peers Peers for which to fetch dialogs.
*/
getPeerDialogs(peers: InputPeerLike): Promise<Dialog>
/**
* Get dialogs with certain peers.
*
* @param peers Peers for which to fetch dialogs.
*/
getPeerDialogs(peers: InputPeerLike[]): Promise<Dialog[]>
_parseDialogs(res: tl.messages.TypeDialogs | tl.messages.TypePeerDialogs): Dialog[]
/**
* Reorder folders
*
@ -1943,10 +1975,22 @@ export interface TelegramClient extends BaseTelegramClient {
link?: string
/**
* Maximum number of users to return (by default returns all)
* Maximum number of users to return
*
* @default 100
*/
limit?: number
/**
* Offset request/join date used as an anchor for pagination.
*/
offsetDate?: Date | number
/**
* Offset user used as an anchor for pagination
*/
offsetUser?: tl.TypeInputUser
/**
* Whether to get users who have requested to join
* the chat but weren't accepted yet
@ -1955,13 +1999,13 @@ export interface TelegramClient extends BaseTelegramClient {
/**
* Search for a user in the pending join requests list
* (only works if {@link requested} is true)
* (if passed, {@link requested} is assumed to be true)
*
* Doesn't work when {@link link} is set (Telegram limitation)
*/
requestedSearch?: string
},
): AsyncIterableIterator<ChatInviteLinkJoinedMember>
): Promise<ArrayWithTotal<ChatInviteLinkMember>>
/**
* Get detailed information about an invite link
*
@ -1982,8 +2026,14 @@ export interface TelegramClient extends BaseTelegramClient {
*/
getInviteLinks(
chatId: InputPeerLike,
adminId: InputPeerLike,
params?: {
/**
* Only return this admin's links.
*
* @default `"self"`
*/
admin?: InputPeerLike
/**
* Whether to fetch revoked invite links
*/
@ -1991,18 +2041,22 @@ export interface TelegramClient extends BaseTelegramClient {
/**
* Limit the number of invite links to be fetched.
* By default, all links are fetched.
*
* @default 100
*/
limit?: number
/**
* Size of chunks which are fetched. Usually not needed.
*
* Defaults to `100`
* Offset date used as an anchor for pagination.
*/
chunkSize?: number
offsetDate?: Date | number
/**
* Offset link used as an anchor for pagination
*/
offsetLink?: string
},
): AsyncIterableIterator<ChatInviteLink>
): Promise<ArrayWithTotal<ChatInviteLink>>
/**
* Get primary invite link of a chat
*
@ -2025,6 +2079,60 @@ export interface TelegramClient extends BaseTelegramClient {
* @param action Whether to approve or deny the join request
*/
hideJoinRequest(peer: InputPeerLike, user: InputPeerLike, action: 'approve' | 'deny'): Promise<void>
/**
* Iterate over users who have joined
* the chat with the given invite link.
*
* @param chatId Chat ID
* @param params Additional params
*/
iterInviteLinkMembers(
chatId: InputPeerLike,
params: Parameters<TelegramClient['getInviteLinkMembers']>[1] & {
/**
* Maximum number of users to return
*
* @default `Infinity`, i.e. all users are fetched
*/
limit?: number
/**
* Chunk size which will be passed to `messages.getChatInviteImporters`.
* You shouldn't usually care about this.
*
* @default 100.
*/
chunkSize?: number
},
): AsyncIterableIterator<ChatInviteLinkMember>
/**
* Iterate over invite links created by some administrator in the chat.
*
* As an administrator you can only get your own links
* (i.e. `adminId = "self"`), as a creator you can get
* any other admin's links.
*
* @param chatId Chat ID
* @param adminId Admin who created the links
* @param params
*/
iterInviteLinks(
chatId: InputPeerLike,
params?: Parameters<TelegramClient['getInviteLinks']>[1] & {
/**
* Limit the number of invite links to be fetched.
* By default, all links are fetched.
*/
limit?: number
/**
* Size of chunks which are fetched. Usually not needed.
*
* Defaults to `100`
*/
chunkSize?: number
},
): AsyncIterableIterator<ChatInviteLink>
/**
* Revoke an invite link.
*
@ -3921,6 +4029,7 @@ export class TelegramClient extends BaseTelegramClient {
getChat = getChat
getFullChat = getFullChat
getNearbyChats = getNearbyChats
iterChatEventLog = iterChatEventLog
iterChatMembers = iterChatMembers
joinChat = joinChat
kickChatMember = kickChatMember
@ -3946,10 +4055,10 @@ export class TelegramClient extends BaseTelegramClient {
deleteFolder = deleteFolder
editFolder = editFolder
findFolder = findFolder
getDialogs = getDialogs
getFolders = getFolders
_normalizeInputFolder = _normalizeInputFolder
getPeerDialogs = getPeerDialogs
_parseDialogs = _parseDialogs
iterDialogs = iterDialogs
setFoldersOrder = setFoldersOrder
downloadAsBuffer = downloadAsBuffer
downloadToFile = downloadToFile
@ -3969,6 +4078,8 @@ export class TelegramClient extends BaseTelegramClient {
getPrimaryInviteLink = getPrimaryInviteLink
hideAllJoinRequests = hideAllJoinRequests
hideJoinRequest = hideJoinRequest
iterInviteLinkMembers = iterInviteLinkMembers
iterInviteLinks = iterInviteLinks
revokeInviteLink = revokeInviteLink
closePoll = closePoll
deleteMessages = deleteMessages

View file

@ -17,10 +17,9 @@ import {
BotStoppedUpdate,
CallbackQuery,
Chat,
ChatAction,
ChatEvent,
ChatInviteLink,
ChatInviteLinkJoinedMember,
ChatInviteLinkMember,
ChatJoinRequestUpdate,
ChatMember,
ChatMemberUpdate,
@ -35,6 +34,8 @@ import {
HistoryReadUpdate,
IMessageEntityParser,
InlineQuery,
InputChatEventFilters,
InputDialogFolder,
InputFileLike,
InputInlineResult,
InputMediaLike,

View file

@ -1,14 +1,14 @@
import Long from 'long'
import { assertNever, MaybeArray, tl } from '@mtcute/core'
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { ChatAction, ChatEvent, InputPeerLike, PeersIndex } from '../../types'
import { ChatEvent, InputPeerLike, PeersIndex } from '../../types'
import { InputChatEventFilters, normalizeChatEventFilters } from '../../types/peers/chat-event/filters'
import { normalizeToInputChannel, normalizeToInputUser } from '../../utils/peer-utils'
/**
* Get chat event log ("Recent actions" in official
* clients).
* Get chat event log ("Recent actions" in official clients).
*
* Only available for supergroups and channels, and
* requires (any) administrator rights.
@ -22,7 +22,7 @@ import { normalizeToInputChannel, normalizeToInputUser } from '../../utils/peer-
* @param params
* @internal
*/
export async function* getChatEventLog(
export async function getChatEventLog(
this: TelegramClient,
chatId: InputPeerLike,
params?: {
@ -57,165 +57,62 @@ export async function* getChatEventLog(
* and when passing one or more action types,
* they will be filtered locally.
*/
filters?: tl.TypeChannelAdminLogEventsFilter | MaybeArray<Exclude<ChatAction, null>['type']>
filters?: InputChatEventFilters
/**
* Limit the number of events returned.
*
* Defaults to `Infinity`, i.e. all events are returned
* > Note: when using filters, there will likely be
* > less events returned than specified here.
* > This limit is only used to limit the number of
* > events to fetch from the server.
* >
* > If you need to limit the number of events
* > returned, use {@link iterChatEventLog} instead.
*
* @default 100
*/
limit?: number
/**
* Chunk size, usually not needed.
*
* Defaults to `100`
*/
chunkSize?: number
},
): AsyncIterableIterator<ChatEvent> {
): Promise<ChatEvent[]> {
if (!params) params = {}
const channel = normalizeToInputChannel(await this.resolvePeer(chatId), chatId)
let current = 0
let maxId = params.maxId ?? Long.ZERO
const minId = params.minId ?? Long.ZERO
const query = params.query ?? ''
const { maxId = Long.ZERO, minId = Long.ZERO, query = '', limit = 100, users, filters } = params
const total = params.limit || Infinity
const chunkSize = Math.min(params.chunkSize ?? 100, total)
const admins: tl.TypeInputUser[] | undefined = params.users ?
await this.resolvePeerMany(params.users, normalizeToInputUser) :
const admins: tl.TypeInputUser[] | undefined = users ?
await this.resolvePeerMany(users, normalizeToInputUser) :
undefined
let serverFilter: tl.Mutable<tl.TypeChannelAdminLogEventsFilter> | undefined = undefined
let localFilter: Record<string, true> | undefined = undefined
const { serverFilter, localFilter } = normalizeChatEventFilters(filters)
if (params.filters) {
if (typeof params.filters === 'string' || Array.isArray(params.filters)) {
let input = params.filters
if (!Array.isArray(input)) input = [input]
const res = await this.call({
_: 'channels.getAdminLog',
channel,
q: query,
eventsFilter: serverFilter,
admins,
maxId,
minId,
limit,
})
serverFilter = {
_: 'channelAdminLogEventsFilter',
}
localFilter = {}
if (!res.events.length) return []
input.forEach((type) => {
localFilter![type] = true
const peers = PeersIndex.from(res)
switch (type) {
case 'user_joined':
serverFilter!.join = true
break
case 'user_left':
serverFilter!.leave = true
break
case 'user_invited':
serverFilter!.invite = true
break
case 'title_changed':
case 'description_changed':
case 'linked_chat_changed':
case 'location_changed':
case 'photo_changed':
case 'username_changed':
case 'stickerset_changed':
serverFilter!.info = true
break
case 'invites_toggled':
case 'history_toggled':
case 'signatures_toggled':
case 'def_perms_changed':
serverFilter!.settings = true
break
case 'msg_pinned':
serverFilter!.pinned = true
break
case 'msg_edited':
case 'poll_stopped':
serverFilter!.edit = true
break
case 'msg_deleted':
serverFilter!.delete = true
break
case 'user_perms_changed':
serverFilter!.ban = true
serverFilter!.unban = true
serverFilter!.kick = true
serverFilter!.unkick = true
break
case 'user_admin_perms_changed':
serverFilter!.promote = true
serverFilter!.demote = true
break
case 'slow_mode_changed':
case 'ttl_changed':
// not documented so idk, enable both
serverFilter!.settings = true
serverFilter!.info = true
break
case 'call_started':
case 'call_ended':
serverFilter!.groupCall = true
break
case 'call_setting_changed':
// not documented so idk, enable all
serverFilter!.groupCall = true
serverFilter!.settings = true
serverFilter!.info = true
break
case 'user_joined_invite':
// not documented so idk, enable all
serverFilter!.join = true
serverFilter!.invite = true
serverFilter!.invites = true
break
case 'invite_deleted':
case 'invite_edited':
case 'invite_revoked':
serverFilter!.invites = true
break
default:
assertNever(type)
}
})
} else {
serverFilter = params.filters
const results: ChatEvent[] = []
for (const evt of res.events) {
const parsed = new ChatEvent(this, evt, peers)
if (localFilter && (!parsed.action || !localFilter[parsed.action.type])) {
continue
}
results.push(parsed)
}
for (;;) {
const res = await this.call({
_: 'channels.getAdminLog',
channel,
q: query,
eventsFilter: serverFilter,
admins,
maxId,
minId,
limit: Math.min(chunkSize, total - current),
})
if (!res.events.length) break
const peers = PeersIndex.from(res)
const last = res.events[res.events.length - 1]
maxId = last.id
for (const evt of res.events) {
const parsed = new ChatEvent(this, evt, peers)
if (localFilter && (!parsed.action || !localFilter[parsed.action.type])) {
continue
}
current += 1
yield parsed
if (current >= total) break
}
}
return results
}

View file

@ -0,0 +1,82 @@
import Long from 'long'
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { ChatEvent, InputPeerLike } from '../../types'
import { normalizeChatEventFilters } from '../../types/peers/chat-event/filters'
import { normalizeToInputChannel, normalizeToInputUser } from '../../utils/peer-utils'
/**
* Iterate over chat event log.
*
* Small wrapper over {@link getChatEventLog}
*
* @param chatId Chat ID
* @param params
* @internal
*/
export async function* iterChatEventLog(
this: TelegramClient,
chatId: InputPeerLike,
params?: Parameters<TelegramClient['getChatEventLog']>[1] & {
/**
* Total number of events to return.
*
* @default Infinity
*/
limit?: number
/**
* Chunk size, passed as `limit` to {@link getChatEventLog}.
* Usually you don't need to touch this.
*
* @default 100
*/
chunkSize?: number
},
): AsyncIterableIterator<ChatEvent> {
if (!params) params = {}
const channel = normalizeToInputChannel(await this.resolvePeer(chatId), chatId)
const { minId = Long.ZERO, query = '', limit = Infinity, chunkSize = 100, users, filters } = params
const admins: tl.TypeInputUser[] | undefined = users ?
await this.resolvePeerMany(users, normalizeToInputUser) :
undefined
const { serverFilter, localFilter } = normalizeChatEventFilters(filters)
let current = 0
let maxId = params.maxId ?? Long.ZERO
for (;;) {
const chunk = await this.getChatEventLog(channel, {
minId,
maxId,
query,
limit: localFilter ? chunkSize : Math.min(limit - current, chunkSize),
// provide already resolved users to avoid resolving them again
users: admins,
// local filters may mess with pagination
filters: { serverFilter },
})
if (!chunk.length) break
const last = chunk[chunk.length - 1]
maxId = last.id
for (const item of chunk) {
if (localFilter && (!item.action || !localFilter[item.action.type])) {
continue
}
current += 1
yield item
if (current >= limit) break
}
}
}

View file

@ -1,6 +1,7 @@
import { tl } from '@mtcute/core'
import { MtArgumentError, tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { InputDialogFolder } from '../../types'
/**
* Get list of folders.
@ -11,3 +12,28 @@ export async function getFolders(this: TelegramClient): Promise<tl.TypeDialogFil
_: 'messages.getDialogFilters',
})
}
/** @internal */
export async function _normalizeInputFolder(
this: TelegramClient,
folder: InputDialogFolder,
): Promise<tl.TypeDialogFilter> {
if (typeof folder === 'string' || typeof folder === 'number') {
const folders = await this.getFolders()
const found = folders.find((it) => {
if (it._ === 'dialogFilterDefault') {
return folder === 0
}
return it.id === folder || it.title === folder
})
if (!found) {
throw new MtArgumentError(`Could not find folder ${folder}`)
}
return found
}
return folder
}

View file

@ -38,7 +38,7 @@ export async function getPeerDialogs(
),
})
const dialogs = this._parseDialogs(res)
const dialogs = Dialog.parseTlDialogs(this, res)
return isSingle ? dialogs[0] : dialogs
}

View file

@ -1,15 +1,15 @@
import Long from 'long'
import { MtArgumentError, tl } from '@mtcute/core'
import { MtUnsupportedError, tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { Dialog } from '../../types'
import { Dialog, InputDialogFolder } from '../../types'
import { normalizeDate } from '../../utils/misc-utils'
/**
* Iterate over dialogs.
*
* Note that due to Telegram limitations,
* Note that due to Telegram API limitations,
* ordering here can only be anti-chronological
* (i.e. newest - first), and draft update date
* is not considered when sorting.
@ -17,7 +17,7 @@ import { normalizeDate } from '../../utils/misc-utils'
* @param params Fetch parameters
* @internal
*/
export async function* getDialogs(
export async function* iterDialogs(
this: TelegramClient,
params?: {
/**
@ -38,7 +38,7 @@ export async function* getDialogs(
/**
* Limits the number of dialogs to be received.
*
* Defaults to `Infinity`, i.e. all dialogs are fetched, ignored when `pinned=only`
* @default `Infinity`, i.e. all dialogs are fetched
*/
limit?: number
@ -46,7 +46,7 @@ export async function* getDialogs(
* Chunk size which will be passed to `messages.getDialogs`.
* You shouldn't usually care about this.
*
* Defaults to 100.
* @default 100.
*/
chunkSize?: number
@ -60,11 +60,11 @@ export async function* getDialogs(
* `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.
*
* @default `include`.
*/
pinned?: 'include' | 'exclude' | 'only' | 'keep'
@ -75,11 +75,13 @@ export async function* getDialogs(
* `exclude` them from the list, or `only`
* return archived dialogs
*
* Defaults to `exclude`, ignored for folders since folders
* Ignored for folders, since folders
* themselves contain information about archived chats.
*
* > **Note**: when fetching `only` pinned dialogs
* > passing `keep` will act as passing `only`
* > **Note**: when `pinned=only`, `archived=keep` will act as `only`
* > because of Telegram API limitations.
*
* @default `exclude`
*/
archived?: 'keep' | 'exclude' | 'only'
@ -102,9 +104,9 @@ export async function* getDialogs(
* When a folder with given ID or title is not found,
* {@link MtArgumentError} is thrown
*
* By default fetches from "All" folder
* @default <empty> (fetches from "All" folder)
*/
folder?: string | number | tl.RawDialogFilter
folder?: InputDialogFolder
/**
* Additional filtering for the dialogs.
@ -118,49 +120,59 @@ export async function* getDialogs(
): AsyncIterableIterator<Dialog> {
if (!params) params = {}
// fetch folder if needed
let filters: tl.TypeDialogFilter | undefined
const { limit = Infinity, chunkSize = 100, folder, filter, pinned = 'include' } = params
if (typeof params.folder === 'string' || typeof params.folder === 'number') {
const folders = await this.getFolders()
const found = folders.find((it) => {
if (it._ === 'dialogFilterDefault') {
return params!.folder === 0
}
let { offsetId = 0, offsetPeer = { _: 'inputPeerEmpty' }, archived = 'exclude' } = params
return it.id === params!.folder || it.title === params!.folder
})
let offsetDate = normalizeDate(params.offsetDate) ?? 0
if (!found) {
throw new MtArgumentError(`Could not find folder ${params.folder}`)
}
let localFilters_: tl.TypeDialogFilter | undefined
filters = found as tl.RawDialogFilter
} else {
filters = params.folder
if (folder) {
localFilters_ = await this._normalizeInputFolder(folder)
}
if (params.filter) {
if (filters) {
filters = {
...filters,
...params.filter,
if (filter) {
if (localFilters_ && localFilters_._ !== 'dialogFilterDefault') {
localFilters_ = {
...localFilters_,
...filter,
}
} else {
filters = {
_: 'dialogFilterDefault',
localFilters_ = {
_: 'dialogFilter',
id: 0,
title: '',
pinnedPeers: [],
includePeers: [],
excludePeers: [],
...params.filter,
}
}
}
if (localFilters_?._ === 'dialogFilterDefault') {
localFilters_ = undefined
}
if (localFilters_?._ === 'dialogFilterChatlist') {
throw new MtUnsupportedError('Shared chat folders are not supported yet')
}
const localFilters = localFilters_
if (localFilters) {
archived = localFilters.excludeArchived ? 'exclude' : 'keep'
}
const fetchPinnedDialogsFromFolder = async (): Promise<tl.messages.RawPeerDialogs | null> => {
if (!filters || filters._ === 'dialogFilterDefault' || !filters.pinnedPeers.length) {
if (!localFilters || !localFilters.pinnedPeers.length) {
return null
}
const res = await this.call({
_: 'messages.getPeerDialogs',
peers: filters.pinnedPeers.map((peer) => ({
_: 'inputDialogPeer',
peers: localFilters.pinnedPeers.map((peer) => ({
_: 'inputDialogPeer' as const,
peer,
})),
})
@ -170,17 +182,10 @@ export async function* getDialogs(
return res
}
const pinned = params.pinned ?? 'include'
let archived = params.archived ?? 'exclude'
if (filters) {
archived = filters._ !== 'dialogFilterDefault' && filters.excludeArchived ? 'exclude' : 'keep'
}
if (pinned === 'only') {
let res
if (filters) {
if (localFilters) {
res = await fetchPinnedDialogsFromFolder()
} else {
res = await this.call({
@ -188,38 +193,36 @@ export async function* getDialogs(
folderId: archived === 'exclude' ? 0 : 1,
})
}
if (res) yield* this._parseDialogs(res)
if (res) yield* Dialog.parseTlDialogs(this, res, limit)
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._ !== 'dialogFilterDefault' && filters.pinnedPeers.length && pinned === 'include') {
if (
localFilters?.pinnedPeers.length &&
pinned === 'include' &&
offsetId === 0 &&
offsetDate === 0 &&
offsetPeer._ === 'inputPeerEmpty'
) {
const res = await fetchPinnedDialogsFromFolder()
if (res) {
const dialogs = this._parseDialogs(res)
const dialogs = Dialog.parseTlDialogs(this, res, limit)
for (const d of dialogs) {
yield d
if (++current >= total) return
if (++current >= limit) return
}
}
}
// 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
const filterFolder = filters ? Dialog.filterFolder(filters, pinned !== 'keep') : undefined
const filterFolder = localFilters ? Dialog.filterFolder(localFilters, pinned !== 'keep') : undefined
let folderId
@ -232,7 +235,8 @@ export async function* getDialogs(
}
for (;;) {
const dialogs = this._parseDialogs(
const dialogs = Dialog.parseTlDialogs(
this,
await this.call({
_: 'messages.getDialogs',
excludePinned: params.pinned === 'exclude',
@ -240,8 +244,7 @@ export async function* getDialogs(
offsetDate,
offsetId,
offsetPeer,
limit: chunkSize,
limit: filterFolder ? chunkSize : Math.min(limit - current, chunkSize),
hash: Long.ZERO,
}),
)
@ -250,13 +253,13 @@ export async function* getDialogs(
const last = dialogs[dialogs.length - 1]
offsetPeer = last.chat.inputPeer
offsetId = last.raw.topMessage
offsetDate = normalizeDate(last.lastMessage?.date) ?? 0
offsetDate = normalizeDate(last.lastMessage.date)!
for (const d of dialogs) {
if (filterFolder && !filterFolder(d)) continue
yield d
if (++current >= total) return
if (++current >= limit) return
}
}
}

View file

@ -1,26 +0,0 @@
import { getMarkedPeerId, tl } from '@mtcute/core'
import { assertTypeIsNot } from '@mtcute/core/utils'
import { TelegramClient } from '../../client'
import { Dialog, PeersIndex } from '../../types'
/** @internal */
export function _parseDialogs(
this: TelegramClient,
res: tl.messages.TypeDialogs | tl.messages.TypePeerDialogs,
): Dialog[] {
assertTypeIsNot('parseDialogs', res, 'messages.dialogsNotModified')
const peers = PeersIndex.from(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, peers, messages))
}

View file

@ -1,7 +1,8 @@
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { ChatInviteLinkJoinedMember, InputPeerLike, PeersIndex, User } from '../../types'
import { ArrayWithTotal, ChatInviteLinkMember, InputPeerLike, PeersIndex } from '../../types'
import { makeArrayWithTotal, normalizeDate } from '../../utils'
/**
* Iterate over users who have joined
@ -11,7 +12,7 @@ import { ChatInviteLinkJoinedMember, InputPeerLike, PeersIndex, User } from '../
* @param params Additional params
* @internal
*/
export async function* getInviteLinkMembers(
export async function getInviteLinkMembers(
this: TelegramClient,
chatId: InputPeerLike,
params: {
@ -21,10 +22,22 @@ export async function* getInviteLinkMembers(
link?: string
/**
* Maximum number of users to return (by default returns all)
* Maximum number of users to return
*
* @default 100
*/
limit?: number
/**
* Offset request/join date used as an anchor for pagination.
*/
offsetDate?: Date | number
/**
* Offset user used as an anchor for pagination
*/
offsetUser?: tl.TypeInputUser
/**
* Whether to get users who have requested to join
* the chat but weren't accepted yet
@ -33,59 +46,36 @@ export async function* getInviteLinkMembers(
/**
* Search for a user in the pending join requests list
* (only works if {@link requested} is true)
* (if passed, {@link requested} is assumed to be true)
*
* Doesn't work when {@link link} is set (Telegram limitation)
*/
requestedSearch?: string
},
): AsyncIterableIterator<ChatInviteLinkJoinedMember> {
): Promise<ArrayWithTotal<ChatInviteLinkMember>> {
const peer = await this.resolvePeer(chatId)
const limit = params.limit ?? Infinity
let current = 0
const { limit = 100, link, requestedSearch, requested = Boolean(requestedSearch) } = params
let offsetDate = 0
let offsetUser: tl.TypeInputUser = { _: 'inputUserEmpty' }
const { offsetUser = { _: 'inputUserEmpty' } } = params
for (;;) {
// for some reason ts needs annotation, idk
const res: tl.RpcCallReturn['messages.getChatInviteImporters'] = await this.call({
_: 'messages.getChatInviteImporters',
limit: Math.min(100, limit - current),
peer,
link: params.link,
requested: params.requested,
q: params.requestedSearch,
offsetDate,
offsetUser,
})
const offsetDate = normalizeDate(params.offsetDate) ?? 0
if (!res.importers.length) break
const res = await this.call({
_: 'messages.getChatInviteImporters',
limit,
peer,
link,
requested,
q: requestedSearch,
offsetDate,
offsetUser,
})
const peers = PeersIndex.from(res)
const peers = PeersIndex.from(res)
const last = res.importers[res.importers.length - 1]
offsetDate = last.date
offsetUser = {
_: 'inputUser',
userId: last.userId,
accessHash: (peers.user(last.userId) as tl.RawUser).accessHash!,
}
for (const it of res.importers) {
const user = new User(this, peers.user(it.userId))
yield {
user,
date: new Date(it.date * 1000),
isPendingRequest: it.requested!,
bio: it.about,
approvedBy: it.approvedBy,
}
}
current += res.importers.length
if (current >= limit) break
}
return makeArrayWithTotal(
res.importers.map((it) => new ChatInviteLinkMember(this, it, peers)),
res.count,
)
}

View file

@ -1,8 +1,6 @@
import { tl } from '@mtcute/core'
import { assertTypeIsNot } from '@mtcute/core/utils'
import { TelegramClient } from '../../client'
import { ChatInviteLink, InputPeerLike, PeersIndex } from '../../types'
import { ArrayWithTotal, ChatInviteLink, InputPeerLike, PeersIndex } from '../../types'
import { makeArrayWithTotal, normalizeDate } from '../../utils'
import { normalizeToInputUser } from '../../utils/peer-utils'
/**
@ -17,11 +15,17 @@ import { normalizeToInputUser } from '../../utils/peer-utils'
* @param params
* @internal
*/
export async function* getInviteLinks(
export async function getInviteLinks(
this: TelegramClient,
chatId: InputPeerLike,
adminId: InputPeerLike,
params?: {
/**
* Only return this admin's links.
*
* @default `"self"`
*/
admin?: InputPeerLike
/**
* Whether to fetch revoked invite links
*/
@ -29,55 +33,43 @@ export async function* getInviteLinks(
/**
* Limit the number of invite links to be fetched.
* By default, all links are fetched.
*
* @default 100
*/
limit?: number
/**
* Size of chunks which are fetched. Usually not needed.
*
* Defaults to `100`
* Offset date used as an anchor for pagination.
*/
chunkSize?: number
offsetDate?: Date | number
/**
* Offset link used as an anchor for pagination
*/
offsetLink?: string
},
): AsyncIterableIterator<ChatInviteLink> {
): Promise<ArrayWithTotal<ChatInviteLink>> {
if (!params) params = {}
let current = 0
const total = params.limit || Infinity
const chunkSize = Math.min(params.chunkSize ?? 100, total)
const { revoked = false, limit = Infinity, admin } = params
const peer = await this.resolvePeer(chatId)
const admin = normalizeToInputUser(await this.resolvePeer(adminId), adminId)
const offsetDate = normalizeDate(params.offsetDate)
const offsetLink = params.offsetLink
let offsetDate: number | undefined = undefined
let offsetLink: string | undefined = undefined
const res = await this.call({
_: 'messages.getExportedChatInvites',
peer: await this.resolvePeer(chatId),
revoked,
adminId: admin ? normalizeToInputUser(await this.resolvePeer(admin), admin) : { _: 'inputUserSelf' },
limit,
offsetDate,
offsetLink,
})
for (;;) {
const res: tl.RpcCallReturn['messages.getExportedChatInvites'] = await this.call({
_: 'messages.getExportedChatInvites',
peer,
adminId: admin,
limit: Math.min(chunkSize, total - current),
offsetDate,
offsetLink,
})
const peers = PeersIndex.from(res)
if (!res.invites.length) break
const peers = PeersIndex.from(res)
const last = res.invites[res.invites.length - 1]
assertTypeIsNot('getInviteLinks', last, 'chatInvitePublicJoinRequests')
offsetDate = last.date
offsetLink = last.link
for (const it of res.invites) {
yield new ChatInviteLink(this, it, peers)
}
current += res.invites.length
if (current >= total) break
}
return makeArrayWithTotal(
res.invites.map((it) => new ChatInviteLink(this, it, peers)),
res.count,
)
}

View file

@ -0,0 +1,63 @@
import { TelegramClient } from '../../client'
import { ChatInviteLinkMember, InputPeerLike } from '../../types'
import { normalizeToInputUser } from '../../utils'
/**
* Iterate over users who have joined
* the chat with the given invite link.
*
* @param chatId Chat ID
* @param params Additional params
* @internal
*/
export async function* iterInviteLinkMembers(
this: TelegramClient,
chatId: InputPeerLike,
params: Parameters<TelegramClient['getInviteLinkMembers']>[1] & {
/**
* Maximum number of users to return
*
* @default `Infinity`, i.e. all users are fetched
*/
limit?: number
/**
* Chunk size which will be passed to `messages.getChatInviteImporters`.
* You shouldn't usually care about this.
*
* @default 100.
*/
chunkSize?: number
},
): AsyncIterableIterator<ChatInviteLinkMember> {
const peer = await this.resolvePeer(chatId)
const { limit = Infinity, chunkSize = 100, link, requestedSearch, requested = Boolean(requestedSearch) } = params
let { offsetDate, offsetUser = { _: 'inputUserEmpty' } } = params
let current = 0
for (;;) {
const items = await this.getInviteLinkMembers(peer, {
limit: Math.min(chunkSize, limit - current),
link,
requested,
requestedSearch,
offsetDate,
offsetUser,
})
if (!items.length) break
const last = items[items.length - 1]
offsetDate = last.date
offsetUser = normalizeToInputUser(last.user.inputPeer)
for (const it of items) {
yield it
if (++current >= limit) return
}
}
}

View file

@ -0,0 +1,67 @@
import { TelegramClient } from '../../client'
import { ChatInviteLink, InputPeerLike } from '../../types'
/**
* Iterate over invite links created by some administrator in the chat.
*
* As an administrator you can only get your own links
* (i.e. `adminId = "self"`), as a creator you can get
* any other admin's links.
*
* @param chatId Chat ID
* @param adminId Admin who created the links
* @param params
* @internal
*/
export async function* iterInviteLinks(
this: TelegramClient,
chatId: InputPeerLike,
params?: Parameters<TelegramClient['getInviteLinks']>[1] & {
/**
* Limit the number of invite links to be fetched.
* By default, all links are fetched.
*/
limit?: number
/**
* Size of chunks which are fetched. Usually not needed.
*
* Defaults to `100`
*/
chunkSize?: number
},
): AsyncIterableIterator<ChatInviteLink> {
if (!params) params = {}
const { revoked = false, limit = Infinity, chunkSize = 100, admin } = params
let { offsetDate, offsetLink } = params
let current = 0
const peer = await this.resolvePeer(chatId)
const adminResolved = admin ? await this.resolvePeer(admin) : ({ _: 'inputUserSelf' } as const)
for (;;) {
const links = await this.getInviteLinks(peer, {
admin: adminResolved,
revoked,
limit: Math.min(chunkSize, limit - current),
offsetDate,
offsetLink,
})
if (!links.length) return
const last = links[links.length - 1]
offsetDate = last.date
offsetLink = last.link
for (const link of links) {
yield link
if (++current >= limit) break
}
}
}

View file

@ -1,12 +1,19 @@
import { getMarkedPeerId, tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { makeInspectable } from '../../utils'
import { assertTypeIsNot, hasValueAtKey, makeInspectable } from '../../utils'
import { MtMessageNotFoundError } from '../errors'
import { Chat, PeersIndex } from '../peers'
import { DraftMessage } from './draft-message'
import { Message } from './message'
/**
* Type used as an input for a folder in client methods
*
* You can pass folder object, id or title
*/
export type InputDialogFolder = string | number | tl.RawDialogFilter
/**
* A dialog.
*
@ -21,6 +28,40 @@ export class Dialog {
readonly _messages: Record<number, tl.TypeMessage>,
) {}
/**
* Parse a list of dialogs from a TL object
*
* @param client Client instance
* @param dialogs TL object
* @param limit Maximum number of dialogs to parse
*/
static parseTlDialogs(
client: TelegramClient,
dialogs: tl.messages.TypeDialogs | tl.messages.TypePeerDialogs,
limit?: number,
): Dialog[] {
assertTypeIsNot('parseDialogs', dialogs, 'messages.dialogsNotModified')
const peers = PeersIndex.from(dialogs)
const messages: Record<number, tl.TypeMessage> = {}
dialogs.messages.forEach((msg) => {
if (!msg.peerId) return
messages[getMarkedPeerId(msg.peerId)] = msg
})
const arr = dialogs.dialogs
.filter(hasValueAtKey('_', 'dialog'))
.map((it) => new Dialog(client, it, peers, messages))
if (limit) {
return arr.slice(0, limit)
}
return arr
}
/**
* Find pinned dialogs from a list of dialogs
*
@ -183,7 +224,7 @@ export class Dialog {
/**
* The latest message sent in this chat
*/
get lastMessage(): Message | null {
get lastMessage(): Message {
if (!this._lastMessage) {
const cid = this.chat.id

View file

@ -1,15 +1,12 @@
import { tl, toggleChannelIdMark } from '@mtcute/core'
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { makeInspectable } from '../../utils'
import { Photo } from '../media'
import { Message } from '../messages'
import { ChatInviteLink } from './chat-invite-link'
import { ChatLocation } from './chat-location'
import { ChatMember } from './chat-member'
import { ChatPermissions } from './chat-permissions'
import { PeersIndex } from './index'
import { User } from './user'
import { PeersIndex, TelegramClient, toggleChannelIdMark } from '../../..'
import { Photo } from '../../media'
import { Message } from '../../messages'
import { ChatInviteLink } from '../chat-invite-link'
import { ChatLocation } from '../chat-location'
import { ChatMember } from '../chat-member'
import { ChatPermissions } from '../chat-permissions'
/** A user has joined the group (in the case of big groups, info of the user that has joined isn't shown) */
export interface ChatActionUserJoined {
@ -321,7 +318,28 @@ export type ChatAction =
| ChatActionTtlChanged
| null
function _actionFromTl(this: ChatEvent, e: tl.TypeChannelAdminLogEventAction): ChatAction {
/** @internal */
export function _actionFromTl(
e: tl.TypeChannelAdminLogEventAction,
client: TelegramClient,
peers: PeersIndex,
): ChatAction {
// todo - MTQ-78
// channelAdminLogEventActionParticipantJoinByRequest#afb6144a invite:ExportedChatInvite approved_by:long
// channelAdminLogEventActionToggleNoForwards#cb2ac766 new_value:Bool = ChannelAdminLogEventAction;
// todo - MTQ-57
// channelAdminLogEventActionChangeUsernames#f04fb3a9 prev_value:Vector<string> new_value:Vector<string>
// todo - MTQ-77
// channelAdminLogEventActionToggleForum#2cc6383 new_value:Bool = ChannelAdminLogEventAction;
// channelAdminLogEventActionCreateTopic#58707d28 topic:ForumTopic = ChannelAdminLogEventAction;
// channelAdminLogEventActionEditTopic#f06fe208 prev_topic:ForumTopic new_topic:ForumTopic
// channelAdminLogEventActionDeleteTopic#ae168909 topic:ForumTopic = ChannelAdminLogEventAction;
// channelAdminLogEventActionPinTopic#5d8d353b flags:# prev_topic:flags.0?ForumTopic new_topic:flags.1?ForumTopic
// todo - MTQ-72
// channelAdminLogEventActionSendMessage#278f2868 message:Message = ChannelAdminLogEventAction;
// channelAdminLogEventActionChangeAvailableReactions#be4e0ef8 prev_value:ChatReactions new_value:ChatReactions
// channelAdminLogEventActionToggleAntiSpam#64f36dfc new_value:Bool = ChannelAdminLogEventAction;
switch (e._) {
case 'channelAdminLogEventActionParticipantJoin':
return { type: 'user_joined' }
@ -346,8 +364,8 @@ function _actionFromTl(this: ChatEvent, e: tl.TypeChannelAdminLogEventAction): C
case 'channelAdminLogEventActionChangePhoto':
return {
type: 'photo_changed',
old: new Photo(this.client, e.prevPhoto as tl.RawPhoto),
new: new Photo(this.client, e.newPhoto as tl.RawPhoto),
old: new Photo(client, e.prevPhoto as tl.RawPhoto),
new: new Photo(client, e.newPhoto as tl.RawPhoto),
}
case 'channelAdminLogEventActionToggleInvites':
return {
@ -364,37 +382,37 @@ function _actionFromTl(this: ChatEvent, e: tl.TypeChannelAdminLogEventAction): C
case 'channelAdminLogEventActionUpdatePinned':
return {
type: 'msg_pinned',
message: new Message(this.client, e.message, this._peers),
message: new Message(client, e.message, peers),
}
case 'channelAdminLogEventActionEditMessage':
return {
type: 'msg_edited',
old: new Message(this.client, e.prevMessage, this._peers),
new: new Message(this.client, e.newMessage, this._peers),
old: new Message(client, e.prevMessage, peers),
new: new Message(client, e.newMessage, peers),
}
case 'channelAdminLogEventActionDeleteMessage':
return {
type: 'msg_deleted',
message: new Message(this.client, e.message, this._peers),
message: new Message(client, e.message, peers),
}
case 'channelAdminLogEventActionParticipantLeave':
return { type: 'user_left' }
case 'channelAdminLogEventActionParticipantInvite':
return {
type: 'user_invited',
member: new ChatMember(this.client, e.participant, this._peers),
member: new ChatMember(client, e.participant, peers),
}
case 'channelAdminLogEventActionParticipantToggleBan':
return {
type: 'user_perms_changed',
old: new ChatMember(this.client, e.prevParticipant, this._peers),
new: new ChatMember(this.client, e.newParticipant, this._peers),
old: new ChatMember(client, e.prevParticipant, peers),
new: new ChatMember(client, e.newParticipant, peers),
}
case 'channelAdminLogEventActionParticipantToggleAdmin':
return {
type: 'user_admin_perms_changed',
old: new ChatMember(this.client, e.prevParticipant, this._peers),
new: new ChatMember(this.client, e.newParticipant, this._peers),
old: new ChatMember(client, e.prevParticipant, peers),
new: new ChatMember(client, e.newParticipant, peers),
}
case 'channelAdminLogEventActionChangeStickerSet':
return {
@ -417,7 +435,7 @@ function _actionFromTl(this: ChatEvent, e: tl.TypeChannelAdminLogEventAction): C
case 'channelAdminLogEventActionStopPoll':
return {
type: 'poll_stopped',
message: new Message(this.client, e.message, this._peers),
message: new Message(client, e.message, peers),
}
case 'channelAdminLogEventActionChangeLinkedChat':
return {
@ -428,8 +446,8 @@ function _actionFromTl(this: ChatEvent, e: tl.TypeChannelAdminLogEventAction): C
case 'channelAdminLogEventActionChangeLocation':
return {
type: 'location_changed',
old: e.prevValue._ === 'channelLocationEmpty' ? null : new ChatLocation(this.client, e.prevValue),
new: e.newValue._ === 'channelLocationEmpty' ? null : new ChatLocation(this.client, e.newValue),
old: e.prevValue._ === 'channelLocationEmpty' ? null : new ChatLocation(client, e.prevValue),
new: e.newValue._ === 'channelLocationEmpty' ? null : new ChatLocation(client, e.newValue),
}
case 'channelAdminLogEventActionToggleSlowMode':
return {
@ -460,23 +478,23 @@ function _actionFromTl(this: ChatEvent, e: tl.TypeChannelAdminLogEventAction): C
case 'channelAdminLogEventActionParticipantJoinByInvite':
return {
type: 'user_joined_invite',
link: new ChatInviteLink(this.client, e.invite, this._peers),
link: new ChatInviteLink(client, e.invite, peers),
}
case 'channelAdminLogEventActionExportedInviteDelete':
return {
type: 'invite_deleted',
link: new ChatInviteLink(this.client, e.invite, this._peers),
link: new ChatInviteLink(client, e.invite, peers),
}
case 'channelAdminLogEventActionExportedInviteRevoke':
return {
type: 'invite_revoked',
link: new ChatInviteLink(this.client, e.invite, this._peers),
link: new ChatInviteLink(client, e.invite, peers),
}
case 'channelAdminLogEventActionExportedInviteEdit':
return {
type: 'invite_edited',
old: new ChatInviteLink(this.client, e.prevInvite, this._peers),
new: new ChatInviteLink(this.client, e.newInvite, this._peers),
old: new ChatInviteLink(client, e.prevInvite, peers),
new: new ChatInviteLink(client, e.newInvite, peers),
}
case 'channelAdminLogEventActionChangeHistoryTTL':
return {
@ -488,43 +506,3 @@ function _actionFromTl(this: ChatEvent, e: tl.TypeChannelAdminLogEventAction): C
return null
}
}
export class ChatEvent {
constructor(
readonly client: TelegramClient,
readonly raw: tl.TypeChannelAdminLogEvent,
readonly _peers: PeersIndex,
) {}
/**
* Event ID.
*
* Event IDs are generated in direct chronological order
* (i.e. newer events have bigger event ID)
*/
get id(): tl.Long {
return this.raw.id
}
/**
* Date of the event
*/
get date(): Date {
return new Date(this.raw.date * 1000)
}
private _actor?: User
/**
* Actor of the event
*/
get actor(): User {
return (this._actor ??= new User(this.client, this._peers.user(this.raw.userId)))
}
private _action?: ChatAction
get action(): ChatAction {
return (this._action ??= _actionFromTl.call(this, this.raw.action))
}
}
makeInspectable(ChatEvent)

View file

@ -0,0 +1,123 @@
import { assertNever, MaybeArray, tl } from '@mtcute/core'
import { ChatAction } from './actions'
export interface ChatEventFilters {
serverFilter?: tl.TypeChannelAdminLogEventsFilter
localFilter?: Record<string, true>
}
export type InputChatEventFilters =
| ChatEventFilters
| tl.TypeChannelAdminLogEventsFilter
| MaybeArray<Exclude<ChatAction, null>['type']>
| undefined
/** @internal */
export function normalizeChatEventFilters(input: InputChatEventFilters): ChatEventFilters {
if (!input) {
return {}
}
if (typeof input === 'string' || Array.isArray(input)) {
if (!Array.isArray(input)) input = [input]
const serverFilter: tl.Mutable<tl.TypeChannelAdminLogEventsFilter> = {
_: 'channelAdminLogEventsFilter',
}
const localFilter: Record<string, true> = {}
input.forEach((type) => {
localFilter[type] = true
switch (type) {
case 'user_joined':
serverFilter.join = true
break
case 'user_left':
serverFilter.leave = true
break
case 'user_invited':
serverFilter.invite = true
break
case 'title_changed':
case 'description_changed':
case 'linked_chat_changed':
case 'location_changed':
case 'photo_changed':
case 'username_changed':
case 'stickerset_changed':
serverFilter.info = true
break
case 'invites_toggled':
case 'history_toggled':
case 'signatures_toggled':
case 'def_perms_changed':
serverFilter.settings = true
break
case 'msg_pinned':
serverFilter.pinned = true
break
case 'msg_edited':
case 'poll_stopped':
serverFilter.edit = true
break
case 'msg_deleted':
serverFilter.delete = true
break
case 'user_perms_changed':
serverFilter.ban = true
serverFilter.unban = true
serverFilter.kick = true
serverFilter.unkick = true
break
case 'user_admin_perms_changed':
serverFilter.promote = true
serverFilter.demote = true
break
case 'slow_mode_changed':
case 'ttl_changed':
// not documented so idk, enable both
serverFilter.settings = true
serverFilter.info = true
break
case 'call_started':
case 'call_ended':
serverFilter.groupCall = true
break
case 'call_setting_changed':
// not documented so idk, enable all
serverFilter.groupCall = true
serverFilter.settings = true
serverFilter.info = true
break
case 'user_joined_invite':
// not documented so idk, enable all
serverFilter.join = true
serverFilter.invite = true
serverFilter.invites = true
break
case 'invite_deleted':
case 'invite_edited':
case 'invite_revoked':
serverFilter.invites = true
break
default:
assertNever(type)
}
})
return {
serverFilter,
localFilter,
}
}
if ('_' in input) {
return {
serverFilter: input,
}
}
return input
}

View file

@ -0,0 +1,50 @@
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../../client'
import { makeInspectable } from '../../../utils'
import { PeersIndex } from '../peers-index'
import { User } from '../user'
import { _actionFromTl, ChatAction } from './actions'
export * from './actions'
export { InputChatEventFilters } from './filters'
export class ChatEvent {
constructor(
readonly client: TelegramClient,
readonly raw: tl.TypeChannelAdminLogEvent,
readonly _peers: PeersIndex,
) {}
/**
* Event ID.
*
* Event IDs are generated in direct chronological order
* (i.e. newer events have bigger event ID)
*/
get id(): tl.Long {
return this.raw.id
}
/**
* Date of the event
*/
get date(): Date {
return new Date(this.raw.date * 1000)
}
private _actor?: User
/**
* Actor of the event
*/
get actor(): User {
return (this._actor ??= new User(this.client, this._peers.user(this.raw.userId)))
}
private _action?: ChatAction
get action(): ChatAction {
return (this._action ??= _actionFromTl(this.raw.action, this.client, this._peers))
}
}
makeInspectable(ChatEvent)

View file

@ -0,0 +1,59 @@
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { makeInspectable } from '../../utils'
import { PeersIndex } from './peers-index'
import { User } from './user'
export class ChatInviteLinkMember {
constructor(readonly client: TelegramClient, readonly raw: tl.RawChatInviteImporter, readonly _peers: PeersIndex) {}
private _user?: User
/**
* User who joined the chat
*/
get user(): User {
return (this._user ??= new User(this.client, this._peers.user(this.raw.userId)))
}
/**
* Date when the user joined the chat
*/
get date(): Date {
return new Date(this.raw.date * 1000)
}
/**
* Whether this user currently has a pending join request
* (and is actually not a member yet)
*/
get isPendingRequest(): boolean {
return this.raw.requested!
}
/**
* Whether the participant joined by importing a chat folder deep link
*/
get isViaChatlist(): boolean {
return this.raw.requested!
}
/**
* For users with pending requests, contains bio of the user that requested to join
*/
get bio(): string | null {
return this.raw.about ?? null
}
private _approvedBy?: User
/**
* The administrator that approved the join request of the user
*/
get approvedBy(): User | null {
if (!this.raw.approvedBy) return null
return (this._approvedBy ??= new User(this.client, this._peers.user(this.raw.approvedBy)))
}
}
makeInspectable(ChatInviteLinkMember)

View file

@ -6,32 +6,6 @@ import { makeInspectable } from '../../utils'
import { PeersIndex } from './index'
import { User } from './user'
export interface ChatInviteLinkJoinedMember {
/**
* User who joined the chat
*/
user: User
/**
* Date when the user joined the chat
*/
date: Date
/**
* Whether the user currently has a pending join request
*/
isPendingRequest: boolean
/**
* For users with pending requests,
* contains bio of the user that requested to join
*/
bio?: string
/**
* The administrator that approved the join request of the user
*/
approvedBy?: number
}
/**
* An invite link
*/

View file

@ -3,6 +3,7 @@ import { tl } from '@mtcute/core'
export * from './chat'
export * from './chat-event'
export * from './chat-invite-link'
export * from './chat-invite-link-member'
export * from './chat-location'
export * from './chat-member'
export * from './chat-permissions'

View file

@ -2,7 +2,7 @@ import { getBarePeerId, tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { makeInspectable } from '../../utils'
import { ChatInviteLinkJoinedMember, PeersIndex, User } from '../peers'
import { PeersIndex, User } from '../peers'
/**
* This update is sent when a user requests to join a chat
@ -68,17 +68,6 @@ export class ChatJoinRequestUpdate {
await this.client.hideJoinRequest(this.chatId, id, action)
}
}
/**
* Fetch all pending join requests for this chat
*/
fetchAll(params?: { limit?: number; search?: string }): AsyncIterableIterator<ChatInviteLinkJoinedMember> {
return this.client.getInviteLinkMembers(this.chatId, {
limit: params?.limit,
requested: true,
requestedSearch: params?.search,
})
}
}
makeInspectable(ChatJoinRequestUpdate)

View file

@ -18,14 +18,10 @@ export async function resolveMaybeDynamic<T>(val: MaybeDynamic<T>): Promise<T> {
}
export function makeArrayWithTotal<T>(arr: T[], total: number): ArrayWithTotal<T> {
Object.defineProperty(arr, 'total', {
value: total,
enumerable: false,
configurable: false,
writable: false,
})
const a = arr as ArrayWithTotal<T>
a.total = total
return arr as ArrayWithTotal<T>
return a
}
export function extractChannelIdFromUpdate(upd: tl.TypeUpdate): number | undefined {

View file

@ -44,7 +44,7 @@ export function hasPresentKey<K extends string | number | symbol>(k: K) {
* files[0].imageUrl // TS will know this is present, because already it excluded the other union members.
* ```
*/
export function hasValueAtKey<K extends string | number | symbol, V>(k: K, v: V) {
export function hasValueAtKey<const K extends string | number | symbol, const V>(k: K, v: V) {
return function <T> (a: T & { [k in K]: unknown }): a is T & { [k in K]: V } {
return a[k] === v
}