UNPKG

dograma

Version:

NodeJS/Browser MTProto API Telegram client library,

1,170 lines (1,121 loc) 39.5 kB
import { Api } from "../tl"; import type { DateLike, EntityLike, FileLike, MarkupLike, MessageIDLike, MessageLike, } from "../define"; import { RequestIter } from "../requestIter"; import { _EntityType, _entityType, TotalList, isArrayLike, groupBy, } from "../Helpers"; import { getInputMedia, getMessageId, getPeerId, parseID } from "../Utils"; import type { TelegramClient } from "../"; import { utils } from "../"; import { _parseMessageText } from "./messageParse"; import { _getPeer } from "./users"; import bigInt from "big-integer"; import { _fileToMedia } from "./uploads"; const _MAX_CHUNK_SIZE = 100; interface MessageIterParams { entity: EntityLike; offsetId: number; minId: number; maxId: number; fromUser?: EntityLike; offsetDate: DateLike; addOffset: number; filter: any; search: string; replyTo: MessageIDLike; } export class _MessagesIter extends RequestIter { entity?: Api.TypeInputPeer; request?: | Api.messages.SearchGlobal | Api.messages.GetReplies | Api.messages.GetHistory | Api.messages.Search; addOffset?: number; maxId?: number; minId?: number; lastId?: number; async _init({ entity, offsetId, minId, maxId, fromUser, offsetDate, addOffset, filter, search, replyTo, }: MessageIterParams) { if (entity) { this.entity = await this.client.getInputEntity(entity); } else { this.entity = undefined; if (this.reverse) { throw new Error("Cannot reverse global search"); } } if (this.reverse) { offsetId = Math.max(offsetId, minId); if (offsetId && maxId) { if (maxId - offsetId <= 1) { return false; } } if (!maxId) { maxId = Number.MAX_SAFE_INTEGER; } } else { offsetId = Math.max(offsetId, maxId); if (offsetId && minId) { if (offsetId - minId <= 1) { return false; } } } if (this.reverse) { if (offsetId) { offsetId += 1; } else if (!offsetDate) { offsetId = 1; } } if (fromUser) { fromUser = await this.client.getInputEntity(fromUser); } if (!this.entity && fromUser) { this.entity = new Api.InputPeerEmpty(); } if (!filter) { filter = new Api.InputMessagesFilterEmpty(); } if (!this.entity) { this.request = new Api.messages.SearchGlobal({ q: search || "", filter: filter, minDate: undefined, // TODO fix this smh maxDate: offsetDate, offsetRate: undefined, offsetPeer: new Api.InputPeerEmpty(), offsetId: offsetId, limit: 1, }); } else if (replyTo !== undefined) { this.request = new Api.messages.GetReplies({ peer: this.entity, msgId: replyTo, offsetId: offsetId, offsetDate: offsetDate, addOffset: addOffset, limit: 0, maxId: 0, minId: 0, hash: bigInt.zero, }); } else if ( search !== undefined || !(filter instanceof Api.InputMessagesFilterEmpty) || fromUser !== undefined ) { this.request = new Api.messages.Search({ peer: this.entity, q: search || "", filter: typeof filter === "function" ? new filter() : filter, minDate: undefined, maxDate: offsetDate, offsetId: offsetId, addOffset: addOffset, limit: 0, maxId: 0, minId: 0, hash: bigInt.zero, fromId: fromUser, }); if ( filter instanceof Api.InputMessagesFilterEmpty && offsetDate && !search && !offsetId ) { for await (const m of this.client.iterMessages(this.entity, { limit: 1, offsetDate: offsetDate, })) { this.request.offsetId = m.id + 1; } } } else { this.request = new Api.messages.GetHistory({ peer: this.entity, limit: 1, offsetDate: offsetDate, offsetId: offsetId, minId: 0, maxId: 0, addOffset: addOffset, hash: bigInt.zero, }); } if (this.limit <= 0) { const result = await this.client.invoke(this.request); if (result instanceof Api.messages.MessagesNotModified) { this.total = result.count; } else { if ("count" in result) { this.total = result.count; } else { this.total = result.messages.length; } } return false; } if (!this.waitTime) { this.waitTime = this.limit > 3000 ? 1 : 0; } if ( this.reverse && !(this.request instanceof Api.messages.SearchGlobal) ) { this.request.addOffset -= _MAX_CHUNK_SIZE; } this.addOffset = addOffset; this.maxId = maxId; this.minId = minId; this.lastId = this.reverse ? 0 : Number.MAX_SAFE_INTEGER; } async _loadNextChunk() { if (!this.request) { throw new Error("Request not set yet"); } this.request.limit = Math.min(this.left, _MAX_CHUNK_SIZE); if (this.reverse && this.request.limit != _MAX_CHUNK_SIZE) { if (!(this.request instanceof Api.messages.SearchGlobal)) { this.request.addOffset = this.addOffset! - this.request.limit; } } const r = await this.client.invoke(this.request); if (r instanceof Api.messages.MessagesNotModified) { return true; } if ("count" in r) { this.total = r.count; } else { this.total = r.messages.length; } const entities = new Map(); for (const x of [...r.users, ...r.chats]) { entities.set(getPeerId(x), x); } const messages: Api.Message[] = this.reverse ? (r.messages.reverse() as unknown as Api.Message[]) : (r.messages as unknown as Api.Message[]); for (const message of messages) { if (!this._messageInRange(message)) { return true; } this.lastId = message.id; try { // if this fails it shouldn't be a big problem message._finishInit(this.client, entities, this.entity); } catch (e) {} message._entities = entities; this.buffer?.push(message); } if (r.messages.length < this.request.limit) { return true; } if (this.buffer) { this._updateOffset(this.buffer[this.buffer.length - 1], r); } else { return true; } } _messageInRange(message: Api.Message) { if (this.entity) { if (this.reverse) { if (message.id <= this.lastId! || message.id >= this.maxId!) { return false; } } else { if (message.id >= this.lastId! || message.id <= this.minId!) { return false; } } } return true; } [Symbol.asyncIterator](): AsyncIterator<Api.Message, any, undefined> { return super[Symbol.asyncIterator](); } _updateOffset(lastMessage: Api.Message, response: any) { if (!this.request) { throw new Error("Request not set yet"); } this.request.offsetId = Number(lastMessage.id); if (this.reverse) { this.request.offsetId += 1; } if (this.request instanceof Api.messages.Search) { this.request.maxDate = -1; } else { if (!(this.request instanceof Api.messages.SearchGlobal)) { this.request.offsetDate = lastMessage.date!; } } if (this.request instanceof Api.messages.SearchGlobal) { if (lastMessage.inputChat) { this.request.offsetPeer = lastMessage.inputChat; } else { this.request.offsetPeer = new Api.InputPeerEmpty(); } this.request.offsetRate = response.nextRate; } } } interface IDsIterInterface { entity: EntityLike; ids: Api.TypeInputMessage[]; } export class _IDsIter extends RequestIter { _ids?: Api.TypeInputMessage[]; _offset?: number; _ty: number | undefined; private _entity: Api.TypeInputPeer | undefined; async _init({ entity, ids }: IDsIterInterface) { this.total = ids.length; this._ids = this.reverse ? ids.reverse() : ids; this._offset = 0; this._entity = entity ? await this.client.getInputEntity(entity) : undefined; this._ty = this._entity ? _entityType(this._entity) : undefined; if (!this.waitTime) { this.waitTime = this.limit > 300 ? 10 : 0; } } [Symbol.asyncIterator](): AsyncIterator<Api.Message, any, undefined> { return super[Symbol.asyncIterator](); } async _loadNextChunk() { const ids = this._ids!.slice( this._offset, this._offset! + _MAX_CHUNK_SIZE ); if (!ids.length) { return false; } this._offset! += _MAX_CHUNK_SIZE; let fromId; let r; if (this._ty == _EntityType.CHANNEL) { try { r = await this.client.invoke( new Api.channels.GetMessages({ channel: this._entity, id: ids, }) ); } catch (e: any) { if (e.errorMessage == "MESSAGE_IDS_EMPTY") { r = new Api.messages.MessagesNotModified({ count: ids.length, }); } else { throw e; } } } else { r = await this.client.invoke( new Api.messages.GetMessages({ id: ids, }) ); if (this._entity) { fromId = await _getPeer(this.client, this._entity); } } if (r instanceof Api.messages.MessagesNotModified) { this.buffer?.push(...Array(ids.length)); return; } const entities = new Map(); for (const entity of [...r.users, ...r.chats]) { entities.set(utils.getPeerId(entity), entity); } let message: Api.TypeMessage; for (message of r.messages) { if ( message instanceof Api.MessageEmpty || (fromId && utils.getPeerId(message.peerId) != utils.getPeerId(fromId)) ) { this.buffer?.push(undefined); } else { const temp: Api.Message = message as unknown as Api.Message; temp._finishInit(this.client, entities, this._entity); temp._entities = entities; this.buffer?.push(temp); } } } } /** * Interface for iterating over messages. used in both {@link iterMessages} and {@link getMessages}. */ export interface IterMessagesParams { /** Number of messages to be retrieved.<br/> * Due to limitations with the API retrieving more than 3000 messages will take longer than half a minute. (might even take longer)<br/> * if undefined is passed instead of a number the library will try to retrieve all the messages.*/ limit?: number; /** Offset date (messages previous to this date will be retrieved). Exclusive. */ offsetDate?: DateLike; /** Offset message ID (only messages previous to the given ID will be retrieved). Exclusive. */ offsetId: number; /** All the messages with a higher (newer) ID or equal to this will be excluded. */ maxId: number; /** All the messages with a lower (older) ID or equal to this will be excluded. */ minId: number; /** Additional message offset (all of the specified offsets + this offset = older messages). */ addOffset: number; /** The string to be used as a search query. */ search?: string; /** The filter to use when returning messages.<br/> * For instance, InputMessagesFilterPhotos would yield only messages containing photos. */ filter?: Api.TypeMessagesFilter | Api.TypeMessagesFilter[]; /** Only messages from this user will be returned. */ fromUser?: EntityLike; /** Wait time (in seconds) between different GetHistory requests.<br/> * Use this parameter to avoid hitting the FloodWaitError as needed.<br/> * If left to undefined, it will default to 1 second only if the number of messages is higher than 3000. * If the ids parameter is used, this time will default to 10 seconds only if the amount of IDs is higher than 300. */ waitTime?: number; /** A single integer ID (or several IDs) for the message that should be returned.<br/> * This parameter takes precedence over the rest (which will be ignored if this is set).<br/> * This can for instance be used to get the message with ID 123 from a channel.<br/> * **Note** that if the message doesn"t exist, undefined will appear in its place. */ ids?: number | number[] | Api.TypeInputMessage | Api.TypeInputMessage[]; /** If set to `true`, the messages will be returned in reverse order (from oldest to newest, instead of the default newest to oldest).<br/> * This also means that the meaning of offsetId and offsetDate parameters is reversed, although they will still be exclusive.<br/> * `minId` becomes equivalent to `offsetId` instead of being `maxId` as well since messages are returned in ascending order.<br/> * You cannot use this if both entity and ids are undefined. */ reverse?: boolean; /** If set to a message ID, the messages that reply to this ID will be returned.<br/> * This feature is also known as comments in posts of broadcast channels, or viewing threads in groups.<br/> * This feature can only be used in broadcast channels and their linked supergroups. Using it in a chat or private conversation will result in PEER_ID_INVALID error.<br/> * When using this parameter, the filter and search parameters have no effect, since Telegram's API doesn't support searching messages in replies. */ replyTo?: number; /** If set to `true`, messages which are scheduled will be returned. * All other parameters will be ignored for this, except `entity`. */ scheduled: boolean; } const IterMessagesDefaults: IterMessagesParams = { limit: undefined, offsetDate: undefined, offsetId: 0, maxId: 0, minId: 0, addOffset: 0, search: undefined, filter: undefined, fromUser: undefined, waitTime: undefined, ids: undefined, reverse: false, replyTo: undefined, scheduled: false, }; /** * Interface for sending a message. only message is required */ export interface SendMessageParams { /** The message to be sent, or another message object to resend as a copy.<br/> * The maximum length for a message is 35,000 bytes or 4,096 characters.<br/> * Longer messages will not be sliced automatically, and you should slice them manually if the text to send is longer than said length. */ message?: MessageLike; /** Whether to reply to a message or not. If an integer is provided, it should be the ID of the message that it should reply to. */ replyTo?: number | Api.Message; /** Optional attributes that override the inferred ones, like DocumentAttributeFilename and so on. */ attributes?: Api.TypeDocumentAttribute[]; /** See the {@link parseMode} property for allowed values. Markdown parsing will be used by default. */ parseMode?: any; /** A list of message formatting entities. When provided, the parseMode is ignored. */ formattingEntities?: Api.TypeMessageEntity[]; /** Should the link preview be shown? */ linkPreview?: boolean; /** Sends a message with a file attached (e.g. a photo, video, audio or document). The message may be empty. */ file?: FileLike | FileLike[]; /** Optional JPEG thumbnail (for documents). Telegram will ignore this parameter unless you pass a .jpg file!<br/> * The file must also be small in dimensions and in disk size. Successful thumbnails were files below 20kB and 320x320px.<br/> * Width/height and dimensions/size ratios may be important. * For Telegram to accept a thumbnail, you must provide the dimensions of the underlying media through `attributes:` with DocumentAttributesVideo. */ thumb?: FileLike; /** Whether to send the given file as a document or not. */ forceDocument?: false; /** Whether the existing draft should be cleared or not. */ clearDraft?: false; /** The matrix (list of lists), row list or button to be shown after sending the message.<br/> * This parameter will only work if you have signed in as a bot. You can also pass your own ReplyMarkup here.<br/> * <br/> * All the following limits apply together: * - There can be 100 buttons at most (any more are ignored). * - There can be 8 buttons per row at most (more are ignored). * - The maximum callback data per button is 64 bytes. * - The maximum data that can be embedded in total is just over 4KB, shared between inline callback data and text. */ buttons?: MarkupLike; /** Whether the message should notify people in a broadcast channel or not. Defaults to false, which means it will notify them. Set it to True to alter this behaviour. */ silent?: boolean; /** Whether the sent video supports streaming or not.<br/> * Note that Telegram only recognizes as streamable some formats like MP4, and others like AVI or MKV will not work.<br/> * You should convert these to MP4 before sending if you want them to be streamable. Unsupported formats will result in VideoContentTypeError. */ supportStreaming?: boolean; /** If set, the message won't send immediately, and instead it will be scheduled to be automatically sent at a later time. */ schedule?: DateLike; noforwards?: boolean; /** Similar to ``replyTo``, but replies in the linked group of a broadcast channel instead (effectively leaving a "comment to" the specified message). This parameter takes precedence over ``replyTo``. If there is no linked chat, `SG_ID_INVALID` is thrown. */ commentTo?: number | Api.Message; } /** interface used for forwarding messages */ export interface ForwardMessagesParams { /** The message(s) to forward, or their integer IDs. */ messages: MessageIDLike | MessageIDLike[]; /** If the given messages are integer IDs and not instances of the Message class, this must be specified in order for the forward to work.<br/> */ fromPeer: EntityLike; /** Whether the message should notify people with sound or not.<br/> * Defaults to false (send with a notification sound unless the person has the chat muted). Set it to true to alter this behaviour. */ silent?: boolean; /** If set, the message(s) won't forward immediately, and instead they will be scheduled to be automatically sent at a later time. */ schedule?: DateLike; noforwards?: boolean; } /** Interface for editing messages */ export interface EditMessageParams { /** The ID of the message (or Message itself) to be edited. If the entity was a Message, then this message will be treated as the new text. */ message: Api.Message | number; /** The new text of the message. Does nothing if the entity was a Message. */ text?: string; /** See the {@link TelegramClient.parseMode} property for allowed values. Markdown parsing will be used by default. */ parseMode?: any; /** A list of message formatting entities. When provided, the parseMode is ignored. */ formattingEntities?: Api.TypeMessageEntity[]; /** Should the link preview be shown? */ linkPreview?: boolean; /** The file object that should replace the existing media in the message. Does nothing if entity was a Message */ file?: FileLike; /** Whether to send the given file as a document or not. */ forceDocument?: false; /** The matrix (list of lists), row list or button to be shown after sending the message.<br/> * This parameter will only work if you have signed in as a bot. You can also pass your own ReplyMarkup here.<br/> * <br/> * All the following limits apply together: * - There can be 100 buttons at most (any more are ignored). * - There can be 8 buttons per row at most (more are ignored). * - The maximum callback data per button is 64 bytes. * - The maximum data that can be embedded in total is just over 4KB, shared between inline callback data and text. */ buttons?: MarkupLike; /** If set, the message won't be edited immediately, and instead it will be scheduled to be automatically edited at a later time. */ schedule?: DateLike; } /** Interface for editing messages */ export interface UpdatePinMessageParams { /** Whether the pin should notify people or not. <br /> * By default it has the opposite behavior of official clients, it will not notify members. */ notify?: boolean; /** Whether the message should be pinned for everyone or not. <br /> * By default it has the opposite behavior of official clients, and it will pin the message for both sides, in private chats. */ pmOneSide?: boolean; } /** Interface for mark message as read */ export interface MarkAsReadParams { /** * Until which message should the read acknowledge be sent for. <br /> * This has priority over the `message` parameter. */ maxId?: number; /** * Whether the mention badge should be cleared (so that there are no more mentions) or not for the given entity. <br /> * If no message is provided, this will be the only action taken. */ clearMentions?: boolean; } /** @hidden */ export function iterMessages( client: TelegramClient, entity: EntityLike | undefined, options: Partial<IterMessagesParams> ) { const { limit, offsetDate, offsetId, maxId, minId, addOffset, search, filter, fromUser, waitTime, ids, reverse, replyTo, } = { ...IterMessagesDefaults, ...options }; if (ids) { let idsArray; if (!isArrayLike(ids)) { idsArray = [ids]; } else { idsArray = ids; } return new _IDsIter( client, idsArray.length, { reverse: reverse, waitTime: waitTime, }, { entity: entity, ids: idsArray, } ); } return new _MessagesIter( client, limit, { waitTime: waitTime, reverse: reverse, }, { entity: entity, offsetId: offsetId, minId: minId, maxId: maxId, fromUser: fromUser, offsetDate: offsetDate, addOffset: addOffset, filter: filter, search: search, replyTo: replyTo, } ); } /** @hidden */ export async function getMessages( client: TelegramClient, entity: EntityLike | undefined, params: Partial<IterMessagesParams> ): Promise<TotalList<Api.Message>> { if (Object.keys(params).length == 1 && params.limit === undefined) { if (params.minId === undefined && params.maxId === undefined) { params.limit = undefined; } else { params.limit = 1; } } const it = client.iterMessages(entity, params); const ids = params.ids; if (ids && !isArrayLike(ids)) { for await (const message of it) { return [message]; } return []; } return (await it.collect()) as TotalList<Api.Message>; } // region Message /** @hidden */ export async function sendMessage( client: TelegramClient, /** To who will it be sent. */ entity: EntityLike, /** The message to be sent, or another message object to resend as a copy.<br/> * The maximum length for a message is 35,000 bytes or 4,096 characters.<br/> * Longer messages will not be sliced automatically, and you should slice them manually if the text to send is longer than said length. */ { message, replyTo, attributes, parseMode, formattingEntities, linkPreview = true, file, thumb, forceDocument, clearDraft, buttons, silent, supportStreaming, schedule, noforwards, commentTo, }: SendMessageParams = {} ) { if (file) { return client.sendFile(entity, { file: file, caption: message ? typeof message == "string" ? message : message.message : "", forceDocument: forceDocument, clearDraft: clearDraft, replyTo: replyTo, attributes: attributes, thumb: thumb, supportsStreaming: supportStreaming, parseMode: parseMode, formattingEntities: formattingEntities, silent: silent, scheduleDate: schedule, buttons: buttons, noforwards: noforwards, commentTo: commentTo, }); } entity = await client.getInputEntity(entity); if (commentTo != undefined) { const discussionData = await getCommentData(client, entity, commentTo); entity = discussionData.entity; replyTo = discussionData.replyTo; } let markup, request; if (message && message instanceof Api.Message) { if (buttons == undefined) { markup = message.replyMarkup; } else { markup = client.buildReplyMarkup(buttons); } if (silent == undefined) { silent = message.silent; } if ( message.media && !(message.media instanceof Api.MessageMediaWebPage) ) { return client.sendFile(entity, { file: message.media, caption: message.message, silent: silent, replyTo: replyTo, buttons: markup, formattingEntities: message.entities, scheduleDate: schedule, }); } request = new Api.messages.SendMessage({ peer: entity, message: message.message || "", silent: silent, replyToMsgId: getMessageId(replyTo), replyMarkup: markup, entities: message.entities, clearDraft: clearDraft, noWebpage: !(message.media instanceof Api.MessageMediaWebPage), scheduleDate: schedule, noforwards: noforwards, }); message = message.message; } else { if (formattingEntities == undefined) { [message, formattingEntities] = await _parseMessageText( client, message || "", parseMode ); } if (!message) { throw new Error( "The message cannot be empty unless a file is provided" ); } request = new Api.messages.SendMessage({ peer: entity, message: message.toString(), entities: formattingEntities, noWebpage: !linkPreview, replyToMsgId: getMessageId(replyTo), clearDraft: clearDraft, silent: silent, replyMarkup: client.buildReplyMarkup(buttons), scheduleDate: schedule, noforwards: noforwards, }); } const result = await client.invoke(request); if (result instanceof Api.UpdateShortSentMessage) { const msg = new Api.Message({ id: result.id, peerId: await _getPeer(client, entity), message: message, date: result.date, out: result.out, media: result.media, entities: result.entities, replyMarkup: request.replyMarkup, ttlPeriod: result.ttlPeriod, }); msg._finishInit(client, new Map(), entity); return msg; } return client._getResponseMessage(request, result, entity) as Api.Message; } /** @hidden */ export async function forwardMessages( client: TelegramClient, entity: EntityLike, { messages, fromPeer, silent, schedule, noforwards }: ForwardMessagesParams ) { if (!isArrayLike(messages)) { messages = [messages]; } entity = await client.getInputEntity(entity); let fromPeerId: string | undefined; if (fromPeer) { fromPeer = await client.getInputEntity(fromPeer); fromPeerId = await client.getPeerId(fromPeer); } const getKey = (m: string | Api.Message) => { if (m instanceof Api.Message) { return m.chatId; } let msgId = parseID(m); if (msgId) { if (fromPeerId !== undefined) { return fromPeerId; } throw new Error("fromPeer must be given if integer IDs are used"); } else { throw new Error(`Cannot forward ${m}`); } }; const sent: Api.Message[] = []; for (let [chatId, chunk] of groupBy(messages, getKey) as Map< number, Api.Message[] | number[] >) { let chat; let numbers: number[] = []; if (typeof chunk[0] == "number") { chat = fromPeer; numbers = chunk as number[]; } else { chat = await chunk[0].getInputChat(); numbers = (chunk as Api.Message[]).map((m: Api.Message) => m.id); } chunk.push(); const request = new Api.messages.ForwardMessages({ fromPeer: chat, id: numbers, toPeer: entity, silent: silent, scheduleDate: schedule, noforwards: noforwards, }); const result = await client.invoke(request); sent.push( client._getResponseMessage(request, result, entity) as Api.Message ); } return sent; } /** @hidden */ export async function editMessage( client: TelegramClient, entity: EntityLike, { message, text, parseMode, formattingEntities, linkPreview = true, file, forceDocument, buttons, schedule, }: EditMessageParams ) { if ( typeof message === "number" && typeof text === "undefined" && !file && !schedule ) { throw Error( "You have to provide either file or text or schedule property." ); } entity = await client.getInputEntity(entity); let id: number | undefined; let markup: Api.TypeReplyMarkup | undefined; let entities: Api.TypeMessageEntity[] | undefined; let inputMedia: Api.TypeInputMedia | undefined; if (file) { const { fileHandle, media, image } = await _fileToMedia(client, { file, forceDocument, }); inputMedia = media; } if (message instanceof Api.Message) { id = getMessageId(message); text = message.message; entities = message.entities; if (buttons == undefined) { markup = message.replyMarkup; } else { markup = client.buildReplyMarkup(buttons); } if (message.media) { inputMedia = getInputMedia(message.media, { forceDocument }); } } else { if (typeof message !== "number") { throw Error( "editMessageParams.message must be either a number or a Api.Message type" ); } id = message; if (formattingEntities == undefined) { [text, entities] = await _parseMessageText( client, text || "", parseMode ); } else { entities = formattingEntities; } markup = client.buildReplyMarkup(buttons); } const request = new Api.messages.EditMessage({ peer: entity, id, message: text, noWebpage: !linkPreview, entities, media: inputMedia, replyMarkup: markup, scheduleDate: schedule, }); const result = await client.invoke(request); return client._getResponseMessage(request, result, entity) as Api.Message; } /** @hidden */ export async function deleteMessages( client: TelegramClient, entity: EntityLike | undefined, messageIds: MessageIDLike[], { revoke = false } ) { let ty = _EntityType.USER; if (entity) { entity = await client.getInputEntity(entity); ty = _entityType(entity); } const ids: number[] = []; for (const messageId of messageIds) { if ( messageId instanceof Api.Message || messageId instanceof Api.MessageService || messageId instanceof Api.MessageEmpty ) { ids.push(messageId.id); } else if (typeof messageId === "number") { ids.push(messageId); } else { throw new Error(`Cannot convert ${messageId} to an integer`); } } const results = []; if (ty == _EntityType.CHANNEL) { for (const chunk of utils.chunks(ids)) { results.push( client.invoke( new Api.channels.DeleteMessages({ channel: entity, id: chunk, }) ) ); } } else { for (const chunk of utils.chunks(ids)) { results.push( client.invoke( new Api.messages.DeleteMessages({ id: chunk, revoke: revoke, }) ) ); } } return Promise.all(results); } /** @hidden */ export async function pinMessage( client: TelegramClient, entity: EntityLike, message?: MessageIDLike, pinMessageParams?: UpdatePinMessageParams ) { return await _pin( client, entity, message, false, pinMessageParams?.notify, pinMessageParams?.pmOneSide ); } /** @hidden */ export async function unpinMessage( client: TelegramClient, entity: EntityLike, message?: MessageIDLike, unpinMessageParams?: UpdatePinMessageParams ) { return await _pin( client, entity, message, true, unpinMessageParams?.notify, unpinMessageParams?.pmOneSide ); } /** @hidden */ export async function _pin( client: TelegramClient, entity: EntityLike, message: MessageIDLike | undefined, unpin: boolean, notify: boolean = false, pmOneSide: boolean = false ) { message = utils.getMessageId(message) || 0; if (message === 0) { return await client.invoke( new Api.messages.UnpinAllMessages({ peer: entity, }) ); } entity = await client.getInputEntity(entity); const request = new Api.messages.UpdatePinnedMessage({ silent: !notify, unpin, pmOneside: pmOneSide, peer: entity, id: message, }); const result = await client.invoke(request); /** * Unpinning does not produce a service message. * Pinning a message that was already pinned also produces no service message. * Pinning a message in your own chat does not produce a service message, * but pinning on a private conversation with someone else does. */ if ( unpin || !("updates" in result) || ("updates" in result && !result.updates) ) { return; } // Pinning a message that doesn't exist would RPC-error earlier return client._getResponseMessage(request, result, entity) as Api.Message; } /** @hidden */ export async function markAsRead( client: TelegramClient, entity: EntityLike, message?: MessageIDLike | MessageIDLike[], markAsReadParams?: MarkAsReadParams ): Promise<boolean> { let maxId: number = markAsReadParams?.maxId || 0; const maxIdIsUndefined = markAsReadParams?.maxId === undefined; if (maxIdIsUndefined) { if (message) { if (Array.isArray(message)) { maxId = Math.max( ...message.map((v) => utils.getMessageId(v) as number) ); } else { maxId = utils.getMessageId(message) as number; } } } entity = await client.getInputEntity(entity); if (markAsReadParams && !markAsReadParams.clearMentions) { await client.invoke(new Api.messages.ReadMentions({ peer: entity })); if (maxIdIsUndefined && message === undefined) { return true; } } if (_entityType(entity) === _EntityType.CHANNEL) { return await client.invoke( new Api.channels.ReadHistory({ channel: entity, maxId }) ); } else { await client.invoke( new Api.messages.ReadHistory({ peer: entity, maxId }) ); return true; } } /** @hidden */ export async function getCommentData( client: TelegramClient, entity: EntityLike, message: number | Api.Message ) { const result = await client.invoke( new Api.messages.GetDiscussionMessage({ peer: entity, msgId: utils.getMessageId(message), }) ); const relevantMessage = result.messages[0]; let chat; for (const c of result.chats) { if ( relevantMessage.peerId instanceof Api.PeerChannel && c.id.eq(relevantMessage.peerId.channelId) ) { chat = c; break; } } return { entity: utils.getInputPeer(chat), replyTo: relevantMessage.id, }; } // TODO do the rest