diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cf9a6f3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,63 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next +.idea +package-lock.json \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a9f2514 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: node_js +node_js: + - "11" + - "10" + - "8" +script: npm tests \ No newline at end of file diff --git a/README.md b/README.md index b22be8c..ad3fccd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,53 @@ # retailCRM Message Gateway Bot API JS client +This is js retailCRM bot API client. + +# Installation +``` +npm install --save mg-bot-api-client-js +``` +In your file +``` +var RetailcrmBotApiClient = require('mg-bot-api-client-js'); +``` +# Usage +#### Get users +```javascript +var api = new RetailcrmBotApiClient({ + host: 'https://api.example.com', + token: 'your bot token', + apiVersion: 'v1' // optional +}).getClient(); + +api.getUsers() + .then(function (users) { + console.log(users); + }) + .catch(function (e) { + console.log(e); + }); +``` + +#### Send message +```javascript +var api = new RetailcrmBotApiClient({ + host: 'https://api.example.com', + token: 'your bot token', + apiVersion: 'v1' // optional +}).getClient(); + +var message = { + chat_id: 1, + content: 'Text message', + scope: 'public', + type: 'text' +}; + +api.sendMessage(message) + .then(function (result) { + console.log(result); + }) + .catch(function (e) { + console.log(e); + }); +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..a80ccf2 --- /dev/null +++ b/index.js @@ -0,0 +1,48 @@ +'use strict'; + +var v1 = require('./lib/v1/client'); +var request = require('./lib/request'); + +module.exports = RetailcrmBotApiClient; + +/** + * @param {Object} options + * @throws {Error} + * @constructor + */ +function RetailcrmBotApiClient(options) { + if (!options.host) { + throw new Error('Url is required'); + } + + if (options.host.indexOf('https') !== 0) { + throw new Error('HTTPS required'); + } + + if (!(options.token)) { + throw new Error('Token is required'); + } + + var currentVersion; + var lastApiVersion = 'v1'; + + var clients = { + 'v1': v1.Client + }; + + if (options.apiVersion) { + currentVersion = options.apiVersion; + } else { + currentVersion = lastApiVersion; + } + + this._client = new clients[currentVersion](new request.Request(options)); +} + +/** + * Get API client + * @returns {Client} + */ +RetailcrmBotApiClient.prototype.getClient = function () { + return this._client; +}; diff --git a/lib/request.js b/lib/request.js new file mode 100644 index 0000000..7266179 --- /dev/null +++ b/lib/request.js @@ -0,0 +1,161 @@ +'use strict'; + +var url = require('url'); +var https = require('https'); +var querystring = require('querystring'); + +exports.Request = Request; + +/** + * @param {Object} options + * @constructor + */ +function Request(options) { + this._host = url.parse(options.host).host; + this._token = options.token; +} + +/** + * Get request path + * @param {string} endpoint + * @returns {string} + * @private + */ +Request.prototype._getPath = function (endpoint) { + return '/api/bot/' + endpoint; +}; + +/** + * Make request + * @param {string} endpoint + * @param {string} method + * @param {Object} data + * @returns {Promise} + * @throws {Error} + * @private + */ +Request.prototype._request = function (endpoint, method, data) { + var path = this._getPath(endpoint); + var response = ''; + + if (method === 'GET' && data.length > 0) { + path += '?' + querystring.stringify(data); + } + + var options = { + host: this._host, + method: method, + path: path, + headers: { + 'x-bot-token': this._token + } + }; + + return new Promise(function(resolve, reject) { + var request = https.request(options, function (res) { + res.on('data', function (chunk) { + response += chunk; + }); + + res.on('end', function () { + try { + var result = JSON.parse(response); + + if (res.statusCode < 400) { + resolve(result); + } else { + reject(new Error(result.errors.join(','))); + } + } catch (e) { + reject(e); + } + }); + + res.on('error', function (e) { + reject(e); + }) + }); + + if (['POST', 'PUT', 'PATCH'].includes(method)) { + request.write(JSON.stringify(data)); + } + + request.end(); + + request.on('error', function(e) { + reject(e); + }); + }); +}; + +/** + * Method GET + * @param {string} endpoint + * @param {Object} params + * @returns {Promise} + */ +Request.prototype.get = function (endpoint, params) { + if (params === undefined) { + params = {}; + } + + return this._request(endpoint, 'GET', params); +}; + +/** + * Method POST + * @param {string} endpoint + * @param {Object} data + * @returns {Promise} + * @throws {Error} + */ +Request.prototype.post = function (endpoint, data) { + if (!data) { + throw new Error('Body is not be empty'); + } + + return this._request(endpoint, 'POST', data); +}; + +/** + * Method PATCH + * @param {string} endpoint + * @param {Object} data + * @returns {Promise} + * @throws {Error} + */ +Request.prototype.patch = function (endpoint, data) { + if (!data) { + throw new Error('Body is not be empty'); + } + + return this._request(endpoint, 'PATCH', data); +}; + +/** + * Method PUT + * @param {string} endpoint + * @param {Object} data + * @returns {Promise} + * @throws {Error} + */ +Request.prototype.put = function (endpoint, data) { + if (!data) { + throw new Error('Body is not be empty'); + } + + return this._request(endpoint, 'PUT', data); +}; + +/** + * Method DELETE + * @param {string} endpoint + * @returns {Promise} + */ +Request.prototype.delete = function (endpoint) { + return this._request(endpoint, 'DELETE', {}); +}; + +Request.prototype.getHost = function () { + return this._host; +}; diff --git a/lib/v1/client.js b/lib/v1/client.js new file mode 100644 index 0000000..c7dadbb --- /dev/null +++ b/lib/v1/client.js @@ -0,0 +1,210 @@ +'use strict'; + +exports.Client = Client; + +/** + * @param {Request} request + * @constructor + */ +function Client(request) { + this._version = 'v1'; + this._request = request; +} + +/** + * Get bots + * @param {string} params + * @returns {Promise} + */ +Client.prototype.getBots = function (params) { + return this._request.get(this._version + '/bots', params); +}; + +/** + * Get channels + * @param {string} params + * @returns {Promise} + */ +Client.prototype.getChannels = function (params) { + return this._request.get(this._version + '/channels', params); +}; + +/** + * Get chats + * @param {string} params + * @returns {Promise} + */ +Client.prototype.getChats = function (params) { + return this._request.get(this._version + '/chats', params); +}; + +/** + * Get customers + * @param {string} params + * @returns {Promise} + */ +Client.prototype.getCustomers = function (params) { + return this._request.get(this._version + '/customers', params); +}; + +/** + * Get dialogs + * @param {string} params + * @returns {Promise} + */ +Client.prototype.getDialogs = function (params) { + return this._request.get(this._version + '/dialogs', params); +}; + +/** + * Get members + * @param {string} params + * @returns {Promise} + */ +Client.prototype.getMembers = function (params) { + return this._request.get(this._version + '/members', params); +}; + +/** + * Assign dialog + * @param {Number} dialog_id + * @param {Object} dialog + * @returns {Promise} + */ +Client.prototype.assignDialog = function (dialog_id, dialog) { + return this._request.patch(this._version + '/dialogs/'+ dialog_id + '/assign', dialog); +}; + +/** + * Close dialog + * @param {Number} dialog_id + * @returns {Promise} + * @throws {Error} + */ +Client.prototype.closeDialog = function (dialog_id) { + if (!dialog_id) { + throw new Error('dialog_id is required'); + } + + return this._request.delete(this._version + '/dialogs/'+ dialog_id + '/close'); +}; + +/** + * Send message + * @param {Object} data + * @returns {Promise} + */ +Client.prototype.sendMessage = function (data) { + return this._request.post(this._version + '/messages', data); +}; + +/** + * Get messages + * @param {Object} params + * @returns {Promise} + */ +Client.prototype.getMessages = function (params) { + return this._request.get(this._version + '/messages', params); +}; + +/** + * Delete message + * @param {Number} message_id + * @returns {Promise} + * @throws {Error} + */ +Client.prototype.deleteMessage = function (message_id) { + if (!message_id) { + throw new Error('message_id is required'); + } + + return this._request.delete(this._version + '/messages/' + message_id); +}; + +/** + * Edit message + * @param {Number} message_id + * @param {Object} message + * @returns {Promise} + */ +Client.prototype.editMessage = function (message_id, message) { + return this._request.patch(this._version + '/messages/' + message_id, message); +}; + +/** + * Get bot commands + * @param {Object} params + * @returns {Promise} + */ +Client.prototype.getCommands = function (params) { + return this._request.get(this._version + '/my/commands', params); +}; + +/** + * Edit bot command + * @param {string} command_name + * @param {Object} data + * @returns {Promise} + * @throws {Error} + */ +Client.prototype.editCommand = function (command_name, data) { + if (!command_name) { + throw new Error('Parameter command name is required'); + } + + return this._request.put(this._version + '/my/commands/' + command_name, data); +}; + +/** + * Delete bot command + * @param {string} command_name + * @returns {Promise} + * @throws {Error} + */ +Client.prototype.deleteCommand = function (command_name) { + if (!command_name) { + throw new Error('command_name is required'); + } + + return this._request.delete(this._version + '/my/commands/' + command_name); +}; + +/** + * Bot information update + * @param {Object} data + * @returns {Promise} + */ +Client.prototype.info = function (data) { + return this._request.patch(this._version + '/my/info', data); +}; + +/** + * Get users + * @param {Object} params + * @returns {Promise} + */ +Client.prototype.getUsers = function (params) { + return this._request.get(this._version + '/users', params); +}; + +/** + * Get websocket url + * @param {array} events + * @returns {string} + * @throws {Error} + */ +Client.prototype.getWebsocketUrl = function (events) { + if (!events) { + throw new Error('Events is required'); + } + + var url = 'wss://' + this._request.getHost() + '/api/bot/' + this._version + '/ws?events='; + + events.forEach(function (event) { + url += event + ','; + }); + + url = url.slice(0, -1); + + return url; +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..c599480 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "mg-bot-api-client-js", + "description": "JS client for retailCRM Bot API", + "tags": ["API", "retailCRM", "Bot", "Chat"], + "version": "1.0.0", + "scripts": { + "test": "./node_modules/.bin/_mocha ./tests/." + }, + "author": "retailCRM", + "license": "MIT", + "main": "index.js", + "bugs": { + "url": "https://github.com/retailcrm/mg-bot-api-client-js/issues" + }, + "dependencies": {}, + "devDependencies": { + "chai": "^4.2.0", + "mocha": "^5.2.0", + "nock": "^10.0.6" + }, + "homepage": "https://github.com/retailcrm/mg-bot-api-client-js#readme", + "repository": { + "type": "git", + "url": "git@github.com:retailcrm/mg-bot-api-client-js.git" + } +} diff --git a/tests/test_index.js b/tests/test_index.js new file mode 100644 index 0000000..a58a973 --- /dev/null +++ b/tests/test_index.js @@ -0,0 +1,26 @@ +var nock = require('nock'); +var chai = require('chai'); +var RetailcrmBotApiClient = require('../index'); + +describe('#Constructor', function () { + it('Empty url', function () { + chai.expect(function() { + new RetailcrmBotApiClient({token: 'test_token'}); + }).to.throw('Url is required'); + }); + + it('Incorrect url', function () { + chai.expect(function() { + new RetailcrmBotApiClient({ + host: 'http://api.example.com', + token: 'test_token' + }); + }).to.throw('HTTPS required'); + }); + + it('Empty token', function () { + chai.expect(function() { + new RetailcrmBotApiClient({host: 'https://api.example.com'}); + }).to.throw('Token is required'); + }); +}); diff --git a/tests/test_request.js b/tests/test_request.js new file mode 100644 index 0000000..f88ca69 --- /dev/null +++ b/tests/test_request.js @@ -0,0 +1,14 @@ +var chai = require('chai'); +var request = require('../lib/request'); + +describe('#Request', function () { + var req = new request.Request({ + host: 'http://api.example.com', + token: 'test_token' + }); + + it('Request parameters', function () { + chai.expect(req._host).to.equal('api.example.com'); + chai.expect(req._token).to.equal('test_token'); + }) +}); diff --git a/tests/test_v1_client.js b/tests/test_v1_client.js new file mode 100644 index 0000000..49d5f83 --- /dev/null +++ b/tests/test_v1_client.js @@ -0,0 +1,349 @@ +var nock = require('nock'); +var chai = require('chai'); +var RetailcrmBotApiClient = require('../index'); + +describe('#API client v1', function() { + beforeEach(function() { + nock.cleanAll(); + }); + + var retailcrm = new RetailcrmBotApiClient({ + host: 'https://api.example.com', + token: 'test_token' + }).getClient(); + + it('Get bots list', function() { + nock('https://api.example.com/api/bot/v1').get('/bots').reply(200, [{ + id: 1, + isActive: true + }]); + + retailcrm.getBots().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.not.empty; + }); + }); + + it('Get empty bots list', function () { + nock('https://api.example.com/api/bot/v1').get('/bots').reply(200, []); + + retailcrm.getBots().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.empty; + }); + }); + + it('Get channels list', function () { + nock('https://api.example.com/api/bot/v1').get('/channels').reply(200, [{ + id: 1 + }]); + + retailcrm.getChannels().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.not.empty; + }); + }); + + it('Get empty channels list', function () { + nock('https://api.example.com/api/bot/v1').get('/channels').reply(200, []); + + retailcrm.getChannels().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.empty; + }); + }); + + it('Get chats list', function () { + nock('https://api.example.com/api/bot/v1').get('/chats').reply(200, [{ + author_id: 1, + id: 1 + }]); + + retailcrm.getChats().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.not.empty; + }); + }); + + it('Get empty chats list', function () { + nock('https://api.example.com/api/bot/v1').get('/chats').reply(200, []); + + retailcrm.getChats().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.empty; + }); + }); + + it('Get customers list', function () { + nock('https://api.example.com/api/bot/v1').get('/customers').reply(200, [{ + external_id: 1, + channel_id: 1, + id: 1 + }]); + + retailcrm.getCustomers().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.not.empty; + }); + }); + + it('Get empty customers list', function () { + nock('https://api.example.com/api/bot/v1').get('/customers').reply(200, []); + + retailcrm.getCustomers().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.empty; + }); + }); + + it('Get dialogs list', function () { + nock('https://api.example.com/api/bot/v1').get('/dialogs').reply(200, [{ + begin_message_id: 1, + id: 1 + }]); + + retailcrm.getDialogs().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.not.empty; + }); + }); + + it('Get empty dialogs list', function () { + nock('https://api.example.com/api/bot/v1').get('/dialogs').reply(200, []); + + retailcrm.getDialogs().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.empty; + }); + }); + + it('Get members list', function () { + nock('https://api.example.com/api/bot/v1').get('/members').reply(200, [{ + id: 1 + }]); + + retailcrm.getMembers().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.not.empty; + }); + }); + + it('Get empty members list', function () { + nock('https://api.example.com/api/bot/v1').get('/members').reply(200, []); + + retailcrm.getMembers().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.empty; + }); + }); + + it('Assign dialog', function () { + nock('https://api.example.com/api/bot/v1').patch('/dialogs/1/assign').reply(200, { + is_reassign: true, + responsible: { + id: 1 + } + }); + + retailcrm.assignDialog(1, { + manager_id: 1 + }).then(function (value) { + chai.expect(value).to.be.an('object'); + }); + }); + + it('Assign dialog incorrect', function () { + chai.expect(retailcrm.assignDialog.bind(retailcrm)).to.throw('Body is not be empty'); + }); + + it('Close dialog', function () { + nock('https://api.example.com/api/bot/v1').delete('/dialogs/1/close').reply(200, {}); + + retailcrm.closeDialog(1).then(function (value) { + chai.expect(value).to.be.empty; + }); + }); + + it('Close dialog incorrect', function () { + chai.expect(retailcrm.closeDialog.bind(retailcrm)).to.throw('dialog_id is required'); + }); + + it('Send message', function () { + nock('https://api.example.com/api/bot/v1').post('/messages', { + chat_id: 1, + scope: 'public', + type: 'text', + content: 'tests message' + }).reply(200, { + message_id: 1 + }); + + retailcrm.sendMessage({ + chat_id: 1, + scope: 'public', + type: 'text', + content: 'tests message' + }).then(function (value) { + chai.expect(value).to.be.an('object'); + chai.expect(value).to.be.not.empty; + }); + }); + + it('Send message incorrect', function () { + chai.expect(retailcrm.sendMessage.bind(retailcrm)).to.throw('Body is not be empty'); + }); + + it('Get messages', function() { + nock('https://api.example.com/api/bot/v1').get('/messages').reply(200, [{ + id: 1, + chat_id: 1, + from: { + id: 1 + } + }]); + + retailcrm.getMessages().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.not.empty; + }); + }); + + it('Get empty messages', function () { + nock('https://api.example.com/api/bot/v1').get('/messages').reply(200, []); + + retailcrm.getMessages().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.empty; + }); + }); + + it('Delete message', function () { + nock('https://api.example.com/api/bot/v1').delete('/messages/1').reply(200, {}); + + retailcrm.deleteMessage(1).then(function (value) { + chai.expect(value).to.be.empty; + }); + }); + + it('Delete message incorrect', function () { + chai.expect(retailcrm.deleteMessage.bind(retailcrm)).to.throw('message_id is required'); + }); + + it('Edit message', function () { + nock('https://api.example.com/api/bot/v1').patch('/messages/1', { + content: 'tests message' + }).reply(200, {}); + + retailcrm.editMessage(1, { + content: 'tests message' + }).then(function (value) { + chai.expect(value).to.be.empty; + }); + }); + + it('Edit message incorrect', function () { + chai.expect(retailcrm.editMessage.bind(retailcrm)).to.throw('Body is not be empty'); + }); + + it('Get commands', function () { + nock('https://api.example.com/api/bot/v1').get('/my/commands').reply(200, [{ + id: 1, + name: 'Command name' + }]); + + retailcrm.getCommands().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.not.empty; + }); + }); + + it('Get empty commands', function () { + nock('https://api.example.com/api/bot/v1').get('/my/commands').reply(200, []); + + retailcrm.getCommands().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.empty; + }); + }); + + it('Edit command', function () { + nock('https://api.example.com/api/bot/v1').put('/my/commands/command', { + description: 'Desc', + name: 'name' + }).reply(200, {}); + + retailcrm.editCommand('command', { + description: 'Desc', + name: 'name' + }).then(function (value) { + chai.expect(value).to.be.empty; + }); + }); + + it('Edit command incorrect', function () { + chai.expect(retailcrm.editCommand.bind(retailcrm, 'command')).to.throw('Body is not be empty'); + chai.expect(retailcrm.editCommand.bind(retailcrm)).to.throw('Parameter command name is required'); + }); + + it('Delete command', function () { + nock('https://api.example.com/api/bot/v1').delete('/my/commands/command').reply(200, {}); + + retailcrm.deleteCommand('command').then(function (value) { + chai.expect(value).to.be.empty; + }); + }); + + it('Delete command incorrect', function () { + chai.expect(retailcrm.deleteCommand.bind(retailcrm)).to.throw('command_name is required'); + }); + + it('Update bot info', function () { + nock('https://api.example.com/api/bot/v1').patch('/my/info', { + avatar_url: 'http://tests.ru/avatar.png', + name: 'Bot' + }).reply(200, {}); + + retailcrm.info({ + avatar_url: 'http://tests.ru/avatar.png', + name: 'Bot' + }).then(function (value) { + chai.expect(value).to.be.empty; + }); + }); + + it('Update bot info incorrect', function () { + chai.expect(retailcrm.info.bind(retailcrm)).to.throw('Body is not be empty'); + }); + + it('Get users', function () { + nock('https://api.example.com/api/bot/v1').get('/users').reply(200, [{ + id: 1, + name: 'Username' + }]); + + retailcrm.getUsers().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.not.empty; + }); + }); + + it('Get empty users', function () { + nock('https://api.example.com/api/bot/v1').get('/users').reply(200, []); + + retailcrm.getUsers().then(function (value) { + chai.expect(value).to.be.an('array'); + chai.expect(value).to.be.empty; + }); + }); + + it('Get websocket url', function () { + var url = retailcrm.getWebsocketUrl(['message_new', 'message_updated']); + var expected = 'wss://api.example.com/api/bot/v1/ws?events=message_new,message_updated'; + + chai.assert.equal(url, expected); + }); + + it('Get websocket url incorrect', function () { + chai.expect(retailcrm.getWebsocketUrl.bind(retailcrm)).to.throw('Events is required'); + }); +});