Skip to content

Commit

Permalink
init commit
Browse files Browse the repository at this point in the history
  • Loading branch information
max-kut committed May 8, 2021
0 parents commit 4f1d678
Show file tree
Hide file tree
Showing 8 changed files with 327 additions and 0 deletions.
16 changes: 16 additions & 0 deletions .babelrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = api => {
api.cache.never()

const presets = [
[ '@babel/env', { modules: false } ],
]
const plugins = [
'@babel/plugin-proposal-class-properties',
]

if (process.env.NODE_ENV === 'testing') {
plugins.push('istanbul')
}

return { presets, plugins, comments: false }
}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/node_modules/
package-lock.json
dist/
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# amoCRM widget installer

Сервисный класс, который упаковывает директорию в zip-архив и загружает в аккаунт amoCRM. Если в amoCRM виджет не сущестует, он будет создан, иначе будет обновлен.

## Установка
```bash
$ npm install --save-dev @amopro/widget-installer
```

## Использование
```javascript
const path = require('path');
const WIDGET_DIR = path.resolve('widget');

// ...
// Здесь собирается виджет в папку "widget"
// ...

try {
const WidgetInstaller = require('@amopro/widget-installer');
const wi = new WidgetInstaller(
process.env.AMO_SUB_DOMAIN,
process.env.AMO_LOGIN,
process.env.AMO_PASSWORD,
WIDGET_DIR
);

await wi.upload();
console.log('Widget uploaded!');
// после загрузки можно удалить архив
const fse = require('fs-extra');
fse.removeSync(path.resolve('widget.zip'));
} catch (e) {
console.error('uploading error', e.toString());
throw e;
}

```
36 changes: 36 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "@amopro/widget-installer",
"version": "1.0.0",
"description": "Automatic installation of a widget in amoCRM",
"main": "dist/widget-installer.js",
"scripts": {
"build": "rollup --config rollup.config.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/max-kut/amocrm-widget-installer.git"
},
"keywords": [
"amocrm",
"widget"
],
"author": "Maksim Kutishchev",
"license": "MIT",
"bugs": {
"url": "https://github.com/max-kut/amocrm-widget-installer/issues"
},
"homepage": "https://github.com/max-kut/amocrm-widget-installer#readme",
"dependencies": {
"archiver": "^2.1.1",
"content-type": "^1.0.4",
"jsdom": "^16.4.0",
"lodash": "^4.17.19",
"request-promise": "^4.2.6"
},
"devDependencies": {
"@babel/core": "^7.11.6",
"@babel/plugin-proposal-class-properties": "^7.13.0",
"@babel/preset-env": "^7.11.5",
"@rollup/plugin-commonjs": "^11.1.0"
}
}
8 changes: 8 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
input: 'src/index.js',
output: {
file: 'dist/widget-installer.js',
format: 'cjs',
exports: 'default'
}
};
222 changes: 222 additions & 0 deletions src/WidgetInstaller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
const contentType = require('content-type');
const _get = require('lodash/get');
const request = require('request-promise');
const {JSDOM} = require("jsdom");


export default class WidgetInstaller {
/**
* @param {String} subDomain
* @param {String} login
* @param {String} password
* @param {String} widgetDirectory
* @param {String} redirectUri
* @param {String} defaultLocale
*/
constructor(subDomain, login, password, widgetDirectory, redirectUri = 'https://amocrm.ru/', defaultLocale = 'ru') {
this.login = login;
this.password = password;
this.domain = subDomain;
this.redirectUri = redirectUri;
this.widgetDirectory = widgetDirectory;
this.defaultLocale = defaultLocale;

this.accessToken = null;
this.refreshToken = null;

this.widget_code = null;
this.widget_uuid = null;

this._request = request.defaults({
headers: {
'User-Agent': 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:51.0) Gecko/20100101 Firefox/51.0',
'X-Requested-With': 'XMLHttpRequest',
'Referer': this._url('/settings/widgets/')
},
jar: true,
transform(body, response) {
const ct = contentType.parse(response.headers['content-type']);
if (ct.type === 'application/json' || ct.type === 'text/json') {
try {
return JSON.parse(body);
} catch (e) {
return body;
}
}

return body;
}
});
}

async upload() {
await this._createAuthenticatedSession();
const widgetName = this._getManifestLocalizedValue('widget.name');

await this._findWidgetData(widgetName);

if (this.widget_uuid === null) {
const {uuid} = await this._request.post(this._url(`/v3/clients/`), {
json: this._getWidgetData()
});
await this.uploadWidget(uuid);
} else {
const {uuid} = await this._request.patch(this._url(`/v3/clients/${this.widget_uuid}`), {
json: this._getWidgetData()
});
await this.uploadWidget(uuid);
}
}

_url(path) {
return `https://${this.domain}.amocrm.ru/` + path.replace(/^\/*(.*)/, '$1')
}

/**
* Create amocrm session
* @private
*/
async _createAuthenticatedSession() {
let csrfToken;
try {
// this request has 401 response
await this._request.get(this._url(`/`));
} catch (e) {
const {response} = e;
const dom = new JSDOM(response);
csrfToken = dom.window.document.querySelector('input[name="csrf_token"]').value;
}

const res = await this._request.post(this._url(`/oauth2/authorize`), {
headers: {
Referer: this._url(`/`)
},
json: {
csrf_token: csrfToken,
username: this.login,
password: this.password,
temporary_auth: 'N'
}
});

this.accessToken = res.access_token;
this.refreshToken = res.refresh_token;
}

async _findWidgetData(name) {
if (!name) {
throw new Error('name is required')
}

const {widgets} = await this._request.get(this._url(`/ajax/settings/widgets/category/own_integrations/1/`));

for (const type of Object.keys(widgets.own_integrations)) {
const integrations = widgets.own_integrations[type];
for (const code in integrations) if (integrations.hasOwnProperty(code)) {
if (integrations[code].type === 'widget' && name === integrations[code].name) {
this.widget_code = integrations[code].code;
this.widget_uuid = integrations[code].client.uuid;
return;
}
}
}

return null;
}

_getWidgetData() {
return {
name: {
en: this._getManifestLocalizedValue('widget.name', 'en') || "",
es: this._getManifestLocalizedValue('widget.name', 'es') || "",
pt: this._getManifestLocalizedValue('widget.name', 'pt') || "",
ru: this._getManifestLocalizedValue('widget.name', 'ru') || ""
},
description: {
en: this._getManifestLocalizedValue('widget.description', 'en') || "",
es: this._getManifestLocalizedValue('widget.description', 'es') || "",
pt: this._getManifestLocalizedValue('widget.description', 'pt') || "",
ru: this._getManifestLocalizedValue('widget.description', 'ru') || ""
},
redirect_uri: this.redirectUri,
scopes: ['crm', 'notifications'],
uuid: this.widget_uuid
};
}

_getManifest() {
if (typeof this._manifest === 'undefined') {
const manifestPath = path.resolve(this.widgetDirectory, 'manifest.json');
this._manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
}
return this._manifest;
}

/**
* Получает локализованное значение из манифеста
* @param {string} key - path in dot notation
* @param {string} locale
* @returns {*}
* @private
*/
_getManifestLocalizedValue(key, locale = null) {
locale = locale || this.defaultLocale;
if (typeof this._manifest_i18n === 'undefined' || typeof this._manifest_i18n[locale] === 'undefined') {
this._manifest_i18n = this._manifest_i18n || {};
try {
this._manifest_i18n[locale] = JSON.parse(
fs.readFileSync(path.resolve(this.widgetDirectory, `i18n/${locale}.json`), 'utf8')
)
} catch (e) {
return null;
}
}

return _get(this._manifest_i18n[locale], key, _get(this._getManifest(), key));
}

async uploadWidget(uuid) {
const archivePath = await WidgetInstaller.zipArchive(this.widgetDirectory);

const result = await this._request.post(this._url(`/ajax/widgets/${uuid}/widget/upload/?fileapi${Date.now()}`), {
formData: {
'widget': {
value: fs.createReadStream(archivePath),
options: {
filename: 'widget.zip',
contentType: 'application/x-zip-compressed'
}
},
'_widget': 'widget.zip',
}
});

return result;
}

static zipArchive(widgetFolder) {
return new Promise((resolve, reject) => {
const widgetPath = path.resolve('widget.zip');
const widgetFileStream = fs.createWriteStream(widgetPath);

const archive = archiver('zip', {
zlib: {level: 1}
});

widgetFileStream.on('close', () => {
resolve(widgetPath);
});

archive.on('error', (err) => {
reject(err);
});

archive.pipe(widgetFileStream);
archive.directory(path.resolve(widgetFolder), false);
archive.finalize();
});
}
}
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import WidgetInstaller from './WidgetInstaller';

export default WidgetInstaller;

0 comments on commit 4f1d678

Please sign in to comment.