From 69f59ab97e8830d96dd4d01d81fb372a83228911 Mon Sep 17 00:00:00 2001 From: Alina Sireneva Date: Sat, 16 Dec 2023 02:54:06 +0300 Subject: [PATCH] feat(client): `openChat` method --- packages/client/src/client.ts | 64 ++++++++++ packages/client/src/methods/_imports.ts | 1 + .../client/src/methods/chats/open-chat.ts | 50 ++++++++ .../client/src/methods/updates/manager.ts | 113 ++++++++++++++++++ packages/client/src/methods/updates/types.ts | 4 + packages/client/src/utils/updates-utils.ts | 37 +++--- 6 files changed, 254 insertions(+), 15 deletions(-) create mode 100644 packages/client/src/methods/chats/open-chat.ts diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 7157d730..002e0606 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -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 + /** + * 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 /** * 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) } diff --git a/packages/client/src/methods/_imports.ts b/packages/client/src/methods/_imports.ts index cb3d1b85..02cba68e 100644 --- a/packages/client/src/methods/_imports.ts +++ b/packages/client/src/methods/_imports.ts @@ -42,6 +42,7 @@ import { ForumTopic, GameHighScore, HistoryReadUpdate, + InlineCallbackQuery, InlineQuery, InputChatEventFilters, InputDialogFolder, diff --git a/packages/client/src/methods/chats/open-chat.ts b/packages/client/src/methods/chats/open-chat.ts new file mode 100644 index 00000000..7317ebca --- /dev/null +++ b/packages/client/src/methods/chats/open-chat.ts @@ -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 { + 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 { + 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) +} diff --git a/packages/client/src/methods/updates/manager.ts b/packages/client/src/methods/updates/manager.ts index ab86cac2..75f01904 100644 --- a/packages/client/src/methods/updates/manager.ts +++ b/packages/client/src/methods/updates/manager.ts @@ -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 { + // 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, diff --git a/packages/client/src/methods/updates/types.ts b/packages/client/src/methods/updates/types.ts index b02ce79e..3c7cc3f6 100644 --- a/packages/client/src/methods/updates/types.ts +++ b/packages/client/src/methods/updates/types.ts @@ -128,6 +128,8 @@ export interface UpdatesState { cpts: Map cptsMod: Map + channelDiffTimeouts: Map + channelsOpened: Map 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, diff --git a/packages/client/src/utils/updates-utils.ts b/packages/client/src/utils/updates-utils.ts index 9f7b73fa..0351e19e 100644 --- a/packages/client/src/utils/updates-utils.ts +++ b/packages/client/src/utils/updates-utils.ts @@ -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 */