Convert signal.js and preload.js to Typescript
This commit is contained in:
parent
e18510e41c
commit
2464e0a9c1
94 changed files with 2113 additions and 1848 deletions
|
@ -55,7 +55,6 @@ import * as Errors from '../types/errors';
|
|||
import { isEnabled } from '../RemoteConfig';
|
||||
|
||||
import { SignalService as Proto } from '../protobuf';
|
||||
import type { UnprocessedType } from '../textsecure.d';
|
||||
import { deriveGroupFields, MASTER_KEY_LENGTH } from '../groups';
|
||||
|
||||
import createTaskWithTimeout from './TaskWithTimeout';
|
||||
|
@ -81,6 +80,7 @@ import type {
|
|||
ProcessedSent,
|
||||
ProcessedEnvelope,
|
||||
IRequestHandler,
|
||||
UnprocessedType,
|
||||
} from './Types.d';
|
||||
import {
|
||||
EmptyEvent,
|
||||
|
@ -114,6 +114,7 @@ import * as log from '../logging/log';
|
|||
import * as durations from '../util/durations';
|
||||
import { areArraysMatchingSets } from '../util/areArraysMatchingSets';
|
||||
import { generateBlurHash } from '../util/generateBlurHash';
|
||||
import { APPLICATION_OCTET_STREAM } from '../types/MIME';
|
||||
|
||||
const GROUPV1_ID_LENGTH = 16;
|
||||
const GROUPV2_ID_LENGTH = 32;
|
||||
|
@ -1800,8 +1801,14 @@ export default class MessageReceiver
|
|||
}
|
||||
|
||||
if (msg.textAttachment) {
|
||||
const { text } = msg.textAttachment;
|
||||
if (!text) {
|
||||
throw new Error('Text attachments must have text!');
|
||||
}
|
||||
|
||||
attachments.push({
|
||||
size: msg.textAttachment.text?.length,
|
||||
size: text.length,
|
||||
contentType: APPLICATION_OCTET_STREAM,
|
||||
textAttachment: msg.textAttachment,
|
||||
blurHash: generateBlurHash(
|
||||
(msg.textAttachment.color ||
|
||||
|
|
|
@ -38,7 +38,11 @@ import type {
|
|||
WebAPIType,
|
||||
} from './WebAPI';
|
||||
import createTaskWithTimeout from './TaskWithTimeout';
|
||||
import type { CallbackResultType } from './Types.d';
|
||||
import type {
|
||||
CallbackResultType,
|
||||
StorageServiceCallOptionsType,
|
||||
StorageServiceCredentials,
|
||||
} from './Types.d';
|
||||
import type {
|
||||
SerializedCertificateType,
|
||||
SendLogCallbackType,
|
||||
|
@ -47,10 +51,6 @@ import OutgoingMessage from './OutgoingMessage';
|
|||
import type { CDSResponseType } from './CDSSocketManager';
|
||||
import * as Bytes from '../Bytes';
|
||||
import { getRandomBytes, getZeroes, encryptAttachment } from '../Crypto';
|
||||
import type {
|
||||
StorageServiceCallOptionsType,
|
||||
StorageServiceCredentials,
|
||||
} from '../textsecure.d';
|
||||
import {
|
||||
MessageError,
|
||||
SignedPreKeyRotationError,
|
||||
|
@ -73,6 +73,7 @@ import {
|
|||
numberToEmailType,
|
||||
numberToAddressType,
|
||||
} from '../types/EmbeddedContact';
|
||||
import type { StickerWithHydratedData } from '../types/Stickers';
|
||||
|
||||
export type SendMetadataType = {
|
||||
[identifier: string]: {
|
||||
|
@ -106,13 +107,7 @@ type GroupCallUpdateType = {
|
|||
eraId: string;
|
||||
};
|
||||
|
||||
export type StickerType = {
|
||||
packId: string;
|
||||
stickerId: number;
|
||||
packKey: string;
|
||||
data: Readonly<AttachmentType>;
|
||||
emoji?: string;
|
||||
|
||||
export type StickerType = StickerWithHydratedData & {
|
||||
attachmentPointer?: Proto.IAttachmentPointer;
|
||||
};
|
||||
|
||||
|
@ -631,7 +626,7 @@ export default class MessageSender {
|
|||
);
|
||||
}
|
||||
|
||||
getRandomPadding(): Uint8Array {
|
||||
static getRandomPadding(): Uint8Array {
|
||||
// Generate a random int from 1 and 512
|
||||
const buffer = getRandomBytes(2);
|
||||
const paddingLength = (new Uint16Array(buffer)[0] & 0x1ff) + 1;
|
||||
|
@ -996,7 +991,7 @@ export default class MessageSender {
|
|||
};
|
||||
}
|
||||
|
||||
createSyncMessage(): Proto.SyncMessage {
|
||||
static createSyncMessage(): Proto.SyncMessage {
|
||||
const syncMessage = new Proto.SyncMessage();
|
||||
|
||||
syncMessage.padding = this.getRandomPadding();
|
||||
|
@ -1287,7 +1282,7 @@ export default class MessageSender {
|
|||
];
|
||||
}
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
const syncMessage = MessageSender.createSyncMessage();
|
||||
syncMessage.sent = sentMessage;
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
@ -1303,12 +1298,12 @@ export default class MessageSender {
|
|||
});
|
||||
}
|
||||
|
||||
getRequestBlockSyncMessage(): SingleProtoJobData {
|
||||
static getRequestBlockSyncMessage(): SingleProtoJobData {
|
||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
|
||||
const request = new Proto.SyncMessage.Request();
|
||||
request.type = Proto.SyncMessage.Request.Type.BLOCKED;
|
||||
const syncMessage = this.createSyncMessage();
|
||||
const syncMessage = MessageSender.createSyncMessage();
|
||||
syncMessage.request = request;
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
@ -1326,12 +1321,12 @@ export default class MessageSender {
|
|||
};
|
||||
}
|
||||
|
||||
getRequestConfigurationSyncMessage(): SingleProtoJobData {
|
||||
static getRequestConfigurationSyncMessage(): SingleProtoJobData {
|
||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
|
||||
const request = new Proto.SyncMessage.Request();
|
||||
request.type = Proto.SyncMessage.Request.Type.CONFIGURATION;
|
||||
const syncMessage = this.createSyncMessage();
|
||||
const syncMessage = MessageSender.createSyncMessage();
|
||||
syncMessage.request = request;
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
@ -1349,7 +1344,7 @@ export default class MessageSender {
|
|||
};
|
||||
}
|
||||
|
||||
getRequestGroupSyncMessage(): SingleProtoJobData {
|
||||
static getRequestGroupSyncMessage(): SingleProtoJobData {
|
||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
|
||||
const request = new Proto.SyncMessage.Request();
|
||||
|
@ -1372,7 +1367,7 @@ export default class MessageSender {
|
|||
};
|
||||
}
|
||||
|
||||
getRequestContactSyncMessage(): SingleProtoJobData {
|
||||
static getRequestContactSyncMessage(): SingleProtoJobData {
|
||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
|
||||
const request = new Proto.SyncMessage.Request();
|
||||
|
@ -1395,7 +1390,7 @@ export default class MessageSender {
|
|||
};
|
||||
}
|
||||
|
||||
getRequestPniIdentitySyncMessage(): SingleProtoJobData {
|
||||
static getRequestPniIdentitySyncMessage(): SingleProtoJobData {
|
||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
|
||||
const request = new Proto.SyncMessage.Request();
|
||||
|
@ -1418,7 +1413,7 @@ export default class MessageSender {
|
|||
};
|
||||
}
|
||||
|
||||
getFetchManifestSyncMessage(): SingleProtoJobData {
|
||||
static getFetchManifestSyncMessage(): SingleProtoJobData {
|
||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
|
||||
const fetchLatest = new Proto.SyncMessage.FetchLatest();
|
||||
|
@ -1442,7 +1437,7 @@ export default class MessageSender {
|
|||
};
|
||||
}
|
||||
|
||||
getFetchLocalProfileSyncMessage(): SingleProtoJobData {
|
||||
static getFetchLocalProfileSyncMessage(): SingleProtoJobData {
|
||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
|
||||
const fetchLatest = new Proto.SyncMessage.FetchLatest();
|
||||
|
@ -1466,7 +1461,7 @@ export default class MessageSender {
|
|||
};
|
||||
}
|
||||
|
||||
getRequestKeySyncMessage(): SingleProtoJobData {
|
||||
static getRequestKeySyncMessage(): SingleProtoJobData {
|
||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
|
||||
const request = new Proto.SyncMessage.Request();
|
||||
|
@ -1500,7 +1495,7 @@ export default class MessageSender {
|
|||
): Promise<CallbackResultType> {
|
||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
const syncMessage = MessageSender.createSyncMessage();
|
||||
syncMessage.read = [];
|
||||
for (let i = 0; i < reads.length; i += 1) {
|
||||
const proto = new Proto.SyncMessage.Read({
|
||||
|
@ -1534,7 +1529,7 @@ export default class MessageSender {
|
|||
): Promise<CallbackResultType> {
|
||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
const syncMessage = MessageSender.createSyncMessage();
|
||||
syncMessage.viewed = views.map(
|
||||
view =>
|
||||
new Proto.SyncMessage.Viewed({
|
||||
|
@ -1577,7 +1572,7 @@ export default class MessageSender {
|
|||
|
||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
const syncMessage = MessageSender.createSyncMessage();
|
||||
|
||||
const viewOnceOpen = new Proto.SyncMessage.ViewOnceOpen();
|
||||
if (senderE164 !== undefined) {
|
||||
|
@ -1601,7 +1596,7 @@ export default class MessageSender {
|
|||
});
|
||||
}
|
||||
|
||||
getMessageRequestResponseSync(
|
||||
static getMessageRequestResponseSync(
|
||||
options: Readonly<{
|
||||
threadE164?: string;
|
||||
threadUuid?: string;
|
||||
|
@ -1611,7 +1606,7 @@ export default class MessageSender {
|
|||
): SingleProtoJobData {
|
||||
const myUuid = window.textsecure.storage.user.getCheckedUuid();
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
const syncMessage = MessageSender.createSyncMessage();
|
||||
|
||||
const response = new Proto.SyncMessage.MessageRequestResponse();
|
||||
if (options.threadE164 !== undefined) {
|
||||
|
@ -1642,7 +1637,7 @@ export default class MessageSender {
|
|||
};
|
||||
}
|
||||
|
||||
getStickerPackSync(
|
||||
static getStickerPackSync(
|
||||
operations: ReadonlyArray<{
|
||||
packId: string;
|
||||
packKey: string;
|
||||
|
@ -1663,7 +1658,7 @@ export default class MessageSender {
|
|||
return operation;
|
||||
});
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
const syncMessage = MessageSender.createSyncMessage();
|
||||
syncMessage.stickerPackOperation = packOperations;
|
||||
|
||||
const contentMessage = new Proto.Content();
|
||||
|
@ -1682,7 +1677,7 @@ export default class MessageSender {
|
|||
};
|
||||
}
|
||||
|
||||
getVerificationSync(
|
||||
static getVerificationSync(
|
||||
destinationE164: string | undefined,
|
||||
destinationUuid: string | undefined,
|
||||
state: number,
|
||||
|
@ -1694,7 +1689,7 @@ export default class MessageSender {
|
|||
throw new Error('syncVerification: Neither e164 nor UUID were provided');
|
||||
}
|
||||
|
||||
const padding = this.getRandomPadding();
|
||||
const padding = MessageSender.getRandomPadding();
|
||||
|
||||
const verified = new Proto.Verified();
|
||||
verified.state = state;
|
||||
|
@ -1707,7 +1702,7 @@ export default class MessageSender {
|
|||
verified.identityKey = identityKey;
|
||||
verified.nullMessage = padding;
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
const syncMessage = MessageSender.createSyncMessage();
|
||||
syncMessage.verified = verified;
|
||||
|
||||
const contentMessage = new Proto.Content();
|
||||
|
@ -1832,7 +1827,7 @@ export default class MessageSender {
|
|||
});
|
||||
}
|
||||
|
||||
getNullMessage({
|
||||
static getNullMessage({
|
||||
uuid,
|
||||
e164,
|
||||
padding,
|
||||
|
@ -1848,7 +1843,7 @@ export default class MessageSender {
|
|||
throw new Error('sendNullMessage: Got neither uuid nor e164!');
|
||||
}
|
||||
|
||||
nullMessage.padding = padding || this.getRandomPadding();
|
||||
nullMessage.padding = padding || MessageSender.getRandomPadding();
|
||||
|
||||
const contentMessage = new Proto.Content();
|
||||
contentMessage.nullMessage = nullMessage;
|
||||
|
|
|
@ -29,19 +29,12 @@ class SyncRequestInner extends EventTarget {
|
|||
|
||||
timeoutMillis: number;
|
||||
|
||||
constructor(
|
||||
private sender: MessageSender,
|
||||
private receiver: MessageReceiver,
|
||||
timeoutMillis?: number
|
||||
) {
|
||||
constructor(private receiver: MessageReceiver, timeoutMillis?: number) {
|
||||
super();
|
||||
|
||||
if (
|
||||
!(sender instanceof MessageSender) ||
|
||||
!(receiver instanceof MessageReceiver)
|
||||
) {
|
||||
if (!(receiver instanceof MessageReceiver)) {
|
||||
throw new Error(
|
||||
'Tried to construct a SyncRequest without MessageSender and MessageReceiver'
|
||||
'Tried to construct a SyncRequest without MessageReceiver'
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -61,8 +54,6 @@ class SyncRequestInner extends EventTarget {
|
|||
}
|
||||
this.started = true;
|
||||
|
||||
const { sender } = this;
|
||||
|
||||
if (window.ConversationController.areWePrimaryDevice()) {
|
||||
log.warn('SyncRequest.start: We are primary device; returning early');
|
||||
return;
|
||||
|
@ -73,10 +64,12 @@ class SyncRequestInner extends EventTarget {
|
|||
);
|
||||
try {
|
||||
await Promise.all([
|
||||
singleProtoJobQueue.add(sender.getRequestConfigurationSyncMessage()),
|
||||
singleProtoJobQueue.add(sender.getRequestBlockSyncMessage()),
|
||||
singleProtoJobQueue.add(sender.getRequestContactSyncMessage()),
|
||||
singleProtoJobQueue.add(sender.getRequestGroupSyncMessage()),
|
||||
singleProtoJobQueue.add(
|
||||
MessageSender.getRequestConfigurationSyncMessage()
|
||||
),
|
||||
singleProtoJobQueue.add(MessageSender.getRequestBlockSyncMessage()),
|
||||
singleProtoJobQueue.add(MessageSender.getRequestContactSyncMessage()),
|
||||
singleProtoJobQueue.add(MessageSender.getRequestGroupSyncMessage()),
|
||||
]);
|
||||
} catch (error: unknown) {
|
||||
log.error(
|
||||
|
@ -135,12 +128,8 @@ export default class SyncRequest {
|
|||
handler: EventHandler
|
||||
) => void;
|
||||
|
||||
constructor(
|
||||
sender: MessageSender,
|
||||
receiver: MessageReceiver,
|
||||
timeoutMillis?: number
|
||||
) {
|
||||
const inner = new SyncRequestInner(sender, receiver, timeoutMillis);
|
||||
constructor(receiver: MessageReceiver, timeoutMillis?: number) {
|
||||
const inner = new SyncRequestInner(receiver, timeoutMillis);
|
||||
this.inner = inner;
|
||||
this.addEventListener = inner.addEventListener.bind(inner);
|
||||
this.removeEventListener = inner.removeEventListener.bind(inner);
|
||||
|
|
5
ts/textsecure/Types.d.ts
vendored
5
ts/textsecure/Types.d.ts
vendored
|
@ -6,6 +6,7 @@ import type { IncomingWebSocketRequest } from './WebsocketResources';
|
|||
import type { UUID } from '../types/UUID';
|
||||
import type { TextAttachmentType } from '../types/Attachment';
|
||||
import { GiftBadgeStates } from '../components/conversation/Message';
|
||||
import { MIMEType } from '../types/MIME';
|
||||
|
||||
export {
|
||||
IdentityKeyType,
|
||||
|
@ -97,9 +98,9 @@ export type ProcessedAttachment = {
|
|||
cdnId?: string;
|
||||
cdnKey?: string;
|
||||
digest?: string;
|
||||
contentType?: string;
|
||||
contentType: MIMEType;
|
||||
key?: string;
|
||||
size?: number;
|
||||
size: number;
|
||||
fileName?: string;
|
||||
flags?: number;
|
||||
width?: number;
|
||||
|
|
|
@ -52,10 +52,6 @@ import { calculateAgreement, generateKeyPair } from '../Curve';
|
|||
import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch';
|
||||
import { isBadgeImageFileUrlValid } from '../badges/isBadgeImageFileUrlValid';
|
||||
|
||||
import type {
|
||||
StorageServiceCallOptionsType,
|
||||
StorageServiceCredentials,
|
||||
} from '../textsecure.d';
|
||||
import { SocketManager } from './SocketManager';
|
||||
import type { CDSResponseType } from './CDSSocketManager';
|
||||
import { CDSSocketManager } from './CDSSocketManager';
|
||||
|
@ -64,7 +60,12 @@ import { SignalService as Proto } from '../protobuf';
|
|||
|
||||
import { HTTPError } from './Errors';
|
||||
import type MessageSender from './SendMessage';
|
||||
import type { WebAPICredentials, IRequestHandler } from './Types.d';
|
||||
import type {
|
||||
WebAPICredentials,
|
||||
IRequestHandler,
|
||||
StorageServiceCallOptionsType,
|
||||
StorageServiceCredentials,
|
||||
} from './Types.d';
|
||||
import { handleStatusCode, translateError } from './Utils';
|
||||
import * as log from '../logging/log';
|
||||
import { maybeParseUrl } from '../util/url';
|
||||
|
@ -601,16 +602,16 @@ type InitializeOptionsType = {
|
|||
directoryUrl?: string;
|
||||
directoryEnclaveId?: string;
|
||||
directoryTrustAnchor?: string;
|
||||
directoryV2Url: string;
|
||||
directoryV2PublicKey: string;
|
||||
directoryV2CodeHashes: ReadonlyArray<string>;
|
||||
directoryV2Url?: string;
|
||||
directoryV2PublicKey?: string;
|
||||
directoryV2CodeHashes?: ReadonlyArray<string>;
|
||||
cdnUrlObject: {
|
||||
readonly '0': string;
|
||||
readonly [propName: string]: string;
|
||||
};
|
||||
certificateAuthority: string;
|
||||
contentProxyUrl: string;
|
||||
proxyUrl: string;
|
||||
proxyUrl: string | undefined;
|
||||
version: string;
|
||||
};
|
||||
|
||||
|
@ -1018,6 +1019,13 @@ export type ProxiedRequestOptionsType = {
|
|||
end?: number;
|
||||
};
|
||||
|
||||
export type TopLevelType = {
|
||||
multiRecipient200ResponseSchema: typeof multiRecipient200ResponseSchema;
|
||||
multiRecipient409ResponseSchema: typeof multiRecipient409ResponseSchema;
|
||||
multiRecipient410ResponseSchema: typeof multiRecipient410ResponseSchema;
|
||||
initialize: (options: InitializeOptionsType) => WebAPIConnectType;
|
||||
};
|
||||
|
||||
// We first set up the data that won't change during this session of the app
|
||||
export function initialize({
|
||||
url,
|
||||
|
@ -1138,8 +1146,15 @@ export function initialize({
|
|||
socketManager.authenticate({ username, password });
|
||||
}
|
||||
|
||||
const cdsUrl = directoryV2Url || directoryUrl;
|
||||
if (!cdsUrl) {
|
||||
throw new Error('No CDS url available!');
|
||||
}
|
||||
if (!directoryV2PublicKey || !directoryV2CodeHashes?.length) {
|
||||
throw new Error('No CDS public key or code hashes available');
|
||||
}
|
||||
const cdsSocketManager = new CDSSocketManager({
|
||||
url: directoryV2Url,
|
||||
url: cdsUrl,
|
||||
publicKey: directoryV2PublicKey,
|
||||
codeHashes: directoryV2CodeHashes,
|
||||
certificateAuthority,
|
||||
|
|
|
@ -12,7 +12,25 @@ import { Storage } from './Storage';
|
|||
import * as WebAPI from './WebAPI';
|
||||
import WebSocketResource from './WebsocketResources';
|
||||
|
||||
export const textsecure = {
|
||||
export type TextSecureType = {
|
||||
utils: typeof utils;
|
||||
storage: Storage;
|
||||
|
||||
AccountManager: typeof AccountManager;
|
||||
ContactBuffer: typeof ContactBuffer;
|
||||
EventTarget: typeof EventTarget;
|
||||
GroupBuffer: typeof GroupBuffer;
|
||||
MessageReceiver: typeof MessageReceiver;
|
||||
MessageSender: typeof MessageSender;
|
||||
SyncRequest: typeof SyncRequest;
|
||||
WebAPI: typeof WebAPI;
|
||||
WebSocketResource: typeof WebSocketResource;
|
||||
|
||||
server?: WebAPI.WebAPIType;
|
||||
messaging?: MessageSender;
|
||||
};
|
||||
|
||||
export const textsecure: TextSecureType = {
|
||||
utils,
|
||||
storage: new Storage(),
|
||||
|
||||
|
@ -26,5 +44,3 @@ export const textsecure = {
|
|||
WebAPI,
|
||||
WebSocketResource,
|
||||
};
|
||||
|
||||
export default textsecure;
|
||||
|
|
|
@ -26,6 +26,7 @@ import type {
|
|||
} from './Types.d';
|
||||
import { WarnOnlyError } from './Errors';
|
||||
import { GiftBadgeStates } from '../components/conversation/Message';
|
||||
import { APPLICATION_OCTET_STREAM, stringToMIMEType } from '../types/MIME';
|
||||
|
||||
const FLAGS = Proto.DataMessage.Flags;
|
||||
export const ATTACHMENT_MAX = 32;
|
||||
|
@ -47,12 +48,21 @@ export function processAttachment(
|
|||
const { cdnId } = attachment;
|
||||
const hasCdnId = Long.isLong(cdnId) ? !cdnId.isZero() : Boolean(cdnId);
|
||||
|
||||
const { contentType, digest, key, size } = attachment;
|
||||
if (!size) {
|
||||
throw new Error('Missing size on incoming attachment!');
|
||||
}
|
||||
|
||||
return {
|
||||
...shallowDropNull(attachment),
|
||||
|
||||
cdnId: hasCdnId ? String(cdnId) : undefined,
|
||||
key: attachment.key ? Bytes.toBase64(attachment.key) : undefined,
|
||||
digest: attachment.digest ? Bytes.toBase64(attachment.digest) : undefined,
|
||||
contentType: contentType
|
||||
? stringToMIMEType(contentType)
|
||||
: APPLICATION_OCTET_STREAM,
|
||||
digest: digest ? Bytes.toBase64(digest) : undefined,
|
||||
key: key ? Bytes.toBase64(key) : undefined,
|
||||
size,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue