@alexsab-ru/scripts 0.14.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,312 +0,0 @@
1
- /**
2
- * Модуль для отправки данных в CallTouch API
3
- * @module calltouch-integration
4
- */
5
-
6
- /**
7
- * Проверяет и очищает значение от потенциально опасного содержимого
8
- * @param {*} value - Проверяемое значение
9
- * @return {string} Проверенное и очищенное значение или пустая строка
10
- */
11
- const getValueOrEmpty = (value) => {
12
- if (value === undefined || value === null || value === 'undefined') {
13
- return '';
14
- }
15
-
16
- const stringValue = String(value);
17
- // Базовая очистка от потенциально опасного содержимого
18
- return stringValue
19
- .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
20
- .replace(/[<>]/g, '')
21
- .trim();
22
- };
23
-
24
- /**
25
- * Валидация телефонного номера
26
- * @param {string} phone - Номер телефона для валидации
27
- * @return {boolean} true если номер валиден
28
- */
29
- const validatePhone = (phone) => {
30
- if (!phone || typeof phone !== 'string') return false;
31
-
32
- // Удаляем все символы кроме цифр
33
- const cleanPhone = phone.replace(/\D/g, '');
34
-
35
- // Проверяем длину (российские номера)
36
- return cleanPhone.length >= 10 && cleanPhone.length <= 11;
37
- };
38
-
39
- /**
40
- * Валидация email адреса
41
- * @param {string} email - Email для валидации
42
- * @return {boolean} true если email валиден
43
- */
44
- const validateEmail = (email) => {
45
- if (!email || typeof email !== 'string') return false;
46
-
47
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
48
- return emailRegex.test(email) && email.length <= 254;
49
- };
50
-
51
- /**
52
- * Обрабатывает данные события и формирует объект для отправки в CallTouch
53
- * @param {Object} eventData - Данные о событии
54
- * @param {string} sessionId - ID сессии
55
- * @return {Object} Объект с данными и комментарием для отправки
56
- */
57
- function processEventData(eventData, sessionId) {
58
- if (!eventData || typeof eventData !== 'object') {
59
- throw new Error('Event data must be an object');
60
- }
61
-
62
- // Основные поля для передачи в API
63
- const basicFields = {
64
- name: { key: 'fio', label: null },
65
- phone: { key: 'phoneNumber', label: null },
66
- email: { key: 'email', label: null },
67
- form: { key: 'subject', label: null }
68
- };
69
-
70
- // Поля для включения в комментарий
71
- const commentFields = {
72
- dealer: { label: 'Дилер' },
73
- dealershipName: { label: 'ДЦ' },
74
- salon: { label: 'Салон' },
75
- vehicleNameplate: { label: 'Авто' },
76
- priceDealership: { label: 'Цена' },
77
- vehicleModel: { label: 'Модель' },
78
- vehicleBrand: { label: 'Марка' },
79
- service: { label: 'Услуга' },
80
- source: { label: 'Источник' },
81
- medium: { label: 'Канал' },
82
- campaign: { label: 'Кампания' }
83
- };
84
-
85
- // Собираем основные данные
86
- const ct_data = {
87
- requestUrl: location.href,
88
- sessionId: sessionId || ''
89
- };
90
-
91
- // Обрабатываем основные поля с валидацией
92
- Object.entries(basicFields).forEach(([fieldName, config]) => {
93
- const value = getValueOrEmpty(eventData[fieldName]);
94
- if (value) {
95
- // Дополнительная валидация для специфичных полей
96
- if (fieldName === 'phone' && !validatePhone(value)) {
97
- console.warn(`Invalid phone number: ${value}`);
98
- return;
99
- }
100
- if (fieldName === 'email' && !validateEmail(value)) {
101
- console.warn(`Invalid email: ${value}`);
102
- return;
103
- }
104
- ct_data[config.key] = value;
105
- }
106
- });
107
-
108
- // Формируем комментарий из всех доступных полей
109
- const ct_comment = [];
110
-
111
- Object.entries(commentFields).forEach(([fieldName, config]) => {
112
- const value = getValueOrEmpty(eventData[fieldName]);
113
- if (value && value.length <= 100) { // Ограничиваем длину значений
114
- ct_comment.push(`${config.label}: ${value}`);
115
- }
116
- });
117
-
118
- // Добавляем комментарий в объект данных, если он не пустой
119
- if (ct_comment.length > 0) {
120
- ct_data.comment = ct_comment.join(', ').substring(0, 500); // Ограничиваем общую длину
121
- }
122
-
123
- return ct_data;
124
- }
125
-
126
- /**
127
- * Создает XMLHttpRequest с таймаутом
128
- * @param {string} url - URL для запроса
129
- * @param {string} postData - Данные для отправки
130
- * @param {number} timeout - Таймаут в миллисекундах
131
- * @return {Promise} Promise с результатом запроса
132
- */
133
- function createTimeoutRequest(url, postData, timeout = 10000) {
134
- return new Promise((resolve, reject) => {
135
- const request = new XMLHttpRequest();
136
- let timeoutId;
137
-
138
- // Устанавливаем таймаут
139
- timeoutId = setTimeout(() => {
140
- request.abort();
141
- reject(new Error('Request timeout'));
142
- }, timeout);
143
-
144
- request.open("POST", url, true);
145
- request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
146
-
147
- request.onload = function() {
148
- clearTimeout(timeoutId);
149
- if (this.status >= 200 && this.status < 400) {
150
- try {
151
- const response = JSON.parse(this.responseText);
152
- resolve({
153
- status: 'success',
154
- response: response,
155
- rawResponse: this.responseText
156
- });
157
- } catch (parseError) {
158
- resolve({
159
- status: 'success',
160
- response: this.responseText,
161
- rawResponse: this.responseText
162
- });
163
- }
164
- } else {
165
- reject(new Error(`API returned status: ${this.status}`));
166
- }
167
- };
168
-
169
- request.onerror = function() {
170
- clearTimeout(timeoutId);
171
- reject(new Error('Connection error'));
172
- };
173
-
174
- request.onabort = function() {
175
- clearTimeout(timeoutId);
176
- reject(new Error('Request aborted'));
177
- };
178
-
179
- request.send(postData);
180
- });
181
- }
182
-
183
- /**
184
- * Отправляет данные лида в CallTouch API
185
- * @param {Object} options - Параметры для отправки
186
- * @param {string} options.siteId - ID сайта в CallTouch
187
- * @param {string} options.eventCategory - Категория события (например, 'Lead')
188
- * @param {Object} options.eventProperties - Данные о событии
189
- * @param {string} [options.eventProperties.name] - Имя клиента
190
- * @param {string} options.eventProperties.phone - Телефон клиента
191
- * @param {string} [options.eventProperties.email] - Email клиента
192
- * @param {string} [options.eventProperties.form] - Название формы
193
- * @param {string} [options.eventProperties.dealershipName] - Название дилерского центра
194
- * @param {string} [options.eventProperties.salon] - Название салона
195
- * @param {string} [options.eventProperties.vehicleNameplate] - Название автомобиля
196
- * @param {string} [options.eventProperties.priceDealership] - Цена автомобиля
197
- * @param {string} [options.sessionId] - ID сессии (опционально, по умолчанию window.call_value)
198
- * @param {number} [options.timeout] - Таймаут запроса в миллисекундах (по умолчанию 10000)
199
- * @return {Promise} Promise с результатом запроса
200
- */
201
- function sendToCallTouch(options) {
202
- return new Promise((resolve, reject) => {
203
- try {
204
- // Валидация входных параметров
205
- if (!options || typeof options !== 'object') {
206
- return reject(new Error('Options object is required'));
207
- }
208
-
209
- const {
210
- siteId,
211
- eventCategory,
212
- eventProperties,
213
- sessionId = window.call_value,
214
- timeout = 10000
215
- } = options;
216
-
217
- // Проверяем, что категория события - Lead
218
- if (eventCategory !== 'Lead') {
219
- return resolve({ status: 'skipped', message: 'Not a Lead event' });
220
- }
221
-
222
- // Проверяем обязательные параметры
223
- if (!siteId || typeof siteId !== 'string') {
224
- return reject(new Error('Valid siteId is required'));
225
- }
226
-
227
- if (!eventProperties || typeof eventProperties !== 'object') {
228
- return reject(new Error('Event properties object is required'));
229
- }
230
-
231
- // Обрабатываем данные с помощью оптимизированной функции
232
- let ct_data;
233
- try {
234
- ct_data = processEventData(eventProperties, sessionId);
235
- } catch (processError) {
236
- return reject(new Error(`Data processing error: ${processError.message}`));
237
- }
238
-
239
- // Проверяем наличие телефона
240
- if (!ct_data.phoneNumber) {
241
- return resolve({ status: 'error', message: 'Phone number is required' });
242
- }
243
-
244
- // Формируем строку параметров для POST-запроса
245
- const post_data = Object.keys(ct_data)
246
- .filter(key => ct_data[key] && ct_data[key] !== '') // Исключаем пустые поля
247
- .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(ct_data[key])}`)
248
- .join('&');
249
-
250
- if (!post_data) {
251
- return reject(new Error('No valid data to send'));
252
- }
253
-
254
- // URL для запроса к API CallTouch
255
- const CT_URL = `https://api.calltouch.ru/calls-service/RestAPI/requests/${encodeURIComponent(siteId)}/register/`;
256
-
257
- // Проверяем, не отправляется ли уже запрос (защита от дублирования)
258
- if (window.ct_snd_flag) {
259
- return resolve({ status: 'skipped', message: 'Another request is in progress' });
260
- }
261
-
262
- // Устанавливаем флаг для предотвращения дублирования запросов
263
- window.ct_snd_flag = 1;
264
- const flagTimeout = setTimeout(() => {
265
- window.ct_snd_flag = 0;
266
- }, Math.max(timeout + 5000, 25000)); // Сбрасываем флаг через таймаут + буфер
267
-
268
- // Отправляем запрос с таймаутом
269
- createTimeoutRequest(CT_URL, post_data, timeout)
270
- .then(result => {
271
- clearTimeout(flagTimeout);
272
- window.ct_snd_flag = 0;
273
- resolve({
274
- ...result,
275
- data: ct_data
276
- });
277
- })
278
- .catch(error => {
279
- clearTimeout(flagTimeout);
280
- window.ct_snd_flag = 0;
281
- reject(error);
282
- });
283
-
284
- } catch (error) {
285
- console.error('CallTouch error:', error);
286
- reject(error);
287
- }
288
- });
289
- }
290
-
291
- // Экспорт функций для использования в других модулях
292
- export { sendToCallTouch, getValueOrEmpty, validatePhone, validateEmail };
293
-
294
- // Для совместимости с CommonJS
295
- if (typeof module !== 'undefined' && module.exports) {
296
- module.exports = {
297
- sendToCallTouch,
298
- getValueOrEmpty,
299
- validatePhone,
300
- validateEmail
301
- };
302
- }
303
-
304
- // Для использования через тег <script>
305
- if (typeof window !== 'undefined') {
306
- window.CallTouchAPI = {
307
- sendToCallTouch,
308
- getValueOrEmpty,
309
- validatePhone,
310
- validateEmail
311
- };
312
- }
@@ -1,176 +0,0 @@
1
- /**
2
- * Создает запрос на обратный звонок через CallTouch Widget API
3
- * @param {string} routeKey - Ключ маршрута виджета
4
- * @param {string} phoneValue - Номер телефона клиента
5
- * @param {string} nameValue - Имя клиента (опционально)
6
- * @param {boolean} verbose - Включить подробное логирование
7
- * @param {number} timeout - Таймаут для операций (по умолчанию 15000ms)
8
- * @return {Promise} Promise с результатом создания запроса
9
- */
10
- export const createRequest = (routeKey, phoneValue, nameValue = '', verbose = false, timeout = 15000) => {
11
- return new Promise((resolve, reject) => {
12
- // Валидация входных параметров
13
- if (!routeKey || typeof routeKey !== 'string') {
14
- return reject(new Error('Route key is required and must be a string'));
15
- }
16
-
17
- if (!phoneValue || typeof phoneValue !== 'string') {
18
- return reject(new Error('Phone value is required and must be a string'));
19
- }
20
-
21
- // Проверяем доступность CallTouch Widget API
22
- if (typeof window === 'undefined' || !window.ctw) {
23
- return reject(new Error('CallTouch Widget API is not available (window.ctw is not defined)'));
24
- }
25
-
26
- let timeoutId;
27
- let isResolved = false;
28
-
29
- // Устанавливаем общий таймаут для всей операции
30
- timeoutId = setTimeout(() => {
31
- if (!isResolved) {
32
- isResolved = true;
33
- reject(new Error(`Operation timeout after ${timeout}ms`));
34
- }
35
- }, timeout);
36
-
37
- const cleanup = () => {
38
- if (timeoutId) {
39
- clearTimeout(timeoutId);
40
- timeoutId = null;
41
- }
42
- };
43
-
44
- try {
45
- // Получаем данные маршрута
46
- window.ctw.getRouteKeyData(routeKey, function(success, data) {
47
- if (isResolved) return;
48
-
49
- if (verbose) {
50
- console.log('getRouteKeyData result:', { success, data });
51
- }
52
-
53
- if (!success) {
54
- cleanup();
55
- isResolved = true;
56
- const errorMsg = data && data.message ? data.message : 'Failed to get route key data';
57
- reject(new Error(`Route key data error: ${errorMsg}`));
58
- return;
59
- }
60
-
61
- // Проверяем наличие виджета
62
- if (!data || !data.widgetFound) {
63
- cleanup();
64
- isResolved = true;
65
- reject(new Error(`Widget not found for route key: ${routeKey}, or callback service is not active`));
66
- return;
67
- }
68
-
69
- // Проверяем настройки виджета
70
- const widgetData = data.widgetData || {};
71
-
72
- if (verbose) {
73
- if (widgetData.callCenterWorkingMode === 'working_hours') {
74
- console.log('Call center is working, displaying widget');
75
- } else {
76
- if (widgetData.collectNonWorkingRequests) {
77
- console.log('Call center is not working, but can display non-working hours form');
78
- } else {
79
- console.log('Call center is not working, not collecting non-working hours requests');
80
- }
81
- }
82
- }
83
-
84
- // Нормализуем номер телефона
85
- let phone_ct = phoneValue.replace(/[^0-9]/g, '');
86
-
87
- // Убираем префиксы и добавляем российский код
88
- if (phone_ct.startsWith('8')) {
89
- phone_ct = phone_ct.substring(1);
90
- }
91
- if (phone_ct.startsWith('7')) {
92
- phone_ct = phone_ct.substring(1);
93
- }
94
- phone_ct = '7' + phone_ct;
95
-
96
- // Валидируем нормализованный номер
97
- if (phone_ct.length !== 11 || !phone_ct.startsWith('7')) {
98
- cleanup();
99
- isResolved = true;
100
- reject(new Error('Invalid phone number format after normalization'));
101
- return;
102
- }
103
-
104
- // Подготавливаем дополнительные поля
105
- const additionalFields = [];
106
- if (nameValue && nameValue.trim().length > 0) {
107
- additionalFields.push({
108
- "name": "Name",
109
- "value": nameValue.trim().substring(0, 100) // Ограничиваем длину
110
- });
111
- }
112
-
113
- // Создаем запрос на обратный звонок
114
- window.ctw.createRequest(
115
- routeKey,
116
- phone_ct,
117
- additionalFields,
118
- function (success, requestData) {
119
- if (isResolved) return;
120
-
121
- cleanup();
122
- isResolved = true;
123
-
124
- if (verbose) {
125
- console.log('createRequest result:', { success, requestData });
126
- }
127
-
128
- if (success) {
129
- if (verbose && requestData && requestData.callbackRequestId) {
130
- console.log('Callback request created with ID:', requestData.callbackRequestId);
131
- }
132
- resolve(requestData || { success: true });
133
- } else {
134
- let errorText = 'Error in createRequest';
135
-
136
- if (requestData && requestData.type) {
137
- switch (requestData.type) {
138
- case "request_throttle_timeout":
139
- case "request_throttle_count":
140
- errorText = 'Request limit reached, please try again later';
141
- break;
142
- case "request_phone_blacklisted":
143
- errorText = 'Phone number is blacklisted';
144
- break;
145
- case "validation_error":
146
- errorText = 'Invalid data provided';
147
- break;
148
- case "widget_not_found":
149
- errorText = 'Widget configuration not found';
150
- break;
151
- case "service_unavailable":
152
- errorText = 'Callback service is temporarily unavailable';
153
- break;
154
- default:
155
- errorText = `Request failed: ${requestData.type}`;
156
- }
157
-
158
- if (verbose) {
159
- console.log('CallTouch error:', errorText);
160
- }
161
- }
162
-
163
- reject(new Error(errorText));
164
- }
165
- }
166
- );
167
- });
168
- } catch (error) {
169
- cleanup();
170
- if (!isResolved) {
171
- isResolved = true;
172
- reject(new Error(`Unexpected error: ${error.message}`));
173
- }
174
- }
175
- });
176
- };