Массовая рассылка сообщений покупателям в Ozon Messenger
Size
143.0 KB
Version
1.1.165
Created
Jan 30, 2026
Updated
2 months ago
1// ==UserScript==
2// @name Ozon BASE Messenger Bulk Sender
3// @description Массовая рассылка сообщений покупателям в Ozon Messenger
4// @version 1.1.165
5// @match *://seller.ozon.ru/*
6// @grant GM.getValue
7// @grant GM.setValue
8// @grant GM.openInTab
9// ==/UserScript==
10(function() {
11 'use strict';
12
13 console.log('Ozon Messenger Bulk Sender загружен - ВЕРСИЯ 1.1.6');
14
15 // Глобальные переменные для управления процессом
16 let isPaused = false;
17 let shouldStop = false;
18
19 // ============= ИНИЦИАЛИЗАЦИЯ INDEXEDDB =============
20
21 let db = null;
22 const DB_NAME = 'OzonMessengerDB';
23 const DB_VERSION = 2;
24 const STORE_NAME = 'storage';
25
26 // Инициализация IndexedDB
27 async function initIndexedDB() {
28 return new Promise((resolve, reject) => {
29 try {
30 console.log('Попытка открытия IndexedDB:', DB_NAME);
31
32 // Проверяем доступность IndexedDB
33 if (!window.indexedDB) {
34 const error = new Error('IndexedDB не поддерживается в этом браузере');
35 console.error(error.message);
36 reject(error);
37 return;
38 }
39
40 const request = indexedDB.open(DB_NAME, DB_VERSION);
41
42 request.onerror = () => {
43 const errorMsg = request.error ?
44 `${request.error.name}: ${request.error.message}` :
45 'Неизвестная ошибка IndexedDB';
46 console.error('Ошибка открытия IndexedDB:', errorMsg);
47 console.error('Полный объект ошибки:', request.error);
48 reject(new Error(errorMsg));
49 };
50
51 request.onsuccess = () => {
52 db = request.result;
53 console.log('✅ IndexedDB успешно инициализирована');
54
55 // Обработчик ошибок базы данных
56 db.onerror = (event) => {
57 console.error('Ошибка базы данных:', event);
58 };
59
60 // Обработчик закрытия базы данных
61 db.onclose = () => {
62 console.warn('База данных была закрыта');
63 db = null;
64 };
65
66 resolve(db);
67 };
68
69 request.onupgradeneeded = (event) => {
70 console.log('Обновление структуры IndexedDB...');
71 const database = event.target.result;
72
73 // Создаем хранилище, если его нет
74 if (!database.objectStoreNames.contains(STORE_NAME)) {
75 database.createObjectStore(STORE_NAME, { keyPath: 'key' });
76 console.log('✅ Создано хранилище IndexedDB:', STORE_NAME);
77 }
78 };
79
80 request.onblocked = () => {
81 console.warn('⚠️ IndexedDB заблокирована. Закройте другие вкладки с этим сайтом.');
82 reject(new Error('IndexedDB заблокирована другой вкладкой'));
83 };
84 } catch (error) {
85 console.error('Критическая ошибка при инициализации IndexedDB:', error);
86 console.error('error.name:', error.name);
87 console.error('error.message:', error.message);
88 console.error('error.stack:', error.stack);
89 reject(error);
90 }
91 });
92 }
93
94 // Сохранение значения в IndexedDB
95 async function setDBValue(key, value) {
96 if (!db) {
97 console.log('База данных не инициализирована, инициализируем...');
98 await initIndexedDB();
99 }
100
101 return new Promise((resolve, reject) => {
102 try {
103 console.log('Сохранение в IndexedDB, ключ:', key, 'размер данных:', JSON.stringify(value).length, 'байт');
104 const transaction = db.transaction([STORE_NAME], 'readwrite');
105
106 transaction.onerror = (event) => {
107 console.error('Ошибка транзакции при сохранении:', event);
108 console.error('transaction.error:', transaction.error);
109 reject(new Error(`Transaction error: ${transaction.error?.name} - ${transaction.error?.message}`));
110 };
111
112 transaction.oncomplete = () => {
113 console.log('Транзакция сохранения завершена успешно');
114 };
115
116 const objectStore = transaction.objectStore(STORE_NAME);
117 const request = objectStore.put({ key: key, value: value });
118
119 request.onsuccess = () => {
120 console.log('✅ Значение сохранено в IndexedDB:', key);
121 resolve();
122 };
123
124 request.onerror = (event) => {
125 console.error('Ошибка сохранения в IndexedDB:', event);
126 console.error('request.error:', request.error);
127 console.error('request.error.name:', request.error?.name);
128 console.error('request.error.message:', request.error?.message);
129 reject(new Error(`Put error: ${request.error?.name} - ${request.error?.message}`));
130 };
131 } catch (error) {
132 console.error('Ошибка при создании транзакции IndexedDB:', error);
133 console.error('error.name:', error.name);
134 console.error('error.message:', error.message);
135 console.error('error.stack:', error.stack);
136 reject(error);
137 }
138 });
139 }
140
141 // Получение значения из IndexedDB
142 async function getDBValue(key, defaultValue = null) {
143 if (!db) {
144 console.log('База данных не инициализирована, инициализируем...');
145 await initIndexedDB();
146 }
147
148 return new Promise((resolve, reject) => {
149 try {
150 console.log('Чтение из IndexedDB, ключ:', key);
151 const transaction = db.transaction([STORE_NAME], 'readonly');
152
153 transaction.onerror = (event) => {
154 console.error('Ошибка транзакции при чтении:', event);
155 console.error('transaction.error:', transaction.error);
156 reject(new Error(`Transaction error: ${transaction.error?.name} - ${transaction.error?.message}`));
157 };
158
159 const objectStore = transaction.objectStore(STORE_NAME);
160 const request = objectStore.get(key);
161
162 request.onsuccess = () => {
163 if (request.result) {
164 console.log('✅ Значение получено из IndexedDB:', key);
165 resolve(request.result.value);
166 } else {
167 console.log('⚠️ Значение не найдено в IndexedDB, возвращаем default:', key);
168 resolve(defaultValue);
169 }
170 };
171
172 request.onerror = (event) => {
173 console.error('Ошибка чтения из IndexedDB:', event);
174 console.error('request.error:', request.error);
175 console.error('request.error.name:', request.error?.name);
176 console.error('request.error.message:', request.error?.message);
177 reject(new Error(`Get error: ${request.error?.name} - ${request.error?.message}`));
178 };
179 } catch (error) {
180 console.error('Ошибка при создании транзакции IndexedDB:', error);
181 console.error('error.name:', error.name);
182 console.error('error.message:', error.message);
183 console.error('error.stack:', error.stack);
184 reject(error);
185 }
186 });
187 }
188
189 // Удаление значения из IndexedDB
190 async function deleteDBValue(key) {
191 if (!db) {
192 console.log('База данных не инициализирована, инициализируем...');
193 await initIndexedDB();
194 }
195
196 return new Promise((resolve, reject) => {
197 try {
198 console.log('Удаление из IndexedDB, ключ:', key);
199 const transaction = db.transaction([STORE_NAME], 'readwrite');
200
201 transaction.onerror = (event) => {
202 console.error('Ошибка транзакции при удалении:', event);
203 console.error('transaction.error:', transaction.error);
204 reject(new Error(`Transaction error: ${transaction.error?.name} - ${transaction.error?.message}`));
205 };
206
207 const objectStore = transaction.objectStore(STORE_NAME);
208 const request = objectStore.delete(key);
209
210 request.onsuccess = () => {
211 console.log('✅ Значение удалено из IndexedDB:', key);
212 resolve();
213 };
214
215 request.onerror = (event) => {
216 console.error('Ошибка удаления из IndexedDB:', event);
217 console.error('request.error:', request.error);
218 console.error('request.error.name:', request.error?.name);
219 console.error('request.error.message:', request.error?.message);
220 reject(new Error(`Delete error: ${request.error?.name} - ${request.error?.message}`));
221 };
222 } catch (error) {
223 console.error('Ошибка при создании транзакции IndexedDB:', error);
224 console.error('error.name:', error.name);
225 console.error('error.message:', error.message);
226 console.error('error.stack:', error.stack);
227 reject(error);
228 }
229 });
230 }
231
232 // ============= ФУНКЦИИ ДЛЯ РАБОТЫ С БАЗОЙ КОНТАКТОВ =============
233
234 // Загрузка базы контактов
235 async function loadContactsDatabase() {
236 try {
237 const database = await getDBValue('contactsDatabase', {
238 contacts: {},
239 lastUpdate: null
240 });
241 console.log('База контактов загружена:', Object.keys(database.contacts).length, 'контактов');
242 return database;
243 } catch (error) {
244 console.error('Ошибка загрузки базы контактов:', error);
245 return {
246 contacts: {},
247 lastUpdate: null
248 };
249 }
250 }
251
252 // Сохранение базы контактов
253 async function saveContactsDatabase(database) {
254 try {
255 database.lastUpdate = new Date().toISOString();
256 await setDBValue('contactsDatabase', database);
257 console.log('✅ База контактов сохранена:', Object.keys(database.contacts).length, 'контактов');
258 return true;
259 } catch (error) {
260 console.error('❌ Ошибка сохранения базы контактов:', error);
261 return false;
262 }
263 }
264
265 // Добавление контакта в базу
266 function addContactToDatabase(database, chatId, contactData) {
267 database.contacts[chatId] = {
268 id: chatId,
269 name: contactData.name,
270 lastMessageDate: contactData.lastMessageDate,
271 lastSentDate: contactData.lastSentDate,
272 messageCount: contactData.messageCount || 0
273 };
274 }
275
276 // Получение статистики базы данных
277 function getDatabaseStats(database) {
278 const contacts = Object.values(database.contacts);
279 const total = contacts.length;
280 const withDates = contacts.filter(c => c.lastMessageDate).length;
281 const sent = contacts.filter(c => c.lastSentDate).length;
282 const notSent = total - sent;
283
284 return {
285 total,
286 withDates,
287 sent,
288 notSent,
289 lastUpdate: database.lastUpdate
290 };
291 }
292
293 // Очистка базы контактов
294 async function clearContactsDatabase() {
295 if (confirm('Вы уверены, что хотите очистить всю базу контактов? Это действие нельзя отменить.')) {
296 try {
297 await setDBValue('contactsDatabase', {
298 contacts: {},
299 lastUpdate: null
300 });
301 console.log('✅ База контактов очищена');
302 return true;
303 } catch (error) {
304 console.error('❌ Ошибка очистки базы контактов:', error);
305 return false;
306 }
307 }
308 return false;
309 }
310
311 // Удаление дубликатов из базы
312 async function removeDuplicatesFromDatabase() {
313 const database = await loadContactsDatabase();
314 const contacts = Object.values(database.contacts);
315
316 // Группируем по имени
317 const grouped = {};
318 contacts.forEach(contact => {
319 if (!grouped[contact.name]) {
320 grouped[contact.name] = [];
321 }
322 grouped[contact.name].push(contact);
323 });
324
325 // Находим дубликаты
326 let duplicatesCount = 0;
327 Object.keys(grouped).forEach(name => {
328 const group = grouped[name];
329 if (group.length > 1) {
330 // Оставляем только первый контакт
331 for (let i = 1; i < group.length; i++) {
332 delete database.contacts[group[i].id];
333 duplicatesCount++;
334 }
335 }
336 });
337
338 if (duplicatesCount > 0) {
339 await saveContactsDatabase(database);
340 alert(`Удалено дубликатов: ${duplicatesCount}`);
341 } else {
342 alert('Дубликаты не найдены');
343 }
344 }
345
346 // ============= ФУНКЦИИ ДЛЯ РАБОТЫ С СОСТОЯНИЕМ РАССЫЛКИ =============
347
348 // Загрузка состояния рассылки
349 async function loadSendingState() {
350 try {
351 const state = await getDBValue('sendingState', null);
352 if (state) {
353 console.log('Состояние рассылки загружено:', state);
354 }
355 return state;
356 } catch (error) {
357 console.error('Ошибка загрузки состояния рассылки:', error);
358 return null;
359 }
360 }
361
362 // Сохранение состояния рассылки
363 async function saveSendingState(state) {
364 try {
365 await setDBValue('sendingState', state);
366 console.log('✅ Состояние рассылки сохранено');
367 return true;
368 } catch (error) {
369 console.error('❌ Ошибка сохранения состояния рассылки:', error);
370 return false;
371 }
372 }
373
374 // Очистка состояния рассылки
375 async function clearSendingState() {
376 try {
377 await deleteDBValue('sendingState');
378 console.log('✅ Состояние рассылки очищено');
379 return true;
380 } catch (error) {
381 console.error('❌ Ошибка очистки состояния рассылки:', error);
382 return false;
383 }
384 }
385
386 // ============= ФУНКЦИИ ДЛЯ РАБОТЫ С ЧЕРНЫМ СПИСКОМ =============
387
388 // Загрузка черного списка
389 async function loadBlacklist() {
390 try {
391 const blacklist = await getDBValue('blacklist', []);
392 console.log('Черный список загружен:', blacklist.length, 'ID');
393 return blacklist;
394 } catch (error) {
395 console.error('Ошибка загрузки черного списка:', error);
396 return [];
397 }
398 }
399
400 // Сохранение черного списка
401 async function saveBlacklist(blacklist) {
402 try {
403 await setDBValue('blacklist', blacklist);
404 console.log('✅ Черный список сохранен:', blacklist.length, 'ID');
405 return true;
406 } catch (error) {
407 console.error('❌ Ошибка сохранения черного списка:', error);
408 return false;
409 }
410 }
411
412 // Добавление ID в черный список
413 async function addToBlacklist(chatIds) {
414 const blacklist = await loadBlacklist();
415 const db = await loadContactsDatabase();
416
417 let addedCount = 0;
418 let removedFromDbCount = 0;
419
420 for (const chatId of chatIds) {
421 // Проверяем, нет ли уже в черном списке
422 if (!blacklist.includes(chatId)) {
423 blacklist.push(chatId);
424 addedCount++;
425 console.log('Добавлен в ЧС:', chatId);
426 }
427
428 // Удаляем из базы контактов, если есть
429 if (db.contacts[chatId]) {
430 delete db.contacts[chatId];
431 removedFromDbCount++;
432 console.log('Удален из базы:', chatId);
433 }
434 }
435
436 await saveBlacklist(blacklist);
437 await saveContactsDatabase(db);
438
439 return { addedCount, removedFromDbCount };
440 }
441
442 // Удаление ID из черного списка
443 async function removeFromBlacklist(chatIds) {
444 const blacklist = await loadBlacklist();
445 const db = await loadContactsDatabase();
446
447 let removedCount = 0;
448 let addedToDbCount = 0;
449
450 for (const chatId of chatIds) {
451 const index = blacklist.indexOf(chatId);
452 if (index > -1) {
453 blacklist.splice(index, 1);
454 removedCount++;
455 console.log('Удален из ЧС:', chatId);
456
457 // Возвращаем в базу контактов
458 if (!db.contacts[chatId]) {
459 addContactToDatabase(db, chatId, {
460 name: 'Восстановлен из ЧС',
461 lastMessageDate: null,
462 lastSentDate: null,
463 messageCount: 0
464 });
465 addedToDbCount++;
466 console.log('Возвращен в базу:', chatId);
467 }
468 }
469 }
470
471 await saveBlacklist(blacklist);
472 await saveContactsDatabase(db);
473
474 return { removedCount, addedToDbCount };
475 }
476
477 // Проверка, находится ли ID в черном списке
478 async function isInBlacklist(chatId) {
479 const blacklist = await loadBlacklist();
480 return blacklist.includes(chatId);
481 }
482
483 // Очистка черного списка
484 async function clearBlacklist() {
485 const blacklist = await loadBlacklist();
486 const count = blacklist.length;
487
488 if (count === 0) {
489 alert('Черный список уже пуст');
490 return { count: 0 };
491 }
492
493 if (confirm(`Вы уверены, что хотите очистить весь черный список? (${count} ID)\n\nВнимание: ID будут возвращены в базу контактов.`)) {
494 // Возвращаем все ID в базу
495 const result = await removeFromBlacklist(blacklist);
496 console.log('Черный список очищен');
497 return result;
498 }
499
500 return { count: 0 };
501 }
502
503 // Массовое добавление видимых чатов в черный список
504 async function collectChatsToBlacklist() {
505 console.log('=== НАЧАЛО МАССОВОГО ДОБАВЛЕНИЯ В ЧЕРНЫЙ СПИСОК ===');
506
507 const chatIdsToAdd = [];
508 let scrollAttempts = 0;
509 let noNewChatsCount = 0;
510 const maxNoNewChatsAttempts = 3;
511
512 // Трекер обработанных ID в этой сессии
513 const processedInSession = new Set();
514
515 while (!shouldStop) {
516 // Проверка на паузу
517 while (isPaused && !shouldStop) {
518 updateStatus('Пауза сбора ЧС', `Собрано: ${chatIdsToAdd.length} ID`);
519 await sleep(500);
520 }
521
522 if (shouldStop) {
523 console.log('Сбор ЧС остановлен пользователем');
524 break;
525 }
526
527 // Получаем текущие видимые чаты
528 const currentChats = getAllChats();
529 console.log(`Попытка ${scrollAttempts + 1}: найдено ${currentChats.length} чатов в DOM`);
530
531 // Фильтруем только те чаты, которые еще не обработали
532 const newChats = currentChats.filter(chat => {
533 const chatId = extractChatId(chat);
534 return chatId && !processedInSession.has(chatId);
535 });
536
537 console.log('Новых необработанных чатов:', newChats.length);
538
539 // Если нет новых чатов - увеличиваем счетчик
540 if (newChats.length === 0) {
541 noNewChatsCount++;
542 console.log(`Новые чаты не появились (${noNewChatsCount}/${maxNoNewChatsAttempts})`);
543
544 if (noNewChatsCount >= maxNoNewChatsAttempts) {
545 console.log('Достигнут конец списка чатов');
546 break;
547 }
548
549 // Скроллим для подгрузки новых
550 console.log('Скроллим для подгрузки новых чатов...');
551 await scrollChatList();
552 await sleep(5000);
553 continue;
554 } else {
555 noNewChatsCount = 0;
556 }
557
558 // Обрабатываем новые чаты
559 for (const chat of newChats) {
560 // Проверка на паузу
561 while (isPaused && !shouldStop) {
562 updateStatus('Пауза сбора ЧС', `Собрано: ${chatIdsToAdd.length} ID`);
563 await sleep(500);
564 }
565
566 if (shouldStop) break;
567
568 const chatId = extractChatId(chat);
569 const chatName = chat.querySelector('.index_chatTitle_TiXTq')?.textContent ||
570 chat.querySelector('.index_chatTitle_x9txX')?.textContent ||
571 'Неизвестно';
572
573 processedInSession.add(chatId);
574 chatIdsToAdd.push(chatId);
575
576 updateStatus(
577 `Сбор чатов для ЧС: ${chatName}`,
578 `Собрано: ${chatIdsToAdd.length} ID`
579 );
580
581 console.log(`[${chatIdsToAdd.length}] Добавлен ID: ${chatId} (${chatName})`);
582 }
583
584 // Скроллим для подгрузки следующей порции
585 if (!shouldStop && newChats.length > 0) {
586 await scrollChatList();
587 await sleep(5000);
588 }
589
590 scrollAttempts++;
591 }
592
593 console.log('=== СБОР ЗАВЕРШЕН ===');
594 console.log('Всего собрано ID:', chatIdsToAdd.length);
595
596 // Добавляем в черный список
597 if (chatIdsToAdd.length > 0) {
598 updateStatus('Добавление в черный список...', `Обработка ${chatIdsToAdd.length} ID...`);
599 const result = await addToBlacklist(chatIdsToAdd);
600
601 updateStatus(
602 'Добавление в ЧС завершено!',
603 `Добавлено в ЧС: ${result.addedCount} | Удалено из базы: ${result.removedFromDbCount}`
604 );
605
606 alert(`✅ Массовое добавление завершено!\n\nДобавлено в черный список: ${result.addedCount}\nУдалено из базы контактов: ${result.removedFromDbCount}`);
607 } else {
608 updateStatus('Сбор завершен', 'Не найдено чатов для добавления');
609 alert('Не найдено чатов для добавления в черный список');
610 }
611
612 // Сбрасываем флаги
613 shouldStop = false;
614
615 return chatIdsToAdd.length;
616 }
617
618 // Функция для создания модального окна черного списка
619 function createBlacklistModal() {
620 // Проверяем, не создано ли уже окно
621 if (document.getElementById('blacklistModal')) {
622 return;
623 }
624
625 const modalHTML = `
626 <div id="blacklistModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10002; justify-content: center; align-items: center;">
627 <div style="background: white; padding: 30px; border-radius: 12px; width: 700px; max-width: 95%; max-height: 90vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
628 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
629 <h2 style="margin: 0; color: #333; font-size: 24px;">🚫 Черный список</h2>
630 <button id="closeBlacklistButton" style="padding: 8px 16px; background: #999; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">Закрыть</button>
631 </div>
632
633 <div id="blacklistStatsBlock" style="margin-bottom: 20px; padding: 15px; background: #fff3f3; border-radius: 6px; border-left: 4px solid #f44336;">
634 <div style="font-weight: 600; color: #f44336; margin-bottom: 10px;">Статистика черного списка:</div>
635 <div id="blacklistStatsContent" style="color: #333; font-size: 14px;">Загрузка...</div>
636 </div>
637
638 <div style="margin-bottom: 20px;">
639 <label style="display: block; margin-bottom: 8px; color: #555; font-weight: 600;">Добавить ID вручную:</label>
640 <div style="display: flex; gap: 10px;">
641 <input type="text" id="manualBlacklistId" placeholder="Введите ID чата..." style="flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
642 <button id="addManualBlacklistButton" style="padding: 10px 18px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">➕ Добавить</button>
643 </div>
644 </div>
645
646 <div style="display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap;">
647 <button id="massAddBlacklistButton" style="padding: 10px 18px; background: #ff5722; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">📥 Массовое добавление</button>
648 <button id="clearAllBlacklistButton" style="padding: 10px 18px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">🗑️ Очистить все</button>
649 <button id="pauseBlacklistButton" style="display: none; padding: 10px 18px; background: #ff9800; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">⏸️ Пауза</button>
650 <button id="stopBlacklistButton" style="display: none; padding: 10px 18px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">⏹️ Стоп</button>
651 </div>
652
653 <div style="margin-bottom: 15px;">
654 <input type="text" id="searchBlacklistInput" placeholder="Поиск по ID..." style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
655 </div>
656
657 <div id="blacklistContainer" style="max-height: 400px; overflow-y: auto; border: 1px solid #ddd; border-radius: 6px;">
658 <div id="blacklistContent" style="padding: 10px;"></div>
659 </div>
660 </div>
661 </div>
662 `;
663
664 document.body.insertAdjacentHTML('beforeend', modalHTML);
665 console.log('Модальное окно черного списка создано');
666
667 // Обработчики событий
668 document.getElementById('closeBlacklistButton').addEventListener('click', closeBlacklistModal);
669
670 document.getElementById('addManualBlacklistButton').addEventListener('click', async () => {
671 const input = document.getElementById('manualBlacklistId');
672 const chatId = input.value.trim();
673
674 if (!chatId) {
675 alert('Введите ID чата');
676 return;
677 }
678
679 const result = await addToBlacklist([chatId]);
680 alert(`✅ ID добавлен в черный список!\n\nДобавлено в ЧС: ${result.addedCount}\nУдалено из базы: ${result.removedFromDbCount}`);
681
682 input.value = '';
683 await loadAndDisplayBlacklist();
684 await updateDatabaseStats();
685 });
686
687 document.getElementById('massAddBlacklistButton').addEventListener('click', async () => {
688 if (confirm('Начать массовое добавление видимых чатов в черный список?\n\nВсе видимые чаты будут добавлены в ЧС и удалены из базы контактов.')) {
689 // Показываем кнопки паузы и остановки
690 document.getElementById('massAddBlacklistButton').style.display = 'none';
691 document.getElementById('pauseBlacklistButton').style.display = 'inline-block';
692 document.getElementById('stopBlacklistButton').style.display = 'inline-block';
693
694 shouldStop = false;
695 isPaused = false;
696
697 await collectChatsToBlacklist();
698
699 // Скрываем кнопки паузы и остановки
700 document.getElementById('massAddBlacklistButton').style.display = 'inline-block';
701 document.getElementById('pauseBlacklistButton').style.display = 'none';
702 document.getElementById('stopBlacklistButton').style.display = 'none';
703
704 await loadAndDisplayBlacklist();
705 await updateDatabaseStats();
706 }
707 });
708
709 document.getElementById('clearAllBlacklistButton').addEventListener('click', async () => {
710 const result = await clearBlacklist();
711 if (result.count > 0 || result.removedCount > 0) {
712 alert(`✅ Черный список очищен!\n\nУдалено из ЧС: ${result.removedCount}\nВозвращено в базу: ${result.addedToDbCount}`);
713 await loadAndDisplayBlacklist();
714 await updateDatabaseStats();
715 }
716 });
717
718 // Обработчик паузы
719 document.getElementById('pauseBlacklistButton').addEventListener('click', () => {
720 isPaused = !isPaused;
721 const pauseButton = document.getElementById('pauseBlacklistButton');
722
723 if (isPaused) {
724 pauseButton.textContent = '▶️ Продолжить';
725 pauseButton.style.background = '#4caf50';
726 console.log('Сбор ЧС приостановлен');
727 } else {
728 pauseButton.textContent = '⏸️ Пауза';
729 pauseButton.style.background = '#ff9800';
730 console.log('Сбор ЧС возобновлен');
731 }
732 });
733
734 // Обработчик остановки
735 document.getElementById('stopBlacklistButton').addEventListener('click', () => {
736 if (confirm('Вы уверены, что хотите остановить сбор?')) {
737 shouldStop = true;
738 isPaused = false;
739 console.log('Запрошена остановка сбора ЧС');
740 }
741 });
742
743 // Поиск по черному списку
744 document.getElementById('searchBlacklistInput').addEventListener('input', (e) => {
745 filterBlacklist(e.target.value);
746 });
747 }
748
749 // Функция для открытия модального окна черного списка
750 async function openBlacklistModal() {
751 createBlacklistModal();
752 const modal = document.getElementById('blacklistModal');
753 if (modal) {
754 modal.style.display = 'flex';
755 await loadAndDisplayBlacklist();
756 console.log('Модальное окно черного списка открыто');
757 }
758 }
759
760 // Функция для закрытия модального окна черного списка
761 function closeBlacklistModal() {
762 const modal = document.getElementById('blacklistModal');
763 if (modal) {
764 modal.style.display = 'none';
765 console.log('Модальное окно черного списка закрыто');
766 }
767 }
768
769 // Функция для загрузки и отображения черного списка
770 async function loadAndDisplayBlacklist(searchQuery = '') {
771 const blacklist = await loadBlacklist();
772
773 // Обновляем статистику
774 const statsContent = document.getElementById('blacklistStatsContent');
775 if (statsContent) {
776 statsContent.innerHTML = `
777 <div><strong>Всего ID в черном списке:</strong> ${blacklist.length}</div>
778 `;
779 }
780
781 // Фильтруем по поисковому запросу
782 const filteredBlacklist = searchQuery
783 ? blacklist.filter(id => id.toLowerCase().includes(searchQuery.toLowerCase()))
784 : blacklist;
785
786 // Отображаем список
787 const blacklistContent = document.getElementById('blacklistContent');
788 if (blacklistContent) {
789 if (filteredBlacklist.length === 0) {
790 blacklistContent.innerHTML = '<div style="text-align: center; padding: 40px; color: #999;">Черный список пуст</div>';
791 } else {
792 blacklistContent.innerHTML = filteredBlacklist.map(chatId => {
793 return `
794 <div style="padding: 12px; margin-bottom: 8px; background: #fff8f8; border: 1px solid #ffcdd2; border-radius: 6px; font-size: 13px; display: flex; justify-content: space-between; align-items: center;">
795 <div style="font-family: monospace; color: #333;">${chatId}</div>
796 <button class="removeFromBlacklistBtn" data-chat-id="${chatId}" style="padding: 6px 12px; background: #4caf50; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 600;">✓ Вернуть</button>
797 </div>
798 `;
799 }).join('');
800
801 // Добавляем обработчики для кнопок удаления
802 document.querySelectorAll('.removeFromBlacklistBtn').forEach(btn => {
803 btn.addEventListener('click', async (e) => {
804 const chatId = e.target.getAttribute('data-chat-id');
805 if (confirm(`Удалить ID из черного списка?\n\n${chatId}\n\nID будет возвращен в базу контактов.`)) {
806 const result = await removeFromBlacklist([chatId]);
807 alert(`✅ ID удален из черного списка!\n\nУдалено из ЧС: ${result.removedCount}\nВозвращено в базу: ${result.addedToDbCount}`);
808 await loadAndDisplayBlacklist();
809 await updateDatabaseStats();
810 }
811 });
812 });
813 }
814 }
815 }
816
817 // Функция для фильтрации черного списка
818 function filterBlacklist(searchQuery) {
819 loadAndDisplayBlacklist(searchQuery);
820 }
821
822 // Функция для создания модального окна просмотра базы
823 function createViewDatabaseModal() {
824 // Проверяем, не создано ли уже окно
825 if (document.getElementById('viewDbModal')) {
826 return;
827 }
828
829 const modalHTML = `
830 <div id="viewDbModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10001; justify-content: center; align-items: center;">
831 <div style="background: white; padding: 30px; border-radius: 12px; width: 800px; max-width: 95%; max-height: 90vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
832 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
833 <h2 style="margin: 0; color: #333; font-size: 24px;">👁️ Просмотр базы контактов</h2>
834 <button id="closeViewDbButton" style="padding: 8px 16px; background: #999; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">Закрыть</button>
835 </div>
836
837 <div id="viewDbStatsBlock" style="margin-bottom: 20px; padding: 15px; background: #f0f8ff; border-radius: 6px; border-left: 4px solid #0066cc;">
838 <div style="font-weight: 600; color: #0066cc; margin-bottom: 10px;">Статистика базы:</div>
839 <div id="viewDbStatsContent" style="color: #333; font-size: 14px;"></div>
840 </div>
841
842 <div style="display: flex; gap: 10px; align-items: center;">
843 <input type="text" id="searchContactInput" placeholder="Поиск по имени..." style="flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
844 <button id="removeDuplicatesButton" style="padding: 10px 18px; background: #ff9800; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">🗑️ Удалить дубликаты</button>
845 </div>
846
847 <div id="contactsListContainer" style="max-height: 500px; overflow-y: auto; border: 1px solid #ddd; border-radius: 6px;">
848 <div id="contactsList" style="padding: 10px;"></div>
849 </div>
850 </div>
851 </div>
852 `;
853
854 document.body.insertAdjacentHTML('beforeend', modalHTML);
855 console.log('Модальное окно просмотра базы создано');
856
857 // Обработчики событий
858 document.getElementById('closeViewDbButton').addEventListener('click', closeViewDatabaseModal);
859 document.getElementById('removeDuplicatesButton').addEventListener('click', async () => {
860 await removeDuplicatesFromDatabase();
861 await loadAndDisplayContacts();
862 });
863
864 // Поиск по контактам
865 document.getElementById('searchContactInput').addEventListener('input', (e) => {
866 filterContactsList(e.target.value);
867 });
868 }
869
870 // Функция для открытия модального окна просмотра базы
871 async function openViewDatabaseModal() {
872 createViewDatabaseModal();
873 const modal = document.getElementById('viewDbModal');
874 if (modal) {
875 modal.style.display = 'flex';
876 await loadAndDisplayContacts();
877 console.log('Модальное окно просмотра базы открыто');
878 }
879 }
880
881 // Функция для закрытия модального окна просмотра базы
882 function closeViewDatabaseModal() {
883 const modal = document.getElementById('viewDbModal');
884 if (modal) {
885 modal.style.display = 'none';
886 console.log('Модальное окно просмотра базы закрыто');
887 }
888 }
889
890 // Функция для загрузки и отображения контактов
891 async function loadAndDisplayContacts(searchQuery = '') {
892 const db = await loadContactsDatabase();
893 const stats = getDatabaseStats(db);
894 const contacts = Object.values(db.contacts);
895
896 // Обновляем статистику
897 const statsContent = document.getElementById('viewDbStatsContent');
898 if (statsContent) {
899 statsContent.innerHTML = `
900 <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;">
901 <div><strong>Всего контактов:</strong> ${stats.total}</div>
902 <div><strong>С датами:</strong> ${stats.withDates}</div>
903 <div><strong>Отправлено:</strong> ${stats.sent}</div>
904 <div><strong>Не отправлено:</strong> ${stats.notSent}</div>
905 </div>
906 `;
907 }
908
909 // Фильтруем контакты по поисковому запросу
910 const filteredContacts = searchQuery
911 ? contacts.filter(c => c.name.toLowerCase().includes(searchQuery.toLowerCase()))
912 : contacts;
913
914 // Отображаем список контактов
915 const contactsList = document.getElementById('contactsList');
916 if (contactsList) {
917 if (filteredContacts.length === 0) {
918 contactsList.innerHTML = '<div style="text-align: center; padding: 40px; color: #999;">Контакты не найдены</div>';
919 } else {
920 contactsList.innerHTML = filteredContacts.map(contact => {
921 const lastMsgDate = contact.lastMessageDate
922 ? new Date(contact.lastMessageDate).toLocaleDateString('ru-RU')
923 : 'Нет данных';
924 const lastSentDate = contact.lastSentDate
925 ? new Date(contact.lastSentDate).toLocaleDateString('ru-RU')
926 : 'Не отправлялось';
927 const sentStatus = contact.lastSentDate ? '✅' : '⏳';
928
929 return `
930 <div style="padding: 12px; margin-bottom: 8px; background: ${contact.lastSentDate ? '#f0f8ff' : '#fff8f0'}; border: 1px solid #ddd; border-radius: 6px; font-size: 13px;">
931 <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
932 <div style="font-weight: 600; color: #333; font-size: 14px;">${sentStatus} ${contact.name}</div>
933 <div style="font-size: 11px; color: #999;">${contact.id.substring(0, 8)}...</div>
934 </div>
935 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; color: #666;">
936 <div><strong>Последнее сообщение:</strong> ${lastMsgDate}</div>
937 <div><strong>Отправлено:</strong> ${lastSentDate}</div>
938 </div>
939 ${contact.messageCount > 0 ? `<div style="margin-top: 5px; color: #666;"><strong>Отправлено сообщений:</strong> ${contact.messageCount}</div>` : ''}
940 </div>
941 `;
942 }).join('');
943 }
944 }
945 }
946
947 // Функция для фильтрации списка контактов
948 function filterContactsList(searchQuery) {
949 loadAndDisplayContacts(searchQuery);
950 }
951
952 // Функция для получения всех чатов
953 function getAllChats() {
954 // Сначала проверяем, есть ли результаты поиска (класс index_chat_EHlBq)
955 const searchChats = document.querySelectorAll('.index_chat_EHlBq');
956
957 if (searchChats.length > 0) {
958 // Если есть результаты поиска, используем их
959 console.log('Найдены результаты поиска:', searchChats.length);
960 return Array.from(searchChats);
961 }
962
963 // Если поиска нет, используем обычные чаты
964 const chats = document.querySelectorAll('.index_chat_4fr82');
965 // Фильтруем только видимые чаты (учитываем фильтр поиска)
966 const visibleChats = Array.from(chats).filter(chat => {
967 const style = window.getComputedStyle(chat);
968 // Проверяем, что чат не скрыт (убрали проверку высоты, т.к. чаты виртуализированы)
969 return style.display !== 'none' &&
970 style.visibility !== 'hidden' &&
971 style.opacity !== '0' &&
972 chat.offsetParent !== null; // Проверяем offsetParent для фильтрации
973 });
974 console.log('Найдено чатов:', visibleChats.length, '(всего в DOM:', chats.length, ')');
975 return visibleChats;
976 }
977
978 // Функция для клика по чату
979 async function clickChat(chat) {
980 // Пробуем оба селектора для имени чата (обычный и поисковый)
981 const chatName = chat.querySelector('.index_chatTitle_TiXTq')?.textContent?.trim() ||
982 chat.querySelector('.index_chatTitle_x9txX')?.textContent ||
983 'Неизвестно';
984 console.log('Клик по чату:', chatName);
985 chat.click();
986 await sleep(1000); // Сокращено с 2000 до 1000
987 }
988
989 // Функция для открытия чата по ID (через прямую ссылку в новой вкладке)
990 async function openChatById(chatId) {
991 console.log('Открытие чата по ID в новой вкладке:', chatId);
992
993 // Формируем прямую ссылку на чат
994 const chatUrl = `https://seller.ozon.ru/app/messenger/?group=customers_v2&id=${chatId}`;
995 console.log('URL чата:', chatUrl);
996
997 // Открываем чат в новой вкладке
998 await GM.openInTab(chatUrl, false); // false = открыть в активной вкладке (на переднем плане)
999
1000 // Убираем задержку - вкладка откроется сама
1001
1002 return true;
1003 }
1004
1005 // Функция для получения даты последнего сообщения в открытом чате
1006 function getLastMessageDate() {
1007 const dateElements = document.querySelectorAll('.om_1_n4');
1008 if (dateElements.length > 0) {
1009 // Берем последнюю дату (последнее сообщение)
1010 const lastDateElement = dateElements[dateElements.length - 1];
1011 const dateText = lastDateElement.textContent.trim();
1012 console.log('Дата последнего сообщения:', dateText);
1013 return parseDate(dateText);
1014 }
1015 console.log('Элемент даты не найден');
1016 return null;
1017 }
1018
1019 // Функция для отправки сообщения
1020 async function sendMessage(messageText) {
1021 console.log('Отправка сообщения:', messageText);
1022
1023 // Находим текстовое поле
1024 const textarea = document.querySelector('.om_17_a4');
1025 if (!textarea) {
1026 console.error('Текстовое поле не найдено');
1027 return false;
1028 }
1029
1030 // Вставляем текст
1031 textarea.value = messageText;
1032 textarea.dispatchEvent(new Event('input', { bubbles: true }));
1033 console.log('Текст вставлен в поле');
1034
1035 // Сокращено ожидание с 1000 до 500
1036 await sleep(500);
1037
1038 // Находим кнопку отправки (вторая кнопка с классом om_17_a8)
1039 const sendButtons = document.querySelectorAll('.om_17_a8');
1040 if (sendButtons.length < 2) {
1041 console.error('Кнопка отправки не найдена');
1042 return false;
1043 }
1044
1045 // Кликаем на вторую кнопку (первая - это прикрепление файла)
1046 const sendButton = sendButtons[1];
1047 sendButton.click();
1048 console.log('Сообщение отправлено (клик выполнен)');
1049
1050 // Сокращено ожидание с 1000 до 500
1051 await sleep(500);
1052
1053 return true;
1054 }
1055
1056 // Функция задержки
1057 function sleep(ms) {
1058 return new Promise(resolve => setTimeout(resolve, ms));
1059 }
1060
1061 // Функция для парсинга даты из текста Ozon
1062 function parseDate(dateText) {
1063 if (!dateText) return null;
1064
1065 const today = new Date();
1066 today.setHours(0, 0, 0, 0);
1067
1068 // Если только время (например "11:01", "10:39") - значит сегодня
1069 if (/^\d{1,2}:\d{2}$/.test(dateText)) {
1070 console.log('Формат времени обнаружен:', dateText, '- считаем сегодняшней датой');
1071 return today;
1072 }
1073
1074 // Если "Сегодня" или "сегодня"
1075 if (dateText.toLowerCase().includes('сегодня')) {
1076 return today;
1077 }
1078
1079 // Если "Вчера" или "вчера"
1080 if (dateText.toLowerCase().includes('вчера')) {
1081 const yesterday = new Date(today);
1082 yesterday.setDate(yesterday.getDate() - 1);
1083 return yesterday;
1084 }
1085
1086 // Если формат "28.01" или "21.12" (день.месяц без года)
1087 const shortDateMatch = dateText.match(/^(\d{1,2})\.(\d{1,2})$/);
1088 if (shortDateMatch) {
1089 const day = parseInt(shortDateMatch[1]);
1090 const month = parseInt(shortDateMatch[2]) - 1;
1091 const date = new Date(today.getFullYear(), month, day);
1092 // Если дата в будущем, значит это прошлый год
1093 if (date > today) {
1094 date.setFullYear(date.getFullYear() - 1);
1095 }
1096 console.log('Формат день.месяц обнаружен:', dateText, '- распознано как', date.toLocaleDateString('ru-RU'));
1097 return date;
1098 }
1099
1100 // Если формат "21 янв" или "21 января"
1101 const monthMatch = dateText.match(/(\d{1,2})\s+(янв|фев|мар|апр|мая|июн|июл|авг|сен|окт|ноя|дек)/i);
1102 if (monthMatch) {
1103 const day = parseInt(monthMatch[1]);
1104 const monthStr = monthMatch[2].toLowerCase();
1105
1106 const months = {
1107 'янв': 0, 'фев': 1, 'мар': 2, 'апр': 3, 'мая': 4, 'май': 4,
1108 'июн': 5, 'июл': 6, 'авг': 7, 'сен': 8, 'окт': 9, 'ноя': 10, 'дек': 11
1109 };
1110
1111 const month = months[monthStr.substring(0, 3)];
1112 if (month !== undefined) {
1113 const date = new Date(today.getFullYear(), month, day);
1114 // Если дата в будущем, значит это прошлый год
1115 if (date > today) {
1116 date.setFullYear(date.getFullYear() - 1);
1117 }
1118 return date;
1119 }
1120 }
1121
1122 // Если формат "21.01.2024" или "21/01/2024" или "18.06.2025"
1123 const dateMatch = dateText.match(/(\d{1,2})[./](\d{1,2})[./](\d{4})/);
1124 if (dateMatch) {
1125 const day = parseInt(dateMatch[1]);
1126 const month = parseInt(dateMatch[2]) - 1;
1127 const year = parseInt(dateMatch[3]);
1128 return new Date(year, month, day);
1129 }
1130
1131 console.log('Не удалось распознать дату:', dateText);
1132 return null;
1133 }
1134
1135 // Функция для обновления статуса в интерфейсе
1136 function updateStatus(statusText, progressText = '') {
1137 const statusBlock = document.getElementById('statusBlock');
1138 const statusTextElement = document.getElementById('statusText');
1139 const progressTextElement = document.getElementById('progressText');
1140
1141 if (statusBlock && statusTextElement) {
1142 statusBlock.style.display = 'block';
1143 statusTextElement.textContent = statusText;
1144 if (progressTextElement && progressText) {
1145 progressTextElement.textContent = progressText;
1146 }
1147 }
1148 }
1149
1150 // Функция для открытия модального окна
1151 function openModal() {
1152 const modal = document.getElementById('bulkSenderModal');
1153 if (modal) {
1154 modal.style.display = 'flex';
1155 console.log('Модальное окно открыто');
1156 // Обновляем статистику при открытии
1157 updateDatabaseStats();
1158 }
1159 }
1160
1161 // Функция для закрытия модального окна
1162 function closeModal() {
1163 const modal = document.getElementById('bulkSenderModal');
1164 if (modal) {
1165 modal.style.display = 'none';
1166 console.log('Модальное окно закрыто');
1167 }
1168 }
1169
1170 // Функция переключения паузы
1171 function togglePause() {
1172 isPaused = !isPaused;
1173 const pauseButton = document.getElementById('pauseButton');
1174
1175 if (isPaused) {
1176 pauseButton.textContent = '▶️ Продолжить';
1177 pauseButton.style.background = '#4caf50';
1178 console.log('Рассылка приостановлена');
1179 } else {
1180 pauseButton.textContent = '⏸️ Пауза';
1181 pauseButton.style.background = '#ff9800';
1182 console.log('Рассылка возобновлена');
1183 }
1184 }
1185
1186 // Функция остановки рассылки
1187 function stopBulkSending() {
1188 if (confirm('Вы уверены, что хотите остановить рассылку?')) {
1189 shouldStop = true;
1190 isPaused = false;
1191 console.log('Запрошена остановка рассылки');
1192 }
1193 }
1194
1195 // Основная функция рассылки
1196 async function startBulkSending() {
1197 console.log('=== ФУНКЦИЯ startBulkSending ВЫЗВАНА ===');
1198
1199 // Сразу меняем цвет кнопки для визуальной обратной связи
1200 const startButton = document.getElementById('startButton');
1201 if (startButton) {
1202 startButton.style.background = '#ff9800';
1203 startButton.textContent = 'Запуск...';
1204 startButton.disabled = true;
1205 }
1206
1207 const messageText = document.getElementById('messageText').value.trim();
1208 const filterDateInput = document.getElementById('filterDate').value;
1209 const testModeCheckbox = document.getElementById('testMode');
1210 const isTestMode = testModeCheckbox ? testModeCheckbox.checked : false;
1211
1212 // Получаем выбранный режим рассылки
1213 const sendingMode = document.querySelector('input[name="sendingMode"]:checked')?.value || 'database';
1214
1215 console.log('Текст сообщения:', messageText);
1216 console.log('Дата фильтра:', filterDateInput);
1217 console.log('Тестовый режим:', isTestMode);
1218 console.log('Режим рассылки:', sendingMode);
1219
1220 if (!messageText) {
1221 alert('Введите текст сообщения');
1222 console.log('Остановка: нет текста сообщения');
1223 // Возвращаем кнопку в исходное состояние
1224 if (startButton) {
1225 startButton.style.background = '#0066cc';
1226 startButton.textContent = 'Запустить';
1227 startButton.disabled = false;
1228 }
1229 return;
1230 }
1231
1232 if (!filterDateInput) {
1233 alert('Выберите дату фильтра');
1234 console.log('Остановка: нет даты фильтра');
1235 // Возвращаем кнопку в исходное состояние
1236 if (startButton) {
1237 startButton.style.background = '#0066cc';
1238 startButton.textContent = 'Запустить';
1239 startButton.disabled = false;
1240 }
1241 return;
1242 }
1243
1244 // Выбираем функцию в зависимости от режима
1245 if (sendingMode === 'visible') {
1246 await startBulkSendingByVisibleChats(messageText, filterDateInput, isTestMode);
1247 } else {
1248 await startBulkSendingByDatabase(messageText, filterDateInput, isTestMode);
1249 }
1250 }
1251
1252 // Функция рассылки по базе данных
1253 async function startBulkSendingByDatabase(messageText, filterDateInput, isTestMode) {
1254 console.log('=== РАССЫЛКА ПО БАЗЕ ДАННЫХ ===');
1255
1256 // Загружаем базу данных
1257 const db = await loadContactsDatabase();
1258 const totalContacts = Object.keys(db.contacts).length;
1259
1260 if (totalContacts === 0) {
1261 alert('База контактов пуста! Сначала соберите базу контактов с помощью кнопки "Собрать базу".');
1262 // Возвращаем кнопку в исходное состояние
1263 const startButton = document.getElementById('startButton');
1264 if (startButton) {
1265 startButton.style.background = '#0066cc';
1266 startButton.textContent = 'Запустить';
1267 startButton.disabled = false;
1268 }
1269 return;
1270 }
1271
1272 const filterDate = new Date(filterDateInput);
1273 filterDate.setHours(23, 59, 59, 999); // Устанавливаем конец дня для корректного сравнения
1274
1275 console.log('Всего контактов в базе:', totalContacts);
1276 console.log('Фильтр по дате:', filterDate.toLocaleDateString('ru-RU'));
1277 console.log('Тестовый режим:', isTestMode ? 'ДА (только 1 сообщение)' : 'НЕТ');
1278
1279 // Фильтруем контакты по дате
1280 const contactsToSend = Object.values(db.contacts).filter(contact => {
1281 if (!contact.lastMessageDate) {
1282 console.log('❌ Контакт без даты:', contact.name);
1283 return false;
1284 }
1285 const contactDate = new Date(contact.lastMessageDate);
1286 const matches = contactDate <= filterDate;
1287 console.log(`${matches ? '✅' : '❌'} ${contact.name}: ${contactDate.toLocaleDateString('ru-RU')} ${matches ? '<=' : '>'} ${filterDate.toLocaleDateString('ru-RU')}`);
1288 return matches;
1289 });
1290
1291 console.log('Контактов подходящих по дате:', contactsToSend.length);
1292
1293 if (contactsToSend.length === 0) {
1294 alert(`Нет контактов, подходящих по указанной дате.\n\nВсего в базе: ${totalContacts}\nС датами: ${Object.values(db.contacts).filter(c => c.lastMessageDate).length}\nПодходящих по фильтру (дата <= ${filterDate.toLocaleDateString('ru-RU')}): 0`);
1295 // Возвращаем кнопку в исходное состояние
1296 const startButton = document.getElementById('startButton');
1297 if (startButton) {
1298 startButton.style.background = '#0066cc';
1299 startButton.textContent = 'Запустить';
1300 startButton.disabled = false;
1301 }
1302 return;
1303 }
1304
1305 // Показываем статистику фильтрации
1306 const filterStats = `Всего в базе: ${totalContacts} | Подходящих по фильтру: ${contactsToSend.length}`;
1307 console.log(filterStats);
1308
1309 // Показываем пользователю статистику перед запуском
1310 if (!confirm(`Готово к запуску!\n\nВсего контактов в базе: ${totalContacts}\nПодходящих по фильтру (дата <= ${filterDate.toLocaleDateString('ru-RU')}): ${contactsToSend.length}\n\nТестовый режим: ${isTestMode ? 'ДА (1 сообщение)' : 'НЕТ'}\n\nНачать рассылку?`)) {
1311 console.log('Рассылка отменена пользователем');
1312 // Возвращаем кнопку в исходное состояние
1313 const startButton = document.getElementById('startButton');
1314 if (startButton) {
1315 startButton.style.background = '#0066cc';
1316 startButton.textContent = 'Запустить';
1317 startButton.disabled = false;
1318 }
1319 return;
1320 }
1321
1322 // Сохраняем состояние рассылки
1323 const sendingState = {
1324 isActive: true,
1325 messageText: messageText,
1326 filterDate: filterDate.toISOString(), // Сохраняем дату фильтра
1327 testMode: isTestMode,
1328 contactsList: contactsToSend,
1329 totalContacts: contactsToSend.length,
1330 processed: 0,
1331 sent: 0,
1332 skipped: 0,
1333 currentContactId: contactsToSend[0].id,
1334 firstChatOpened: false // Флаг, что первый чат еще не открыт
1335 };
1336
1337 await saveSendingState(sendingState);
1338 console.log('Состояние рассылки сохранено, запускаем мониторинг');
1339
1340 // Запускаем мониторинг прогресса (он откроет первый чат)
1341 console.log('Запускаем мониторинг прогресса рассылки');
1342 monitorSendingProgress();
1343 }
1344
1345 // Функция рассылки по видимым чатам
1346 async function startBulkSendingByVisibleChats(messageText, filterDateInput, isTestMode) {
1347 console.log('=== РАССЫЛКА ПО ВИДИМЫМ ЧАТАМ ===');
1348
1349 const filterDate = new Date(filterDateInput);
1350 filterDate.setHours(23, 59, 59, 999);
1351
1352 console.log('Фильтр по дате:', filterDate);
1353 console.log('Тестовый режим:', isTestMode ? 'ДА (только 1 сообщение)' : 'НЕТ');
1354
1355 // Загружаем черный список
1356 const blacklist = await loadBlacklist();
1357 console.log('Загружен черный список:', blacklist.length, 'ID');
1358
1359 // Меняем интерфейс
1360 isPaused = false;
1361 shouldStop = false;
1362
1363 const testModeCheckbox = document.getElementById('testMode');
1364 document.getElementById('startButton').style.display = 'none';
1365 document.getElementById('pauseButton').style.display = 'inline-block';
1366 document.getElementById('stopButton').style.display = 'inline-block';
1367 document.getElementById('messageText').disabled = true;
1368 document.getElementById('filterDate').disabled = true;
1369 if (testModeCheckbox) testModeCheckbox.disabled = true;
1370
1371 // Блокируем радио-кнопки режима
1372 document.querySelectorAll('input[name="sendingMode"]').forEach(radio => radio.disabled = true);
1373
1374 updateStatus(
1375 'Запуск рассылки по видимым чатам...',
1376 'Подготовка к рассылке...'
1377 );
1378
1379 let processed = 0;
1380 let sent = 0;
1381 let skipped = 0;
1382 let skippedBlacklist = 0;
1383
1384 // Трекер обработанных ID в этой сессии
1385 const processedChatIds = new Set();
1386
1387 // Счетчик попыток без новых чатов
1388 let noNewChatsCount = 0;
1389 const maxNoNewChatsAttempts = 3;
1390
1391 // Счетчик обработанных чатов с момента последнего скролла
1392 let chatsProcessedSinceScroll = 0;
1393 const scrollAfterChats = 10; // Скроллим каждые 10 обработанных чатов
1394
1395 while (!shouldStop) {
1396 // Проверка на паузу
1397 while (isPaused && !shouldStop) {
1398 updateStatus('Пауза', `Обработано: ${processed}, Отправлено: ${sent}, Пропущено: ${skipped}, ЧС: ${skippedBlacklist}`);
1399 await sleep(500);
1400 }
1401
1402 if (shouldStop) {
1403 updateStatus('Остановлено пользователем', `Обработано: ${processed}, Отправлено: ${sent}, Пропущено: ${skipped}, ЧС: ${skippedBlacklist}`);
1404 break;
1405 }
1406
1407 // Проверка тестового режима
1408 if (isTestMode && sent >= 1) {
1409 console.log('Тестовый режим: достигнут лимит в 1 сообщение, останавливаем рассылку');
1410 shouldStop = true;
1411 break;
1412 }
1413
1414 // Получаем текущие видимые чаты
1415 const currentChats = getAllChats();
1416 console.log('Найдено видимых чатов в DOM:', currentChats.length);
1417
1418 // Фильтруем только те чаты, которые еще не обработали
1419 const newChats = currentChats.filter(chat => {
1420 const chatId = extractChatId(chat);
1421 return chatId && !processedChatIds.has(chatId);
1422 });
1423
1424 console.log('Новых необработанных чатов:', newChats.length);
1425 console.log('Всего обработано за сессию:', processedChatIds.size);
1426
1427 // Если нет новых чатов - увеличиваем счетчик
1428 if (newChats.length === 0) {
1429 noNewChatsCount++;
1430 console.log(`Новые чаты не появились (${noNewChatsCount}/${maxNoNewChatsAttempts})`);
1431
1432 if (noNewChatsCount >= maxNoNewChatsAttempts) {
1433 console.log('Достигнут конец списка чатов (новые чаты не подгружаются)');
1434 break;
1435 }
1436
1437 // Скроллим список чатов для подгрузки новых
1438 console.log('Нет новых чатов, скроллим для подгрузки...');
1439
1440 // Запоминаем количество чатов ДО скролла
1441 const chatsBeforeScroll = currentChats.length;
1442 console.log('Чатов в DOM до скролла:', chatsBeforeScroll);
1443
1444 await scrollChatList();
1445
1446 // Ждем подгрузки новых чатов - увеличено до 10 секунд
1447 console.log('Ждем 10 секунд для рендеринга новых чатов...');
1448 await sleep(10000);
1449
1450 // Проверяем, изменилось ли количество чатов
1451 const chatsAfterScroll = getAllChats().length;
1452 console.log('Чатов в DOM после скролла и ожидания:', chatsAfterScroll);
1453 console.log('Изменение количества чатов:', chatsAfterScroll - chatsBeforeScroll);
1454
1455 continue;
1456 } else {
1457 noNewChatsCount = 0; // Сбрасываем счетчик, если появились новые чаты
1458 }
1459
1460 // Обрабатываем новые чаты
1461 for (const chat of newChats) {
1462 // Проверка на паузу
1463 while (isPaused && !shouldStop) {
1464 updateStatus('Пауза', `Обработано: ${processed}, Отправлено: ${sent}, Пропущено: ${skipped}, ЧС: ${skippedBlacklist}`);
1465 await sleep(500);
1466 }
1467
1468 // Проверка на остановку
1469 if (shouldStop) {
1470 break;
1471 }
1472
1473 // Проверка тестового режима
1474 if (isTestMode && sent >= 1) {
1475 console.log('Тестовый режим: достигнут лимит в 1 сообщение, останавливаем рассылку');
1476 shouldStop = true;
1477 break;
1478 }
1479
1480 const chatId = extractChatId(chat);
1481 const chatName = chat.querySelector('.index_chatTitle_TiXTq')?.textContent ||
1482 chat.querySelector('.index_chatTitle_x9txX')?.textContent ||
1483 'Неизвестно';
1484
1485 // Отмечаем чат как обработанный
1486 processedChatIds.add(chatId);
1487
1488 // Проверяем черный список
1489 if (blacklist.includes(chatId)) {
1490 console.log(`⛔ Чат в черном списке, пропускаем: ${chatName} (${chatId})`);
1491 skippedBlacklist++;
1492 processed++;
1493 chatsProcessedSinceScroll++;
1494 continue;
1495 }
1496
1497 updateStatus(
1498 `Обработка чата: ${chatName}`,
1499 `Обработано: ${processed}, Отправлено: ${sent}, Пропущено: ${skipped}, ЧС: ${skippedBlacklist}`
1500 );
1501
1502 console.log(`[${processed + 1}] Обрабатываем чат: ${chatName} (${chatId})`);
1503
1504 // Кликаем по чату
1505 await clickChat(chat);
1506
1507 // Получаем дату последнего сообщения
1508 const lastMessageDate = getLastMessageDate();
1509
1510 if (!lastMessageDate) {
1511 console.log('Не удалось получить дату, пропускаем чат');
1512 skipped++;
1513 processed++;
1514 chatsProcessedSinceScroll++;
1515 continue;
1516 }
1517
1518 // Проверяем дату перед отправкой - сравниваем реальную дату из чата с фильтром
1519 let shouldSend = false;
1520 if (lastMessageDate && filterDate) {
1521 console.log('Сравнение дат:');
1522 console.log(' Дата из чата:', lastMessageDate.toLocaleDateString('ru-RU'));
1523 console.log(' Дата фильтра:', filterDate.toLocaleDateString('ru-RU'));
1524 console.log(' Дата из чата <= Дата фильтра?', lastMessageDate <= filterDate);
1525
1526 // Отправляем только если дата последнего сообщения <= дате фильтра
1527 shouldSend = lastMessageDate <= filterDate;
1528 } else {
1529 console.log('Не удалось получить дату или фильтр, пропускаем отправку');
1530 shouldSend = false;
1531 }
1532
1533 // Отправляем сообщение только если дата подходит
1534 let success = false;
1535 if (shouldSend) {
1536 console.log('✅ Дата подходит, отправляем сообщение');
1537 success = await sendMessage(messageText);
1538 } else {
1539 console.log('⏭️ Пропускаем отправку (дата не подходит)');
1540 success = false;
1541 }
1542
1543 if (success) {
1544 sent++;
1545 console.log(`✅ Сообщение отправлено в чат: ${chatName}`);
1546
1547 // Обновляем базу данных
1548 const db = await loadContactsDatabase();
1549 if (db.contacts[chatId]) {
1550 db.contacts[chatId].lastSentDate = new Date().toISOString();
1551 db.contacts[chatId].messageCount = (db.contacts[chatId].messageCount || 0) + 1;
1552 await saveContactsDatabase(db);
1553 }
1554 } else {
1555 console.error('❌ Ошибка отправки или дата не подходит');
1556 skipped++;
1557 }
1558
1559 processed++;
1560 chatsProcessedSinceScroll++;
1561
1562 // Скроллим список чатов каждые N обработанных чатов для подгрузки новых
1563 if (chatsProcessedSinceScroll >= scrollAfterChats) {
1564 console.log(`Обработано ${chatsProcessedSinceScroll} чатов, скроллим для подгрузки новых...`);
1565 await scrollChatList();
1566 chatsProcessedSinceScroll = 0;
1567 }
1568
1569 // Пауза между чатами - сокращено с 1000 до 500
1570 await sleep(500);
1571 }
1572
1573 // Скроллим для подгрузки новых чатов после обработки текущей порции
1574 if (!shouldStop && newChats.length > 0) {
1575 console.log('Обработана порция чатов, скроллим для подгрузки следующей...');
1576 await scrollChatList();
1577 // ВАЖНО: Ждем дольше, чтобы новые чаты успели отрендериться в DOM
1578 console.log('Ждем 5 секунд для рендеринга новых чатов...');
1579 await sleep(5000);
1580 }
1581 }
1582
1583 // Завершение
1584 isPaused = false;
1585 shouldStop = false;
1586
1587 document.getElementById('startButton').style.display = 'inline-block';
1588 document.getElementById('pauseButton').style.display = 'none';
1589 document.getElementById('stopButton').style.display = 'none';
1590 document.getElementById('messageText').disabled = false;
1591 document.getElementById('filterDate').disabled = false;
1592 if (testModeCheckbox) testModeCheckbox.disabled = false;
1593 document.querySelectorAll('input[name="sendingMode"]').forEach(radio => radio.disabled = false);
1594
1595 updateStatus(
1596 'Рассылка завершена!',
1597 `Всего обработано: ${processed} | Отправлено: ${sent} | Пропущено: ${skipped} | Черный список: ${skippedBlacklist}`
1598 );
1599
1600 console.log('=== РАССЫЛКА ЗАВЕРШЕНА ===');
1601 console.log('Обработано:', processed);
1602 console.log('Отправлено:', sent);
1603 console.log('Пропущено:', skipped);
1604 console.log('Пропущено (ЧС):', skippedBlacklist);
1605 }
1606
1607 // Функция для создания плавающей кнопки
1608 function createFloatingButton() {
1609 // Проверяем, не создана ли уже кнопка
1610 if (document.getElementById('bulkSenderFloatingBtn')) {
1611 return;
1612 }
1613
1614 const button = document.createElement('button');
1615 button.id = 'bulkSenderFloatingBtn';
1616 button.textContent = '📧 Рассылка';
1617 button.style.cssText = `
1618 position: fixed;
1619 bottom: 20px;
1620 right: 20px;
1621 padding: 15px 25px;
1622 background: #0066cc;
1623 color: white;
1624 border: none;
1625 border-radius: 50px;
1626 cursor: pointer;
1627 font-size: 16px;
1628 font-weight: 600;
1629 box-shadow: 0 4px 12px rgba(0, 102, 204, 0.4);
1630 z-index: 9999;
1631 transition: all 0.3s;
1632 `;
1633
1634 button.addEventListener('mouseenter', () => {
1635 button.style.background = '#0052a3';
1636 button.style.transform = 'scale(1.05)';
1637 button.style.boxShadow = '0 6px 16px rgba(0, 102, 204, 0.6)';
1638 });
1639
1640 button.addEventListener('mouseleave', () => {
1641 button.style.background = '#0066cc';
1642 button.style.transform = 'scale(1)';
1643 button.style.boxShadow = '0 4px 12px rgba(0, 102, 204, 0.4)';
1644 });
1645
1646 button.addEventListener('click', openModal);
1647
1648 document.body.appendChild(button);
1649 console.log('Плавающая кнопка создана');
1650 }
1651
1652 // Функция мониторинга прогресса рассылки в главной вкладке
1653 async function monitorSendingProgress() {
1654 console.log('🚀 Запуск мониторинга прогресса рассылки');
1655
1656 let lastProcessed = -1;
1657 let lastOpenedIndex = -1; // Индекс последнего открытого чата
1658 const maxParallelChats = 3; // Максимум одновременно открытых чатов
1659
1660 const monitorInterval = setInterval(async () => {
1661 const currentState = await loadSendingState();
1662
1663 if (!currentState || !currentState.isActive) {
1664 console.log('Рассылка завершена или остановлена, останавливаем мониторинг');
1665 clearInterval(monitorInterval);
1666
1667 // Обновляем интерфейс
1668 const finalStats = currentState ?
1669 `Обработано: ${currentState.processed}/${currentState.totalContacts} | Отправлено: ${currentState.sent} | Пропущено: ${currentState.skipped}` :
1670 'Проверьте результаты';
1671 updateStatus('Рассылка завершена!', finalStats);
1672 await updateDatabaseStats();
1673
1674 // Разблокируем интерфейс
1675 isPaused = false;
1676 document.getElementById('startButton').style.display = 'inline-block';
1677 document.getElementById('startButton').disabled = false;
1678 document.getElementById('startButton').style.background = '#0066cc';
1679 document.getElementById('startButton').textContent = 'Запустить';
1680 document.getElementById('pauseButton').style.display = 'none';
1681 document.getElementById('stopButton').style.display = 'none';
1682 document.getElementById('messageText').disabled = false;
1683 document.getElementById('filterDate').disabled = false;
1684 const testModeCheckbox = document.getElementById('testMode');
1685 if (testModeCheckbox) testModeCheckbox.disabled = false;
1686 document.querySelectorAll('input[name="sendingMode"]').forEach(radio => radio.disabled = false);
1687
1688 return;
1689 }
1690
1691 console.log('🔍 Проверка состояния:', {
1692 processed: currentState.processed,
1693 totalContacts: currentState.totalContacts,
1694 lastOpenedIndex: lastOpenedIndex,
1695 maxParallel: maxParallelChats
1696 });
1697
1698 // Обновляем статус только если изменился прогресс
1699 if (currentState.processed !== lastProcessed) {
1700 lastProcessed = currentState.processed;
1701
1702 const currentContact = currentState.contactsList[currentState.processed - 1];
1703 const contactName = currentContact ? currentContact.name : 'Неизвестно';
1704
1705 updateStatus(
1706 `Обработка: ${contactName}`,
1707 `Обработано: ${currentState.processed}/${currentState.totalContacts} | Отправлено: ${currentState.sent} | Пропущено: ${currentState.skipped}`
1708 );
1709
1710 console.log('📊 Обновлен прогресс:', currentState.processed, '/', currentState.totalContacts);
1711 }
1712
1713 // Открываем чаты параллельно
1714 // Логика: открываем до maxParallelChats чатов вперед от текущего processed
1715 const remainingContacts = currentState.totalContacts - currentState.processed;
1716 const nextIndexToOpen = lastOpenedIndex + 1;
1717 const chatsToOpen = Math.min(
1718 maxParallelChats - (lastOpenedIndex - currentState.processed + 1), // Сколько уже открыто впереди
1719 remainingContacts,
1720 currentState.totalContacts - nextIndexToOpen // Сколько осталось до конца
1721 );
1722
1723 console.log('Можно открыть чатов:', chatsToOpen, '(lastOpened:', lastOpenedIndex, ', processed:', currentState.processed, ', осталось:', remainingContacts, ')');
1724
1725 if (chatsToOpen > 0) {
1726 for (let i = 0; i < chatsToOpen; i++) {
1727 const contactIndex = nextIndexToOpen + i;
1728 const contact = currentState.contactsList[contactIndex];
1729
1730 if (contact) {
1731 console.log('📂 Открываем чат [', contactIndex, ']:', contact.name, '(', contact.id, ')');
1732 lastOpenedIndex = contactIndex;
1733 await openChatById(contact.id);
1734 await sleep(200); // Небольшая задержка между открытием вкладок
1735 }
1736 }
1737 }
1738 }, 1000); // Проверяем каждую секунду
1739 }
1740
1741 // Функция для создания фильтра по дате в списке чатов
1742 function createDateFilter() {
1743 // Проверяем, не создан ли уже фильтр
1744 if (document.getElementById('dateFilterContainer')) {
1745 return;
1746 }
1747
1748 // Ищем контейнер с поиском
1749 const searchContainer = document.querySelector('.om_1_f0')?.parentElement;
1750 if (!searchContainer) {
1751 console.log('Контейнер для фильтра не найден');
1752 return;
1753 }
1754
1755 // Создаем контейнер для фильтра
1756 const filterContainer = document.createElement('div');
1757 filterContainer.id = 'dateFilterContainer';
1758 filterContainer.style.cssText = `
1759 padding: 15px;
1760 background: #f0f8ff;
1761 border-bottom: 1px solid #ddd;
1762 display: flex;
1763 gap: 10px;
1764 align-items: center;
1765 flex-wrap: wrap;
1766 `;
1767
1768 filterContainer.innerHTML = `
1769 <label style="font-size: 13px; font-weight: 600; color: #555;">Фильтр по дате последнего сообщения:</label>
1770 <input type="date" id="dateFilterFrom" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
1771 <span style="color: #555;">—</span>
1772 <input type="date" id="dateFilterTo" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
1773 <button id="applyDateFilter" style="padding: 8px 16px; background: #0066cc; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 600;">Применить</button>
1774 <button id="clearDateFilter" style="padding: 8px 16px; background: #999; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 13px; font-weight: 600; display: none;">Сбросить</button>
1775 `;
1776
1777 // Вставляем фильтр перед списком чатов
1778 const chatList = document.querySelector('.om_1_f0');
1779 if (chatList && chatList.parentElement) {
1780 chatList.parentElement.insertBefore(filterContainer, chatList);
1781 console.log('Фильтр по дате создан');
1782
1783 // Обработчики событий
1784 document.getElementById('applyDateFilter').addEventListener('click', applyDateFilter);
1785 document.getElementById('clearDateFilter').addEventListener('click', clearDateFilter);
1786 }
1787 }
1788
1789 // Функция применения фильтра по дате
1790 async function applyDateFilter() {
1791 const dateFrom = document.getElementById('dateFilterFrom').value;
1792 const dateTo = document.getElementById('dateFilterTo').value;
1793
1794 if (!dateFrom || !dateTo) {
1795 alert('Выберите обе даты для фильтрации');
1796 return;
1797 }
1798
1799 const filterFrom = new Date(dateFrom);
1800 const filterTo = new Date(dateTo);
1801
1802 console.log('Применение фильтра по дате:', filterFrom, '-', filterTo);
1803
1804 // Показываем индикатор загрузки
1805 const applyButton = document.getElementById('applyDateFilter');
1806 const originalText = applyButton.textContent;
1807 applyButton.textContent = 'Фильтрация...';
1808 applyButton.disabled = true;
1809
1810 // Получаем все чаты
1811 const allChats = document.querySelectorAll('.index_chat_4fr82');
1812 let visibleCount = 0;
1813 let hiddenCount = 0;
1814
1815 allChats.forEach(chat => {
1816 // Ищем дату в самом элементе чата (не открывая его)
1817 const dateElement = chat.querySelector('.index_chatDate_z4mNc');
1818 const dateText = dateElement?.textContent?.trim();
1819
1820 const chatDate = parseDate(dateText);
1821
1822 if (chatDate) {
1823 // Проверяем, попадает ли дата в диапазон
1824 if (chatDate >= filterFrom && chatDate <= filterTo) {
1825 // Дата подходит - оставляем чат видимым
1826 chat.style.display = 'grid';
1827 visibleCount++;
1828 console.log('Чат подходит:', chat.querySelector('.index_chatTitle_TiXTq')?.textContent, chatDate);
1829 } else {
1830 // Дата не подходит - скрываем чат
1831 chat.style.display = 'none';
1832 hiddenCount++;
1833 }
1834 } else {
1835 // Не удалось распознать дату - оставляем видимым
1836 chat.style.display = 'grid';
1837 visibleCount++;
1838 }
1839 });
1840
1841 // Восстанавливаем кнопку
1842 applyButton.textContent = originalText;
1843 applyButton.disabled = false;
1844
1845 // Показываем кнопку сброса
1846 document.getElementById('clearDateFilter').style.display = 'inline-block';
1847
1848 console.log(`Фильтр применен. Показано: ${visibleCount}, Скрыто: ${hiddenCount}`);
1849 alert(`Фильтр применен!\nПоказано чатов: ${visibleCount}\nСкрыто чатов: ${hiddenCount}`);
1850 }
1851
1852 // Функция сброса фильтра
1853 function clearDateFilter() {
1854 // Показываем все чаты
1855 const allChats = document.querySelectorAll('.index_chat_4fr82');
1856 allChats.forEach(chat => {
1857 chat.style.display = 'grid';
1858 });
1859
1860 // Очищаем поля
1861 document.getElementById('dateFilterFrom').value = '';
1862 document.getElementById('dateFilterTo').value = '';
1863
1864 // Скрываем кнопку сброса
1865 document.getElementById('clearDateFilter').style.display = 'none';
1866
1867 console.log('Фильтр сброшен');
1868 }
1869
1870 // Функция для обновления статистики базы данных в интерфейсе
1871 async function updateDatabaseStats() {
1872 const db = await loadContactsDatabase();
1873 const stats = getDatabaseStats(db);
1874
1875 console.log('Обновление статистики базы данных:', stats);
1876
1877 // Ждем, пока элемент появится в DOM (максимум 5 секунд)
1878 let attempts = 0;
1879 const maxAttempts = 50;
1880
1881 while (attempts < maxAttempts) {
1882 const statsBlock = document.getElementById('dbStatsBlock');
1883
1884 if (statsBlock) {
1885 const lastUpdateText = stats.lastUpdate
1886 ? new Date(stats.lastUpdate).toLocaleString('ru-RU')
1887 : 'Никогда';
1888
1889 statsBlock.innerHTML = `
1890 <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;">
1891 <div><strong>Всего контактов:</strong> ${stats.total}</div>
1892 <div><strong>С датами:</strong> ${stats.withDates}</div>
1893 <div><strong>Отправлено:</strong> ${stats.sent}</div>
1894 <div><strong>Не отправлено:</strong> ${stats.notSent}</div>
1895 </div>
1896 <div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #e0e0e0; font-size: 12px; color: #999;">
1897 Последнее обновление: ${lastUpdateText}
1898 </div>
1899 `;
1900 console.log('Статистика обновлена в интерфейсе');
1901 return true;
1902 }
1903
1904 // Ждем 100мс перед следующей попыткой
1905 await sleep(100);
1906 attempts++;
1907 }
1908
1909 console.error('Элемент dbStatsBlock не найден после', maxAttempts, 'попыток!');
1910 return false;
1911 }
1912
1913 // Функция для фильтрации списка контактов
1914 function filterContactsList(searchQuery) {
1915 loadAndDisplayContacts(searchQuery);
1916 }
1917
1918 // ============= ФУНКЦИИ ДЛЯ СБОРА КОНТАКТОВ =============
1919
1920 // Функция для извлечения ID из URL чата
1921 function extractChatId(chatElement) {
1922 // Ищем атрибут deeplink в элементе чата
1923 const deeplink = chatElement.getAttribute('deeplink');
1924 if (deeplink) {
1925 // Пробуем оба формата:
1926 // 1. Обычный: ?group=customers_v2&id=ec4ee215-e8f6-49bd-968a-8334a98d8aa7
1927 // 2. Поиск: /communications/chats/chat?id=3d7d80d4-5da1-4cb1-8221-2e332a048d39&context=dialog_list
1928 const match = deeplink.match(/[?&]id=([a-f0-9-]+)/);
1929 if (match) {
1930 console.log('Извлечен ID из deeplink:', match[1]);
1931 return match[1];
1932 }
1933 }
1934
1935 // Запасной вариант - ищем ссылку внутри элемента
1936 const link = chatElement.querySelector('a[href*="id="]');
1937 if (link) {
1938 const href = link.getAttribute('href');
1939 const match = href.match(/[?&]id=([a-f0-9-]+)/);
1940 if (match) {
1941 console.log('Извлечен ID из ссылки:', match[1]);
1942 return match[1];
1943 }
1944 }
1945
1946 console.log('Не удалось извлечь ID чата');
1947 return null;
1948 }
1949
1950 // Функция для получения данных чата из элемента списка
1951 function getChatDataFromElement(chatElement) {
1952 const chatName = chatElement.querySelector('.index_chatTitle_TiXTq')?.textContent?.trim() ||
1953 chatElement.querySelector('.index_chatTitle_x9txX')?.textContent?.trim() ||
1954 'Неизвестно';
1955
1956 // Ищем элемент с датой - используем правильный селектор
1957 // Для обычных чатов: .index_chatDate_z4mNc
1958 // Для результатов поиска: .index_chatDate_WJ-+mb
1959 const dateElement = chatElement.querySelector('.index_chatDate_z4mNc, .index_chatDate_WJ-+mb');
1960 const dateText = dateElement?.textContent?.trim();
1961
1962 console.log('📋 Получение данных чата:', chatName);
1963 console.log(' Элемент даты найден:', !!dateElement);
1964 console.log(' Текст даты:', dateText || 'НЕТ');
1965
1966 const lastMessageDate = dateText ? parseDate(dateText) : null;
1967
1968 if (lastMessageDate) {
1969 console.log(' ✅ Дата распознана:', lastMessageDate.toLocaleDateString('ru-RU'));
1970 } else {
1971 console.log(' ❌ Дата НЕ распознана');
1972 }
1973
1974 return {
1975 name: chatName,
1976 lastMessageDate: lastMessageDate ? lastMessageDate.toISOString() : null,
1977 lastSentDate: null,
1978 messageCount: 0
1979 };
1980 }
1981
1982 // Функция для скролла списка чатов
1983 async function scrollChatList() {
1984 console.log('=== НАЧАЛО СКРОЛЛА СПИСКА ЧАТОВ ===');
1985
1986 // Проверяем, применен ли фильтр Ozon
1987 const clearFilterBtn = document.querySelector('.c8s110-a2, .c8s110-c0, .c8s110-a4, .c8s110-a5');
1988 const hasOzonFilter = !!clearFilterBtn;
1989
1990 console.log('Фильтр Ozon активен:', hasOzonFilter);
1991
1992 // ВАЖНО: Находим ПРАВИЛЬНЫЙ элемент списка чатов (тот, который реально скроллится)
1993 // Может быть несколько элементов с классом .om_1_f0, нам нужен тот, у которого scrollHeight > clientHeight
1994 const allChatLists = document.querySelectorAll('.om_1_f0');
1995 console.log('Найдено элементов .om_1_f0:', allChatLists.length);
1996
1997 const chatList = Array.from(allChatLists).find(el => el.scrollHeight > el.clientHeight);
1998
1999 if (!chatList) {
2000 console.error('Скроллируемый список чатов не найден');
2001 // Пробуем скроллить window как запасной вариант
2002 console.log('Пробуем скроллить window...');
2003 window.scrollBy({ top: 500, behavior: 'smooth' });
2004 await sleep(3000);
2005 return true;
2006 }
2007
2008 console.log('Найден скроллируемый список чатов:', {
2009 scrollTop: chatList.scrollTop,
2010 scrollHeight: chatList.scrollHeight,
2011 clientHeight: chatList.clientHeight
2012 });
2013
2014 // Сохраняем текущую позицию скролла
2015 const oldScrollTop = chatList.scrollTop;
2016 console.log('Текущий scrollTop:', oldScrollTop);
2017
2018 // Скроллим 6 раз подряд с небольшими задержками
2019 console.log('Выполняем 6 скроллов подряд...');
2020 for (let i = 0; i < 6; i++) {
2021 chatList.scrollTop += 500;
2022 console.log(`Скролл ${i + 1}/6: scrollTop =`, chatList.scrollTop);
2023 await sleep(500);
2024 }
2025
2026 console.log('scrollTop изменен с', oldScrollTop, 'на', chatList.scrollTop);
2027
2028 // Ждем подгрузки новых чатов - увеличено время ожидания для рендеринга DOM
2029 console.log('Ждем подгрузки и рендеринга новых чатов...');
2030 await sleep(5000);
2031
2032 console.log('Финальный scrollTop:', chatList.scrollTop);
2033 console.log('=== СКРОЛЛ ЗАВЕРШЕН ===');
2034 return true;
2035 }
2036
2037 // Основная функция сбора базы контактов
2038 async function collectContactsDatabase(updateExisting = false, testMode = false) {
2039 console.log('=== НАЧАЛО СБОРА БАЗЫ КОНТАКТОВ ===');
2040 console.log('Режим:', updateExisting ? 'Обновление существующей базы' : 'Полный сбор');
2041 console.log('Тестовый режим:', testMode ? 'ДА (только 1 контакт)' : 'НЕТ');
2042
2043 // Загружаем существующую базу
2044 const db = await loadContactsDatabase();
2045 const initialCount = Object.keys(db.contacts).length;
2046 console.log('Начальное количество контактов в базе:', initialCount);
2047
2048 // Загружаем черный список
2049 const blacklist = await loadBlacklist();
2050 console.log('Загружен черный список:', blacklist.length, 'ID');
2051
2052 // В режиме обновления - создаем Set из ID контактов для быстрой проверки
2053 const existingContactIds = updateExisting ? new Set(Object.keys(db.contacts)) : null;
2054
2055 // Трекер обработанных ID в этой сессии
2056 const processedInSession = new Set();
2057
2058 let newContacts = 0;
2059 let updatedContacts = 0;
2060 let skippedBlacklist = 0;
2061 let scrollAttempts = 0;
2062 let previousChatCount = 0;
2063 let noNewChatsCount = 0;
2064 const maxNoNewChatsAttempts = 3; // Если 3 раза подряд не появились новые чаты - останавливаемся
2065
2066 // Счетчик для периодического сохранения
2067 let contactsSinceLastSave = 0;
2068 const saveEveryNContacts = 10; // Сохраняем каждые 10 контактов
2069
2070 while (!shouldStop) {
2071 // Проверка на паузу
2072 while (isPaused && !shouldStop) {
2073 updateStatus('Пауза сбора базы', `Новых: ${newContacts}, Обновлено: ${updatedContacts}, Пропущено (ЧС): ${skippedBlacklist}`);
2074 await sleep(500);
2075 }
2076
2077 if (shouldStop) {
2078 console.log('Сбор базы остановлен пользователем');
2079 break;
2080 }
2081
2082 // Проверка тестового режима
2083 if (testMode && (newContacts >= 1 || updatedContacts >= 1)) {
2084 console.log('Тестовый режим: собран 1 контакт, останавливаем сбор');
2085 shouldStop = true;
2086 break;
2087 }
2088
2089 // Проверка режима обновления
2090 if (updateExisting && existingContactIds) {
2091 const remainingToUpdate = Array.from(existingContactIds).filter(id => !processedInSession.has(id));
2092 console.log('Осталось обновить контактов:', remainingToUpdate.length, 'из', existingContactIds.size);
2093
2094 if (remainingToUpdate.length === 0) {
2095 console.log('Все контакты из базы обновлены, завершаем');
2096 break;
2097 }
2098 }
2099
2100 // Получаем текущие видимые чаты
2101 const currentChats = getAllChats();
2102 console.log(`Попытка ${scrollAttempts + 1}: найдено ${currentChats.length} чатов в DOM`);
2103
2104 // Фильтруем только те чаты, которые еще не обработали
2105 const newChats = currentChats.filter(chat => {
2106 const chatId = extractChatId(chat);
2107 return chatId && !processedInSession.has(chatId);
2108 });
2109
2110 console.log('Новых необработанных чатов:', newChats.length);
2111 console.log('Всего обработано за сессию:', processedInSession.size);
2112
2113 // Если нет новых чатов - увеличиваем счетчик
2114 if (newChats.length === 0) {
2115 noNewChatsCount++;
2116 console.log(`Новые чаты не появились (${noNewChatsCount}/${maxNoNewChatsAttempts})`);
2117
2118 if (noNewChatsCount >= maxNoNewChatsAttempts) {
2119 console.log('Достигнут конец списка чатов (новые чаты не подгружаются)');
2120 break;
2121 }
2122
2123 // Скроллим список чатов для подгрузки новых
2124 console.log('Нет новых чатов, скроллим для подгрузки...');
2125 await scrollChatList();
2126 // Ждем подгрузки новых чатов
2127 console.log('Ждем 5 секунд для рендеринга новых чатов...');
2128 await sleep(5000);
2129 continue;
2130 } else {
2131 noNewChatsCount = 0; // Сбрасываем счетчик, если появились новые чаты
2132 }
2133
2134 // Обрабатываем новые чаты
2135 for (const chat of newChats) {
2136 // Проверка на паузу
2137 while (isPaused && !shouldStop) {
2138 updateStatus('Пауза сбора базы', `Новых: ${newContacts}, Обновлено: ${updatedContacts}, Пропущено (ЧС): ${skippedBlacklist}`);
2139 await sleep(500);
2140 }
2141
2142 // Проверка на остановку
2143 if (shouldStop) {
2144 break;
2145 }
2146
2147 // Проверка тестового режима
2148 if (testMode && (newContacts >= 1 || updatedContacts >= 1)) {
2149 console.log('Тестовый режим: собран 1 контакт, останавливаем сбор');
2150 shouldStop = true;
2151 break;
2152 }
2153
2154 const chatId = extractChatId(chat);
2155 const chatName = chat.querySelector('.index_chatTitle_TiXTq')?.textContent ||
2156 chat.querySelector('.index_chatTitle_x9txX')?.textContent ||
2157 'Неизвестно';
2158
2159 // Отмечаем чат как обработанный
2160 processedInSession.add(chatId);
2161
2162 // Проверяем черный список
2163 if (blacklist.includes(chatId)) {
2164 console.log(`⛔ Чат в черном списке, пропускаем: ${chatName} (${chatId})`);
2165 skippedBlacklist++;
2166 continue;
2167 }
2168
2169 updateStatus(
2170 `Обработка чата: ${chatName}`,
2171 `Обработано: ${newContacts + updatedContacts}, Новых: ${newContacts}, Обновлено: ${updatedContacts}, Пропущено (ЧС): ${skippedBlacklist}`
2172 );
2173
2174 console.log(`[${newContacts + updatedContacts + 1}] Обрабатываем чат: ${chatName} (${chatId})`);
2175
2176 // Получаем данные чата
2177 const chatData = getChatDataFromElement(chat);
2178
2179 // Проверяем, есть ли уже этот контакт в базе
2180 const existsInDb = db.contacts[chatId] !== undefined;
2181
2182 if (existsInDb) {
2183 // Обновляем существующий контакт
2184 db.contacts[chatId].name = chatData.name;
2185 if (chatData.lastMessageDate) {
2186 db.contacts[chatId].lastMessageDate = chatData.lastMessageDate;
2187 console.log(`✅ Обновлена дата для контакта ${chatData.name}: ${chatData.lastMessageDate}`);
2188 } else {
2189 console.log(`⚠️ Дата не найдена для контакта ${chatData.name}`);
2190 }
2191 updatedContacts++;
2192 console.log(`Обновлен контакт: ${chatData.name} (${chatId})`);
2193
2194 // Удаляем из списка оставшихся для обновления
2195 if (updateExisting && existingContactIds) {
2196 existingContactIds.delete(chatId);
2197 }
2198 } else {
2199 // Добавляем новый контакт
2200 addContactToDatabase(db, chatId, chatData);
2201 newContacts++;
2202 console.log(`Добавлен контакт: ${chatData.name} (${chatId})`);
2203 }
2204
2205 // Увеличиваем счетчик контактов с момента последнего сохранения
2206 contactsSinceLastSave++;
2207
2208 // Периодически сохраняем базу (каждые N контактов)
2209 if (contactsSinceLastSave >= saveEveryNContacts) {
2210 console.log(`Сохранение базы (обработано ${contactsSinceLastSave} контактов с последнего сохранения)...`);
2211 const saved = await saveContactsDatabase(db);
2212 if (saved) {
2213 console.log('✅ База успешно сохранена');
2214 contactsSinceLastSave = 0;
2215 } else {
2216 console.error('❌ Ошибка сохранения базы, продолжаем сбор');
2217 }
2218 }
2219
2220 // Проверка тестового режима - останавливаемся после первого собранного контакта
2221 if (testMode && (newContacts >= 1 || updatedContacts >= 1)) {
2222 break;
2223 }
2224 }
2225
2226 // Обновляем статус
2227 updateStatus(
2228 'Сбор базы контактов...',
2229 `Обработано: ${newContacts + updatedContacts}, Новых: ${newContacts}, Обновлено: ${updatedContacts}, Пропущено (ЧС): ${skippedBlacklist}`
2230 );
2231
2232 // Если тестовый режим и уже собрали контакт - выходим
2233 if (testMode && (newContacts >= 1 || updatedContacts >= 1)) {
2234 break;
2235 }
2236
2237 // Проверяем, появились ли новые чаты после скролла
2238 if (currentChats.length === previousChatCount) {
2239 noNewChatsCount++;
2240 console.log(`Новые чаты не появились (${noNewChatsCount}/${maxNoNewChatsAttempts})`);
2241
2242 if (noNewChatsCount >= maxNoNewChatsAttempts) {
2243 console.log('Достигнут конец списка чатов (новые чаты не подгружаются)');
2244 break;
2245 }
2246
2247 // Скроллим список чатов для подгрузки новых
2248 console.log('Нет новых чатов, скроллим для подгрузки...');
2249 await scrollChatList();
2250 continue;
2251 } else {
2252 noNewChatsCount = 0; // Сбрасываем счетчик, если появились новые чаты
2253 }
2254
2255 previousChatCount = currentChats.length;
2256
2257 // Скроллим для подгрузки следующей порции
2258 const scrollSuccess = await scrollChatList();
2259 if (!scrollSuccess) {
2260 console.error('Ошибка скролла, останавливаем сбор');
2261 break;
2262 }
2263 }
2264
2265 // Финальное сохранение базы
2266 console.log('Финальное сохранение базы данных...');
2267 await saveContactsDatabase(db);
2268
2269 const finalCount = Object.keys(db.contacts).length;
2270 console.log('=== СБОР БАЗЫ ЗАВЕРШЕН ===');
2271 console.log('Всего контактов в базе:', finalCount);
2272 console.log('Было:', initialCount, '| Стало:', finalCount, '| Добавлено:', newContacts, '| Обновлено:', updatedContacts, '| Пропущено (ЧС):', skippedBlacklist);
2273
2274 const statusMessage = testMode
2275 ? `Тестовый сбор завершен! Собрано: ${newContacts + updatedContacts} контакт`
2276 : `Всего в базе: ${finalCount} контактов | Добавлено: ${newContacts} | Обновлено: ${updatedContacts} | Пропущено (ЧС): ${skippedBlacklist}`;
2277
2278 updateStatus('Сбор базы завершен!', statusMessage);
2279
2280 // Сбрасываем флаги
2281 shouldStop = false;
2282
2283 return {
2284 total: finalCount,
2285 added: newContacts,
2286 updated: updatedContacts,
2287 skippedBlacklist: skippedBlacklist
2288 };
2289 }
2290
2291 // Функция для создания модального окна
2292 function createModal() {
2293 const modalHTML = `
2294 <div id="bulkSenderModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; justify-content: center; align-items: center;">
2295 <div style="background: white; padding: 30px; border-radius: 12px; width: 600px; max-width: 90%; max-height: 90vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
2296 <h2 style="margin: 0 0 20px 0; color: #333; font-size: 24px;">Массовая рассылка</h2>
2297
2298 <!-- Блок управления базой данных -->
2299 <div style="margin-bottom: 25px; padding: 20px; background: #f9f9f9; border-radius: 8px; border: 1px solid #e0e0e0;">
2300 <h3 style="margin: 0 0 15px 0; color: #555; font-size: 18px;">📊 База данных контактов</h3>
2301
2302 <div id="dbStatsBlock" style="margin-bottom: 15px; padding: 15px; background: #f0f8ff; border-radius: 6px; border-left: 4px solid #0066cc;">
2303 <div style="font-weight: 600; color: #0066cc; margin-bottom: 10px;">Статистика базы:</div>
2304 <div style="color: #333; font-size: 14px;">Загрузка...</div>
2305 </div>
2306
2307 <div style="margin-bottom: 15px;">
2308 <label style="display: flex; align-items: center; cursor: pointer;">
2309 <input type="checkbox" id="testModeCollect" style="width: 18px; height: 18px; margin-right: 10px; cursor: pointer;">
2310 <span style="color: #555; font-weight: 600; font-size: 14px;">Тестовый режим сбора (только 1 контакт)</span>
2311 </label>
2312 </div>
2313
2314 <div style="display: flex; flex-wrap: wrap; gap: 10px;">
2315 <button id="collectDbButton" style="padding: 10px 18px; background: #4caf50; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">📥 Собрать базу</button>
2316 <button id="updateDbButton" style="padding: 10px 18px; background: #2196f3; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">🔄 Обновить базу</button>
2317 <button id="viewDbButton" style="padding: 10px 18px; background: #9c27b0; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">👁️ Просмотр</button>
2318 <button id="blacklistButton" style="padding: 10px 18px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">🚫 Черный список</button>
2319 <button id="refreshStatsButton" style="padding: 10px 18px; background: #ff9800; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">📊 Обновить статистику</button>
2320 <button id="clearDbButton" style="padding: 10px 18px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">🗑️ Очистить</button>
2321 <button id="pauseDbButton" style="display: none; padding: 10px 18px; background: #ff9800; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">⏸️ Пауза</button>
2322 <button id="stopDbButton" style="display: none; padding: 10px 18px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">⏹️ Стоп</button>
2323 </div>
2324 </div>
2325
2326 <!-- Выбор режима рассылки -->
2327 <div style="margin-bottom: 25px; padding: 20px; background: #f9f9f9; border-radius: 8px; border: 1px solid #e0e0e0;">
2328 <h3 style="margin: 0 0 15px 0; color: #555; font-size: 18px;">🎯 Режим рассылки</h3>
2329 <div style="display: flex; flex-direction: column; gap: 12px;">
2330 <label style="display: flex; align-items: center; cursor: pointer; padding: 12px; background: white; border-radius: 6px; border: 2px solid #ddd; transition: all 0.2s;">
2331 <input type="radio" name="sendingMode" value="database" checked style="width: 18px; height: 18px; margin-right: 12px; cursor: pointer;">
2332 <div>
2333 <div style="color: #333; font-weight: 600; font-size: 14px;">📊 По базе данных</div>
2334 <div style="color: #777; font-size: 12px; margin-top: 4px;">Отправка по собранной базе контактов с фильтром по дате</div>
2335 </div>
2336 </label>
2337 <label style="display: flex; align-items: center; cursor: pointer; padding: 12px; background: white; border-radius: 6px; border: 2px solid #ddd; transition: all 0.2s;">
2338 <input type="radio" name="sendingMode" value="visible" style="width: 18px; height: 18px; margin-right: 12px; cursor: pointer;">
2339 <div>
2340 <div style="color: #333; font-weight: 600; font-size: 14px;">👁️ По видимым чатам</div>
2341 <div style="color: #777; font-size: 12px; margin-top: 4px;">Отправка только по чатам, видимым в списке (с учетом фильтров)</div>
2342 </div>
2343 </label>
2344 </div>
2345 </div>
2346
2347 <div style="margin-bottom: 20px;">
2348 <label style="display: block; margin-bottom: 8px; color: #555; font-weight: 600;">Текст сообщения:</label>
2349 <textarea id="messageText" style="width: 100%; height: 120px; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; font-family: inherit; resize: vertical;" placeholder="Введите текст сообщения..."></textarea>
2350 </div>
2351
2352 <div style="margin-bottom: 25px;">
2353 <label style="display: block; margin-bottom: 8px; color: #555; font-weight: 600;">Дата последнего сообщения (до какой даты отправляли):</label>
2354 <input type="date" id="filterDate" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
2355 <small style="color: #777; display: block; margin-top: 5px; margin-left: 28px;">Будут обработаны чаты с датой последнего сообщения до указанной включительно</small>
2356 </div>
2357
2358 <div style="margin-bottom: 20px;">
2359 <label style="display: flex; align-items: center; cursor: pointer;">
2360 <input type="checkbox" id="testMode" checked style="width: 18px; height: 18px; margin-right: 10px; cursor: pointer;">
2361 <span style="color: #555; font-weight: 600;">Тестовый режим (отправить только 1 сообщение)</span>
2362 </label>
2363 <small style="color: #ff6600; display: block; margin-top: 5px; margin-left: 28px;">⚠️ Рекомендуется для первого запуска</small>
2364 </div>
2365
2366 <div id="statusBlock" style="display: none; margin-bottom: 20px; padding: 15px; background: #f0f8ff; border-radius: 6px; border-left: 4px solid #0066cc;">
2367 <div style="font-weight: 600; color: #0066cc; margin-bottom: 5px;">Статус:</div>
2368 <div id="statusText" style="color: #333;">Готов к запуску</div>
2369 <div id="progressText" style="color: #666; margin-top: 5px; font-size: 13px;"></div>
2370 </div>
2371
2372 <div style="display: flex; gap: 10px; justify-content: flex-end; flex-wrap: wrap;">
2373 <button id="startButton" style="padding: 12px 24px; background: #0066cc; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">Запустить</button>
2374 <button id="pauseButton" style="display: none; padding: 12px 24px; background: #ff9800; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">Пауза</button>
2375 <button id="stopButton" style="display: none; padding: 12px 24px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">Стоп</button>
2376 <button id="emergencyStopButton" style="padding: 12px 24px; background: #d32f2f; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">🚨 Экстренная остановка</button>
2377 <button id="closeButton" style="padding: 12px 24px; background: #999; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;">Закрыть</button>
2378 </div>
2379 </div>
2380 </div>
2381 `;
2382
2383 document.body.insertAdjacentHTML('beforeend', modalHTML);
2384 console.log('Модальное окно создано');
2385
2386 // Обработчики событий
2387 document.getElementById('closeButton').addEventListener('click', closeModal);
2388 document.getElementById('startButton').addEventListener('click', () => {
2389 console.log('!!! КЛИК ПО КНОПКЕ ЗАПУСТИТЬ !!!');
2390 startBulkSending();
2391 });
2392 document.getElementById('pauseButton').addEventListener('click', togglePause);
2393 document.getElementById('stopButton').addEventListener('click', stopBulkSending);
2394
2395 // Обработчик для кнопки черного списка
2396 document.getElementById('blacklistButton').addEventListener('click', openBlacklistModal);
2397
2398 // Обработчики для кнопок управления базой
2399 document.getElementById('collectDbButton').addEventListener('click', async () => {
2400 const testModeCollect = document.getElementById('testModeCollect').checked;
2401 const confirmMessage = testModeCollect
2402 ? 'Начать тестовый сбор базы контактов (только 1 контакт)?'
2403 : 'Начать полный сбор базы контактов? Это может занять некоторое время.';
2404
2405 if (confirm(confirmMessage)) {
2406 // Показываем кнопки паузы и остановки
2407 document.getElementById('collectDbButton').style.display = 'none';
2408 document.getElementById('updateDbButton').style.display = 'none';
2409 document.getElementById('pauseDbButton').style.display = 'inline-block';
2410 document.getElementById('stopDbButton').style.display = 'inline-block';
2411
2412 shouldStop = false;
2413 isPaused = false;
2414 await collectContactsDatabase(false, testModeCollect);
2415
2416 // Скрываем кнопки паузы и остановки
2417 document.getElementById('collectDbButton').style.display = 'inline-block';
2418 document.getElementById('updateDbButton').style.display = 'inline-block';
2419 document.getElementById('pauseDbButton').style.display = 'none';
2420 document.getElementById('stopDbButton').style.display = 'none';
2421
2422 await updateDatabaseStats();
2423 }
2424 });
2425
2426 document.getElementById('updateDbButton').addEventListener('click', async () => {
2427 const testModeCollect = document.getElementById('testModeCollect').checked;
2428 const confirmMessage = testModeCollect
2429 ? 'Обновить базу контактов в тестовом режиме (только 1 контакт)?'
2430 : 'Обновить существующую базу контактов?';
2431
2432 if (confirm(confirmMessage)) {
2433 // Показываем кнопки паузы и остановки
2434 document.getElementById('collectDbButton').style.display = 'none';
2435 document.getElementById('updateDbButton').style.display = 'none';
2436 document.getElementById('pauseDbButton').style.display = 'inline-block';
2437 document.getElementById('stopDbButton').style.display = 'inline-block';
2438
2439 shouldStop = false;
2440 isPaused = false;
2441 await collectContactsDatabase(true, testModeCollect);
2442
2443 // Скрываем кнопки паузы и остановки
2444 document.getElementById('collectDbButton').style.display = 'inline-block';
2445 document.getElementById('updateDbButton').style.display = 'inline-block';
2446 document.getElementById('pauseDbButton').style.display = 'none';
2447 document.getElementById('stopDbButton').style.display = 'none';
2448
2449 await updateDatabaseStats();
2450 }
2451 });
2452
2453 // Обработчик паузы для сбора базы
2454 document.getElementById('pauseDbButton').addEventListener('click', () => {
2455 isPaused = !isPaused;
2456 const pauseDbButton = document.getElementById('pauseDbButton');
2457
2458 if (isPaused) {
2459 pauseDbButton.textContent = '▶️ Продолжить';
2460 pauseDbButton.style.background = '#4caf50';
2461 console.log('Сбор базы приостановлен');
2462 } else {
2463 pauseDbButton.textContent = '⏸️ Пауза';
2464 pauseDbButton.style.background = '#ff9800';
2465 console.log('Сбор базы возобновлен');
2466 }
2467 });
2468
2469 // Обработчик остановки для сбора базы
2470 document.getElementById('stopDbButton').addEventListener('click', () => {
2471 if (confirm('Вы уверены, что хотите остановить сбор базы?')) {
2472 shouldStop = true;
2473 isPaused = false;
2474 console.log('Запрошена остановка сбора базы');
2475 }
2476 });
2477
2478 // Обработчик для кнопки просмотра базы
2479 document.getElementById('viewDbButton').addEventListener('click', openViewDatabaseModal);
2480
2481 // Обработчик для кнопки очистки базы
2482 document.getElementById('clearDbButton').addEventListener('click', async () => {
2483 const cleared = await clearContactsDatabase();
2484 if (cleared) {
2485 await updateDatabaseStats();
2486 alert('База данных успешно очищена!');
2487 }
2488 });
2489
2490 // Обработчик для кнопки обновления статистики
2491 document.getElementById('refreshStatsButton').addEventListener('click', async () => {
2492 console.log('Принудительное обновление статистики');
2493 await updateDatabaseStats();
2494
2495 // Также выводим в консоль для диагностики
2496 const db = await loadContactsDatabase();
2497 const dbString = JSON.stringify(db);
2498 const sizeBytes = dbString.length;
2499 const sizeKB = (sizeBytes / 1024).toFixed(2);
2500 const sizeMB = (sizeBytes / (1024 * 1024)).toFixed(4);
2501 const totalContacts = Object.keys(db.contacts).length;
2502 const avgContactSize = totalContacts > 0 ? (sizeBytes / totalContacts).toFixed(0) : 0;
2503
2504 // Расчет максимального количества контактов (при лимите 50 MB)
2505 const estimatedMaxContacts = totalContacts > 0 ? Math.floor((50 * 1024 * 1024) / (sizeBytes / totalContacts)) : 0;
2506
2507 console.log('=== ДИАГНОСТИКА БАЗЫ ДАННЫХ ===');
2508 console.log('Всего контактов в базе:', totalContacts);
2509 console.log('Размер базы:', sizeKB, 'KB (', sizeMB, 'MB )');
2510 console.log('Средний размер контакта:', avgContactSize, 'байт');
2511 console.log('Примерно поместится контактов (при лимите 50 MB):', estimatedMaxContacts);
2512 console.log('Первые 3 контакта:', Object.values(db.contacts).slice(0, 3));
2513 console.log('');
2514 console.log('📊 ЛИМИТЫ IndexedDB ПО БРАУЗЕРАМ:');
2515 console.log('Chrome/Edge: ~60% свободного места на диске (обычно несколько GB)');
2516 console.log('Firefox: ~50% свободного места на диске (обычно несколько GB)');
2517 console.log('Safari: ~1 GB');
2518 console.log('');
2519 console.log('✅ Ваша база легко поместится! IndexedDB поддерживает гораздо больше данных.');
2520
2521 alert(`📊 Статистика базы данных:\n\nКонтактов: ${totalContacts}\nРазмер: ${sizeKB} KB (${sizeMB} MB)\nСредний размер контакта: ${avgContactSize} байт\n\nПримерно поместится: ~${estimatedMaxContacts.toLocaleString()} контактов\n(при консервативном лимите 50 MB)\n\n✅ IndexedDB поддерживает гораздо больше!\nChrome/Firefox: несколько GB\nSafari: ~1 GB`);
2522 });
2523
2524 // Обработчик для кнопки экстренной остановки
2525 document.getElementById('emergencyStopButton').addEventListener('click', async () => {
2526 if (confirm('Экстренная остановка очистит все активные процессы рассылки. Продолжить?')) {
2527 await clearSendingState();
2528 shouldStop = true;
2529 isPaused = false;
2530 alert('Процесс остановлен! Все активные рассылки прекращены.');
2531 console.log('Экстренная остановка выполнена');
2532
2533 // Обновляем интерфейс
2534 updateStatus('Остановлено', 'Процесс прерван экстренной остановкой');
2535 }
2536 });
2537
2538 // Загружаем статистику при создании модального окна
2539 updateDatabaseStats();
2540 }
2541
2542 // Функция для фильтрации списка контактов
2543 function filterContactsList(searchQuery) {
2544 loadAndDisplayContacts(searchQuery);
2545 }
2546
2547 // Инициализация
2548 async function init() {
2549 console.log('Инициализация расширения');
2550
2551 // Проверяем, что мы на странице мессенджера с покупателями
2552 if (!window.location.href.includes('group=customers_v2')) {
2553 console.log('Не на странице чатов с покупателями, расширение не активно');
2554 return;
2555 }
2556
2557 console.log('Страница подходит, проверяем состояние');
2558
2559 // Проверяем, есть ли сохраненное состояние рассылки
2560 const savedState = await loadSendingState();
2561
2562 if (savedState && savedState.isActive) {
2563 console.log('Обнаружено активное состояние рассылки');
2564
2565 // Проверяем, это рабочая вкладка или вкладка для отправки
2566 const urlParams = new URLSearchParams(window.location.search);
2567 const chatId = urlParams.get('id');
2568
2569 if (chatId) {
2570 // Это вкладка с чатом - проверяем, есть ли этот ID в списке контактов
2571 const isInContactsList = savedState.contactsList.some(contact => contact.id === chatId);
2572
2573 if (isInContactsList) {
2574 console.log('🎯 Это рабочая вкладка для чата:', chatId);
2575 console.log('⚡ СРАЗУ начинаем обработку без ожидания очереди');
2576
2577 // Ждем загрузки страницы
2578 console.log('Ждем загрузки страницы 3 секунды...');
2579 await sleep(3000);
2580 console.log('Ожидание завершено, начинаем обработку');
2581
2582 // Получаем дату последнего сообщения в этом чате
2583 const lastMessageDate = getLastMessageDate();
2584 console.log('Дата последнего сообщения в чате:', lastMessageDate);
2585
2586 // Получаем дату фильтра из состояния
2587 const filterDate = savedState.filterDate ? new Date(savedState.filterDate) : null;
2588 console.log('Дата фильтра из состояния:', filterDate);
2589
2590 // Проверяем дату перед отправкой
2591 let shouldSend = false;
2592 if (lastMessageDate && filterDate) {
2593 console.log('Сравнение дат:');
2594 console.log(' Дата из чата:', lastMessageDate.toLocaleDateString('ru-RU'));
2595 console.log(' Дата фильтра:', filterDate.toLocaleDateString('ru-RU'));
2596 console.log(' Дата из чата <= Дата фильтра?', lastMessageDate <= filterDate);
2597
2598 shouldSend = lastMessageDate <= filterDate;
2599 } else {
2600 console.log('Не удалось получить дату или фильтр, пропускаем отправку');
2601 shouldSend = false;
2602 }
2603
2604 // Отправляем сообщение только если дата подходит
2605 let success = false;
2606 if (shouldSend) {
2607 console.log('✅ Дата подходит, отправляем сообщение');
2608 success = await sendMessage(savedState.messageText);
2609 } else {
2610 console.log('⏭️ Пропускаем отправку (дата не подходит)');
2611 success = false;
2612 }
2613
2614 // Обновляем состояние АТОМАРНО
2615 const currentState = await loadSendingState();
2616
2617 if (!currentState || !currentState.isActive) {
2618 console.log('Рассылка завершена, закрываем вкладку');
2619 await sleep(300);
2620 window.close();
2621 return;
2622 }
2623
2624 if (success) {
2625 console.log('✅ Сообщение отправлено');
2626
2627 // Обновляем базу данных
2628 const db = await loadContactsDatabase();
2629 if (db.contacts[chatId]) {
2630 db.contacts[chatId].lastSentDate = new Date().toISOString();
2631 db.contacts[chatId].messageCount = (db.contacts[chatId].messageCount || 0) + 1;
2632 await saveContactsDatabase(db);
2633 }
2634
2635 currentState.sent++;
2636 } else {
2637 console.error('❌ Ошибка отправки или дата не подходит');
2638 currentState.skipped++;
2639 }
2640
2641 currentState.processed++;
2642
2643 // Проверяем, нужно ли завершать
2644 if (currentState.testMode && currentState.sent >= 1) {
2645 console.log('Тестовый режим: достигнут лимит, завершаем');
2646 await clearSendingState();
2647 await sleep(300);
2648 window.close();
2649 return;
2650 }
2651
2652 if (currentState.processed >= currentState.totalContacts) {
2653 console.log('Все контакты обработаны, завершаем');
2654 await clearSendingState();
2655 await sleep(300);
2656 window.close();
2657 return;
2658 }
2659
2660 // Обновляем currentContactId на следующий контакт
2661 const nextContact = currentState.contactsList[currentState.processed];
2662 if (nextContact) {
2663 const oldContactId = currentState.currentContactId;
2664 currentState.currentContactId = nextContact.id;
2665 console.log('🔄 Обновлен currentContactId:', oldContactId, '->', nextContact.id);
2666 console.log(' Следующий контакт:', nextContact.name);
2667 }
2668
2669 // Сохраняем обновленное состояние
2670 await saveSendingState(currentState);
2671 console.log('✅ Состояние сохранено');
2672
2673 // Закрываем текущую вкладку
2674 console.log('Закрываем текущую вкладку');
2675 await sleep(300);
2676 window.close();
2677
2678 return; // Не создаем интерфейс
2679 }
2680
2681 console.log('Это главная вкладка (ID чата не в списке контактов)');
2682 // Продолжаем создание интерфейса ниже
2683 } else {
2684 console.log('Это главная вкладка, ожидаем результатов от рабочих вкладок');
2685
2686 // Создаем интерфейс и показываем прогресс
2687 createFloatingButton();
2688 createModal();
2689 openModal();
2690
2691 // Запускаем мониторинг состояния
2692 monitorSendingProgress();
2693
2694 return;
2695 }
2696 }
2697
2698 // Создаем плавающую кнопку сразу
2699 createFloatingButton();
2700 createModal();
2701
2702 console.log('Интерфейс создан');
2703 }
2704
2705 // Запуск при загрузке страницы
2706 if (document.readyState === 'loading') {
2707 document.addEventListener('DOMContentLoaded', init);
2708 } else {
2709 init();
2710 }
2711})();