diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index 77350238c..f47c0e1b8 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -10,22 +10,23 @@ import { buildApiUser } from '../apiBuilders/users'; import { buildApiBotInlineMediaResult, buildApiBotInlineResult, buildBotSwitchPm } from '../apiBuilders/bots'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { addEntitiesWithPhotosToLocalDb, addUserToLocalDb, deserializeBytes } from '../helpers'; +import { omitVirtualClassFields } from '../apiBuilders/helpers'; export function init() { } -export function answerCallbackButton( - { - chatId, accessHash, messageId, data, - }: { - chatId: string; accessHash?: string; messageId: number; data: string; - }, -) { - return invokeRequest(new GramJs.messages.GetBotCallbackAnswer({ +export async function answerCallbackButton({ + chatId, accessHash, messageId, data, +}: { + chatId: string; accessHash?: string; messageId: number; data: string; +}) { + const result = await invokeRequest(new GramJs.messages.GetBotCallbackAnswer({ peer: buildInputPeer(chatId, accessHash), msgId: messageId, data: deserializeBytes(data), })); + + return result ? omitVirtualClassFields(result) : undefined; } export async function fetchTopInlineBots() { diff --git a/src/api/gramjs/methods/calls.ts b/src/api/gramjs/methods/calls.ts index 8476021b4..ce22d7c0f 100644 --- a/src/api/gramjs/methods/calls.ts +++ b/src/api/gramjs/methods/calls.ts @@ -164,7 +164,7 @@ export async function joinGroupCall({ data: JSON.stringify(params), }), inviteHash, - }), true); + })); if (!result) return undefined; @@ -187,7 +187,7 @@ export async function createGroupCall({ const result = await invokeRequest(new GramJs.phone.CreateGroupCall({ peer: buildInputPeer(peer.id, peer.accessHash), randomId, - }), true); + })); if (!result) return undefined; diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index c7bc0db48..3fb4abd69 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -508,7 +508,7 @@ export async function createChannel({ broadcast: true, title, about, - }), true); + })); // `createChannel` can return a lot of different update types according to docs, // but currently channel creation returns only `Updates` type. @@ -537,7 +537,7 @@ export async function createChannel({ await invokeRequest(new GramJs.channels.InviteToChannel({ channel: buildInputEntity(channel.id, channel.accessHash) as GramJs.InputChannel, users: users.map(({ id, accessHash }) => buildInputEntity(id, accessHash)) as GramJs.InputUser[], - }), true, noErrorUpdate); + }), undefined, noErrorUpdate); } catch (err) { // `noErrorUpdate` will cause an exception which we don't want either } @@ -606,7 +606,7 @@ export async function createGroupChat({ const result = await invokeRequest(new GramJs.messages.CreateChat({ title, users: users.map(({ id, accessHash }) => buildInputEntity(id, accessHash)) as GramJs.InputUser[], - }), true, true); + }), undefined, true); // `createChat` can return a lot of different update types according to docs, // but currently chat creation returns only `Updates` type. @@ -975,12 +975,12 @@ export function setDiscussionGroup({ return invokeRequest(new GramJs.channels.SetDiscussionGroup({ broadcast: buildInputPeer(channel.id, channel.accessHash), group: chat ? buildInputPeer(chat.id, chat.accessHash) : new GramJs.InputChannelEmpty(), - })); + }), true); } export async function migrateChat(chat: ApiChat) { const result = await invokeRequest( - new GramJs.messages.MigrateChat({ chatId: buildInputEntity(chat.id) as BigInt.BigInteger }), true, + new GramJs.messages.MigrateChat({ chatId: buildInputEntity(chat.id) as BigInt.BigInteger }), ); // `migrateChat` can return a lot of different update types according to docs, @@ -1049,16 +1049,16 @@ export async function openChatByInvite(hash: string) { return { chatId: chat.id }; } -export function addChatMembers(chat: ApiChat, users: ApiUser[], noErrorUpdate = false) { +export async function addChatMembers(chat: ApiChat, users: ApiUser[], noErrorUpdate = false) { try { if (chat.type === 'chatTypeChannel' || chat.type === 'chatTypeSuperGroup') { - return invokeRequest(new GramJs.channels.InviteToChannel({ + return await invokeRequest(new GramJs.channels.InviteToChannel({ channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel, users: users.map((user) => buildInputEntity(user.id, user.accessHash)) as GramJs.InputUser[], }), true, noErrorUpdate); } - return Promise.all(users.map((user) => { + return await Promise.all(users.map((user) => { return invokeRequest(new GramJs.messages.AddChatUser({ chatId: buildInputEntity(chat.id) as BigInt.BigInteger, userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser, @@ -1139,7 +1139,7 @@ function updateLocalDb(result: ( } export async function importChatInvite({ hash }: { hash: string }) { - const updates = await invokeRequest(new GramJs.messages.ImportChatInvite({ hash }), true); + const updates = await invokeRequest(new GramJs.messages.ImportChatInvite({ hash })); if (!(updates instanceof GramJs.Updates) || !updates.chats.length) { return undefined; } diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index d7ea953dc..21705fa7f 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -161,9 +161,21 @@ function handleGramJsUpdate(update: any) { export async function invokeRequest( request: T, - shouldHandleUpdates = false, + shouldReturnTrue: true, + shouldThrow?: boolean, +): Promise; + +export async function invokeRequest( + request: T, + shouldReturnTrue?: boolean, + shouldThrow?: boolean, +): Promise; + +export async function invokeRequest( + request: T, + shouldReturnTrue = false, shouldThrow = false, -): Promise { +) { if (!isConnected) { if (DEBUG) { // eslint-disable-next-line no-console @@ -186,35 +198,9 @@ export async function invokeRequest( console.log(`[GramJs/client] INVOKE RESPONSE ${request.className}`, result); } - if (shouldHandleUpdates) { - type ResultWithUpdates = - typeof result - & { updates?: GramJs.Updates | GramJs.UpdatesCombined }; + handleUpdatesFromRequest(request, result); - let updatesContainer; - if (result instanceof GramJs.Updates || result instanceof GramJs.UpdatesCombined) { - updatesContainer = result; - } else if ('updates' in (result as ResultWithUpdates) && ( - (result as ResultWithUpdates).updates instanceof GramJs.Updates - || (result as ResultWithUpdates).updates instanceof GramJs.UpdatesCombined - )) { - updatesContainer = (result as ResultWithUpdates).updates; - } - - if (updatesContainer) { - injectUpdateEntities(updatesContainer); - - updatesContainer.updates.forEach((update) => { - updater(update, request); - }); - } else if (result instanceof GramJs.UpdatesTooLong) { - // TODO Implement - } else { - updater(result as GramJs.TypeUpdates, request); - } - } - - return result; + return shouldReturnTrue ? result && true : result; } catch (err) { if (DEBUG) { // eslint-disable-next-line no-console @@ -233,6 +219,36 @@ export async function invokeRequest( } } +function handleUpdatesFromRequest(request: T, result: T['__response']) { + let manyUpdates; + let singleUpdate; + + if (result instanceof GramJs.UpdatesCombined || result instanceof GramJs.Updates) { + manyUpdates = result; + } else if (typeof result === 'object' && 'updates' in result && ( + result.updates instanceof GramJs.Updates || result.updates instanceof GramJs.UpdatesCombined + )) { + manyUpdates = result.updates; + } else if ( + result instanceof GramJs.UpdateShortMessage + || result instanceof GramJs.UpdateShortChatMessage + || result instanceof GramJs.UpdateShort + || result instanceof GramJs.UpdateShortSentMessage + ) { + singleUpdate = result; + } + + if (manyUpdates) { + injectUpdateEntities(manyUpdates); + + manyUpdates.updates.forEach((update) => { + updater(update, request); + }); + } else if (singleUpdate) { + updater(singleUpdate, request); + } +} + export function downloadMedia( args: { url: string; mediaFormat: ApiMediaFormat; start?: number; end?: number; isHtmlAllowed?: boolean }, onProgress?: ApiOnProgress, diff --git a/src/api/gramjs/methods/management.ts b/src/api/gramjs/methods/management.ts index 73c98a485..bb555809a 100644 --- a/src/api/gramjs/methods/management.ts +++ b/src/api/gramjs/methods/management.ts @@ -10,19 +10,11 @@ export function init(_onUpdate: OnApiUpdate) { onUpdate = _onUpdate; } -export async function checkChatUsername( - { username }: { username: string }, -) { - try { - const result = await invokeRequest(new GramJs.channels.CheckUsername({ - channel: new GramJs.InputChannelEmpty(), - username, - }), undefined, true); - - return result!; - } catch (err) { - return false; - } +export function checkChatUsername({ username }: { username: string }) { + return invokeRequest(new GramJs.channels.CheckUsername({ + channel: new GramJs.InputChannelEmpty(), + username, + })); } export async function setChatUsername( diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index d25b1497b..4839de37c 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -48,7 +48,7 @@ export function updateProfile({ firstName: firstName || '', lastName: lastName || '', about: about || '', - })); + }), true); } export function checkUsername(username: string) { @@ -56,14 +56,14 @@ export function checkUsername(username: string) { } export function updateUsername(username: string) { - return invokeRequest(new GramJs.account.UpdateUsername({ username })); + return invokeRequest(new GramJs.account.UpdateUsername({ username }), true); } export async function updateProfilePhoto(file: File) { const inputFile = await uploadFile(file); return invokeRequest(new GramJs.photos.UploadProfilePhoto({ file: inputFile, - })); + }), true); } export async function uploadProfilePhoto(file: File) { @@ -311,7 +311,7 @@ export async function fetchLangPack({ sourceLangPacks, langCode }: { return undefined; } - return { langPack: Object.assign({}, ...collections.reverse()) }; + return { langPack: Object.assign({}, ...collections.reverse()) as typeof collections[0] }; } export async function fetchLangStrings({ langPack, langCode, keys }: { diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts index 89073c0fa..aef49542a 100644 --- a/src/api/gramjs/methods/users.ts +++ b/src/api/gramjs/methods/users.ts @@ -157,7 +157,7 @@ export function updateContact({ firstName: firstName || '', lastName: lastName || '', })], - })); + }), true); } export function addContact({ diff --git a/src/api/gramjs/worker/provider.ts b/src/api/gramjs/worker/provider.ts index d1a4de9e1..ff53748b5 100644 --- a/src/api/gramjs/worker/provider.ts +++ b/src/api/gramjs/worker/provider.ts @@ -1,6 +1,7 @@ +import { Api } from '../../../lib/gramjs'; import { ApiInitialArgs, ApiOnProgress, OnApiUpdate } from '../../types'; import { Methods, MethodArgs, MethodResponse } from '../methods/types'; -import { WorkerMessageEvent, ThenArg, OriginRequest } from './types'; +import { WorkerMessageEvent, OriginRequest } from './types'; import { DEBUG } from '../../../config'; import generateIdFor from '../../../util/generateIdFor'; @@ -53,11 +54,36 @@ export function callApi(fnName: T, ...args: MethodArgs< return undefined; } - return makeRequest({ + const promise = makeRequest({ type: 'callMethod', name: fnName, args, - }) as MethodResponse; + }); + + // Some TypeScript magic to make sure `VirtualClass` is never returned from any method + if (DEBUG) { + (async () => { + try { + type ForbiddenTypes = + Api.VirtualClass + | (Api.VirtualClass | undefined)[]; + type ForbiddenResponses = + ForbiddenTypes + | (AnyLiteral & { [k: string]: ForbiddenTypes }); + + // Unwrap all chained promises + const response = await promise; + // Make sure responses do not include `VirtualClass` instances + const allowedResponse: Exclude = response; + // Suppress "unused variable" constraint + void allowedResponse; + } catch (err) { + // Do noting + } + })(); + } + + return promise as MethodResponse; } export function cancelApiProgress(progressCallback: ApiOnProgress) { @@ -105,7 +131,7 @@ function makeRequest(message: OriginRequest) { const requestState = { messageId } as RequestStates; // Re-wrap type because of `postMessage` - const promise: Promise>> = new Promise((resolve, reject) => { + const promise: Promise> = new Promise((resolve, reject) => { Object.assign(requestState, { resolve, reject }); }); diff --git a/src/api/gramjs/worker/worker.ts b/src/api/gramjs/worker/worker.ts index 3c7720a14..cbfadd957 100644 --- a/src/api/gramjs/worker/worker.ts +++ b/src/api/gramjs/worker/worker.ts @@ -8,8 +8,6 @@ declare const self: WorkerGlobalScope; handleErrors(); -// TODO Re-use `util/createWorkerInterface.ts` - const callbackState = new Map(); if (DEBUG) { @@ -45,6 +43,12 @@ onmessage = async (message: OriginMessageEvent) => { } const response = await callApi(name, ...args); + + if (DEBUG && typeof response === 'object' && 'CONSTRUCTOR_ID' in response) { + // eslint-disable-next-line no-console + console.error(`[GramJs/worker] \`${name}\`: Unexpected response \`${(response as any).className}\``); + } + const { arrayBuffer } = (typeof response === 'object' && 'arrayBuffer' in response && response) || {}; if (messageId) { diff --git a/src/lib/gramjs/tl/api.d.ts b/src/lib/gramjs/tl/api.d.ts index 2a567798b..da0d2ad7b 100644 --- a/src/lib/gramjs/tl/api.d.ts +++ b/src/lib/gramjs/tl/api.d.ts @@ -16,8 +16,7 @@ namespace Api { type Client = any; // To be defined. type Utils = any; // To be defined. - type X = unknown; - type Type = unknown; + type X = AnyLiteral; type Bool = boolean; type int = number; type int128 = BigInteger; diff --git a/src/lib/gramjs/tl/types-generator/template.js b/src/lib/gramjs/tl/types-generator/template.js index 01bce3e6e..a5633c56d 100644 --- a/src/lib/gramjs/tl/types-generator/template.js +++ b/src/lib/gramjs/tl/types-generator/template.js @@ -1,5 +1,5 @@ // Not sure what they are for. -const WEIRD_TYPES = new Set(['Bool', 'X', 'Type']) +const RAW_TYPES = new Set(['Bool', 'X']) module.exports = ({ types, constructors, functions }) => { function groupByKey(collection, key) { @@ -55,9 +55,10 @@ ${indent}};`.trim() function renderRequests(requests, indent) { return requests.map(({ name, argsConfig, result }) => { const argKeys = Object.keys(argsConfig) + const renderedResult = renderResult(result); if (!argKeys.length) { - return `export class ${upperFirst(name)} extends Request {};` + return `export class ${upperFirst(name)} extends Request {};` } let hasRequiredArgs = argKeys.some((argName) => argName !== 'flags' && !argsConfig[argName].isFlag) @@ -68,7 +69,7 @@ ${indent} ${argKeys.map((argName) => ` ${renderArg(argName, argsConfig[argName])}; `.trim()) .join(`\n${indent} `)} -${indent}}${!hasRequiredArgs ? ` | void` : ''}>, ${renderResult(result)}> { +${indent}}${!hasRequiredArgs ? ` | void` : ''}>, ${renderedResult}> { ${indent} ${argKeys.map((argName) => ` ${renderArg(argName, argsConfig[argName])}; `.trim()) @@ -98,7 +99,7 @@ ${indent}};`.trim() } function renderValueType(type, isVector, isTlType) { - if (WEIRD_TYPES.has(type)) { + if (RAW_TYPES.has(type)) { return type } @@ -148,8 +149,7 @@ namespace Api { type Client = any; // To be defined. type Utils = any; // To be defined. - type X = unknown; - type Type = unknown; + type X = AnyLiteral; type Bool = boolean; type int = number; type int128 = BigInteger;