feat(client): methods for dialogs, also added support for drafts and clearing them

This commit is contained in:
teidesu 2021-04-14 21:31:51 +03:00
parent 7a463f22a3
commit e6dd822644
11 changed files with 493 additions and 11 deletions

View file

@ -26,10 +26,12 @@ import { getChatMember } from './methods/chats/get-chat-member'
import { getChatMembers } from './methods/chats/get-chat-members' import { getChatMembers } from './methods/chats/get-chat-members'
import { getChatPreview } from './methods/chats/get-chat-preview' import { getChatPreview } from './methods/chats/get-chat-preview'
import { getChat } from './methods/chats/get-chat' import { getChat } from './methods/chats/get-chat'
import { getDialogs } from './methods/chats/get-dialogs'
import { getFullChat } from './methods/chats/get-full-chat' import { getFullChat } from './methods/chats/get-full-chat'
import { iterChatMembers } from './methods/chats/iter-chat-members' import { iterChatMembers } from './methods/chats/iter-chat-members'
import { joinChat } from './methods/chats/join-chat' import { joinChat } from './methods/chats/join-chat'
import { leaveChat } from './methods/chats/leave-chat' import { leaveChat } from './methods/chats/leave-chat'
import { saveDraft } from './methods/chats/save-draft'
import { setChatDefaultPermissions } from './methods/chats/set-chat-default-permissions' import { setChatDefaultPermissions } from './methods/chats/set-chat-default-permissions'
import { setChatDescription } from './methods/chats/set-chat-description' import { setChatDescription } from './methods/chats/set-chat-description'
import { setChatPhoto } from './methods/chats/set-chat-photo' import { setChatPhoto } from './methods/chats/set-chat-photo'
@ -83,6 +85,7 @@ import {
Chat, Chat,
ChatMember, ChatMember,
ChatPreview, ChatPreview,
Dialog,
FileDownloadParameters, FileDownloadParameters,
InputChatPermissions, InputChatPermissions,
InputFileLike, InputFileLike,
@ -560,6 +563,45 @@ export class TelegramClient extends BaseTelegramClient {
getChat(chatId: InputPeerLike): Promise<Chat> { getChat(chatId: InputPeerLike): Promise<Chat> {
return getChat.apply(this, arguments) return getChat.apply(this, arguments)
} }
/**
* Get a chunk of dialogs
*
* You can get up to 100 dialogs at once
*
* @param params Fetch parameters
*/
getDialogs(params?: {
/**
* Offset date used as an anchor for pagination.
*
* Use {@link Dialog.date} for this value.
*/
offsetDate?: Date | number
/**
* Limits the number of dialogs to be received.
*
* Defaults to 100.
*/
limit?: number
/**
* How to handle pinned dialogs?
* Whether to `include` them, `exclude`,
* or `only` return pinned dialogs.
*
* Defaults to `include`
*/
pinned?: 'include' | 'exclude' | 'only'
/**
* Whether to get dialogs from the
* archived dialogs list.
*/
archived?: boolean
}): Promise<Dialog[]> {
return getDialogs.apply(this, arguments)
}
/** /**
* Get full information about a chat. * Get full information about a chat.
* *
@ -614,6 +656,18 @@ export class TelegramClient extends BaseTelegramClient {
leaveChat(chatId: InputPeerLike, clear?: boolean): Promise<void> { leaveChat(chatId: InputPeerLike, clear?: boolean): Promise<void> {
return leaveChat.apply(this, arguments) return leaveChat.apply(this, arguments)
} }
/**
* Save or delete a draft message associated with some chat
*
* @param chatId ID of the chat, its username, phone or `"me"` or `"self"`
* @param draft Draft message, or `null` to delete.
*/
saveDraft(
chatId: InputPeerLike,
draft: null | Omit<tl.RawDraftMessage, '_' | 'date'>
): Promise<void> {
return saveDraft.apply(this, arguments)
}
/** /**
* Change default chat permissions for all members. * Change default chat permissions for all members.
* *
@ -1293,6 +1347,13 @@ export class TelegramClient extends BaseTelegramClient {
* @param total Total file size * @param total Total file size
*/ */
progressCallback?: (uploaded: number, total: number) => void progressCallback?: (uploaded: number, total: number) => void
/**
* Whether to clear draft after sending this message.
*
* Defaults to `false`
*/
clearDraft?: boolean
} }
): Promise<Message> { ): Promise<Message> {
return sendMedia.apply(this, arguments) return sendMedia.apply(this, arguments)
@ -1366,6 +1427,13 @@ export class TelegramClient extends BaseTelegramClient {
* @param total Total file size * @param total Total file size
*/ */
progressCallback?: (uploaded: number, total: number) => void progressCallback?: (uploaded: number, total: number) => void
/**
* Whether to clear draft after sending this message.
*
* Defaults to `false`
*/
clearDraft?: boolean
} }
): Promise<Message> { ): Promise<Message> {
return sendPhoto.apply(this, arguments) return sendPhoto.apply(this, arguments)
@ -1423,6 +1491,13 @@ export class TelegramClient extends BaseTelegramClient {
* to hide a reply keyboard or to force a reply. * to hide a reply keyboard or to force a reply.
*/ */
replyMarkup?: ReplyMarkup replyMarkup?: ReplyMarkup
/**
* Whether to clear draft after sending this message.
*
* Defaults to `false`
*/
clearDraft?: boolean
} }
): Promise<Message> { ): Promise<Message> {
return sendText.apply(this, arguments) return sendText.apply(this, arguments)

View file

@ -12,6 +12,7 @@ import {
Chat, Chat,
ChatPreview, ChatPreview,
ChatMember, ChatMember,
Dialog,
InputChatPermissions, InputChatPermissions,
TermsOfService, TermsOfService,
SentCode, SentCode,

View file

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

View file

@ -0,0 +1,33 @@
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
import { tl } from '@mtcute/tl'
import { normalizeToInputPeer } from '../../utils/peer-utils'
/**
* Save or delete a draft message associated with some chat
*
* @param chatId ID of the chat, its username, phone or `"me"` or `"self"`
* @param draft Draft message, or `null` to delete.
* @internal
*/
export async function saveDraft(
this: TelegramClient,
chatId: InputPeerLike,
draft: null | Omit<tl.RawDraftMessage, '_' | 'date'>
): Promise<void> {
const peer = normalizeToInputPeer(await this.resolvePeer(chatId))
if (draft) {
await this.call({
_: 'messages.saveDraft',
peer,
...draft
})
} else {
await this.call({
_: 'messages.saveDraft',
peer,
message: ''
})
}
}

View file

@ -65,6 +65,13 @@ export async function sendMedia(
* @param total Total file size * @param total Total file size
*/ */
progressCallback?: (uploaded: number, total: number) => void progressCallback?: (uploaded: number, total: number) => void
/**
* Whether to clear draft after sending this message.
*
* Defaults to `false`
*/
clearDraft?: boolean
} }
): Promise<Message> { ): Promise<Message> {
if (!params) params = {} if (!params) params = {}
@ -143,9 +150,10 @@ export async function sendMedia(
w: media.width || 0, w: media.width || 0,
h: media.height || 0, h: media.height || 0,
supportsStreaming: media.supportsStreaming, supportsStreaming: media.supportsStreaming,
roundMessage: media.isRound roundMessage: media.isRound,
}) })
if (media.isAnimated) attributes.push({ _: 'documentAttributeAnimated' }) if (media.isAnimated)
attributes.push({ _: 'documentAttributeAnimated' })
} }
if (media.type === 'audio' || media.type === 'voice') { if (media.type === 'audio' || media.type === 'voice') {
@ -155,7 +163,7 @@ export async function sendMedia(
duration: media.duration || 0, duration: media.duration || 0,
title: media.type === 'audio' ? media.title : undefined, title: media.type === 'audio' ? media.title : undefined,
performer: media.type === 'audio' ? media.performer : undefined, performer: media.type === 'audio' ? media.performer : undefined,
waveform: media.type === 'voice' ? media.waveform : undefined waveform: media.type === 'voice' ? media.waveform : undefined,
}) })
} }
@ -166,7 +174,7 @@ export async function sendMedia(
file: inputFile, file: inputFile,
thumb, thumb,
mimeType: mime, mimeType: mime,
attributes attributes,
} }
} }
@ -179,7 +187,6 @@ export async function sendMedia(
const peer = normalizeToInputPeer(await this.resolvePeer(chatId)) const peer = normalizeToInputPeer(await this.resolvePeer(chatId))
const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup) const replyMarkup = BotKeyboard._convertToTl(params.replyMarkup)
const res = await this.call({ const res = await this.call({
_: 'messages.sendMedia', _: 'messages.sendMedia',
peer, peer,
@ -195,6 +202,7 @@ export async function sendMedia(
replyMarkup, replyMarkup,
message, message,
entities, entities,
clearDraft: params.clearDraft,
}) })
return this._findMessageInUpdate(res) return this._findMessageInUpdate(res)

View file

@ -82,6 +82,13 @@ export async function sendPhoto(
* @param total Total file size * @param total Total file size
*/ */
progressCallback?: (uploaded: number, total: number) => void progressCallback?: (uploaded: number, total: number) => void
/**
* Whether to clear draft after sending this message.
*
* Defaults to `false`
*/
clearDraft?: boolean
} }
): Promise<Message> { ): Promise<Message> {
if (!params) params = {} if (!params) params = {}
@ -141,6 +148,7 @@ export async function sendPhoto(
replyMarkup, replyMarkup,
message, message,
entities, entities,
clearDraft: params.clearDraft,
}) })
return this._findMessageInUpdate(res) return this._findMessageInUpdate(res)

View file

@ -2,12 +2,7 @@ import { TelegramClient } from '../../client'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { inputPeerToPeer, normalizeToInputPeer } from '../../utils/peer-utils' import { inputPeerToPeer, normalizeToInputPeer } from '../../utils/peer-utils'
import { normalizeDate, randomUlong } from '../../utils/misc-utils' import { normalizeDate, randomUlong } from '../../utils/misc-utils'
import { import { InputPeerLike, Message, BotKeyboard, ReplyMarkup } from '../../types'
InputPeerLike,
Message,
BotKeyboard,
ReplyMarkup,
} from '../../types'
/** /**
* Send a text message * Send a text message
@ -64,6 +59,13 @@ export async function sendText(
* to hide a reply keyboard or to force a reply. * to hide a reply keyboard or to force a reply.
*/ */
replyMarkup?: ReplyMarkup replyMarkup?: ReplyMarkup
/**
* Whether to clear draft after sending this message.
*
* Defaults to `false`
*/
clearDraft?: boolean
} }
): Promise<Message> { ): Promise<Message> {
if (!params) params = {} if (!params) params = {}
@ -92,6 +94,7 @@ export async function sendText(
replyMarkup, replyMarkup,
message, message,
entities, entities,
clearDraft: params.clearDraft,
}) })
if (res._ === 'updateShortSentMessage') { if (res._ === 'updateShortSentMessage') {

View file

@ -0,0 +1,133 @@
import { TelegramClient } from '../../client'
import { tl } from '@mtcute/tl'
import { Chat } from '../peers'
import { Message } from './message'
import { DraftMessage } from './draft-message'
import { makeInspectable } from '../utils'
/**
* A dialog.
*
* Think of it as something that is listed
* in Telegram's main window.
*/
export class Dialog {
readonly client: TelegramClient
readonly raw: tl.RawDialog
/** Map of users in this object. Mainly for internal use */
readonly _users: Record<number, tl.TypeUser>
/** Map of chats in this object. Mainly for internal use */
readonly _chats: Record<number, tl.TypeChat>
/** Map of messages in this object. Mainly for internal use */
readonly _messages: Record<number, tl.TypeMessage>
constructor(
client: TelegramClient,
raw: tl.RawDialog,
users: Record<number, tl.TypeUser>,
chats: Record<number, tl.TypeChat>,
messages: Record<number, tl.TypeMessage>
) {
this.client = client
this.raw = raw
this._users = users
this._chats = chats
this._messages = messages
}
/**
* Whether this dialog is pinned
*/
get isPinned(): boolean {
return !!this.raw.pinned
}
/**
* Whether this chat was manually marked as unread
*/
get isManuallyUnread(): boolean {
return !!this.raw.unreadMark
}
/**
* Whether this chat should be considered unread
* (i.e. has more than 1 unread message, or has
* a "manually unread" mark)
*/
get isUnread(): boolean {
return this.raw.unreadMark || this.raw.unreadCount > 1
}
private _chat?: Chat
/**
* Chat that this dialog represents
*/
get chat(): Chat {
if (!this._chat) {
const peer = this.raw.peer
let chat
if (peer._ === 'peerChannel' || peer._ === 'peerChat') {
chat = this._chats[peer._ === 'peerChannel' ? peer.channelId : peer.chatId]
} else {
chat = this._users[peer.userId]
}
this._chat = new Chat(this.client, chat)
}
return this._chat
}
private _lastMessage?: Message | null
/**
* The latest message sent in this chat
*/
get lastMessage(): Message | null {
if (this._lastMessage === undefined) {
const cid = this.chat.id
if (cid in this._messages) {
this._lastMessage = new Message(this.client, this._messages[cid], this._users, this._chats)
} else {
this._lastMessage = null
}
}
return this._lastMessage
}
/**
* Number of unread messages
*/
get unreadCount(): number {
return this.raw.unreadCount
}
/**
* Number of unread messages
*/
get unreadMentionsCount(): number {
return this.raw.unreadMentionsCount
}
private _draftMessage?: DraftMessage | null
/**
* Draft message in this dialog
*/
get draftMessage(): DraftMessage | null {
if (this._draftMessage === undefined) {
if (this.raw.draft?._ === 'draftMessage') {
this._draftMessage = new DraftMessage(this.client, this.raw.draft, this.chat.inputPeer)
} else {
this._draftMessage = null
}
}
return this._draftMessage
}
}
makeInspectable(Dialog)

View file

@ -0,0 +1,120 @@
/**
* A draft message
*/
import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client'
import { MessageEntity } from './message-entity'
import { Message } from './message'
import { InputPeerLike } from '../peers'
import { makeInspectable } from '../utils'
import { InputMediaLike } from '../media'
export class DraftMessage {
readonly client: TelegramClient
readonly raw: tl.RawDraftMessage
private _chatId: InputPeerLike
constructor(
client: TelegramClient,
raw: tl.RawDraftMessage,
chatId: InputPeerLike
) {
this.client = client
this.raw = raw
this._chatId = chatId
}
/**
* Text of the draft message
*/
get text(): string {
return this.raw.message
}
/**
* The message this message will reply to
*/
get replyToMessageId(): number | null {
return this.raw.replyToMsgId ?? null
}
/**
* Date of the last time this draft was updated
*/
get date(): Date {
return new Date(this.raw.date * 1000)
}
/**
* Whether no webpage preview will be generated
*/
get disableWebPreview(): boolean {
return !!this.raw.noWebpage
}
private _entities?: MessageEntity[]
/**
* Message text entities (may be empty)
*/
get entities(): MessageEntity[] {
if (!this._entities) {
this._entities = []
if (this.raw.entities?.length) {
for (const ent of this.raw.entities) {
const parsed = MessageEntity._parse(ent)
if (parsed) this._entities.push(parsed)
}
}
}
return this._entities
}
/**
* Send this draft as a message.
* Calling this method will clear current draft.
*
* @param params Additional sending parameters
* @link TelegramClient.sendText
*/
send(params?: Parameters<TelegramClient['sendText']>[2]): Promise<Message> {
return this.client.sendText(this._chatId, this.raw.message, {
clearDraft: true,
disableWebPreview: this.raw.noWebpage,
entities: this.raw.entities,
replyTo: this.raw.replyToMsgId,
...(params || {}),
})
}
/**
* Send this draft as a message with media.
* Calling this method will clear current draft.
*
* If passed media does not have an
* explicit caption, it will be set to {@link text},
* and its entities to {@link entities}
*
* @param media Media to be sent
* @param params Additional sending parameters
* @link TelegramClient.sendMedia
*/
sendWithMedia(
media: InputMediaLike,
params?: Parameters<TelegramClient['sendMedia']>[2]
): Promise<Message> {
if (!media.caption) {
media.caption = this.raw.message
media.entities = this.raw.entities
}
return this.client.sendMedia(this._chatId, media, {
clearDraft: true,
replyTo: this.raw.replyToMsgId,
...(params || {}),
})
}
}
makeInspectable(DraftMessage)

View file

@ -1,3 +1,5 @@
export * from './message-entity' export * from './message-entity'
export * from './message' export * from './message'
export * from './search-filters' export * from './search-filters'
export * from './draft-message'
export * from './dialog'

View file

@ -163,6 +163,11 @@ export class Chat {
return this.peer._ === 'user' && this.peer.support! return this.peer._ === 'user' && this.peer.support!
} }
/** Whether this chat is chat with yourself (i.e. Saved Messages) */
get isSelf(): boolean {
return this.peer._ === 'user' && this.peer.self!
}
/** /**
* Title, for supergroups, channels and groups * Title, for supergroups, channels and groups
*/ */