From 29a61b55a9a759cb4716abe804112e8ba5f3978c Mon Sep 17 00:00:00 2001 From: Rastislav Date: Thu, 29 Aug 2024 13:54:52 +0200 Subject: [PATCH] Added counting and help --- README.md | 5 ++- bin/txms | 86 +++++++++++++++++++++++++++++++++++++++++------ package.json | 2 +- src/index.ts | 42 +++++++++++++++++++++++ test/index.js | 64 +++++++++++++++++++++++++++++++++-- test/samples.json | 10 ++++-- 6 files changed, 191 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 9d702c6..b77f9ec 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ The library is designed to be compatible with both module systems, so you can ch - `encode(hex: string): string` — Convert hex transaction into UTF-16BE. - `decode(data: string): string` — Convert UTF-16BE into hex transaction. +- `count(data: string, type: 'sms' | 'mms'): number` — Count the number of characters/SMS/MMS needed for the transaction. - `getEndpoint(network?: number | string, countriesList?: string | Array): { [key: string]: Array }` — Get an object of SMS endpoints (phone numbers) per country. - `sms(number?: boolean | string | number | Array, message?: string, network?: number | string, encodeMessage?: boolean, platform?: string): string` — Create an SMS URI based on the provided parameters. - `mms(number?: boolean | string | number | Array, message?: string, network?: number | string, encodeMessage?: boolean, platform?: string): string` — Create an MMS URI based on the provided parameters. @@ -181,10 +182,12 @@ Types: - `--version` (`-v`) - Get the version of the library. - `--encode` (`-e`) - Encode the HEX transaction. - `--decode` (`-d`) - Decode the UTF-16BE transaction. +- `--count` (`-ct`) - Count the number of characters needed for the transaction. You can choose type of count: `sms`, `mms`. (To perform a count, you need to provide `encode` command.) - `--getendpoint` (`-g`) - Get the SMS/MMS endpoint for the network and country. - `--sms` - Create an SMS URI based on the provided parameters. - `--mms` - Create an MMS URI based on the provided parameters. -- `--download` (`-dl`) - Boolean value to download a file with the encoded content as `.txms.txt` file in your working directory. +- `--download` (`-dl`) - Boolean value to download a file with the encoded content as `.txms.txt` file in your working directory. (To download a file, you need to provide `encode` command.) +- `--help` (`-h`) - Show help. (Only for TTY mode.) ### Piping diff --git a/bin/txms b/bin/txms index 2a90847..c2b1f4a 100644 --- a/bin/txms +++ b/bin/txms @@ -18,6 +18,8 @@ function parseArgs(argv) { countryCodes: null, // For getEndpoint phoneNumbers: null, // For SMS/MMS phone numbers download: false, // Flag to indicate download + countType: null, // Count type: sms, mms, or null for characters + showHelp: false, // Flag to indicate if help is requested }; argv.forEach((arg) => { @@ -35,6 +37,9 @@ function parseArgs(argv) { args.kind = 'decode'; args.value = value; break; + case '--count': + args.countType = value || true; + break; case '--getendpoint': args.kind = 'getendpoint'; args.value = value; // Network type for getEndpoint @@ -59,6 +64,9 @@ function parseArgs(argv) { case '--countries': args.countryCodes = value.split(','); // Comma-separated country codes break; + case '--help': + args.showHelp = true; + break; default: break; } @@ -76,6 +84,9 @@ function parseArgs(argv) { args.kind = 'decode'; args.value = value; break; + case '-ct': + args.countType = value || true; + break; case '-g': args.kind = 'getendpoint'; args.value = value; @@ -97,6 +108,9 @@ function parseArgs(argv) { case '-c': args.countryCodes = value.split(','); break; + case '-h': + args.showHelp = true; + break; default: break; } @@ -109,12 +123,46 @@ function parseArgs(argv) { return args; } +// Function to display help +function displayHelp(newline = '\n') { + const helpText = ` +\x1b[1mUsage:\x1b[0m txms \x1b[38;5;214m[options]\x1b[0m + +\x1b[1mOptions:\x1b[0m + \x1b[38;5;214m-v, --version\x1b[0m Get the version of the library. + \x1b[38;5;214m-e, --encode\x1b[0m Encode the HEX transaction. + \x1b[38;5;214m-d, --decode\x1b[0m Decode the UTF-16BE transaction. + \x1b[38;5;214m-ct, --count\x1b[0m Count the number of characters needed for the transaction. You can choose the type of count: 'sms' or 'mms'. + \x1b[38;5;214m-g, --getendpoint\x1b[0m Get the SMS/MMS endpoint for the network and country. + \x1b[38;5;214m-s, --sms\x1b[0m Create an SMS URI based on the provided parameters. + \x1b[38;5;214m-m, --mms\x1b[0m Create an MMS URI based on the provided parameters. + \x1b[38;5;214m-dl, --download\x1b[0m Download a file with the encoded content as .txms.txt file in your working directory. + \x1b[38;5;214m-o, --output\x1b[0m Specify the output directory for downloads. + \x1b[38;5;214m-f, --filename\x1b[0m Specify the filename for downloads. + \x1b[38;5;214m-c, --countries\x1b[0m Specify a comma-separated list of country codes. + \x1b[38;5;214m-h, --help\x1b[0m Show this help message and exit. + +\x1b[1mExamples:\x1b[0m + txms --encode=yourHexValue + txms -e=yourHexValue --download + txms -d=yourUTF16String + echo yourHexValue | txms --encode +`; + process.stdout.write(helpText + newline); +} + // Parse the arguments const args = parseArgs(process.argv.slice(2)); +// If the help flag is set, display the help message immediately +if (args.showHelp && process.stdin.isTTY) { + displayHelp(); + process.exit(0); +} + if (process.stdin.isTTY) { // If the script is run with a TTY, process the command-line arguments - run(args.kind, args.value, args.output, args.countryCodes, args.phoneNumbers, args.download, args.filename, '\n'); + run(args.kind, args.value, args.output, args.countryCodes, args.phoneNumbers, args.download, args.filename, args.countType, '\n'); } else { // If data is being piped into the script, capture it let content = ''; @@ -124,16 +172,18 @@ if (process.stdin.isTTY) { }); process.stdin.on('end', () => { content = content.trim(); - - if (!content) { - run(args.kind, args.value, args.output, args.countryCodes, args.phoneNumbers, args.download, args.filename, ''); + if (!content && !args.showHelp) { + run(args.kind, args.value, args.output, args.countryCodes, args.phoneNumbers, args.download, args.filename, args.countType, ''); + } else if (args.showHelp) { + displayHelp(); + process.exit(0); } else { - run(args.kind, content, args.output, args.countryCodes, args.phoneNumbers, args.download, args.filename, ''); + run(args.kind, content, args.output, args.countryCodes, args.phoneNumbers, args.download, args.filename, args.countType, ''); } }); } -async function run(kind, value, output, countryCodes, phoneNumbers, download, filename, newline) { +async function run(kind, value, output, countryCodes, phoneNumbers, download, filename, countType, newline) { if (!value && kind !== 'version') { process.stderr.write('Value is required' + newline); process.exit(1); @@ -152,12 +202,26 @@ async function run(kind, value, output, countryCodes, phoneNumbers, download, fi process.exit(0); } else if (kind === 'encode' || kind === 'e') { if (download) { - const filenm = await txms.downloadMessage(value, filename ? filename : undefined, output); - process.stdout.write(`TxMS file was downloaded as "${filenm}".${newline}`); - } else { - const encoded = txms.encode(value); - process.stdout.write(encoded + newline); + const filenamePrint = await txms.downloadMessage(value, filename ? filename : undefined, output); + process.stdout.write(`TxMS file was downloaded as "${filenamePrint}".${newline}`); + process.exit(0); } + + if (countType) { + let calculated; + if (countType === 'sms') { + calculated = txms.count(value, 'sms'); + } else if (countType === 'mms') { + calculated = txms.count(value, 'mms'); + } else { + calculated = txms.count(value); + } + process.stdout.write(calculated + newline); + process.exit(0); + } + + let encodedMessage = txms.encode(value); + process.stdout.write(encodedMessage + newline); process.exit(0); } else if (kind === 'decode' || kind === 'd') { const decoded = txms.decode(value); diff --git a/package.json b/package.json index 5ef4805..070467f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "txms.js", - "version": "1.2.9", + "version": "1.2.10", "description": "Transaction messaging service protocol", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/index.ts b/src/index.ts index 1b3b61a..41a7740 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ export interface Transport { encode(hex: string): string; decode(data: string): string; + count(hex: string, type?: 'sms' | 'mms' | true): number; getEndpoint(network?: number | string, countriesList?: string | Array): { [key: string]: Array }; sms(number?: boolean | string | number | Array, message?: string, network?: number | string, encodeMessage?: boolean, platform?: string): string; mms(number?: boolean | string | number | Array, message?: string, network?: number | string, encodeMessage?: boolean, platform?: string): string; @@ -85,6 +86,47 @@ const txms: Transport = { return '0x' + hex.replace(/^0+/, ''); }, + count(hex: string, type?: 'sms' | 'mms' | true): number { + // Encode the hex string to the message + const message = this.encode(hex); + + // If type is not provided, return the length of the message in characters + if (!type) { + return message.length; + } + + // SMS calculation logic + if (type === 'sms') { + // UTF-16 encoding, so the character limit is 70 characters for a single SMS + const singleSmsLimit = 70; + const multipartSmsLimit = 67; // Character limit per SMS in a concatenated message + + if (message.length <= singleSmsLimit) { + // Fits within a single SMS + return 1; + } else { + // Calculate the number of segments required for a multipart SMS + return Math.ceil(message.length / multipartSmsLimit); + } + } + + // MMS calculation logic + if (type === 'mms') { + // Assume a typical size limit of 300 KB for MMS (this may vary by carrier) + const mmsSizeLimit = 300 * 1024; // 300 KB in bytes + // Estimate size of the message in bytes (UTF-16, so each character is 2 bytes) + const messageSizeInBytes = message.length * 2; + + if (messageSizeInBytes <= mmsSizeLimit) { + return 1; + } else { + return Math.ceil(messageSizeInBytes / mmsSizeLimit); + } + } + + return message.length; + }, + getEndpoint(network?: number | string, countriesList?: string | Array): { [key: string]: Array } { let requestedList: Array | undefined; if (countriesList instanceof Array) { diff --git a/test/index.js b/test/index.js index 9ea7404..0f7ea66 100644 --- a/test/index.js +++ b/test/index.js @@ -21,14 +21,14 @@ if (!fs.existsSync(outputDir)) { // Encode/Decode Tests describe('Encode/Decode Tests', () => { samples.valid.forEach((f) => { - test(`OK - Encode - to data. Description: ${f.description}`, () => { + test(`Should encode - to data. Description: ${f.description}`, () => { const actual = txms.encode(f.hex); assert.strictEqual(actual, f.data); }); }); samples.valid.forEach((f) => { - test(`OK - Decode - to hex. Description: ${f.description}`, () => { + test(`Should decode - to hex. Description: ${f.description}`, () => { const actual = txms.decode(f.data); const normalizedActual = actual.startsWith('0x') ? actual.slice(2) : actual; const normalizedExpected = f.hex.startsWith('0x') ? f.hex.slice(2) : f.hex; @@ -37,12 +37,33 @@ describe('Encode/Decode Tests', () => { }); samples.invalid.forEach((f) => { - test(`Encode — ${f.description}`, () => { + test(`Should encode — ${f.description}`, () => { assert.throws(() => { txms.encode(f.hex); }, /Not a hex format/); }); }); + + samples.valid.forEach((f) => { + test(`Should count - characters. Description: ${f.description}`, () => { + const length = txms.count(f.hex); + assert.strictEqual(length, f.length); + }); + }); + + samples.valid.forEach((f) => { + test(`Should count - SMS. Description: ${f.description}`, () => { + const lengthSms = txms.count(f.hex, 'sms'); + assert.strictEqual(lengthSms, f.sms); + }); + }); + + samples.valid.forEach((f) => { + test(`Should count - MMS. Description: ${f.description}`, () => { + const lengthSms = txms.count(f.hex, 'mms'); + assert.strictEqual(lengthSms, f.mms); + }); + }); }); // Endpoint Tests @@ -263,4 +284,41 @@ describe('CLI Tests', () => { assert.strictEqual(result.status, 0); assert.strictEqual(result.stdout.toString().trim(), samples.valid[0].hex); }); + + test('Should count length', () => { + const hexValue = samples.valid[0].hex; + const result = spawnSync('node', [txmsPath, `--encode=${hexValue}`, '--count']); + assert.strictEqual(result.status, 0); + assert.strictEqual(parseInt(result.stdout.toString(), 10), samples.valid[0].length); + }); + + test('Should count amount of SMS', () => { + const hexValue = samples.valid[0].hex; + const result = spawnSync('node', [txmsPath, `--encode=${hexValue}`, '--count=sms']); + assert.strictEqual(result.status, 0); + assert.strictEqual(parseInt(result.stdout.toString(), 10), samples.valid[0].sms); + }); + + test('Should count amount of MMS', () => { + const hexValue = samples.valid[0].hex; + const result = spawnSync('node', [txmsPath, `--encode=${hexValue}`, '-ct=mms']); + assert.strictEqual(result.status, 0); + assert.strictEqual(parseInt(result.stdout.toString(), 10), samples.valid[0].mms); + }); + + test('Should count with piping', () => { + const hexValue = samples.valid[0].hex; + const echo = spawnSync('echo', [hexValue]); + const result = spawnSync('node', [txmsPath, '--encode', '--count'], { + input: echo.stdout + }); + assert.strictEqual(result.status, 0); + assert.strictEqual(parseInt(result.stdout.toString(), 10), samples.valid[0].length); + }); + + test('Should print help text', () => { + const result = spawnSync('node', [txmsPath, '--help']); + assert.strictEqual(result.status, 0); + assert.match(result.stdout.toString(), /^\n\x1B\[1mUsage:\x1B\[0m txms \x1B\[38;5;214m\[options\]\x1B\[0m/); + }); }); diff --git a/test/samples.json b/test/samples.json index 3200784..7a0500d 100644 --- a/test/samples.json +++ b/test/samples.json @@ -3,12 +3,18 @@ { "hex": "0xf8d880843b9aca008252080396ab2896c9af969b11394e3f0be750da75d0076e1d1afd880de0b6b3a764000080b8ab16acb60152c254244a6e3edcd5e66c029caf909f4f2dc4972ff01716d96de84afc62d94c47810b55b38eb0c3572b728552611d3fbb0209e100fde2e8404d168dd431c6d3dd2eec1574d86ad34e2374506cb98ee4576bbf5a78f07003c653d9e8887582020fb369c16fe1ae5e46f0dd4e0b00771fb7ab745c15389be291530859010281f2746b652118a47b07b9763c789e90576f6a2585d530a89892aa3aba2577895e63a3492cbbf38d00", "data": "~Ǹǘ肄㮚쨀艒ࠃ隫⢖즯際ᄹ丿௧僚痐ݮᴚﶈ~čǠ뚳Ꝥ~ĀĀ肸ꬖ겶Œ쉔⑊渾~ǜǕ~ǦŬʜ꾐齏ⷄ霯~ǰėᛙ淨䫼拙䱇脋喳躰썗⭲蕒愝㾻ȉ~ǡĀ~ǽǢ~Ǩŀ䴖跔㇆폝⻬ᕴ~ǘŪ퍎⍴偬릎~Ǥŗ殿婸~ǰŰφ叙~Ǩƈ疂ȏ덩셯~ǡƮ幆~ǰǝ下wᾷꭴ尕㢛~ǢƑ匈夁ʁ~DzŴ步℘ꑻ~ćƹ瘼碞遗潪▅픰ꢘ銪㪺╷襞掣䤬믳贀", - "description": "Correct transaction on Devin." + "description": "Correct transaction on Devin.", + "length": 145, + "sms": 3, + "mms": 1 }, { "hex": "f8dc821ae4850ee6b280008252080196cb65d677385703c528527f2a0f0e401b4af1988d91c5896e3f4f2ab21845000080b8abcffa127f34f8dc8d8bc9a50da5def786a16ecab58d9d1cdc3e1347077f531ad0339797568345464f542f8da3bcd50fd683878f52e6d094610025d6e4a5fb3699acd20ebd1ec2fdde9d12f5e82fe5f4c8d9061466475b3293bb18c34504c6eb43bc0ba48d61a8edfda686c69773fa96b90d00760d8277330d90589ba26fb63874952b013a8af1a5edacbcabb37108b47518c79abd6e50be00da0a08fb9126fd265175cace1ac93d1f809b80", "data": "~Ǹǜ舚~Ǥƅ~ĎǦ늀~ĀƂ刈Ɩ쭥홷㡗υ⡒缪༎䀛䫱颍釅襮㽏⪲ᡅ~ĀĀ肸ꯏ晴缴~Ǹǜ趋즥ඥ~ǞǷ蚡滊떍鴜~ǜľፇݿ匚퀳鞗嚃䕆佔⾍ꎼ픏횃螏勦킔愀◖~Ǥƥזּ馬툎봞싽~ǞƝድ~Ǩį~ǥǴ죙ؔ晇嬲鎻ᣃ䔄웫䎼த赡꣭ﶦ蛆靳殺뤍vං眳ඐ墛ꉯ똸璕⬁㪊~DZƥ~ǭƬ벫덱ࢴ甘잚뵮傾Úਈﮑ⛽♑痊츚줽ᾀ鮀", - "description": "Correct transaction without 0x prefix." + "description": "Correct transaction without 0x prefix.", + "length": 139, + "sms": 3, + "mms": 1 } ], "invalid": [