[Refactoring] Make sure VirtualClass instances are never returned from API methods

This commit is contained in:
Alexander Zinchuk 2022-01-21 17:29:13 +01:00
parent caa0ba76c5
commit 73eade5289
11 changed files with 119 additions and 81 deletions

View File

@ -10,22 +10,23 @@ import { buildApiUser } from '../apiBuilders/users';
import { buildApiBotInlineMediaResult, buildApiBotInlineResult, buildBotSwitchPm } from '../apiBuilders/bots'; import { buildApiBotInlineMediaResult, buildApiBotInlineResult, buildBotSwitchPm } from '../apiBuilders/bots';
import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { addEntitiesWithPhotosToLocalDb, addUserToLocalDb, deserializeBytes } from '../helpers'; import { addEntitiesWithPhotosToLocalDb, addUserToLocalDb, deserializeBytes } from '../helpers';
import { omitVirtualClassFields } from '../apiBuilders/helpers';
export function init() { export function init() {
} }
export function answerCallbackButton( export async function answerCallbackButton({
{ chatId, accessHash, messageId, data,
chatId, accessHash, messageId, data, }: {
}: { chatId: string; accessHash?: string; messageId: number; data: string;
chatId: string; accessHash?: string; messageId: number; data: string; }) {
}, const result = await invokeRequest(new GramJs.messages.GetBotCallbackAnswer({
) {
return invokeRequest(new GramJs.messages.GetBotCallbackAnswer({
peer: buildInputPeer(chatId, accessHash), peer: buildInputPeer(chatId, accessHash),
msgId: messageId, msgId: messageId,
data: deserializeBytes(data), data: deserializeBytes(data),
})); }));
return result ? omitVirtualClassFields(result) : undefined;
} }
export async function fetchTopInlineBots() { export async function fetchTopInlineBots() {

View File

@ -164,7 +164,7 @@ export async function joinGroupCall({
data: JSON.stringify(params), data: JSON.stringify(params),
}), }),
inviteHash, inviteHash,
}), true); }));
if (!result) return undefined; if (!result) return undefined;
@ -187,7 +187,7 @@ export async function createGroupCall({
const result = await invokeRequest(new GramJs.phone.CreateGroupCall({ const result = await invokeRequest(new GramJs.phone.CreateGroupCall({
peer: buildInputPeer(peer.id, peer.accessHash), peer: buildInputPeer(peer.id, peer.accessHash),
randomId, randomId,
}), true); }));
if (!result) return undefined; if (!result) return undefined;

View File

@ -508,7 +508,7 @@ export async function createChannel({
broadcast: true, broadcast: true,
title, title,
about, about,
}), true); }));
// `createChannel` can return a lot of different update types according to docs, // `createChannel` can return a lot of different update types according to docs,
// but currently channel creation returns only `Updates` type. // but currently channel creation returns only `Updates` type.
@ -537,7 +537,7 @@ export async function createChannel({
await invokeRequest(new GramJs.channels.InviteToChannel({ await invokeRequest(new GramJs.channels.InviteToChannel({
channel: buildInputEntity(channel.id, channel.accessHash) as GramJs.InputChannel, channel: buildInputEntity(channel.id, channel.accessHash) as GramJs.InputChannel,
users: users.map(({ id, accessHash }) => buildInputEntity(id, accessHash)) as GramJs.InputUser[], users: users.map(({ id, accessHash }) => buildInputEntity(id, accessHash)) as GramJs.InputUser[],
}), true, noErrorUpdate); }), undefined, noErrorUpdate);
} catch (err) { } catch (err) {
// `noErrorUpdate` will cause an exception which we don't want either // `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({ const result = await invokeRequest(new GramJs.messages.CreateChat({
title, title,
users: users.map(({ id, accessHash }) => buildInputEntity(id, accessHash)) as GramJs.InputUser[], 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, // `createChat` can return a lot of different update types according to docs,
// but currently chat creation returns only `Updates` type. // but currently chat creation returns only `Updates` type.
@ -975,12 +975,12 @@ export function setDiscussionGroup({
return invokeRequest(new GramJs.channels.SetDiscussionGroup({ return invokeRequest(new GramJs.channels.SetDiscussionGroup({
broadcast: buildInputPeer(channel.id, channel.accessHash), broadcast: buildInputPeer(channel.id, channel.accessHash),
group: chat ? buildInputPeer(chat.id, chat.accessHash) : new GramJs.InputChannelEmpty(), group: chat ? buildInputPeer(chat.id, chat.accessHash) : new GramJs.InputChannelEmpty(),
})); }), true);
} }
export async function migrateChat(chat: ApiChat) { export async function migrateChat(chat: ApiChat) {
const result = await invokeRequest( 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, // `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 }; return { chatId: chat.id };
} }
export function addChatMembers(chat: ApiChat, users: ApiUser[], noErrorUpdate = false) { export async function addChatMembers(chat: ApiChat, users: ApiUser[], noErrorUpdate = false) {
try { try {
if (chat.type === 'chatTypeChannel' || chat.type === 'chatTypeSuperGroup') { 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, channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
users: users.map((user) => buildInputEntity(user.id, user.accessHash)) as GramJs.InputUser[], users: users.map((user) => buildInputEntity(user.id, user.accessHash)) as GramJs.InputUser[],
}), true, noErrorUpdate); }), true, noErrorUpdate);
} }
return Promise.all(users.map((user) => { return await Promise.all(users.map((user) => {
return invokeRequest(new GramJs.messages.AddChatUser({ return invokeRequest(new GramJs.messages.AddChatUser({
chatId: buildInputEntity(chat.id) as BigInt.BigInteger, chatId: buildInputEntity(chat.id) as BigInt.BigInteger,
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser, userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
@ -1139,7 +1139,7 @@ function updateLocalDb(result: (
} }
export async function importChatInvite({ hash }: { hash: string }) { 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) { if (!(updates instanceof GramJs.Updates) || !updates.chats.length) {
return undefined; return undefined;
} }

View File

@ -161,9 +161,21 @@ function handleGramJsUpdate(update: any) {
export async function invokeRequest<T extends GramJs.AnyRequest>( export async function invokeRequest<T extends GramJs.AnyRequest>(
request: T, request: T,
shouldHandleUpdates = false, shouldReturnTrue: true,
shouldThrow?: boolean,
): Promise<true | undefined>;
export async function invokeRequest<T extends GramJs.AnyRequest>(
request: T,
shouldReturnTrue?: boolean,
shouldThrow?: boolean,
): Promise<T['__response'] | undefined>;
export async function invokeRequest<T extends GramJs.AnyRequest>(
request: T,
shouldReturnTrue = false,
shouldThrow = false, shouldThrow = false,
): Promise<T['__response'] | undefined> { ) {
if (!isConnected) { if (!isConnected) {
if (DEBUG) { if (DEBUG) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -186,35 +198,9 @@ export async function invokeRequest<T extends GramJs.AnyRequest>(
console.log(`[GramJs/client] INVOKE RESPONSE ${request.className}`, result); console.log(`[GramJs/client] INVOKE RESPONSE ${request.className}`, result);
} }
if (shouldHandleUpdates) { handleUpdatesFromRequest(request, result);
type ResultWithUpdates =
typeof result
& { updates?: GramJs.Updates | GramJs.UpdatesCombined };
let updatesContainer; return shouldReturnTrue ? result && true : result;
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;
} catch (err) { } catch (err) {
if (DEBUG) { if (DEBUG) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -233,6 +219,36 @@ export async function invokeRequest<T extends GramJs.AnyRequest>(
} }
} }
function handleUpdatesFromRequest<T extends GramJs.AnyRequest>(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( export function downloadMedia(
args: { url: string; mediaFormat: ApiMediaFormat; start?: number; end?: number; isHtmlAllowed?: boolean }, args: { url: string; mediaFormat: ApiMediaFormat; start?: number; end?: number; isHtmlAllowed?: boolean },
onProgress?: ApiOnProgress, onProgress?: ApiOnProgress,

View File

@ -10,19 +10,11 @@ export function init(_onUpdate: OnApiUpdate) {
onUpdate = _onUpdate; onUpdate = _onUpdate;
} }
export async function checkChatUsername( export function checkChatUsername({ username }: { username: string }) {
{ username }: { username: string }, return invokeRequest(new GramJs.channels.CheckUsername({
) { channel: new GramJs.InputChannelEmpty(),
try { username,
const result = await invokeRequest(new GramJs.channels.CheckUsername({ }));
channel: new GramJs.InputChannelEmpty(),
username,
}), undefined, true);
return result!;
} catch (err) {
return false;
}
} }
export async function setChatUsername( export async function setChatUsername(

View File

@ -48,7 +48,7 @@ export function updateProfile({
firstName: firstName || '', firstName: firstName || '',
lastName: lastName || '', lastName: lastName || '',
about: about || '', about: about || '',
})); }), true);
} }
export function checkUsername(username: string) { export function checkUsername(username: string) {
@ -56,14 +56,14 @@ export function checkUsername(username: string) {
} }
export function updateUsername(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) { export async function updateProfilePhoto(file: File) {
const inputFile = await uploadFile(file); const inputFile = await uploadFile(file);
return invokeRequest(new GramJs.photos.UploadProfilePhoto({ return invokeRequest(new GramJs.photos.UploadProfilePhoto({
file: inputFile, file: inputFile,
})); }), true);
} }
export async function uploadProfilePhoto(file: File) { export async function uploadProfilePhoto(file: File) {
@ -311,7 +311,7 @@ export async function fetchLangPack({ sourceLangPacks, langCode }: {
return undefined; 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 }: { export async function fetchLangStrings({ langPack, langCode, keys }: {

View File

@ -157,7 +157,7 @@ export function updateContact({
firstName: firstName || '', firstName: firstName || '',
lastName: lastName || '', lastName: lastName || '',
})], })],
})); }), true);
} }
export function addContact({ export function addContact({

View File

@ -1,6 +1,7 @@
import { Api } from '../../../lib/gramjs';
import { ApiInitialArgs, ApiOnProgress, OnApiUpdate } from '../../types'; import { ApiInitialArgs, ApiOnProgress, OnApiUpdate } from '../../types';
import { Methods, MethodArgs, MethodResponse } from '../methods/types'; import { Methods, MethodArgs, MethodResponse } from '../methods/types';
import { WorkerMessageEvent, ThenArg, OriginRequest } from './types'; import { WorkerMessageEvent, OriginRequest } from './types';
import { DEBUG } from '../../../config'; import { DEBUG } from '../../../config';
import generateIdFor from '../../../util/generateIdFor'; import generateIdFor from '../../../util/generateIdFor';
@ -53,11 +54,36 @@ export function callApi<T extends keyof Methods>(fnName: T, ...args: MethodArgs<
return undefined; return undefined;
} }
return makeRequest({ const promise = makeRequest({
type: 'callMethod', type: 'callMethod',
name: fnName, name: fnName,
args, args,
}) as MethodResponse<T>; });
// Some TypeScript magic to make sure `VirtualClass` is never returned from any method
if (DEBUG) {
(async () => {
try {
type ForbiddenTypes =
Api.VirtualClass<any>
| (Api.VirtualClass<any> | 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<typeof response, ForbiddenResponses> = response;
// Suppress "unused variable" constraint
void allowedResponse;
} catch (err) {
// Do noting
}
})();
}
return promise as MethodResponse<T>;
} }
export function cancelApiProgress(progressCallback: ApiOnProgress) { export function cancelApiProgress(progressCallback: ApiOnProgress) {
@ -105,7 +131,7 @@ function makeRequest(message: OriginRequest) {
const requestState = { messageId } as RequestStates; const requestState = { messageId } as RequestStates;
// Re-wrap type because of `postMessage` // Re-wrap type because of `postMessage`
const promise: Promise<ThenArg<MethodResponse<keyof Methods>>> = new Promise((resolve, reject) => { const promise: Promise<MethodResponse<keyof Methods>> = new Promise((resolve, reject) => {
Object.assign(requestState, { resolve, reject }); Object.assign(requestState, { resolve, reject });
}); });

View File

@ -8,8 +8,6 @@ declare const self: WorkerGlobalScope;
handleErrors(); handleErrors();
// TODO Re-use `util/createWorkerInterface.ts`
const callbackState = new Map<string, ApiOnProgress>(); const callbackState = new Map<string, ApiOnProgress>();
if (DEBUG) { if (DEBUG) {
@ -45,6 +43,12 @@ onmessage = async (message: OriginMessageEvent) => {
} }
const response = await callApi(name, ...args); 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) || {}; const { arrayBuffer } = (typeof response === 'object' && 'arrayBuffer' in response && response) || {};
if (messageId) { if (messageId) {

View File

@ -16,8 +16,7 @@ namespace Api {
type Client = any; // To be defined. type Client = any; // To be defined.
type Utils = any; // To be defined. type Utils = any; // To be defined.
type X = unknown; type X = AnyLiteral;
type Type = unknown;
type Bool = boolean; type Bool = boolean;
type int = number; type int = number;
type int128 = BigInteger; type int128 = BigInteger;

View File

@ -1,5 +1,5 @@
// Not sure what they are for. // 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 }) => { module.exports = ({ types, constructors, functions }) => {
function groupByKey(collection, key) { function groupByKey(collection, key) {
@ -55,9 +55,10 @@ ${indent}};`.trim()
function renderRequests(requests, indent) { function renderRequests(requests, indent) {
return requests.map(({ name, argsConfig, result }) => { return requests.map(({ name, argsConfig, result }) => {
const argKeys = Object.keys(argsConfig) const argKeys = Object.keys(argsConfig)
const renderedResult = renderResult(result);
if (!argKeys.length) { if (!argKeys.length) {
return `export class ${upperFirst(name)} extends Request<void, ${renderResult(result)}> {};` return `export class ${upperFirst(name)} extends Request<void, ${renderedResult}> {};`
} }
let hasRequiredArgs = argKeys.some((argName) => argName !== 'flags' && !argsConfig[argName].isFlag) let hasRequiredArgs = argKeys.some((argName) => argName !== 'flags' && !argsConfig[argName].isFlag)
@ -68,7 +69,7 @@ ${indent} ${argKeys.map((argName) => `
${renderArg(argName, argsConfig[argName])}; ${renderArg(argName, argsConfig[argName])};
`.trim()) `.trim())
.join(`\n${indent} `)} .join(`\n${indent} `)}
${indent}}${!hasRequiredArgs ? ` | void` : ''}>, ${renderResult(result)}> { ${indent}}${!hasRequiredArgs ? ` | void` : ''}>, ${renderedResult}> {
${indent} ${argKeys.map((argName) => ` ${indent} ${argKeys.map((argName) => `
${renderArg(argName, argsConfig[argName])}; ${renderArg(argName, argsConfig[argName])};
`.trim()) `.trim())
@ -98,7 +99,7 @@ ${indent}};`.trim()
} }
function renderValueType(type, isVector, isTlType) { function renderValueType(type, isVector, isTlType) {
if (WEIRD_TYPES.has(type)) { if (RAW_TYPES.has(type)) {
return type return type
} }
@ -148,8 +149,7 @@ namespace Api {
type Client = any; // To be defined. type Client = any; // To be defined.
type Utils = any; // To be defined. type Utils = any; // To be defined.
type X = unknown; type X = AnyLiteral;
type Type = unknown;
type Bool = boolean; type Bool = boolean;
type int = number; type int = number;
type int128 = BigInteger; type int128 = BigInteger;