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: