feat(client): openChat method

This commit is contained in:
alina 🌸 2023-12-16 02:54:06 +03:00
parent 2728166727
commit 69f59ab97e
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
6 changed files with 254 additions and 15 deletions

View file

@ -67,6 +67,7 @@ import { joinChat } from './methods/chats/join-chat.js'
import { kickChatMember } from './methods/chats/kick-chat-member.js'
import { leaveChat } from './methods/chats/leave-chat.js'
import { markChatUnread } from './methods/chats/mark-chat-unread.js'
import { openChat } from './methods/chats/open-chat.js'
import { reorderUsernames } from './methods/chats/reorder-usernames.js'
import { restrictChatMember } from './methods/chats/restrict-chat-member.js'
import { saveDraft } from './methods/chats/save-draft.js'
@ -223,6 +224,8 @@ import {
enableRps,
getCurrentRpsIncoming,
getCurrentRpsProcessing,
notifyChannelClosed,
notifyChannelOpened,
startUpdatesLoop,
stopUpdatesLoop,
} from './methods/updates/manager.js'
@ -274,6 +277,7 @@ import {
ForumTopic,
GameHighScore,
HistoryReadUpdate,
InlineCallbackQuery,
InlineQuery,
InputChatEventFilters,
InputDialogFolder,
@ -415,6 +419,13 @@ export interface TelegramClient extends BaseTelegramClient {
* @param handler Callback query handler
*/
on(name: 'callback_query', handler: (upd: CallbackQuery) => void): this
/**
* Register an inline callback query handler
*
* @param name Event name
* @param handler Inline callback query handler
*/
on(name: 'inline_callback_query', handler: (upd: InlineCallbackQuery) => void): this
/**
* Register a poll update handler
*
@ -1661,6 +1672,17 @@ export interface TelegramClient extends BaseTelegramClient {
* @param chatId Chat ID
*/
markChatUnread(chatId: InputPeerLike): Promise<void>
/**
* Inform the library that the user has opened a chat.
*
* Some library logic depends on this, for example, the library will
* periodically ping the server to keep the updates flowing.
*
* **Available**: both users and bots
*
* @param chat Chat to open
*/
openChat(chat: InputPeerLike): Promise<void>
/**
* Reorder usernames
*
@ -4863,6 +4885,36 @@ export interface TelegramClient extends BaseTelegramClient {
*
*/
catchUp(): void
/**
* **ADVANCED**
*
* Notify the updates manager that some channel was "opened".
* Channel difference for "opened" channels will be fetched on a regular basis.
* This is a low-level method, prefer using {@link openChat} instead.
*
* Channel must be resolve-able with `resolvePeer` method (i.e. be in cache);
* base chat PTS must either be passed (e.g. from {@link Dialog}), or cached in storage.
*
* **Available**: both users and bots
*
* @param channelId Bare ID of the channel
* @param pts PTS of the channel, if known (e.g. from {@link Dialog})
* @returns `true` if the channel was opened for the first time, `false` if it is already opened
*/
notifyChannelOpened(channelId: number, pts?: number): boolean
/**
* **ADVANCED**
*
* Notify the updates manager that some channel was "closed".
* Basically the opposite of {@link notifyChannelOpened}.
* This is a low-level method, prefer using {@link closeChat} instead.
*
* **Available**: both users and bots
*
* @param channelId Bare channel ID
* @returns `true` if the chat was closed for the last time, `false` otherwise
*/
notifyChannelClosed(channelId: number): boolean
/**
* Block a user
*
@ -5377,6 +5429,10 @@ TelegramClient.prototype.markChatUnread = function (...args) {
return markChatUnread(this, ...args)
}
TelegramClient.prototype.openChat = function (...args) {
return openChat(this, ...args)
}
TelegramClient.prototype.reorderUsernames = function (...args) {
return reorderUsernames(this, ...args)
}
@ -6037,6 +6093,14 @@ TelegramClient.prototype.catchUp = function (...args) {
return catchUp(this, ...args)
}
TelegramClient.prototype.notifyChannelOpened = function (...args) {
return notifyChannelOpened(this, ...args)
}
TelegramClient.prototype.notifyChannelClosed = function (...args) {
return notifyChannelClosed(this, ...args)
}
TelegramClient.prototype.blockUser = function (...args) {
return blockUser(this, ...args)
}

View file

@ -42,6 +42,7 @@ import {
ForumTopic,
GameHighScore,
HistoryReadUpdate,
InlineCallbackQuery,
InlineQuery,
InputChatEventFilters,
InputDialogFolder,

View file

@ -0,0 +1,50 @@
import { BaseTelegramClient } from '@mtcute/core'
import { InputPeerLike } from '../../types/peers/index.js'
import { isInputPeerChannel } from '../../utils/peer-utils.js'
import { getPeerDialogs } from '../dialogs/get-peer-dialogs.js'
import { notifyChannelClosed, notifyChannelOpened } from '../updates/manager.js'
import { resolvePeer } from '../users/resolve-peer.js'
/**
* Inform the library that the user has opened a chat.
*
* Some library logic depends on this, for example, the library will
* periodically ping the server to keep the updates flowing.
*
* @param chat Chat to open
*/
export async function openChat(client: BaseTelegramClient, chat: InputPeerLike): Promise<void> {
const peer = await resolvePeer(client, chat)
if (isInputPeerChannel(peer)) {
const [dialog] = await getPeerDialogs(client, peer)
if (!client.network.params.disableUpdates) {
notifyChannelOpened(client, peer.channelId, dialog.raw.pts)
}
}
// todo: once we have proper dialogs/peers db, we should also
// update full info here and fetch auxillary info (like channel members etc)
}
/**
* Inform the library that the user has closed a chat.
* Un-does the effect of {@link openChat}.
*
* Some library logic depends on this, for example, the library will
* periodically ping the server to keep the updates flowing.
*
* @param chat Chat to open
*/
export async function closeChat(client: BaseTelegramClient, chat: InputPeerLike): Promise<void> {
const peer = await resolvePeer(client, chat)
if (isInputPeerChannel(peer) && !client.network.params.disableUpdates) {
notifyChannelClosed(client, peer.channelId)
}
// todo: once we have proper dialogs/peers db, we should also
// update full info here and fetch auxillary info (like channel members etc)
}

View file

@ -5,6 +5,7 @@ import { getBarePeerId, getMarkedPeerId, markedPeerIdToBare, toggleChannelIdMark
import { PeersIndex } from '../../types/index.js'
import { isInputPeerChannel, isInputPeerUser, toInputChannel, toInputUser } from '../../utils/peer-utils.js'
import { RpsMeter } from '../../utils/rps-meter.js'
import { createDummyUpdatesContainer } from '../../utils/updates-utils.js'
import { getAuthState } from '../auth/_state.js'
import { _getChannelsBatched, _getUsersBatched } from '../chats/batched-queries.js'
import { resolvePeer } from '../users/resolve-peer.js'
@ -200,6 +201,11 @@ export function stopUpdatesLoop(client: BaseTelegramClient): void {
const state = getState(client)
if (!state.updatesLoopActive) return
for (const timer of state.channelDiffTimeouts.values()) {
clearTimeout(timer)
}
state.channelDiffTimeouts.clear()
state.updatesLoopActive = false
state.pendingUpdateContainers.clear()
state.pendingUnorderedUpdates.clear()
@ -227,6 +233,81 @@ export function catchUp(client: BaseTelegramClient): void {
handleUpdate(state, { _: 'updatesTooLong' })
}
/**
* **ADVANCED**
*
* Notify the updates manager that some channel was "opened".
* Channel difference for "opened" channels will be fetched on a regular basis.
* This is a low-level method, prefer using {@link openChat} instead.
*
* Channel must be resolve-able with `resolvePeer` method (i.e. be in cache);
* base chat PTS must either be passed (e.g. from {@link Dialog}), or cached in storage.
*
* @param channelId Bare ID of the channel
* @param pts PTS of the channel, if known (e.g. from {@link Dialog})
* @returns `true` if the channel was opened for the first time, `false` if it is already opened
*/
export function notifyChannelOpened(client: BaseTelegramClient, channelId: number, pts?: number): boolean {
// this method is intentionally very dumb to avoid making this file even more unreadable
const state = getState(client)
if (!state) {
throw new MtArgumentError('Updates processing is not enabled, use enableUpdatesProcessing() first')
}
if (state.channelsOpened.has(channelId)) {
state.log.debug('channel %d opened again', channelId)
state.channelsOpened.set(channelId, state.channelsOpened.get(channelId)! + 1)
return false
}
state.channelsOpened.set(channelId, 1)
state.log.debug('channel %d opened (pts=%d)', channelId, pts)
// force fetch channel difference
fetchChannelDifferenceViaUpdate(state, channelId, pts)
return true
}
/**
* **ADVANCED**
*
* Notify the updates manager that some channel was "closed".
* Basically the opposite of {@link notifyChannelOpened}.
* This is a low-level method, prefer using {@link closeChat} instead.
*
* @param channelId Bare channel ID
* @returns `true` if the chat was closed for the last time, `false` otherwise
*/
export function notifyChannelClosed(client: BaseTelegramClient, channelId: number): boolean {
const state = getState(client)
if (!state) {
throw new MtArgumentError('Updates processing is not enabled, use enableUpdatesProcessing() first')
}
const opened = state.channelsOpened.get(channelId)!
if (opened === undefined) {
return false
}
if (opened > 1) {
state.log.debug('channel %d closed, but is opened %d more times', channelId, opened - 1)
state.channelsOpened.set(channelId, opened - 1)
return false
}
state.channelsOpened.delete(channelId)
state.log.debug('channel %d closed', channelId)
return true
}
////////////////////////////////////////////// IMPLEMENTATION //////////////////////////////////////////////
const STATE_SYMBOL = Symbol('updatesState')
@ -670,6 +751,12 @@ async function fetchChannelDifference(
channelId: number,
fallbackPts?: number,
): Promise<boolean> {
// clear timeout if any
if (state.channelDiffTimeouts.has(channelId)) {
clearTimeout(state.channelDiffTimeouts.get(channelId))
state.channelDiffTimeouts.delete(channelId)
}
let _pts: number | null | undefined = state.cpts.get(channelId)
if (!_pts && state.catchUpChannels) {
@ -700,6 +787,8 @@ async function fetchChannelDifference(
limit = 1
}
let lastTimeout = 0
for (;;) {
const diff = await client.call({
_: 'updates.getChannelDifference',
@ -710,6 +799,8 @@ async function fetchChannelDifference(
filter: { _: 'channelMessagesFilterEmpty' },
})
if (diff.timeout) lastTimeout = diff.timeout
if (diff._ === 'updates.channelDifferenceEmpty') {
state.log.debug('getChannelDifference (cid = %d) returned channelDifferenceEmpty', channelId)
break
@ -785,6 +876,15 @@ async function fetchChannelDifference(
state.cpts.set(channelId, pts)
state.cptsMod.set(channelId, pts)
// schedule next fetch
if (lastTimeout !== 0 && state.channelsOpened.has(channelId)) {
state.log.debug('scheduling next fetch for channel %d in %d seconds', channelId, lastTimeout)
state.channelDiffTimeouts.set(
channelId,
setTimeout(() => fetchChannelDifferenceViaUpdate(state, channelId), lastTimeout * 1000),
)
}
return true
}
@ -814,6 +914,19 @@ function fetchChannelDifferenceLater(
}
}
function fetchChannelDifferenceViaUpdate(state: UpdatesState, channelId: number, pts?: number): void {
handleUpdate(
state,
createDummyUpdatesContainer([
{
_: 'updateChannelTooLong',
channelId,
pts,
},
]),
)
}
async function fetchDifference(
client: BaseTelegramClient,
state: UpdatesState,

View file

@ -128,6 +128,8 @@ export interface UpdatesState {
cpts: Map<number, number>
cptsMod: Map<number, number>
channelDiffTimeouts: Map<number, NodeJS.Timeout>
channelsOpened: Map<number, number>
log: Logger
stop: () => void
@ -175,6 +177,8 @@ export function createUpdatesState(
catchUpOnStart: opts.catchUp ?? false,
cpts: new Map(),
cptsMod: new Map(),
channelDiffTimeouts: new Map(),
channelsOpened: new Map(),
log: client.log.create('updates'),
stop: () => {}, // will be set later
handler: opts.onUpdate,

View file

@ -3,6 +3,20 @@ import { MtTypeAssertionError, tl } from '@mtcute/core'
// dummy updates which are used for methods that return messages.affectedHistory.
// that is not an update, but it carries info about pts, and we need to handle it
/**
* Create a dummy `updates` container with given updates.
*/
export function createDummyUpdatesContainer(updates: tl.TypeUpdate[], seq = 0): tl.TypeUpdates {
return {
_: 'updates',
seq,
date: 0,
chats: [],
users: [],
updates,
}
}
/**
* Create a dummy update from PTS and PTS count.
*
@ -11,21 +25,14 @@ import { MtTypeAssertionError, tl } from '@mtcute/core'
* @param channelId Channel ID (bare), if applicable
*/
export function createDummyUpdate(pts: number, ptsCount: number, channelId = 0): tl.TypeUpdates {
return {
_: 'updates',
seq: 0,
date: 0,
chats: [],
users: [],
updates: [
{
_: 'mtcute.dummyUpdate',
channelId,
pts,
ptsCount,
},
],
}
return createDummyUpdatesContainer([
{
_: 'mtcute.dummyUpdate',
channelId,
pts,
ptsCount,
},
])
}
/** @internal */