diff --git a/packages/client/package.json b/packages/client/package.json index 9cbf33fb..014f170a 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -37,5 +37,8 @@ "dependencies": { "@mtcute/core": "workspace:^", "@mtcute/file-id": "workspace:^" + }, + "devDependencies": { + "@mtcute/test": "workspace:^" } } diff --git a/packages/client/src/utils/file-type.test.ts b/packages/client/src/utils/file-type.test.ts new file mode 100644 index 00000000..f0fa9d5e --- /dev/null +++ b/packages/client/src/utils/file-type.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it } from 'vitest' + +import { guessFileMime } from './file-type.js' +import { hexDecodeToBuffer } from './index.js' + +describe('guessFileMime', () => { + it.each([ + ['424d', 'image/bmp'], + ['4d5a', 'application/x-msdownload'], + ['1f9d', 'application/x-compress'], + ['1fa0', 'application/x-compress'], + ['1f8b', 'application/gzip'], + ['425a68', 'application/x-bzip2'], + ['494433', 'audio/mpeg'], + ['fffb', 'audio/mpeg'], + ['fff3', 'audio/mpeg'], + ['fff2', 'audio/mpeg'], + ['504b0304', 'application/zip'], + ['38425053', 'image/vnd.adobe.photoshop'], + ['7f454c46', 'application/x-elf'], + ['feedfacf', 'application/x-mach-binary'], + ['28b52ffd', 'application/zstd'], + ['664c6143', 'audio/x-flac'], + ['ffd8ffdb', 'image/jpeg'], + ['ffd8ffe0', 'image/jpeg'], + ['ffd8ffee', 'image/jpeg'], + ['ffd8ffe1', 'image/jpeg'], + ['4f676753', 'application/ogg'], + ['4f6767530000000000000000000000000000000000000000000000004f70757348656164', 'audio/ogg'], + ['4f67675300000000000000000000000000000000000000000000000001766964656f', 'video/ogg'], + ['4f6767530000000000000000000000000000000000000000000000007f464c4143', 'audio/ogg'], + ['4f67675300000000000000000000000000000000000000000000000001766f72626973', 'audio/ogg'], + ['255044462d', 'application/pdf'], + ['474946383761', 'image/gif'], + ['474946383961', 'image/gif'], + ['377abcaf271c', 'application/x-7z-compressed'], + ['89504e470d0a1a0a', 'image/png'], + ['526172211a0700', 'application/x-rar-compressed'], + ['526172211a0701', 'application/x-rar-compressed'], + ['000000006674797061766966', 'image/avif'], + ['000000006674797061766973', 'image/avif'], + ['00000000667479706d696631', 'image/heif'], + ['000000006674797068656963', 'image/heic'], + ['000000006674797068656978', 'image/heic'], + ['000000006674797068657663', 'image/heic-sequence'], + ['000000006674797068657678', 'image/heic-sequence'], + ['000000006674797071740000', 'video/quicktime'], + ['00000000667479704d345600', 'video/x-m4v'], + ['00000000667479704d345648', 'video/x-m4v'], + ['00000000667479704d345650', 'video/x-m4v'], + ['00000000667479704d345000', 'video/mp4'], + ['00000000667479704d344100', 'audio/x-m4a'], + ['00000000667479704d344200', 'audio/mp4'], + ['000000006674797046344100', 'audio/mp4'], + ['000000006674797046344200', 'audio/mp4'], + ['000000006674797063727800', 'image/x-canon-cr3'], + ['000000006674797033673200', 'video/3gpp2'], + ['000000006674797033670000', 'video/3gpp'], + ])('should detect %s as %s', (header, mime) => { + header += '00'.repeat(16) + + expect(guessFileMime(hexDecodeToBuffer(header))).toEqual(mime) + }) +}) diff --git a/packages/client/src/utils/file-type.ts b/packages/client/src/utils/file-type.ts index 6ea46a35..384c0e6e 100644 --- a/packages/client/src/utils/file-type.ts +++ b/packages/client/src/utils/file-type.ts @@ -76,13 +76,13 @@ export function guessFileMime(chunk: Uint8Array): string | null { } if (b0 === 0x52 && b1 === 0x61 && b2 === 0x72 && b3 === 0x21 && b4 === 0x1a && b5 === 0x07) { - if (chunk[6] === 0x00 || chunk[6] === 0x01) return 'application/x-rar-compressed' + if (b6 === 0x00 || b6 === 0x01) return 'application/x-rar-compressed' } // ftyp - iso container if (b4 === 0x66 && b5 === 0x74 && b6 === 0x79 && b7 === 0x70 && chunk[8] & 0x60) { const brandMajor = String.fromCharCode(...chunk.subarray(8, 12)) - .replace('\0', ' ') + .replace(/\0/g, ' ') .trim() switch (brandMajor) { @@ -105,18 +105,10 @@ export function guessFileMime(chunk: Uint8Array): string | null { case 'M4VH': case 'M4VP': return 'video/x-m4v' - case 'M4P': - return 'video/mp4' - case 'M4B': - return 'audio/mp4' case 'M4A': return 'audio/x-m4a' - case 'F4V': - return 'video/mp4' - case 'F4P': - return 'video/mp4' + case 'M4B': case 'F4A': - return 'audio/mp4' case 'F4B': return 'audio/mp4' case 'crx': diff --git a/packages/client/src/utils/file-utils.test.ts b/packages/client/src/utils/file-utils.test.ts index ba88bfb0..47b27cc3 100644 --- a/packages/client/src/utils/file-utils.test.ts +++ b/packages/client/src/utils/file-utils.test.ts @@ -1,8 +1,14 @@ import { describe, expect, it } from 'vitest' -import { hexDecodeToBuffer, utf8EncodeToBuffer } from '@mtcute/core/utils.js' +import { hexDecodeToBuffer, hexEncode, utf8Decode, utf8EncodeToBuffer } from '@mtcute/core/utils.js' -import { isProbablyPlainText } from './file-utils.js' +import { + extractFileName, + inflateSvgPath, + isProbablyPlainText, + strippedPhotoToJpg, + svgPathToFile, +} from './file-utils.js' describe('isProbablyPlainText', () => { it('should return true for buffers only containing printable ascii', () => { @@ -28,3 +34,125 @@ describe('isProbablyPlainText', () => { expect(isProbablyPlainText(hexDecodeToBuffer('20e8e218e54254c813b261432b0330d7'))).to.be.false }) }) + +describe('extractFileName', () => { + it('should extract file name from a path', () => { + expect(extractFileName('file.txt')).toEqual('file.txt') + expect(extractFileName('/home/user/file.txt')).toEqual('file.txt') + expect(extractFileName('C:\\Users\\user\\file.txt')).toEqual('file.txt') + }) + + it('should skip file: prefix', () => { + expect(extractFileName('file:file.txt')).toEqual('file.txt') + expect(extractFileName('file:/home/user/file.txt')).toEqual('file.txt') + expect(extractFileName('file:C:\\Users\\user\\file.txt')).toEqual('file.txt') + }) +}) + +describe('svgPathToFile', () => { + it('should convert SVG path to a file', () => { + const path = 'M 0 0 L 100 0 L 100 100 L 0 100 L 0 0 Z' + + expect(utf8Decode(svgPathToFile(path))).toMatchInlineSnapshot( + '""', + ) + }) +}) + +describe('inflateSvgPath', () => { + const data = hexDecodeToBuffer( + '1a05b302dc5f4446068649064247424a6a4c704550535b5e665e5e4c044a024c' + + '074e06414d80588863935fad74be4704854684518b528581904695498b488b56' + + '965c85438d8191818543894a8f4d834188818a4284498454895d9a6f86074708' + + '8d0146089283a281b48bbfa386078a04880390098490949997ab828a4b984a9d' + + '8490b395aa9949845c485d4a42434b4a4a46848a8792859d4186468c48938182' + + '91a293ac859781af848701818989928b9c849086a785b880816c8071817c814c' + + '0080520081', + ) + const path = + 'M265,512c-31-4-66,6-96-2-7-2-10-42-12-48-5-16-19-27-30' + + '-38-30-30-124-102-127-146-1-13,0-24,8-35,19-31,45-52,62-74,5-6,4-17,11-18,' + + '5,1,16-6,21-9,11-8,11-22,22-28,5-3,13,1,17,1,5-3,9-10,15-13,3-1,8,1,10-2,4' + + '-9,4-20,9-29,26-47,67-78,131-68,18,3,34,1,52,11,63,35,67,104,83,169,4,16,20,' + + '25,23,43,2,10-11,24-10,29,4,16,51,21,42,25-9,4-28-8-29-10-2-3-11-10-10-6,4,10,' + + '7,18,5,29-1,6-6,12-8,19,1,2,17,34,19,44,5,23,1,47,4,71,1,9,9,18,11,28,4,16,6,39,' + + '5,56,0,1-44,0-49,1-60,1-120,0-180,1z' + + it('should correctly inflate svg path', () => { + expect(inflateSvgPath(data)).toEqual(path) + }) +}) + +describe('strippedPhotoToJpg', () => { + // strippedThumb of @Channel_Bot + const dataPfp = hexDecodeToBuffer('010808b1f2f95fed673451457033ad1f') + // photoStrippedSize of a random image + const dataPicture = hexDecodeToBuffer( + '012728b532aacce4b302d8c1099c74a634718675cb6381f73d3ffd557667d9b5' + + '816f4c28ce69aa58a863238cf62a334590f999042234cbe1986d03eefe14c68e' + + '32847cc00ce709ea7ffad577773f78fe54d6c927f78c3db14ac1ccca91a2ef4f' + + '9d89dd9e53e9455c456072646618e840a28b20bb223275e463b55769b07e4cf1' + + 'c52cedfbb03d38aab9e718356909b2733c839cf5a72dc3646ee6a4bb882c2ac0' + + '70a31c554c1d81f0403eb4d598b5350b8680b03c628aab09ff0044707b1a2a5a' + + '012e016420753cd25b491ac603a723bd14517ba28b46788c5e613f27d2a9cb32' + + 'ceca2353807bd14525b831e6175deccde991eb45145508', + ) + + it('should inflate stripped jpeg (from profile picture)', () => { + expect(hexEncode(strippedPhotoToJpg(dataPfp))).toMatchInlineSnapshot( + '"ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e192' + + '82321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a' + + '0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2' + + 'd2d3c353c76414176f8a58ca5f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f' + + '8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8ffc' + + '00011080008000803012200021101031101ffc4001f000001050101010101010' + + '0000000000000000102030405060708090a0bffc400b51000020103030204030' + + '50504040000017d01020300041105122131410613516107227114328191a1082' + + '342b1c11552d1f02433627282090a161718191a25262728292a3435363738393' + + 'a434445464748494a535455565758595a636465666768696a737475767778797' + + 'a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b' + + '7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf' + + '1f2f3f4f5f6f7f8f9faffc4001f0100030101010101010101010000000000000' + + '102030405060708090a0bffc400b511000201020404030407050404000102770' + + '00102031104052131061241510761711322328108144291a1b1c109233352f01' + + '56272d10a162434e125f11718191a262728292a35363738393a4344454647484' + + '94a535455565758595a636465666768696a737475767778797a8283848586878' + + '8898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c' + + '4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f' + + '9faffda000c03010002110311003f00b1f2f95fed673451457033ad1fffd9"', + ) + }) + + it('should inflate stripped jpeg (from a picture)', () => { + expect(hexEncode(strippedPhotoToJpg(dataPicture))).toMatchInlineSnapshot( + '"ffd8ffe000104a46494600010100000100010000ffdb004300281c1e231e192' + + '82321232d2b28303c64413c37373c7b585d4964918099968f808c8aa0b4e6c3a' + + '0aadaad8a8cc8ffcbdaeef5ffffff9bc1fffffffaffe6fdfff8ffdb0043012b2' + + 'd2d3c353c76414176f8a58ca5f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f' + + '8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8f8ffc' + + '00011080027002803012200021101031101ffc4001f000001050101010101010' + + '0000000000000000102030405060708090a0bffc400b51000020103030204030' + + '50504040000017d01020300041105122131410613516107227114328191a1082' + + '342b1c11552d1f02433627282090a161718191a25262728292a3435363738393' + + 'a434445464748494a535455565758595a636465666768696a737475767778797' + + 'a838485868788898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b' + + '7b8b9bac2c3c4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae1e2e3e4e5e6e7e8e9eaf' + + '1f2f3f4f5f6f7f8f9faffc4001f0100030101010101010101010000000000000' + + '102030405060708090a0bffc400b511000201020404030407050404000102770' + + '00102031104052131061241510761711322328108144291a1b1c109233352f01' + + '56272d10a162434e125f11718191a262728292a35363738393a4344454647484' + + '94a535455565758595a636465666768696a737475767778797a8283848586878' + + '8898a92939495969798999aa2a3a4a5a6a7a8a9aab2b3b4b5b6b7b8b9bac2c3c' + + '4c5c6c7c8c9cad2d3d4d5d6d7d8d9dae2e3e4e5e6e7e8e9eaf2f3f4f5f6f7f8f' + + '9faffda000c03010002110311003f00b532aacce4b302d8c1099c74a63471867' + + '5cb6381f73d3ffd557667d9b5816f4c28ce69aa58a863238cf62a334590f9990' + + '42234cbe1986d03eefe14c68e32847cc00ce709ea7ffad577773f78fe54d6c92' + + '7f78c3db14ac1ccca91a2ef4f9d89dd9e53e9455c456072646618e840a28b20b' + + 'b223275e463b55769b07e4cf1c52cedfbb03d38aab9e718356909b2733c839cf' + + '5a72dc3646ee6a4bb882c2ac070a31c554c1d81f0403eb4d598b5350b8680b03' + + 'c628aab09ff0044707b1a2a5a012e016420753cd25b491ac603a723bd14517ba' + + '28b46788c5e613f27d2a9cb32ceca2353807bd14525b831e6175deccde991eb4' + + '5145508ffd9"', + ) + }) +}) diff --git a/packages/client/src/utils/file-utils.ts b/packages/client/src/utils/file-utils.ts index 67e931f3..ed6b8fdb 100644 --- a/packages/client/src/utils/file-utils.ts +++ b/packages/client/src/utils/file-utils.ts @@ -59,7 +59,7 @@ const JPEG_FOOTER = new Uint8Array([0xff, 0xd9]) */ export function strippedPhotoToJpg(stripped: Uint8Array): Uint8Array { if (stripped.length < 3 || stripped[0] !== 1) { - return stripped + throw new MtArgumentError('Invalid stripped JPEG') } const result = concatBuffers([JPEG_HEADER, stripped.slice(3), JPEG_FOOTER]) @@ -96,6 +96,8 @@ export function inflateSvgPath(encoded: Uint8Array): string { } } + path += 'z' + return path } diff --git a/packages/client/src/utils/inline-utils.test.ts b/packages/client/src/utils/inline-utils.test.ts new file mode 100644 index 00000000..4defb711 --- /dev/null +++ b/packages/client/src/utils/inline-utils.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest' + +import { Long, tl } from '@mtcute/core' + +import { encodeInlineMessageId, normalizeInlineId, parseInlineMessageId } from './inline-utils.js' + +describe('inline message id', () => { + it('should encode and decode legacy inline message id', () => { + const id: tl.RawInputBotInlineMessageID = { + _: 'inputBotInlineMessageID', + dcId: 1, + id: Long.fromBits(123, 456), + accessHash: Long.fromBits(789, 999), + } + const encoded = encodeInlineMessageId(id) + const parsed = parseInlineMessageId(encoded) + + expect(encoded).toEqual('AQAAAHsAAADIAQAAFQMAAOcDAAA') + expect(parsed).toEqual(id) + }) + + it('should encode and decode 64-bit inline message id', () => { + const id: tl.RawInputBotInlineMessageID64 = { + _: 'inputBotInlineMessageID64', + dcId: 1, + ownerId: Long.fromBits(123, 456), + id: 666, + accessHash: Long.fromBits(789, 999), + } + const encoded = encodeInlineMessageId(id) + const parsed = parseInlineMessageId(encoded) + + expect(encoded).toEqual('AQAAAHsAAADIAQAAmgIAABUDAADnAwAA') + expect(parsed).toEqual(id) + }) + + it('should normalize to tl object', () => { + const id: tl.RawInputBotInlineMessageID64 = { + _: 'inputBotInlineMessageID64', + dcId: 1, + ownerId: Long.fromBits(123, 456), + id: 666, + accessHash: Long.fromBits(789, 999), + } + + expect(normalizeInlineId('AQAAAHsAAADIAQAAmgIAABUDAADnAwAA')).toEqual(id) + expect(normalizeInlineId(id)).toBe(id) + }) +}) diff --git a/packages/client/src/utils/inspectable.test.ts b/packages/client/src/utils/inspectable.test.ts new file mode 100644 index 00000000..bf3fa70b --- /dev/null +++ b/packages/client/src/utils/inspectable.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it, vi } from 'vitest' + +import { makeInspectable } from './inspectable.js' + +describe('makeInspectable', () => { + // eslint-disable-next-line + const inspect = (obj: any) => obj.toJSON() + + it('should make all getters inspectable', () => { + class Foo { + get foo() { + return 1 + } + get bar() { + return 2 + } + } + + makeInspectable(Foo) + + expect(inspect(new Foo())).toEqual({ foo: 1, bar: 2 }) + }) + + it('should use nested classes toJSON', () => { + class Inner { + toJSON = vi.fn().mockReturnValue(42) + } + const inner = new Inner() + + class Foo { + get foo() { + return inner + } + } + + makeInspectable(Foo) + + expect(inspect(new Foo())).toEqual({ foo: 42 }) + expect(inner.toJSON).toHaveBeenCalledTimes(1) + }) + + it('should not inspect fields', () => { + class Foo { + get foo() { + return 1 + } + bar = 2 + } + + makeInspectable(Foo) + + expect(inspect(new Foo())).toEqual({ foo: 1 }) + }) + + it('should inspect fields if specified', () => { + class Foo { + bar = 1 + baz = 2 + } + + makeInspectable(Foo, ['bar']) + + expect(inspect(new Foo())).toEqual({ bar: 1 }) + }) + + it('should hide getters if specified', () => { + class Foo { + get foo() { + return 1 + } + get bar() { + return 2 + } + } + + makeInspectable(Foo, undefined, ['foo']) + + expect(inspect(new Foo())).toEqual({ bar: 2 }) + }) + + it('should handle errors', () => { + class Foo { + get foo() { + return 1 + } + get bar() { + throw new Error('whatever') + } + } + + makeInspectable(Foo) + + expect(inspect(new Foo())).toEqual({ + foo: 1, + bar: 'Error: whatever', + }) + }) + + it('should handle Uint8Arrays', () => { + class Foo { + get foo() { + return new Uint8Array([1, 2, 3]) + } + } + + makeInspectable(Foo) + + expect(inspect(new Foo())).toEqual({ + foo: 'AQID', + }) + }) +}) diff --git a/packages/client/src/utils/inspectable.ts b/packages/client/src/utils/inspectable.ts index 253cd5ef..dc9ef932 100644 --- a/packages/client/src/utils/inspectable.ts +++ b/packages/client/src/utils/inspectable.ts @@ -25,10 +25,6 @@ function getAllGettersNames(obj: T): (keyof T)[] { return getters } -const bufferToJsonInspect = function (this: Uint8Array) { - return base64Encode(this) -} - /** * Small helper function that adds `toJSON` and `util.custom.inspect` * methods to a given class based on its getters @@ -48,18 +44,18 @@ export function makeInspectable(obj: new (...args: any[]) => T, props?: (keyo // eslint-disable-next-line @typescript-eslint/no-implied-eval const proto = new Function(`return function ${obj.name}(){}`)().prototype - obj.prototype.toJSON = function (nested = false) { - if (!nested) { - (Uint8Array as any).toJSON = bufferToJsonInspect - } - + obj.prototype.toJSON = function () { const ret: any = Object.create(proto) getters.forEach((it) => { try { let val = this[it] - if (val && typeof val === 'object' && typeof val.toJSON === 'function') { - val = val.toJSON(true) + if (val && typeof val === 'object') { + if (val instanceof Uint8Array) { + val = base64Encode(val) + } else if (typeof val.toJSON === 'function') { + val = val.toJSON(true) + } } ret[it] = val } catch (e: any) { @@ -67,10 +63,6 @@ export function makeInspectable(obj: new (...args: any[]) => T, props?: (keyo } }) - if (!nested) { - delete (Uint8Array as any).prototype.toJSON - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return return ret } diff --git a/packages/client/src/utils/peer-utils.test.ts b/packages/client/src/utils/peer-utils.test.ts new file mode 100644 index 00000000..e7ced882 --- /dev/null +++ b/packages/client/src/utils/peer-utils.test.ts @@ -0,0 +1,146 @@ +import { describe, expect, it } from 'vitest' + +import { createStub } from '@mtcute/test' + +import { MtInvalidPeerTypeError } from '../types/index.js' +import { + inputPeerToPeer, + isInputPeerChannel, + isInputPeerChat, + isInputPeerUser, + normalizeToInputChannel, + normalizeToInputPeer, + normalizeToInputUser, +} from './peer-utils.js' + +describe('normalizeToInputPeer', () => { + it.each([ + ['inputChannelEmpty', 'inputPeerEmpty'], + ['inputUserEmpty', 'inputPeerEmpty'], + ['inputUser', 'inputPeerUser'], + ['inputUserSelf', 'inputPeerSelf'], + ['inputUserSelf', 'inputPeerSelf'], + ['inputChannel', 'inputPeerChannel'], + ['inputChannelFromMessage', 'inputPeerChannelFromMessage'], + ['inputUserFromMessage', 'inputPeerUserFromMessage'], + ] as const)('should convert %s to %s', (fromType, toType) => { + const from = createStub(fromType) + const to = createStub(toType) + + expect(normalizeToInputPeer(from)).toEqual(to) + }) + + it.each([ + ['inputPeerEmpty'], + ['inputPeerSelf'], + ['inputPeerUser'], + ['inputPeerChannel'], + ['inputPeerChannelFromMessage'], + ['inputPeerUserFromMessage'], + ] as const)('should keep %s as is', (type) => { + const obj = createStub(type) + + expect(normalizeToInputPeer(obj)).toBe(obj) + }) +}) + +describe('normalizeToInputUser', () => { + it.each([ + ['inputPeerSelf', 'inputUserSelf'], + ['inputPeerUser', 'inputUser'], + ['inputPeerUserFromMessage', 'inputUserFromMessage'], + ] as const)('should convert %s to %s', (fromType, toType) => { + const from = createStub(fromType) + const to = createStub(toType) + + expect(normalizeToInputUser(from)).toEqual(to) + }) + + it('should throw for other types', () => { + expect(() => normalizeToInputUser(createStub('inputPeerChannel'), 'some_channel')).toThrow( + MtInvalidPeerTypeError, + ) + }) +}) + +describe('normalizeToInputChannel', () => { + it.each([ + ['inputPeerChannel', 'inputChannel'], + ['inputPeerChannelFromMessage', 'inputChannelFromMessage'], + ] as const)('should convert %s to %s', (fromType, toType) => { + const from = createStub(fromType) + const to = createStub(toType) + + expect(normalizeToInputChannel(from)).toEqual(to) + }) + + it('should throw for other types', () => { + expect(() => normalizeToInputChannel(createStub('inputPeerUser'), 'some_user')).toThrow(MtInvalidPeerTypeError) + }) +}) + +describe('isInputPeerUser', () => { + it.each([['inputPeerSelf'], ['inputPeerUser'], ['inputPeerUserFromMessage']] as const)( + 'should return true for %s', + (type) => { + expect(isInputPeerUser(createStub(type))).toBe(true) + }, + ) + + it.each([['inputPeerEmpty'], ['inputPeerChannel'], ['inputPeerChannelFromMessage']] as const)( + 'should return false for %s', + (type) => { + expect(isInputPeerUser(createStub(type))).toBe(false) + }, + ) +}) + +describe('isInputPeerChannel', () => { + it.each([['inputPeerChannel'], ['inputPeerChannelFromMessage']] as const)('should return true for %s', (type) => { + expect(isInputPeerChannel(createStub(type))).toBe(true) + }) + + it.each([['inputPeerEmpty'], ['inputPeerSelf'], ['inputPeerUser'], ['inputPeerUserFromMessage']] as const)( + 'should return false for %s', + (type) => { + expect(isInputPeerChannel(createStub(type))).toBe(false) + }, + ) +}) + +describe('isInputPeerChat', () => { + it('should return true for inputPeerChat', () => { + expect(isInputPeerChat(createStub('inputPeerChat'))).toBe(true) + }) + + it.each([ + ['inputPeerChannel'], + ['inputPeerChannelFromMessage'], + ['inputPeerEmpty'], + ['inputPeerSelf'], + ['inputPeerUser'], + ['inputPeerUserFromMessage'], + ] as const)('should return false for %s', (type) => { + expect(isInputPeerChat(createStub(type))).toBe(false) + }) +}) + +describe('inputPeerToPeer', () => { + it.each([ + ['inputPeerUser', 'peerUser'], + ['inputPeerUserFromMessage', 'peerUser'], + ['inputPeerChat', 'peerChat'], + ['inputPeerChannel', 'peerChannel'], + ['inputPeerChannelFromMessage', 'peerChannel'], + ] as const)('should convert %s to %s', (fromType, toType) => { + const from = createStub(fromType) + const to = createStub(toType) + + expect(inputPeerToPeer(from)).toEqual(to) + }) + + it('should throw for other types', () => { + expect(() => inputPeerToPeer(createStub('inputPeerEmpty'))).toThrow(MtInvalidPeerTypeError) + expect(() => inputPeerToPeer(createStub('inputPeerSelf'))).toThrow(MtInvalidPeerTypeError) + }) +}) diff --git a/packages/client/src/utils/peer-utils.ts b/packages/client/src/utils/peer-utils.ts index 44e437cd..da39d784 100644 --- a/packages/client/src/utils/peer-utils.ts +++ b/packages/client/src/utils/peer-utils.ts @@ -1,4 +1,4 @@ -import { assertNever, Long, tl } from '@mtcute/core' +import { assertNever, tl } from '@mtcute/core' import { MtInvalidPeerTypeError } from '../types/errors.js' import { InputPeerLike } from '../types/peers/index.js' @@ -140,21 +140,6 @@ export function inputPeerToPeer(inp: tl.TypeInputPeer): tl.TypePeer { case 'inputPeerChat': return { _: 'peerChat', chatId: inp.chatId } default: - throw new Error(`Cannot convert ${inp._} to peer`) - } -} - -export function peerToInputPeer(peer: tl.TypePeer, accessHash = Long.ZERO): tl.TypeInputPeer { - switch (peer._) { - case 'peerUser': - return { _: 'inputPeerUser', userId: peer.userId, accessHash } - case 'peerChannel': - return { - _: 'inputPeerChannel', - channelId: peer.channelId, - accessHash, - } - case 'peerChat': - return { _: 'inputPeerChat', chatId: peer.chatId } + throw new MtInvalidPeerTypeError(inp, `Cannot convert ${inp._} to peer`) } } diff --git a/packages/client/src/utils/voice-utils.test.ts b/packages/client/src/utils/voice-utils.test.ts new file mode 100644 index 00000000..78414a74 --- /dev/null +++ b/packages/client/src/utils/voice-utils.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest' + +import { hexDecodeToBuffer, hexEncode } from '@mtcute/core/utils.js' + +import { decodeWaveform, encodeWaveform } from './voice-utils.js' + +describe('decodeWaveform', () => { + it('should correctly decode telegram-encoded waveform', () => { + expect( + decodeWaveform( + hexDecodeToBuffer( + '0000104210428c310821a51463cc39072184524a4aa9b51663acb5e69c7bef41' + + '08618c514a39e7a494d65aadb5f75e8c31ce396badf7de9cf3debbf7feff0f', + ), + ), + ).toEqual([ + 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 10, + 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, + 19, 20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 26, 26, 26, 27, 27, 27, 28, + 28, 28, 29, 29, 29, 30, 30, 30, 31, 31, 31, + ]) + }) +}) + +describe('encodeWaveform', () => { + it('should correctly decode telegram-encoded waveform', () => { + expect( + hexEncode( + encodeWaveform([ + 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8, 9, 9, 9, 10, + 10, 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 14, 14, 14, 15, 15, 15, 16, 16, 16, 16, 17, 17, 17, 18, + 18, 18, 19, 19, 19, 20, 20, 20, 21, 21, 21, 22, 22, 22, 23, 23, 23, 24, 24, 24, 24, 25, 25, 25, 26, + 26, 26, 27, 27, 27, 28, 28, 28, 29, 29, 29, 30, 30, 30, 31, 31, 31, + ]), + ), + ).toEqual( + '0000104210428c310821a51463cc39072184524a4aa9b51663acb5e69c7bef41' + + '08618c514a39e7a494d65aadb5f75e8c31ce396badf7de9cf3debbf7feff0f', + ) + }) +}) diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 6a5497ee..af184bdc 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -8,6 +8,7 @@ "./src", ], "references": [ - { "path": "../core" } + { "path": "../core" }, + { "path": "../test" } ] } diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 63770709..e24e47cd 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -10,5 +10,6 @@ "references": [ { "path": "../tl" }, { "path": "../tl-runtime" }, + { "path": "../test" }, ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc29371d..11b440f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,6 +108,10 @@ importers: '@mtcute/file-id': specifier: workspace:^ version: link:../file-id + devDependencies: + '@mtcute/test': + specifier: workspace:^ + version: link:../test packages/core: dependencies: