[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 { 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() {

View File

@ -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;

View File

@ -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;
}

View File

@ -161,9 +161,21 @@ function handleGramJsUpdate(update: any) {
export async function invokeRequest<T extends GramJs.AnyRequest>(
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,
): Promise<T['__response'] | undefined> {
) {
if (!isConnected) {
if (DEBUG) {
// 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);
}
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<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(
args: { url: string; mediaFormat: ApiMediaFormat; start?: number; end?: number; isHtmlAllowed?: boolean },
onProgress?: ApiOnProgress,

View File

@ -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(

View File

@ -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 }: {

View File

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

View File

@ -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<T extends keyof Methods>(fnName: T, ...args: MethodArgs<
return undefined;
}
return makeRequest({
const promise = makeRequest({
type: 'callMethod',
name: fnName,
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) {
@ -105,7 +131,7 @@ function makeRequest(message: OriginRequest) {
const requestState = { messageId } as RequestStates;
// 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 });
});

View File

@ -8,8 +8,6 @@ declare const self: WorkerGlobalScope;
handleErrors();
// TODO Re-use `util/createWorkerInterface.ts`
const callbackState = new Map<string, ApiOnProgress>();
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) {

View File

@ -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;

View File

@ -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<void, ${renderResult(result)}> {};`
return `export class ${upperFirst(name)} extends Request<void, ${renderedResult}> {};`
}
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;