feat: forums

closes MTQ-77
This commit is contained in:
alina 🌸 2023-10-03 00:58:45 +03:00
parent b2fccf4978
commit 00f30a6495
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
27 changed files with 1133 additions and 71 deletions

View file

@ -100,6 +100,17 @@ import { _normalizeInputFile } from './methods/files/normalize-input-file'
import { _normalizeInputMedia } from './methods/files/normalize-input-media'
import { uploadFile } from './methods/files/upload-file'
import { uploadMedia } from './methods/files/upload-media'
import { createForumTopic } from './methods/forums/create-forum-topic'
import { deleteForumTopicHistory } from './methods/forums/delete-forum-topic-history'
import { editForumTopic } from './methods/forums/edit-forum-topic'
import { getForumTopics, GetForumTopicsOffset } from './methods/forums/get-forum-topics'
import { getForumTopicsById } from './methods/forums/get-forum-topics-by-id'
import { iterForumTopics } from './methods/forums/iter-forum-topics'
import { reorderPinnedForumTopics } from './methods/forums/reorder-pinned-forum-topics'
import { toggleForum } from './methods/forums/toggle-forum'
import { toggleForumTopicClosed } from './methods/forums/toggle-forum-topic-closed'
import { toggleForumTopicPinned } from './methods/forums/toggle-forum-topic-pinned'
import { toggleGeneralTopicHidden } from './methods/forums/toggle-general-topic-hidden'
import { createInviteLink } from './methods/invite-links/create-invite-link'
import { editInviteLink } from './methods/invite-links/edit-invite-link'
import { exportInviteLink } from './methods/invite-links/export-invite-link'
@ -221,6 +232,7 @@ import {
Dialog,
FileDownloadParameters,
FormattedString,
ForumTopic,
GameHighScore,
HistoryReadUpdate,
IMessageEntityParser,
@ -1045,11 +1057,19 @@ export interface TelegramClient extends BaseTelegramClient {
/**
* Create a new broadcast channel
*
* @param title Channel title
* @param description (default: `''`) Channel description
* @returns Newly created channel
*/
createChannel(title: string, description?: string): Promise<Chat>
createChannel(params: {
/**
* Channel title
*/
title: string
/**
* Channel description
*/
description?: string
}): Promise<Chat>
/**
* Create a legacy group chat
*
@ -1065,10 +1085,31 @@ export interface TelegramClient extends BaseTelegramClient {
/**
* Create a new supergroup
*
* @param title Title of the supergroup
* @param description (default: `''`) Description of the supergroup
* @returns Newly created supergroup
*/
createSupergroup(title: string, description?: string): Promise<Chat>
createSupergroup(params: {
/**
* Supergroup title
*/
title: string
/**
* Supergroup description
*/
description?: string
/**
* Whether to create a forum
*/
forum?: boolean
/**
* TTL period (in seconds) for the newly created channel
*
* @default 0 (i.e. messages don't expire)
*/
ttlPeriod?: number
}): Promise<Chat>
/**
* Delete a channel or a supergroup
@ -1916,6 +1957,193 @@ export interface TelegramClient extends BaseTelegramClient {
progressCallback?: (uploaded: number, total: number) => void
},
): Promise<Extract<MessageMedia, Photo | RawDocument>>
/**
* Create a topic in a forum
*
* Only admins with `manageTopics` permission can do this.
*
* @param chatId Chat ID or username
* @returns Service message for the created topic
*/
createForumTopic(
chatId: InputPeerLike,
params: {
/**
* Topic title
*/
title: string
/**
* Icon of the topic.
*
* Can be a number (color in RGB, see {@link ForumTopic} static members for allowed values)
* or a custom emoji ID.
*
* Icon color can't be changed after the topic is created.
*/
icon?: number | tl.Long
/**
* Send as a specific channel
*/
sendAs?: InputPeerLike
},
): Promise<Message>
/**
* Delete a forum topic and all its history
*
* @param chat Chat or user ID, username, phone number, `"me"` or `"self"`
* @param topicId ID of the topic (i.e. its top message ID)
*/
deleteForumTopicHistory(chat: InputPeerLike, topicId: number): Promise<void>
/**
* Modify a topic in a forum
*
* Only admins with `manageTopics` permission can do this.
*
* @param chatId Chat ID or username
* @param topicId ID of the topic (i.e. its top message ID)
* @returns Service message about the modification
*/
editForumTopic(
chatId: InputPeerLike,
topicId: number,
params: {
/**
* New topic title
*/
title?: string
/**
* New icon of the topic.
*
* Can be a custom emoji ID, or `null` to remove the icon
* and use static color instead
*/
icon?: tl.Long | null
},
): Promise<Message>
/**
* Get a single forum topic by its ID
*
* @param chatId Chat ID or username
*/
getForumTopicsById(chatId: InputPeerLike, ids: number): Promise<ForumTopic>
/**
* Get forum topics by their IDs
*
* @param chatId Chat ID or username
*/
getForumTopicsById(chatId: InputPeerLike, ids: number[]): Promise<ForumTopic[]>
/**
* Get forum topics
*
* @param chatId Chat ID or username
*/
getForumTopics(
chatId: InputPeerLike,
params?: {
/**
* Search query
*/
query?: string
/**
* Offset for pagination
*/
offset?: GetForumTopicsOffset
/**
* Maximum number of topics to return.
*
* @default 100
*/
limit?: number
},
): Promise<ArrayPaginated<ForumTopic, GetForumTopicsOffset>>
/**
* Iterate over forum topics. Wrapper over {@link getForumTopics}.
*
* @param chatId Chat ID or username
*/
iterForumTopics(
chatId: InputPeerLike,
params?: Parameters<TelegramClient['getForumTopics']>[1] & {
/**
* Maximum number of topics to return.
*
* @default `Infinity`, i.e. return all topics
*/
limit?: number
/**
* Chunk size. Usually you shouldn't care about this.
*/
chunkSize?: number
},
): AsyncIterableIterator<ForumTopic>
/**
* Reorder pinned forum topics
*
* Only admins with `manageTopics` permission can do this.
*
* @param chatId Chat ID or username
* @param topicId ID of the topic (i.e. its top message ID)
*/
reorderPinnedForumTopics(
chatId: InputPeerLike,
params: {
/**
* Order of the pinned topics
*/
order: number[]
/**
* Whether to un-pin topics not present in the order
*/
force?: boolean
},
): Promise<void>
/**
* Toggle open/close status of a topic in a forum
*
* Only admins with `manageTopics` permission can do this.
*
* @param chatId Chat ID or username
* @param topicId ID of the topic (i.e. its top message ID)
* @param closed Whether the topic should be closed
* @returns Service message about the modification
*/
toggleForumTopicClosed(chatId: InputPeerLike, topicId: number, closed: boolean): Promise<Message>
/**
* Toggle whether a topic in a forum is pinned
*
* Only admins with `manageTopics` permission can do this.
*
* @param chatId Chat ID or username
* @param topicId ID of the topic (i.e. its top message ID)
* @param pinned Whether the topic should be pinned
*/
toggleForumTopicPinned(chatId: InputPeerLike, topicId: number, pinned: boolean): Promise<void>
/**
* Set whether a supergroup is a forum.
*
* Only owner of the supergroup can change this setting.
*
* @param chatId Chat ID or username
* @param enabled (default: `false`) Whether the supergroup should be a forum
*/
toggleForum(chatId: InputPeerLike, enabled?: boolean): Promise<void>
/**
* Toggle whether "General" topic in a forum is hidden or not
*
* Only admins with `manageTopics` permission can do this.
*
* @param chatId Chat ID or username
* @param hidden Whether the topic should be hidden
* @returns Service message about the modification
*/
toggleGeneralTopicHidden(chatId: InputPeerLike, hidden: boolean): Promise<Message>
/**
* Create an additional invite link for the chat.
*
@ -3061,6 +3289,8 @@ export interface TelegramClient extends BaseTelegramClient {
/**
* Message to reply to. Either a message object or message ID.
*
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/
replyTo?: number | Message
@ -3116,6 +3346,8 @@ export interface TelegramClient extends BaseTelegramClient {
params?: {
/**
* Message to reply to. Either a message object or message ID.
*
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/
replyTo?: number | Message
@ -3226,6 +3458,8 @@ export interface TelegramClient extends BaseTelegramClient {
/**
* Message to reply to. Either a message object or message ID.
*
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/
replyTo?: number | Message
@ -3353,6 +3587,8 @@ export interface TelegramClient extends BaseTelegramClient {
params?: {
/**
* Message to reply to. Either a message object or message ID.
*
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/
replyTo?: number | Message
@ -3509,7 +3745,15 @@ export interface TelegramClient extends BaseTelegramClient {
*
* @param chatId Chat or user ID
*/
unpinAllMessages(chatId: InputPeerLike): Promise<void>
unpinAllMessages(
chatId: InputPeerLike,
params?: {
/**
* For forums - unpin only messages from the given topic
*/
topicId?: number
},
): Promise<void>
/**
* Unpin a message in a group, supergroup, channel or PM.
*
@ -4195,6 +4439,17 @@ export class TelegramClient extends BaseTelegramClient {
_normalizeInputMedia = _normalizeInputMedia
uploadFile = uploadFile
uploadMedia = uploadMedia
createForumTopic = createForumTopic
deleteForumTopicHistory = deleteForumTopicHistory
editForumTopic = editForumTopic
getForumTopicsById = getForumTopicsById
getForumTopics = getForumTopics
iterForumTopics = iterForumTopics
reorderPinnedForumTopics = reorderPinnedForumTopics
toggleForumTopicClosed = toggleForumTopicClosed
toggleForumTopicPinned = toggleForumTopicPinned
toggleForum = toggleForum
toggleGeneralTopicHidden = toggleGeneralTopicHidden
createInviteLink = createInviteLink
editInviteLink = editInviteLink
exportInviteLink = exportInviteLink

View file

@ -31,6 +31,7 @@ import {
Dialog,
FileDownloadParameters,
FormattedString,
ForumTopic,
GameHighScore,
HistoryReadUpdate,
IMessageEntityParser,

View file

@ -5,12 +5,25 @@ import { assertIsUpdatesGroup } from '../../utils/updates-utils'
/**
* Create a new broadcast channel
*
* @param title Channel title
* @param description Channel description
* @returns Newly created channel
* @internal
*/
export async function createChannel(this: TelegramClient, title: string, description = ''): Promise<Chat> {
export async function createChannel(
this: TelegramClient,
params: {
/**
* Channel title
*/
title: string
/**
* Channel description
*/
description?: string
},
): Promise<Chat> {
const { title, description = '' } = params
const res = await this.call({
_: 'channels.createChannel',
title,

View file

@ -5,16 +5,44 @@ import { assertIsUpdatesGroup } from '../../utils/updates-utils'
/**
* Create a new supergroup
*
* @param title Title of the supergroup
* @param description Description of the supergroup
* @returns Newly created supergroup
* @internal
*/
export async function createSupergroup(this: TelegramClient, title: string, description = ''): Promise<Chat> {
export async function createSupergroup(
this: TelegramClient,
params: {
/**
* Supergroup title
*/
title: string
/**
* Supergroup description
*/
description?: string
/**
* Whether to create a forum
*/
forum?: boolean
/**
* TTL period (in seconds) for the newly created channel
*
* @default 0 (i.e. messages don't expire)
*/
ttlPeriod?: number
},
): Promise<Chat> {
const { title, description = '', forum, ttlPeriod = 0 } = params
const res = await this.call({
_: 'channels.createChannel',
title,
about: description,
megagroup: true,
forum,
ttlPeriod,
})
assertIsUpdatesGroup('channels.createChannel', res)

View file

@ -0,0 +1,55 @@
import { tl } from '@mtcute/core'
import { randomLong } from '@mtcute/core/utils'
import { TelegramClient } from '../../client'
import { InputPeerLike, Message } from '../../types'
import { normalizeToInputChannel } from '../../utils/peer-utils'
/**
* Create a topic in a forum
*
* Only admins with `manageTopics` permission can do this.
*
* @param chatId Chat ID or username
* @returns Service message for the created topic
* @internal
*/
export async function createForumTopic(
this: TelegramClient,
chatId: InputPeerLike,
params: {
/**
* Topic title
*/
title: string
/**
* Icon of the topic.
*
* Can be a number (color in RGB, see {@link ForumTopic} static members for allowed values)
* or a custom emoji ID.
*
* Icon color can't be changed after the topic is created.
*/
icon?: number | tl.Long
/**
* Send as a specific channel
*/
sendAs?: InputPeerLike
},
): Promise<Message> {
const { title, icon, sendAs } = params
const res = await this.call({
_: 'channels.createForumTopic',
channel: normalizeToInputChannel(await this.resolvePeer(chatId), chatId),
title,
iconColor: typeof icon === 'number' ? icon : undefined,
iconEmojiId: typeof icon !== 'number' ? icon : undefined,
sendAs: sendAs ? await this.resolvePeer(sendAs) : undefined,
randomId: randomLong(),
})
return this._findMessageInUpdate(res)
}

View file

@ -0,0 +1,30 @@
import { assertTypeIsNot } from '@mtcute/core/utils'
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
import { normalizeToInputChannel } from '../../utils/peer-utils'
import { createDummyUpdate } from '../../utils/updates-utils'
/**
* Delete a forum topic and all its history
*
* @param chat Chat or user ID, username, phone number, `"me"` or `"self"`
* @param topicId ID of the topic (i.e. its top message ID)
* @internal
*/
export async function deleteForumTopicHistory(
this: TelegramClient,
chat: InputPeerLike,
topicId: number,
): Promise<void> {
const channel = normalizeToInputChannel(await this.resolvePeer(chat), chat)
assertTypeIsNot('deleteForumTopicHistory', channel, 'inputChannelEmpty')
const res = await this.call({
_: 'channels.deleteTopicHistory',
channel,
topMsgId: topicId,
})
this._handleUpdate(createDummyUpdate(res.pts, res.ptsCount, channel.channelId))
}

View file

@ -0,0 +1,49 @@
import Long from 'long'
import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { InputPeerLike, Message } from '../../types'
import { normalizeToInputChannel } from '../../utils/peer-utils'
/**
* Modify a topic in a forum
*
* Only admins with `manageTopics` permission can do this.
*
* @param chatId Chat ID or username
* @param topicId ID of the topic (i.e. its top message ID)
* @returns Service message about the modification
* @internal
*/
export async function editForumTopic(
this: TelegramClient,
chatId: InputPeerLike,
topicId: number,
params: {
/**
* New topic title
*/
title?: string
/**
* New icon of the topic.
*
* Can be a custom emoji ID, or `null` to remove the icon
* and use static color instead
*/
icon?: tl.Long | null
},
): Promise<Message> {
const { title, icon } = params
const res = await this.call({
_: 'channels.editForumTopic',
channel: normalizeToInputChannel(await this.resolvePeer(chatId), chatId),
topicId,
title,
iconEmojiId: icon ? icon ?? Long.ZERO : undefined,
})
return this._findMessageInUpdate(res)
}

View file

@ -0,0 +1,50 @@
import { MaybeArray } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { ForumTopic, InputPeerLike } from '../../types'
import { normalizeToInputChannel } from '../../utils'
/**
* Get a single forum topic by its ID
*
* @param chatId Chat ID or username
* @internal
*/
export async function getForumTopicsById(this: TelegramClient, chatId: InputPeerLike, ids: number): Promise<ForumTopic>
/**
* Get forum topics by their IDs
*
* @param chatId Chat ID or username
* @internal
*/
export async function getForumTopicsById(
this: TelegramClient,
chatId: InputPeerLike,
ids: number[],
): Promise<ForumTopic[]>
/**
* Get forum topics by their IDs
*
* @param chatId Chat ID or username
* @internal
*/
export async function getForumTopicsById(
this: TelegramClient,
chatId: InputPeerLike,
ids: MaybeArray<number>,
): Promise<MaybeArray<ForumTopic>> {
const single = !Array.isArray(ids)
if (single) ids = [ids as number]
const res = await this.call({
_: 'channels.getForumTopicsByID',
channel: normalizeToInputChannel(await this.resolvePeer(chatId)),
topics: ids as number[],
})
const topics = ForumTopic.parseTlForumTopics(this, res)
return single ? topics[0] : topics
}

View file

@ -0,0 +1,77 @@
import { TelegramClient } from '../../client'
import { ArrayPaginated, ForumTopic, InputPeerLike } from '../../types'
import { makeArrayPaginated } from '../../utils'
import { normalizeToInputChannel } from '../../utils/peer-utils'
// @exported
export interface GetForumTopicsOffset {
date: number
id: number
topic: number
}
const defaultOffset: GetForumTopicsOffset = {
date: 0,
id: 0,
topic: 0,
}
/**
* Get forum topics
*
* @param chatId Chat ID or username
* @internal
*/
export async function getForumTopics(
this: TelegramClient,
chatId: InputPeerLike,
params?: {
/**
* Search query
*/
query?: string
/**
* Offset for pagination
*/
offset?: GetForumTopicsOffset
/**
* Maximum number of topics to return.
*
* @default 100
*/
limit?: number
},
): Promise<ArrayPaginated<ForumTopic, GetForumTopicsOffset>> {
if (!params) params = {}
const {
query,
offset: { date: offsetDate, id: offsetId, topic: offsetTopic } = defaultOffset,
limit = 100,
} = params
const res = await this.call({
_: 'channels.getForumTopics',
channel: normalizeToInputChannel(await this.resolvePeer(chatId), chatId),
q: query,
offsetDate,
offsetId,
offsetTopic,
limit,
})
const topics = ForumTopic.parseTlForumTopics(this, res)
const last = topics[topics.length - 1]
const next = last ?
{
date: res.orderByCreateDate ? last.raw.date : last.lastMessage.raw.date,
id: last.raw.topMessage,
topic: last.raw.id,
} :
undefined
return makeArrayPaginated(topics, res.count, next)
}

View file

@ -0,0 +1,53 @@
import { TelegramClient } from '../../client'
import { ForumTopic, InputPeerLike } from '../../types'
import { normalizeToInputChannel } from '../../utils/peer-utils'
/**
* Iterate over forum topics. Wrapper over {@link getForumTopics}.
*
* @param chatId Chat ID or username
* @internal
*/
export async function* iterForumTopics(
this: TelegramClient,
chatId: InputPeerLike,
params?: Parameters<TelegramClient['getForumTopics']>[1] & {
/**
* Maximum number of topics to return.
*
* @default `Infinity`, i.e. return all topics
*/
limit?: number
/**
* Chunk size. Usually you shouldn't care about this.
*/
chunkSize?: number
},
): AsyncIterableIterator<ForumTopic> {
if (!params) params = {}
const { query, limit = Infinity, chunkSize = 100 } = params
const peer = normalizeToInputChannel(await this.resolvePeer(chatId))
let { offset } = params
let current = 0
for (;;) {
const res = await this.getForumTopics(peer, {
query,
offset,
limit: Math.min(chunkSize, limit - current),
})
for (const topic of res) {
yield topic
if (++current >= limit) return
}
if (!res.next) return
offset = res.next
}
}

View file

@ -0,0 +1,36 @@
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
import { normalizeToInputChannel } from '../../utils/peer-utils'
/**
* Reorder pinned forum topics
*
* Only admins with `manageTopics` permission can do this.
*
* @param chatId Chat ID or username
* @param topicId ID of the topic (i.e. its top message ID)
* @internal
*/
export async function reorderPinnedForumTopics(
this: TelegramClient,
chatId: InputPeerLike,
params: {
/**
* Order of the pinned topics
*/
order: number[]
/**
* Whether to un-pin topics not present in the order
*/
force?: boolean
},
): Promise<void> {
const { order, force } = params
await this.call({
_: 'channels.reorderPinnedForumTopics',
channel: normalizeToInputChannel(await this.resolvePeer(chatId), chatId),
order,
force,
})
}

View file

@ -0,0 +1,30 @@
import { TelegramClient } from '../../client'
import { InputPeerLike, Message } from '../../types'
import { normalizeToInputChannel } from '../../utils/peer-utils'
/**
* Toggle open/close status of a topic in a forum
*
* Only admins with `manageTopics` permission can do this.
*
* @param chatId Chat ID or username
* @param topicId ID of the topic (i.e. its top message ID)
* @param closed Whether the topic should be closed
* @returns Service message about the modification
* @internal
*/
export async function toggleForumTopicClosed(
this: TelegramClient,
chatId: InputPeerLike,
topicId: number,
closed: boolean,
): Promise<Message> {
const res = await this.call({
_: 'channels.editForumTopic',
channel: normalizeToInputChannel(await this.resolvePeer(chatId), chatId),
topicId,
closed,
})
return this._findMessageInUpdate(res)
}

View file

@ -0,0 +1,27 @@
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
import { normalizeToInputChannel } from '../../utils/peer-utils'
/**
* Toggle whether a topic in a forum is pinned
*
* Only admins with `manageTopics` permission can do this.
*
* @param chatId Chat ID or username
* @param topicId ID of the topic (i.e. its top message ID)
* @param pinned Whether the topic should be pinned
* @internal
*/
export async function toggleForumTopicPinned(
this: TelegramClient,
chatId: InputPeerLike,
topicId: number,
pinned: boolean,
): Promise<void> {
await this.call({
_: 'channels.updatePinnedForumTopic',
channel: normalizeToInputChannel(await this.resolvePeer(chatId), chatId),
topicId,
pinned,
})
}

View file

@ -0,0 +1,21 @@
import { TelegramClient } from '../../client'
import { InputPeerLike } from '../../types'
import { normalizeToInputChannel } from '../../utils/peer-utils'
/**
* Set whether a supergroup is a forum.
*
* Only owner of the supergroup can change this setting.
*
* @param chatId Chat ID or username
* @param enabled Whether the supergroup should be a forum
* @internal
*/
export async function toggleForum(this: TelegramClient, chatId: InputPeerLike, enabled = false): Promise<void> {
const res = await this.call({
_: 'channels.toggleForum',
channel: normalizeToInputChannel(await this.resolvePeer(chatId), chatId),
enabled,
})
this._handleUpdate(res)
}

View file

@ -0,0 +1,28 @@
import { TelegramClient } from '../../client'
import { InputPeerLike, Message } from '../../types'
import { normalizeToInputChannel } from '../../utils/peer-utils'
/**
* Toggle whether "General" topic in a forum is hidden or not
*
* Only admins with `manageTopics` permission can do this.
*
* @param chatId Chat ID or username
* @param hidden Whether the topic should be hidden
* @returns Service message about the modification
* @internal
*/
export async function toggleGeneralTopicHidden(
this: TelegramClient,
chatId: InputPeerLike,
hidden: boolean,
): Promise<Message> {
const res = await this.call({
_: 'channels.editForumTopic',
channel: normalizeToInputChannel(await this.resolvePeer(chatId), chatId),
topicId: 1,
hidden,
})
return this._findMessageInUpdate(res)
}

View file

@ -57,6 +57,8 @@ export async function sendCopy(
/**
* Message to reply to. Either a message object or message ID.
*
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/
replyTo?: number | Message

View file

@ -25,6 +25,8 @@ export async function sendMediaGroup(
params?: {
/**
* Message to reply to. Either a message object or message ID.
*
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/
replyTo?: number | Message

View file

@ -48,6 +48,8 @@ export async function sendMedia(
/**
* Message to reply to. Either a message object or message ID.
*
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/
replyTo?: number | Message

View file

@ -30,6 +30,8 @@ export async function sendText(
params?: {
/**
* Message to reply to. Either a message object or message ID.
*
* For forums - can also be an ID of the topic (i.e. its top message ID)
*/
replyTo?: number | Message

View file

@ -9,12 +9,24 @@ import { createDummyUpdate } from '../../utils/updates-utils'
* @param chatId Chat or user ID
* @internal
*/
export async function unpinAllMessages(this: TelegramClient, chatId: InputPeerLike): Promise<void> {
export async function unpinAllMessages(
this: TelegramClient,
chatId: InputPeerLike,
params?: {
/**
* For forums - unpin only messages from the given topic
*/
topicId?: number
},
): Promise<void> {
const { topicId } = params ?? {}
const peer = await this.resolvePeer(chatId)
const res = await this.call({
_: 'messages.unpinAllMessages',
peer,
topMsgId: topicId,
})
if (isInputPeerChannel(peer)) {

View file

@ -25,7 +25,7 @@ export class Dialog {
readonly client: TelegramClient,
readonly raw: tl.RawDialog,
readonly _peers: PeersIndex,
readonly _messages: Record<number, tl.TypeMessage>,
readonly _messages: Map<number, tl.TypeMessage>,
) {}
/**
@ -44,11 +44,11 @@ export class Dialog {
const peers = PeersIndex.from(dialogs)
const messages: Record<number, tl.TypeMessage> = {}
const messages = new Map<number, tl.TypeMessage>()
dialogs.messages.forEach((msg) => {
if (!msg.peerId) return
messages[getMarkedPeerId(msg.peerId)] = msg
messages.set(getMarkedPeerId(msg.peerId), msg)
})
const arr = dialogs.dialogs
@ -228,8 +228,8 @@ export class Dialog {
if (!this._lastMessage) {
const cid = this.chat.id
if (cid in this._messages) {
this._lastMessage = new Message(this.client, this._messages[cid], this._peers)
if (this._messages.has(cid)) {
this._lastMessage = new Message(this.client, this._messages.get(cid)!, this._peers)
} else {
throw new MtMessageNotFoundError(cid, 0)
}
@ -267,12 +267,19 @@ export class Dialog {
}
/**
* Number of unread messages
* Number of unread mentions
*/
get unreadMentionsCount(): number {
return this.raw.unreadMentionsCount
}
/**
* Number of unread reactions
*/
get unreadReactionsCount(): number {
return this.raw.unreadReactionsCount
}
private _draftMessage?: DraftMessage | null
/**
* Draft message in this dialog
@ -280,7 +287,7 @@ export class Dialog {
get draftMessage(): DraftMessage | null {
if (this._draftMessage === undefined) {
if (this.raw.draft?._ === 'draftMessage') {
this._draftMessage = new DraftMessage(this.client, this.raw.draft, this.chat.inputPeer)
this._draftMessage = new DraftMessage(this.client, this.raw.draft)
} else {
this._draftMessage = null
}

View file

@ -2,16 +2,13 @@ import { tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { makeInspectable } from '../../utils'
import { InputMediaLike } from '../media'
import { InputPeerLike } from '../peers'
import { Message } from './message'
import { MessageEntity } from './message-entity'
/**
* A draft message
*/
export class DraftMessage {
constructor(readonly client: TelegramClient, readonly raw: tl.RawDraftMessage, readonly _chatId: InputPeerLike) {}
constructor(readonly client: TelegramClient, readonly raw: tl.RawDraftMessage) {}
/**
* Text of the draft message
@ -59,45 +56,6 @@ export class DraftMessage {
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> {
return this.client.sendMedia(this._chatId, media, {
clearDraft: true,
replyTo: this.raw.replyToMsgId,
caption: this.raw.message,
entities: this.raw.entities,
...(params || {}),
})
}
}
makeInspectable(DraftMessage)

View file

@ -249,6 +249,37 @@ export interface ActionSetTtl {
readonly period: number
}
/** Forum topic was created */
export interface ActionTopicCreated {
readonly type: 'topic_created'
/** Title of the topic */
title: string
/** Icon color of the topic */
iconColor: number
/** Icon emoji of the topic */
iconCustomEmoji?: tl.Long
}
/** Forum topic was modified */
export interface ActionTopicEdited {
readonly type: 'topic_edited'
/** New title of the topic */
title?: string
/** New icon emoji of the topic (may be empty) */
iconCustomEmoji?: tl.Long
/** Whether the topic was opened/closed */
closed?: boolean
/** Whether the topic was (un-)hidden - only for "General" topic (`id=1`) */
hidden?: boolean
}
export type MessageAction =
| ActionChatCreated
| ActionChannelCreated
@ -275,6 +306,8 @@ export type MessageAction =
| ActionGroupCallEnded
| ActionGroupInvite
| ActionSetTtl
| ActionTopicCreated
| ActionTopicEdited
| null
/** @internal */
@ -426,6 +459,21 @@ export function _messageActionFromTl(this: Message, act: tl.TypeMessageAction):
type: 'set_ttl',
period: act.period,
}
case 'messageActionTopicCreate':
return {
type: 'topic_created',
title: act.title,
iconColor: act.iconColor,
iconCustomEmoji: act.iconEmojiId,
}
case 'messageActionTopicEdit':
return {
type: 'topic_edited',
title: act.title,
iconCustomEmoji: act.iconEmojiId,
closed: act.closed,
hidden: act.hidden,
}
default:
return null
}

View file

@ -1,6 +1,7 @@
import { tl } from '@mtcute/core'
import { assertTypeIs } from '@mtcute/core/utils'
import { PeersIndex, TelegramClient, toggleChannelIdMark } from '../../..'
import { ForumTopic, PeersIndex, TelegramClient, toggleChannelIdMark } from '../../..'
import { Photo } from '../../media'
import { Message } from '../../messages'
import { ChatInviteLink } from '../chat-invite-link'
@ -297,6 +298,41 @@ export interface ChatActionTtlChanged {
new: number
}
/** Forum has been toggled */
export interface ChatActionForumToggled {
type: 'forum_toggled'
/** New status */
enabled: boolean
}
/** Forum topic has been created */
export interface ChatActionTopicCreated {
type: 'topic_created'
/** Topic that has been created */
topic: ForumTopic
}
/** Forum topic has been edited */
export interface ChatActionTopicEdited {
type: 'topic_edited'
/** Old topic info */
old: ForumTopic
/** New topic info */
new: ForumTopic
}
/** Forum topic has been edited */
export interface ChatActionTopicDeleted {
type: 'topic_deleted'
/** Old topic info */
topic: ForumTopic
}
/** Chat event action (`null` if unsupported) */
export type ChatAction =
| ChatActionUserJoined
@ -329,6 +365,10 @@ export type ChatAction =
| ChatActionInviteLinkRevoked
| ChatActionUserJoinedApproved
| ChatActionTtlChanged
| ChatActionForumToggled
| ChatActionTopicCreated
| ChatActionTopicEdited
| ChatActionTopicDeleted
| null
/** @internal */
@ -341,12 +381,6 @@ export function _actionFromTl(
// 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
@ -520,6 +554,39 @@ export function _actionFromTl(
link: new ChatInviteLink(client, e.invite, peers),
approvedBy: new User(client, peers.user(e.approvedBy)),
}
// channelAdminLogEventActionCreateTopic#58707d28 topic:ForumTopic = ChannelAdminLogEventAction;
// channelAdminLogEventActionEditTopic#f06fe208 prev_topic:ForumTopic new_topic:ForumTopic
// channelAdminLogEventActionDeleteTopic#ae168909 topic:ForumTopic = ChannelAdminLogEventAction;
case 'channelAdminLogEventActionToggleForum':
return {
type: 'forum_toggled',
enabled: e.newValue,
}
case 'channelAdminLogEventActionCreateTopic':
assertTypeIs('ChannelAdminLogEventActionCreateTopic#topic', e.topic, 'forumTopic')
return {
type: 'topic_created',
topic: new ForumTopic(client, e.topic, peers),
}
case 'channelAdminLogEventActionEditTopic':
assertTypeIs('ChannelAdminLogEventActionCreateTopic#topic', e.prevTopic, 'forumTopic')
assertTypeIs('ChannelAdminLogEventActionCreateTopic#topic', e.newTopic, 'forumTopic')
return {
type: 'topic_edited',
old: new ForumTopic(client, e.prevTopic, peers),
new: new ForumTopic(client, e.newTopic, peers),
}
case 'channelAdminLogEventActionDeleteTopic':
assertTypeIs('ChannelAdminLogEventActionCreateTopic#topic', e.topic, 'forumTopic')
return {
type: 'topic_deleted',
topic: new ForumTopic(client, e.topic, peers),
}
// case 'channelAdminLogEventActionPinTopic'
// ^ looks like it is not used, and pinned topics are not at all presented in the event log
default:
return null
}

View file

@ -57,6 +57,7 @@ export function normalizeChatEventFilters(input: InputChatEventFilters): ChatEve
case 'history_toggled':
case 'signatures_toggled':
case 'def_perms_changed':
case 'forum_toggled':
serverFilter.settings = true
break
case 'msg_pinned':
@ -94,6 +95,11 @@ export function normalizeChatEventFilters(input: InputChatEventFilters): ChatEve
case 'invite_revoked':
serverFilter.invites = true
break
case 'topic_created':
case 'topic_edited':
case 'topic_deleted':
serverFilter.forums = true
break
default:
assertNever(type)
}

View file

@ -0,0 +1,202 @@
import { MtTypeAssertionError, tl } from '@mtcute/core'
import { TelegramClient } from '../../client'
import { hasValueAtKey, makeInspectable } from '../../utils'
import { MtMessageNotFoundError } from '../errors'
import { DraftMessage, Message } from '../messages'
import { Chat } from './chat'
import { PeersIndex } from './peers-index'
import { User } from './user'
export class ForumTopic {
static COLOR_BLUE = 0x6fb9f0
static COLOR_YELLOW = 0xffd67e
static COLOR_PURPLE = 0xcb86db
static COLOR_GREEN = 0x8eee98
static COLOR_PINK = 0xff93b2
static COLOR_RED = 0xfb6f5f
constructor(
readonly client: TelegramClient,
readonly raw: tl.RawForumTopic,
readonly _peers: PeersIndex,
readonly _messages?: Map<number, tl.TypeMessage>,
) {}
static parseTlForumTopics(client: TelegramClient, topics: tl.messages.TypeForumTopics): ForumTopic[] {
const peers = PeersIndex.from(topics)
const messages = new Map<number, tl.TypeMessage>()
topics.messages.forEach((msg) => {
if (!msg.peerId) return
messages.set(msg.id, msg)
})
return topics.topics
.filter(hasValueAtKey('_', 'forumTopic'))
.map((it) => new ForumTopic(client, it, peers, messages))
}
/**
* Whether the topic was created by the current user
*/
get isMy(): boolean {
return this.raw.my!
}
/**
* Whether the topic is closed
*/
get isClosed(): boolean {
return this.raw.closed!
}
/**
* Whether the topic is pinned
*/
get isPinned(): boolean {
return this.raw.pinned!
}
/**
* Whether this constructor is a reduced version of the full topic information.
*
* If `true`, only {@link isMy}, {@link isClosed}, {@link id}, {@link date},
* {@link title}, {@link iconColor}, {@link iconCustomEmoji} and {@link creator}
* parameters will contain valid information.
*/
get isShort(): boolean {
return this.raw.short!
}
/**
* ID of the topic
*/
get id(): number {
return this.raw.id
}
/**
* Date when the topic was created
*/
get date(): Date {
return new Date(this.raw.date * 1000)
}
/**
* Title of the topic
*/
get title(): string {
return this.raw.title
}
/**
* Color of the topic's icon, used as a fallback
* in case {@link iconEmoji} is not set.
*
* One of the static `COLOR_*` fields.
*/
get iconColor(): number | null {
return this.raw.iconColor ?? null
}
/**
* Emoji used as the topic's icon.
*/
get iconCustomEmoji(): tl.Long | null {
return this.raw.iconEmojiId ?? null
}
private _creator?: User | Chat
/**
* Creator of the topic
*/
get creator(): User | Chat {
if (this._creator) return this._creator
switch (this.raw.fromId._) {
case 'peerUser':
return (this._creator = new User(this.client, this._peers.user(this.raw.fromId.userId)))
case 'peerChat':
return (this._creator = new Chat(this.client, this._peers.chat(this.raw.fromId.chatId)))
default:
throw new MtTypeAssertionError('ForumTopic#creator', 'peerUser | peerChat', this.raw.fromId._)
}
}
private _lastMessage?: Message
/**
* The latest message sent in this topic
*/
get lastMessage(): Message {
if (!this._lastMessage) {
const id = this.raw.topMessage
if (this._messages?.has(id)) {
this._lastMessage = new Message(this.client, this._messages.get(id)!, this._peers)
} else {
throw new MtMessageNotFoundError(0, id)
}
}
return this._lastMessage
}
/**
* ID of the last read outgoing message in this topic
*/
get lastReadIngoing(): number {
return this.raw.readInboxMaxId
}
/**
* ID of the last read ingoing message in this topic
*/
get lastReadOutgoing(): number {
return this.raw.readOutboxMaxId
}
/**
* ID of the last read message in this topic
*/
get lastRead(): number {
return Math.max(this.raw.readOutboxMaxId, this.raw.readInboxMaxId)
}
/**
* Number of unread messages in the topic
*/
get unreadCount(): number {
return this.raw.unreadCount
}
/**
* Number of unread mentions in the topic
*/
get unreadMentionsCount(): number {
return this.raw.unreadMentionsCount
}
/**
* Number of unread reactions in the topic
*/
get unreadReactionsCount(): number {
return this.raw.unreadReactionsCount
}
private _draftMessage?: DraftMessage
/**
* Draft message in the topic
*/
get draftMessage(): DraftMessage | null {
if (this._draftMessage) return this._draftMessage
if (!this.raw.draft || this.raw.draft._ === 'draftMessageEmpty') return null
return (this._draftMessage = new DraftMessage(this.client, this.raw.draft))
}
}
makeInspectable(ForumTopic)

View file

@ -9,6 +9,7 @@ export * from './chat-member'
export * from './chat-permissions'
export * from './chat-photo'
export * from './chat-preview'
export * from './forum-topic'
export * from './peers-index'
export * from './typing-status'
export * from './user'