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