├── .eslintrc.json ├── .jsdoc.json ├── .github └── workflows │ └── npmPublish.yml ├── LICENSE ├── exmaples └── example.js ├── package.json ├── .gitignore ├── README.md └── src └── index.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "commonjs": true, 5 | "es2021": true 6 | }, 7 | "extends": [ 8 | "standard" 9 | ], 10 | "parserOptions": { 11 | "ecmaVersion": "latest" 12 | }, 13 | "ignorePatterns": [ 14 | "docs" 15 | ], 16 | "plugins": [ 17 | "jsdoc" 18 | ], 19 | "rules": { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "include": ["src", "README.md", "package.json"] 4 | }, 5 | 6 | "opts": { 7 | "encoding": "utf8", 8 | "readme": "./README.md", 9 | "destination": "docs/", 10 | "recurse": true, 11 | "verbose": true, 12 | "template": "./node_modules/clean-jsdoc-theme", 13 | "theme_opts": { 14 | "theme": "dark" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/npmPublish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - 'v*' 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: 12 14 | - run: npm install 15 | - uses: JS-DevTools/npm-publish@v1 16 | with: 17 | token: ${{ secrets.NPM_TOKEN }} 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Maksim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /exmaples/example.js: -------------------------------------------------------------------------------- 1 | const { GetSMS, ServiceApiError, TimeoutError, errors } = require('getsms') 2 | 3 | const sms = new GetSMS({ 4 | key: 'bc103fa02b63f986cd102a6d2f5c', 5 | url: 'https://smshub.org/stubs/handler_api.php', 6 | service: 'smshub' 7 | }); 8 | 9 | (async () => { 10 | try { 11 | // eslint-disable-next-line camelcase 12 | const { balance_number } = await sms.getBalance() 13 | // eslint-disable-next-line camelcase 14 | if (balance_number > 0) { 15 | // Service - bd, operator - mts, country - russia (0) 16 | const { id, number } = await sms.getNumber('bd', 'mts', 0) 17 | console.log('Number ID:', id) 18 | console.log('Number:', number) 19 | 20 | // Set "message has been sent" status 21 | await sms.setStatus(1, id) 22 | 23 | // Wait for code 24 | const { code } = await sms.getCode(id) 25 | console.log('Code:', code) 26 | 27 | await sms.setStatus(6, id) // Accept, end 28 | } else console.log('No money') 29 | } catch (error) { 30 | if (error instanceof TimeoutError) { 31 | console.log('Timeout reached') 32 | } 33 | 34 | if (error instanceof ServiceApiError) { 35 | if (error.code === errors.BANNED) { 36 | console.log(`Banned! Time ${error.banTime}`) 37 | } else { 38 | console.error(error.code, error.message) 39 | } 40 | } else console.error(error) 41 | } 42 | })() 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getsms", 3 | "version": "2.1.0", 4 | "description": "SMSHub API, SMS-ACTIVATE API module", 5 | "main": "./src/index.js", 6 | "scripts": { 7 | "test": "node test", 8 | "lint": "eslint .", 9 | "jsdoc": "jsdoc --configure .jsdoc.json --verbose", 10 | "remark": "npx remark README.md --use remark-preset-lint-consistent --use remark-preset-lint-recommended" 11 | }, 12 | "dependencies": { 13 | "undici": "^5.28.3" 14 | }, 15 | "devDependencies": { 16 | "eslint": "^8.56.0", 17 | "eslint-config-standard": "^17.1.0", 18 | "eslint-plugin-import": "^2.29.1", 19 | "eslint-plugin-n": "^16.6.2", 20 | "eslint-plugin-promise": "^6.1.1", 21 | "jsdoc": "^4.0.2", 22 | "eslint-plugin-jsdoc": "^48.1.0", 23 | "clean-jsdoc-theme": "^4.2.17", 24 | "remark-cli": "^12.0.0", 25 | "remark-preset-lint-consistent": "^5.1.2", 26 | "remark-preset-lint-recommended": "^6.1.3" 27 | }, 28 | "keywords": [ 29 | "smshub", 30 | "sms", 31 | "smsapi", 32 | "sms-api", 33 | "sms-hub", 34 | "smshub-api", 35 | "sms", 36 | "gate", 37 | "smsgateway", 38 | "smsactivate", 39 | "5sim", 40 | "fivesim" 41 | ], 42 | "author": { 43 | "name": "Maxim Telegin", 44 | "email": "m_telega@mail.ru" 45 | }, 46 | "bugs": { 47 | "url": "https://github.com/Viiprogrammer/getSMS/issues" 48 | }, 49 | "homepage": "https://github.com/Viiprogrammer/getSMS#readme", 50 | "repository": { 51 | "type": "git", 52 | "url": "git+https://github.com/Viiprogrammer/getSMS.git" 53 | }, 54 | "license": "MIT" 55 | } 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Docs 2 | docs 3 | # IDEA 4 | .idea 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | 82 | # Next.js build output 83 | .next 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GetSMS 2 |

3 | NPM version 4 | NPM downloads 5 | 6 |

7 | 8 | [![NPM](https://nodei.co/npm/getsms.png?downloads=true&stars=true)](https://nodei.co/npm/getsms/) 9 | 10 | getSMS - This is [Node.js](https://nodejs.org) module that allows you to interact with the SMS services api 11 | 12 | ## Features 13 | - Promises 14 | - Supports `SMSActivate`, `SMSHub` 15 | - Using [undici](https://github.com/nodejs/undici) http client 16 | 17 | ## Docs 18 | 19 | You can find documentation [here](https://viiprogrammer.github.io/getSMS/) 20 | 21 | ## Installation 22 | > **[Node.js](https://nodejs.org/) 12.0.0 or newer is required** 23 | 24 | ### Yarn 25 | ```bash 26 | yarn add getsms 27 | ``` 28 | 29 | ### NPM 30 | ```bash 31 | npm i getsms 32 | ``` 33 | ## Errors caught 34 | 35 | Errors that can be caught with `catch()`: 36 | - `MAIL_RULE` - For buying this service number you must satisfied additional site rules **(smshub, undocumented)** 37 | - `BAD_KEY` - Invalid api key 38 | - `ERROR_SQL` - Server database error 39 | - `BAD_ACTION` - Bad request data 40 | - `WRONG_SERVICE` - Wrong service identifier 41 | - `BAD_SERVICE` - Wrong service name', 42 | - `NO_ACTIVATION` - Activation not found. 43 | - `NO_BALANCE` - No balance 44 | - `NO_NUMBERS` - No numbers 45 | - `WRONG_ACTIVATION_ID` - Wrong activation id 46 | - `WRONG_EXCEPTION_PHONE` - Wrong exception phone 47 | - `NO_BALANCE_FORWARD` - No balance for forward 48 | - `NOT_AVAILABLE` - Multiservice is not available for selected country 49 | - `BAD_FORWARD` - Incorrect forward 50 | - `WRONG_ADDITIONAL_SERVICE` - Wrong additional service 51 | - `WRONG_SECURITY` - WRONG_SECURITY error 52 | - `REPEAT_ADDITIONAL_SERVICE` - Repeat additional service error 53 | - `BANNED:YYYY-m-d H-i-s` - Account banned 54 | 55 | ***if the ban code is `BANNED:YYYY-m-d H-i-s` then the error object contains the properties `banTime`, `banDate`, `banTimestamp`*** 56 | - `banTime` - `YYYY-m-d H-i-s` (for example `2020-12-31 23-59-59`) 57 | - `banTimestamp` - Unixtime 58 | - `banDate` - JavaScript `new Date()` Object 59 | ## Usage example 60 | 61 | ```javascript 62 | const { GetSMS, ServiceApiError, TimeoutError, errors } = require('getsms') 63 | 64 | const sms = new GetSMS({ 65 | key: 'bc103fa02b63f986cd102a6d2f5c', 66 | url: 'https://smshub.org/stubs/handler_api.php', 67 | service: 'smshub' 68 | }); 69 | 70 | (async () => { 71 | try { 72 | // eslint-disable-next-line camelcase 73 | const { balance_number } = await sms.getBalance() 74 | // eslint-disable-next-line camelcase 75 | if (balance_number > 0) { 76 | // Service - bd, operator - mts, country - russia (0) 77 | const { id, number } = await sms.getNumber('bd', 'mts', 0) 78 | console.log('Number ID:', id) 79 | console.log('Number:', number) 80 | 81 | // Set "message has been sent" status 82 | await sms.setStatus(1, id) 83 | 84 | // Wait for code 85 | const { code } = await sms.getCode(id) 86 | console.log('Code:', code) 87 | 88 | await sms.setStatus(6, id) // Accept, end 89 | } else console.log('No money') 90 | } catch (error) { 91 | if (error instanceof TimeoutError) { 92 | console.log('Timeout reached') 93 | } 94 | 95 | if (error instanceof ServiceApiError) { 96 | if (error.code === errors.BANNED) { 97 | console.log(`Banned! Time ${error.banTime}`) 98 | } else { 99 | console.error(error.code, error.message) 100 | } 101 | } else console.error(error) 102 | } 103 | })() 104 | ``` 105 | 106 | ## License 107 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FViiprogrammer%2FgetSMS.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FViiprogrammer%2FgetSMS?ref=badge_large) 108 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { request } = require('undici') 2 | 3 | const errorsList = { 4 | BANNED: 'Account banned', 5 | MAIL_RULE: 'For buying this service number you must satisfied additional site rules', 6 | NO_KEY: 'Key is empty', 7 | BAD_KEY: 'Invalid api key', 8 | ERROR_SQL: 'Server database error', 9 | BAD_ACTION: 'Bad request data', 10 | WRONG_SERVICE: 'Wrong service identifier', 11 | BAD_SERVICE: 'Wrong service name', 12 | NO_ACTIVATION: 'Activation not found.', 13 | NO_BALANCE: 'No balance', 14 | NO_NUMBERS: 'No numbers', 15 | WRONG_ACTIVATION_ID: 'Wrong activation id', 16 | WRONG_EXCEPTION_PHONE: 'Wrong exception phone', 17 | NO_BALANCE_FORWARD: 'No balance for forward', 18 | NOT_AVAILABLE: 'Multiservice is not available for selected country', 19 | BAD_FORWARD: 'Incorrect forward', 20 | WRONG_ADDITIONAL_SERVICE: 'Wrong additional service', 21 | WRONG_SECURITY: 'WRONG_SECURITY error', 22 | REPEAT_ADDITIONAL_SERVICE: 'Repeat additional service error' 23 | } 24 | 25 | const supportedMethods = { 26 | smshub: [ 27 | 'getNumbersStatus', 'getBalance', 'getNumber', 'setStatus', 'getStatus', 'getPrices', 'getCode', 28 | 'setMaxPrice', 'getCurrentActivations', 'getListOfCountriesAndOperators' 29 | ], 30 | smsactivate: [ 31 | 'getNumbersStatus', 'getBalance', 'getNumber', 'setStatus', 'getStatus', 'getPrices', 'getFullSms', 32 | 'getAdditionalService', 'getCountries', 'getQiwiRequisites', 'getCode' 33 | ] 34 | } 35 | 36 | class TimeoutError extends Error { 37 | /** 38 | * TimeoutError class constructor 39 | * @constructor 40 | * @param {string} message 41 | */ 42 | constructor (message) { 43 | super() 44 | this.message = message 45 | this.name = this.constructor.name 46 | } 47 | } 48 | 49 | class ServiceApiError extends Error { 50 | /** 51 | * ServiceApiError class constructor 52 | * @constructor 53 | * @param {string} code Sms service error string 54 | */ 55 | constructor (code) { 56 | super() 57 | this.message = errorsList[code] 58 | 59 | if (code.indexOf('BANNED:') === 0) { 60 | this.serverResponse = code 61 | this.code = 'BANNED' 62 | const datetime = code.split(':').pop() 63 | 64 | let [date, time] = datetime.split(/\s/) 65 | time = time.split('-').join(':') 66 | date = date.split('-').join('/') 67 | 68 | const obj = new Date(`${date} ${time}`) 69 | 70 | this.banTime = datetime 71 | this.banTimestamp = obj.getTime() / 1000 72 | this.banDate = obj 73 | } else { 74 | this.code = code 75 | } 76 | 77 | this.name = this.constructor.name 78 | } 79 | 80 | /** 81 | * Method for checking response includes an error from errors list 82 | * @method 83 | * @static 84 | * @private 85 | * 86 | * @param {string} response Http request response string 87 | * @return {boolean} 88 | */ 89 | static _check (response) { 90 | return Object.keys(errorsList).includes(response) && (response.indexOf('BANNED:') !== 0) 91 | } 92 | } 93 | 94 | class GetSMS { 95 | /** 96 | * @typedef {object} InitProperties 97 | * @property {string} key Service API key 98 | * @property {string} url Service endpoint url 99 | * @property {string} [secondUrl=https://smshub.org/api.php] Used for method `getListOfCountriesAndOperators` (only SMSHUB) 100 | * @property {'en'|'ru'} [lang=ru] Lang code, smshub can return results with russian or english (Only smshub) 101 | * @property {'smshub'|'smsactivate'} service Service name 102 | * @property {number} [interval=2000] Polling interval of getCode method 103 | */ 104 | 105 | /** 106 | * GetSMS class constructor 107 | * @constructor 108 | * 109 | * @param {InitProperties} options Options object 110 | * @throws {Error} 111 | * @returns {GetSMS} 112 | */ 113 | constructor ({ key, url, secondUrl = 'https://smshub.org/api.php', service, lang = 'ru', interval = 2000 }) { 114 | if (!key || !url || !service) { 115 | throw new Error('Missing argument(s)') 116 | } 117 | 118 | if (!['smshub', 'smsactivate'].includes(service)) { 119 | throw new Error('Invalid service name') 120 | } 121 | 122 | this._key = key 123 | this._url = url 124 | this._lang = lang 125 | this._secondUrl = secondUrl 126 | this._service = service 127 | this._interval = interval 128 | 129 | return new Proxy(this, { 130 | get (target, prop) { 131 | if (typeof target[prop] !== 'function') { 132 | return target[prop] 133 | } 134 | 135 | return function (...args) { 136 | if (!prop.startsWith('_') && !supportedMethods[target._service].includes(prop)) { 137 | throw new Error(`Method "${prop}" not supported by ${target._service}`) 138 | } 139 | 140 | return target[prop].apply(this, args) 141 | } 142 | } 143 | }) 144 | } 145 | 146 | /** 147 | * Method for sending API requests 148 | * @method 149 | * @private 150 | * 151 | * @param {object} qs Query string params 152 | * @param {string} method Request METHOD 153 | * @param {object} [form] Form object 154 | * @param {boolean} [second=false] Use second API endpoint 155 | * @throws {Error|ServiceApiError} 156 | * @returns {Promise} 157 | */ 158 | async _request (qs, { method = 'GET', form, second = false } = {}) { 159 | const url = new URL(second ? this._secondUrl : this._url) 160 | 161 | url.search = new URLSearchParams( 162 | Object.assign(qs, { api_key: this._key }) 163 | ).toString() 164 | 165 | return request(url, { 166 | method, 167 | form: form ? new URLSearchParams(form).toString() : undefined, 168 | headers: { 169 | Cookie: 'lang=' + this._lang 170 | } 171 | }) 172 | .then(data => data.body.text()) 173 | .then(data => { 174 | // Safe json parsing 175 | try { 176 | data = JSON.parse(data) 177 | return data 178 | } catch (e) { 179 | if (!ServiceApiError._check(data)) { 180 | return data 181 | } else { 182 | throw new ServiceApiError(data) 183 | } 184 | } 185 | }) 186 | } 187 | 188 | /** 189 | * Method for getting number status 190 | * @method 191 | * @public 192 | * 193 | * @param {string|number} [country] Country ID 194 | * @param {string} [operator] Mobile operator code name 195 | * @throws {Error|ServiceApiError} 196 | * @returns {object} 197 | */ 198 | async getNumbersStatus (country, operator) { 199 | return this._request({ action: 'getNumbersStatus', country, operator }) 200 | } 201 | 202 | /** 203 | * @typedef {object} BalanceObject 204 | * @property {string} balance_string Balance as string 205 | * @property {number} balance_float Balance as float number 206 | * @property {number} balance_number Balance as multiplied by 100 as integer number 207 | */ 208 | 209 | /** 210 | * Method for getting account balance 211 | * @method 212 | * @public 213 | * 214 | * @throws {Error|ServiceApiError} 215 | * @returns {Promise} 216 | */ 217 | async getBalance () { 218 | const response = await this._request({ action: 'getBalance' }) 219 | 220 | const [, balance] = response.split(':') 221 | const balanceParsed = parseFloat(balance) 222 | 223 | return { 224 | balance_string: balance, 225 | balance_float: balanceParsed, 226 | balance_number: balanceParsed * 100 227 | } 228 | } 229 | 230 | /** 231 | * Method for getting number for using with several services (only for smsactivate) 232 | * @method 233 | * @public 234 | * 235 | * @example 236 | * .getMultiServiceNumber('ok,vk,vi,av', 'mts', 0, '0,1,0,0') 237 | * 238 | * @example 239 | * .getMultiServiceNumber(['ok','vk','vi','av'], 'mts', 0, [0, 1, 0, 0]) 240 | * 241 | * @param {array|string} service Array of services names 242 | * @param {array|string} operator Array of operators names 243 | * @param {string|number} country Country ID 244 | * @param {Array|string} [forward] Number forward, must have values 1 or 0 * 245 | * @param {string} [ref] Referral identifier 246 | * @throws {Error|ServiceApiError} 247 | * @returns {Promise} 248 | */ 249 | async getMultiServiceNumber (service, operator, country, forward, ref) { 250 | if (Array.isArray(service)) service = service.toString() 251 | if (Array.isArray(forward)) forward = forward.toString() 252 | if (Array.isArray(operator)) operator = operator.toString() 253 | return this._request({ action: 'getPrices', service, operator, country, forward, ref }) 254 | } 255 | 256 | /** 257 | * @typedef {object} additionalServiceResponse 258 | * @property {string} id Status code 259 | * @property {string} number Text from SMS 260 | */ 261 | 262 | /** 263 | * Method for getting additional service for numbers with forward (only for smsactivate) 264 | * @method 265 | * @public 266 | * 267 | * @param {string} id Mobile number ID 268 | * @param {string} service Service code name 269 | * @returns {Promise} 270 | * @throws {Error|ServiceApiError} 271 | */ 272 | async getAdditionalService (id, service) { 273 | const response = await this._request({ action: 'getAdditionalService', id, service }) 274 | 275 | const [, _id, number] = response.split(':') 276 | 277 | return { 278 | id: _id, 279 | number 280 | } 281 | } 282 | 283 | /** 284 | * @typedef {object} fullSMSTextResponse 285 | * @property {string} status Status code 286 | * @property {string} [text] Text from SMS. Is returning _**only if status is not `STATUS_WAIT_CODE`, `STATUS_CANCEL`**_ 287 | */ 288 | 289 | /** 290 | * Method for getting full sms text (only for smsactivate) 291 | * @method 292 | * @public 293 | * 294 | * @param {string|number} id Mobile number ID 295 | * @returns {Promise} 296 | * @throws {Error|ServiceApiError} 297 | */ 298 | async getFullSms (id) { 299 | const response = await this._request({ action: 'getFullSms', id }) 300 | 301 | const [status, text] = response.split(':') 302 | const res = { status } 303 | 304 | if (['STATUS_WAIT_CODE', 'STATUS_CANCEL'].includes(status)) { 305 | return res 306 | } 307 | 308 | if (status === 'FULL_SMS') { 309 | res.text = text 310 | return res 311 | } 312 | } 313 | 314 | /** 315 | * Method for getting countries list (only for smsactivate) 316 | * @method 317 | * @public 318 | * 319 | * @throws {Error|ServiceApiError} 320 | * @returns {Promise} 321 | */ 322 | async getCountries () { 323 | return this._request({ action: 'getCountries' }) 324 | } 325 | 326 | /** 327 | * Method for getting Qiwi payment requisites (only for smsactivate) 328 | * @method 329 | * @public 330 | * 331 | * @throws {Error|ServiceApiError} 332 | * @returns {Promise} 333 | */ 334 | async getQiwiRequisites () { 335 | return this._request({ action: 'getQiwiRequisites' }) 336 | } 337 | 338 | /** 339 | * Unofficial / hidden method for getting list of countries & operators (only for smshub) 340 | * 341 | * @method 342 | * @public 343 | * 344 | * @example Answer example: 345 | * { 346 | * status: "success", 347 | * services: [{ 348 | * lb: "Mailru Group", 349 | * vk: "Вконтакте", 350 | * ok: "Ok.ru", 351 | * // ... 352 | * ot: "Любой другой" 353 | * }, 354 | * // ... 355 | * ], 356 | * data: [{ 357 | * name: "Индонезия", 358 | * id: "6", 359 | * operators: ["any", "axis", "indosat", "smartfren", "telkomsel", "three"] 360 | * }], 361 | * currentCountry: "0", 362 | * currentOperator: "any" 363 | * } 364 | * 365 | * @throws {Error|ServiceApiError} 366 | * @returns {Promise} 367 | */ 368 | async getListOfCountriesAndOperators () { 369 | return this._request({ 370 | cat: 'scripts', 371 | act: 'manageActivations', 372 | asc: 'getListOfCountriesAndOperators' 373 | }, { 374 | method: 'POST', 375 | second: true 376 | }) 377 | } 378 | 379 | /** 380 | * Unofficial / hidden method for changing default number settings (only for smshub) 381 | * Change max price of mobile number for country id, enable / disable random 382 | *

WARNING: I don't know why, but really values changed only after ~30 seconds, maybe it's cached on smshub server
383 | * 384 | * @method 385 | * @public 386 | * 387 | * @param {string} service Service code name 388 | * @param {string|number} maxPrice Max buy price 389 | * @param {boolean} random Enable random number 390 | * @param {string|number} country Country ID 391 | * @throws {Error|ServiceApiError} 392 | * @returns {Promise} 393 | */ 394 | async setMaxPrice (service, maxPrice, random = true, country) { 395 | return this._request({ action: 'setMaxPrice', service, maxPrice, country, random }) 396 | } 397 | 398 | /** 399 | * Unofficial / hidden method for getting list of current activations (only for smshub) 400 | * 401 | * @method 402 | * @public 403 | * 404 | * @example Answer example: 405 | * // Success request: 406 | * 407 | * { 408 | * status: 'success', 409 | * array: [ 410 | * { 411 | * id: '1231231231', 412 | * activationId: '1231231231', 413 | * apiKeyId: '12345', 414 | * phone: '79538364598', 415 | * status: '2', 416 | * moreCodes: '', 417 | * createDate: 1654770955, 418 | * receiveSmsDate: -62169993017, 419 | * finishDate: '0000-00-00 00:00:00', 420 | * activation: [Object], 421 | * code: '', 422 | * countryCode: '7', 423 | * countryName: 'Россия', 424 | * timer: 1200 425 | * } 426 | * ], 427 | * time: 1654770985 428 | * } 429 | * 430 | * // No activations: 431 | * { status: 'fail', msg: 'no_activations' } 432 | * 433 | * @throws {Error|ServiceApiError} 434 | * @returns {Promise} 435 | */ 436 | async getCurrentActivations () { 437 | return this._request({ action: 'getCurrentActivations' }) 438 | } 439 | 440 | /** 441 | * @typedef {object} getCodeResponse 442 | * @property {string} status Status code 443 | * @property {string|undefined} code Code from SMS 444 | */ 445 | 446 | /** 447 | * Method for getting new number 448 | * Arguments with * available only for smsactivate 449 | * 450 | * @method 451 | * @public 452 | * 453 | * @param {string} service Service code name 454 | * @param {string} [operator] Mobile operator code name 455 | * @param {string|number} [country] Country ID 456 | * @param {string|number} [forward] Number forward, must be `1` or `0` * 457 | * @param {string} [phoneException] Prefixes for excepting mobile numbers separated by comma * 458 | * @param {string} [ref] Referral identifier * 459 | * 460 | * @throws {Error|ServiceApiError} 461 | * @returns {Promise} 462 | */ 463 | async getNumber (service, operator, country, forward, phoneException, ref) { 464 | const response = await this._request({ action: 'getNumber', service, operator, country, forward, phoneException, ref }) 465 | 466 | const [, id, number] = response.split(':') 467 | 468 | return { 469 | id, 470 | number 471 | } 472 | } 473 | 474 | /** 475 | * Method for set number status 476 | * @method 477 | * @public 478 | * 479 | * @param {string|number} status New mobile number status 480 | * @param {string|number} id Mobile number ID 481 | * 482 | * @throws {Error|ServiceApiError} 483 | * @returns {Promise} 484 | */ 485 | async setStatus (status, id) { 486 | const response = await this._request({ action: 'setStatus', status, id }) 487 | 488 | return { status: response } 489 | } 490 | 491 | /** 492 | * @typedef {object} getCodeResponse 493 | * @property {string} status Status code 494 | * @property {string|undefined} code Code from SMS. Is returning _**only with `STATUS_WAIT_RETRY`, `STATUS_OK` statuses**_ 495 | */ 496 | 497 | /** 498 | * Method which polling with getStatus method and returns new status when it's changes 499 | * 500 | * @method 501 | * @public 502 | * 503 | * @example 504 | * const { id, number } = await sms.getNumber('vk', 'mts', 0) 505 | * console.log('Number ID:', id); 506 | * console.log('Number:', number); 507 | * await sms.setStatus(1, id); 508 | * // Wait for code 509 | * const { code } = await sms.getCode(id) 510 | * console.log('Code:', code) 511 | * await sms.setStatus(1, id) //Accept, end 512 | * 513 | * @param {string|number} id - Mobile number ID 514 | * @param {number} [timeout=0] - Timeout, after reached - forcefully stop and throw `TimeoutError`, pass 0 to disable 515 | * @throws {Error|ServiceApiError|TimeoutError} 516 | * @returns {Promise} 517 | */ 518 | async getCode (id, timeout = 0) { 519 | return new Promise((resolve, reject) => { 520 | let timeoutId = null 521 | 522 | const interval = setInterval(async () => { 523 | try { 524 | const { 525 | status, 526 | code 527 | } = await this.getStatus(id) 528 | 529 | // eslint-disable-next-line no-empty 530 | if (status === 'STATUS_WAIT_CODE') {} 531 | 532 | if (status === 'STATUS_CANCEL') { 533 | clearInterval(interval) 534 | if (timeoutId) clearTimeout(timeoutId) 535 | resolve({ status }) 536 | } 537 | 538 | if (status === 'STATUS_OK') { 539 | clearInterval(interval) 540 | if (timeoutId) clearTimeout(timeoutId) 541 | resolve({ code }) 542 | } 543 | } catch (err) { 544 | clearInterval(interval) 545 | if (timeoutId) clearTimeout(timeoutId) 546 | reject(err) 547 | } 548 | }, this._interval) 549 | 550 | if (timeout) { 551 | timeoutId = setTimeout(() => { 552 | clearInterval(interval) 553 | clearTimeout(timeoutId) 554 | 555 | reject(new TimeoutError(`Timeout ${timeout}ms reached`)) 556 | }, timeout) 557 | } 558 | }) 559 | } 560 | 561 | /** 562 | * Method for getting number status 563 | * @method 564 | * @public 565 | * @param {string|number} id Mobile number ID 566 | * @throws {Error|ServiceApiError} 567 | * @returns {Promise} 568 | */ 569 | async getStatus (id) { 570 | const response = await this._request({ action: 'getStatus', id }) 571 | 572 | if (['STATUS_CANCEL', 'STATUS_WAIT_RESEND', 'STATUS_WAIT_CODE'].includes(response)) { 573 | return { status: response } 574 | } 575 | 576 | const [type, code] = response.split(':') 577 | 578 | if (type === 'STATUS_WAIT_RETRY') { 579 | return { status: type, code } 580 | } 581 | 582 | if (type === 'STATUS_OK') { 583 | return { status: type, code } 584 | } 585 | 586 | return { status: 'UNKNOWN' } 587 | } 588 | 589 | /** 590 | * Method for getting numbers prices 591 | * @method 592 | * @public 593 | * @param {string|number} [country] Country ID 594 | * @param {string} service Service code name 595 | * @throws {Error|ServiceApiError} 596 | * @returns {Promise} 597 | */ 598 | async getPrices (service, country) { 599 | return this._request({ action: 'getPrices', country, service }) 600 | } 601 | } 602 | 603 | module.exports = { 604 | GetSMS, 605 | ServiceApiError, 606 | TimeoutError, 607 | errors: Object.fromEntries( 608 | Object.keys(errorsList) 609 | .map(e => [e, e]) 610 | ) 611 | } 612 | --------------------------------------------------------------------------------