Массовая рассылка сообщений покупателям в Ozon Messenger
Size
90.0 KB
Version
1.1.111
Created
Jan 27, 2026
Updated
about 1 month ago
1// ==UserScript==
2// @name Ozon BASE Messenger Bulk Sender
3// @description Массовая рассылка сообщений покупателям в Ozon Messenger
4// @version 1.1.111
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 isRunning = false;
17 let isPaused = false;
18 let shouldStop = false;
19
20 // ============= ФУНКЦИИ ДЛЯ РАБОТЫ С СОСТОЯНИЕМ РАССЫЛКИ =============
21
22 // Сохранение состояния рассылки
23 async function saveSendingState(state) {
24 try {
25 await GM.setValue('sendingState', JSON.stringify(state));
26 console.log('Состояние рассылки сохранено:', state);
27 return true;
28 } catch (error) {
29 console.error('Ошибка сохранения состояния:', error);
30 return false;
31 }
32 }
33
34 // Загрузка состояния рассылки
35 async function loadSendingState() {
36 try {
37 const stateString = await GM.getValue('sendingState', null);
38 if (stateString) {
39 const state = JSON.parse(stateString);
40 console.log('Состояние рассылки загружено:', state);
41 return state;
42 }
43 } catch (error) {
44 console.error('Ошибка загрузки состояния:', error);
45 }
46 return null;
47 }
48
49 // Очистка состояния рассылки
50 async function clearSendingState() {
51 await GM.setValue('sendingState', null);
52 console.log('Состояние рассылки очищено');
53 }
54
55 // Загрузка базы данных контактов
56 async function loadContactsDatabase() {
57 try {
58 const dbString = await GM.getValue('contactsDatabase', null);
59 if (dbString) {
60 const db = JSON.parse(dbString);
61 console.log('База данных загружена:', Object.keys(db.contacts || {}).length, 'контактов');
62 return db;
63 }
64 } catch (error) {
65 console.error('Ошибка загрузки базы данных:', error);
66 }
67
68 // Возвращаем пустую базу, если её нет
69 return {
70 contacts: {},
71 lastUpdate: null,
72 totalContacts: 0
73 };
74 }
75
76 // Сохранение базы данных контактов
77 async function saveContactsDatabase(db) {
78 try {
79 db.lastUpdate = new Date().toISOString();
80 db.totalContacts = Object.keys(db.contacts).length;
81 await GM.setValue('contactsDatabase', JSON.stringify(db));
82 console.log('База данных сохранена:', db.totalContacts, 'контактов');
83 return true;
84 } catch (error) {
85 console.error('Ошибка сохранения базы данных:', error);
86 return false;
87 }
88 }
89
90 // Добавление контакта в базу
91 function addContactToDatabase(db, contactId, contactData) {
92 db.contacts[contactId] = {
93 id: contactId,
94 name: contactData.name || 'Неизвестно',
95 lastMessageDate: contactData.lastMessageDate || null,
96 lastSentDate: contactData.lastSentDate || null,
97 addedAt: contactData.addedAt || new Date().toISOString(),
98 messageCount: contactData.messageCount || 0
99 };
100 }
101
102 // Получение статистики базы данных
103 function getDatabaseStats(db) {
104 const total = Object.keys(db.contacts).length;
105 const withDates = Object.values(db.contacts).filter(c => c.lastMessageDate).length;
106 const sent = Object.values(db.contacts).filter(c => c.lastSentDate).length;
107
108 return {
109 total,
110 withDates,
111 sent,
112 notSent: total - sent,
113 lastUpdate: db.lastUpdate
114 };
115 }
116
117 // Очистка базы данных
118 async function clearContactsDatabase() {
119 if (confirm('Вы уверены, что хотите удалить всю базу контактов? Это действие нельзя отменить.')) {
120 await GM.setValue('contactsDatabase', null);
121 console.log('База данных очищена');
122 return true;
123 }
124 return false;
125 }
126
127 // Удаление дубликатов из базы данных
128 async function removeDuplicatesFromDatabase() {
129 const db = await loadContactsDatabase();
130 const initialCount = Object.keys(db.contacts).length;
131
132 // Дубликаты уже не могут существовать, т.к. мы используем ID как ключ
133 // Но проверим на всякий случай
134 console.log('Проверка дубликатов в базе данных...');
135 console.log('Всего контактов:', initialCount);
136
137 // Дубликатов быть не может, т.к. используется объект с уникальными ключами
138 alert(`Проверка завершена!\nВсего контактов: ${initialCount}\nДубликатов не обнаружено (используются уникальные ID)`);
139
140 return {
141 initial: initialCount,
142 final: initialCount,
143 removed: 0
144 };
145 }
146
147 // Функция для создания модального окна просмотра базы
148 function createViewDatabaseModal() {
149 // Проверяем, не создано ли уже окно
150 if (document.getElementById('viewDbModal')) {
151 return;
152 }
153
154 const modalHTML = `
155 <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;">
156 <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);">
157 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
158 <h2 style="margin: 0; color: #333; font-size: 24px;">👁️ Просмотр базы контактов</h2>
159 <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>
160 </div>
161
162 <div id="viewDbStatsBlock" style="margin-bottom: 20px; padding: 15px; background: #f0f8ff; border-radius: 6px; border-left: 4px solid #0066cc;">
163 <div style="font-weight: 600; color: #0066cc; margin-bottom: 10px;">Статистика базы:</div>
164 <div id="viewDbStatsContent" style="color: #333; font-size: 14px;"></div>
165 </div>
166
167 <div style="margin-bottom: 15px; display: flex; gap: 10px; align-items: center;">
168 <input type="text" id="searchContactInput" placeholder="Поиск по имени..." style="flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
169 <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>
170 </div>
171
172 <div id="contactsListContainer" style="max-height: 500px; overflow-y: auto; border: 1px solid #ddd; border-radius: 6px;">
173 <div id="contactsList" style="padding: 10px;"></div>
174 </div>
175 </div>
176 </div>
177 `;
178
179 document.body.insertAdjacentHTML('beforeend', modalHTML);
180 console.log('Модальное окно просмотра базы создано');
181
182 // Обработчики событий
183 document.getElementById('closeViewDbButton').addEventListener('click', closeViewDatabaseModal);
184 document.getElementById('removeDuplicatesButton').addEventListener('click', async () => {
185 const result = await removeDuplicatesFromDatabase();
186 await loadAndDisplayContacts();
187 });
188
189 // Поиск по контактам
190 document.getElementById('searchContactInput').addEventListener('input', (e) => {
191 filterContactsList(e.target.value);
192 });
193 }
194
195 // Функция для открытия модального окна просмотра базы
196 async function openViewDatabaseModal() {
197 createViewDatabaseModal();
198 const modal = document.getElementById('viewDbModal');
199 if (modal) {
200 modal.style.display = 'flex';
201 await loadAndDisplayContacts();
202 console.log('Модальное окно просмотра базы открыто');
203 }
204 }
205
206 // Функция для закрытия модального окна просмотра базы
207 function closeViewDatabaseModal() {
208 const modal = document.getElementById('viewDbModal');
209 if (modal) {
210 modal.style.display = 'none';
211 console.log('Модальное окно просмотра базы закрыто');
212 }
213 }
214
215 // Функция для загрузки и отображения контактов
216 async function loadAndDisplayContacts(searchQuery = '') {
217 const db = await loadContactsDatabase();
218 const stats = getDatabaseStats(db);
219 const contacts = Object.values(db.contacts);
220
221 // Обновляем статистику
222 const statsContent = document.getElementById('viewDbStatsContent');
223 if (statsContent) {
224 statsContent.innerHTML = `
225 <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;">
226 <div><strong>Всего контактов:</strong> ${stats.total}</div>
227 <div><strong>С датами:</strong> ${stats.withDates}</div>
228 <div><strong>Отправлено:</strong> ${stats.sent}</div>
229 <div><strong>Не отправлено:</strong> ${stats.notSent}</div>
230 </div>
231 `;
232 }
233
234 // Фильтруем контакты по поисковому запросу
235 const filteredContacts = searchQuery
236 ? contacts.filter(c => c.name.toLowerCase().includes(searchQuery.toLowerCase()))
237 : contacts;
238
239 // Сортируем по дате последнего сообщения (новые сверху)
240 filteredContacts.sort((a, b) => {
241 if (!a.lastMessageDate) return 1;
242 if (!b.lastMessageDate) return -1;
243 return new Date(b.lastMessageDate) - new Date(a.lastMessageDate);
244 });
245
246 // Отображаем список контактов
247 const contactsList = document.getElementById('contactsList');
248 if (contactsList) {
249 if (filteredContacts.length === 0) {
250 contactsList.innerHTML = '<div style="text-align: center; padding: 40px; color: #999;">Контакты не найдены</div>';
251 } else {
252 contactsList.innerHTML = filteredContacts.map(contact => {
253 const lastMsgDate = contact.lastMessageDate
254 ? new Date(contact.lastMessageDate).toLocaleDateString('ru-RU')
255 : 'Нет данных';
256 const lastSentDate = contact.lastSentDate
257 ? new Date(contact.lastSentDate).toLocaleDateString('ru-RU')
258 : 'Не отправлялось';
259 const sentStatus = contact.lastSentDate ? '✅' : '⏳';
260
261 return `
262 <div style="padding: 12px; margin-bottom: 8px; background: ${contact.lastSentDate ? '#f0f8ff' : '#fff8f0'}; border: 1px solid #ddd; border-radius: 6px; font-size: 13px;">
263 <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
264 <div style="font-weight: 600; color: #333; font-size: 14px;">${sentStatus} ${contact.name}</div>
265 <div style="font-size: 11px; color: #999;">${contact.id.substring(0, 8)}...</div>
266 </div>
267 <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; color: #666;">
268 <div><strong>Последнее сообщение:</strong> ${lastMsgDate}</div>
269 <div><strong>Отправлено:</strong> ${lastSentDate}</div>
270 </div>
271 ${contact.messageCount > 0 ? `<div style="margin-top: 5px; color: #666;"><strong>Отправлено сообщений:</strong> ${contact.messageCount}</div>` : ''}
272 </div>
273 `;
274 }).join('');
275 }
276 }
277 }
278
279 // Функция для фильтрации списка контактов
280 function filterContactsList(searchQuery) {
281 loadAndDisplayContacts(searchQuery);
282 }
283
284 // ============= ФУНКЦИИ ДЛЯ СБОРА КОНТАКТОВ =============
285
286 // Функция для извлечения ID из URL чата
287 function extractChatId(chatElement) {
288 // Ищем атрибут deeplink в элементе чата
289 const deeplink = chatElement.getAttribute('deeplink');
290 if (deeplink) {
291 const match = deeplink.match(/id=([a-f0-9-]+)/);
292 if (match) {
293 console.log('Извлечен ID из deeplink:', match[1]);
294 return match[1];
295 }
296 }
297
298 // Запасной вариант - ищем ссылку внутри элемента
299 const link = chatElement.querySelector('a[href*="id="]');
300 if (link) {
301 const href = link.getAttribute('href');
302 const match = href.match(/id=([a-f0-9-]+)/);
303 if (match) {
304 console.log('Извлечен ID из ссылки:', match[1]);
305 return match[1];
306 }
307 }
308
309 console.log('Не удалось извлечь ID чата');
310 return null;
311 }
312
313 // Функция для получения данных чата из элемента списка
314 function getChatDataFromElement(chatElement) {
315 const chatName = chatElement.querySelector('.index_chatTitle_TiXTq')?.textContent?.trim() || 'Неизвестно';
316 const dateElement = chatElement.querySelector('.index_chatDate_z4mNc, .index_chatDate_WJ-\\+mb');
317 const dateText = dateElement?.textContent?.trim();
318 const lastMessageDate = dateText ? parseDate(dateText) : null;
319
320 return {
321 name: chatName,
322 lastMessageDate: lastMessageDate ? lastMessageDate.toISOString() : null,
323 lastSentDate: null,
324 messageCount: 0
325 };
326 }
327
328 // Функция для скролла списка чатов
329 async function scrollChatList() {
330 // Сначала скроллим всю страницу вниз
331 window.scrollTo(0, document.body.scrollHeight);
332 console.log('Проскроллили страницу вниз');
333
334 // Небольшая пауза
335 await sleep(500);
336
337 // Проверяем, применен ли фильтр Ozon (ищем кнопку сброса фильтра)
338 const clearFilterBtn = document.querySelector('.c8s110-a2, .c8s110-c0, .c8s110-a4, .c8s110-a5');
339 let filterWasActive = false;
340
341 if (clearFilterBtn) {
342 console.log('Обнаружен активный фильтр Ozon, временно сбрасываем');
343 filterWasActive = true;
344 clearFilterBtn.click();
345 await sleep(1000); // Ждем сброса фильтра
346 }
347
348 // Получаем все чаты
349 const allChats = document.querySelectorAll('.index_chat_4fr82');
350
351 if (allChats.length === 0) {
352 console.error('Чаты не найдены для скролла');
353 return false;
354 }
355
356 // Находим последний ВИДИМЫЙ чат (не скрытый фильтром)
357 const visibleChats = Array.from(allChats).filter(chat => {
358 const style = window.getComputedStyle(chat);
359 return style.display !== 'none' && chat.offsetParent !== null;
360 });
361
362 if (visibleChats.length === 0) {
363 console.error('Нет видимых чатов для скролла');
364 return false;
365 }
366
367 // Берем последний видимый чат и скроллим к нему
368 const lastVisibleChat = visibleChats[visibleChats.length - 1];
369 console.log('Скроллим к последнему видимому чату, всего видимых чатов:', visibleChats.length, 'из', allChats.length);
370
371 lastVisibleChat.scrollIntoView({ behavior: 'smooth', block: 'end' });
372
373 // Ждем подгрузки новых чатов
374 await sleep(2500);
375
376 // Если фильтр был активен, применяем его обратно
377 if (filterWasActive) {
378 console.log('Применяем фильтр Ozon обратно');
379 // Находим кнопку "Применить" фильтра и кликаем
380 const applyFilterBtn = document.querySelector('button[type="submit"]');
381 if (applyFilterBtn) {
382 applyFilterBtn.click();
383 await sleep(1000);
384 }
385 }
386
387 console.log('Скролл выполнен');
388 return true;
389 }
390
391 // Основная функция сбора базы контактов
392 async function collectContactsDatabase(updateExisting = false, testMode = false) {
393 console.log('=== НАЧАЛО СБОРА БАЗЫ КОНТАКТОВ ===');
394 console.log('Режим:', updateExisting ? 'Обновление существующей базы' : 'Полный сбор');
395 console.log('Тестовый режим:', testMode ? 'ДА (только 1 контакт)' : 'НЕТ');
396
397 // Загружаем существующую базу
398 const db = await loadContactsDatabase();
399 const initialCount = Object.keys(db.contacts).length;
400 console.log('Начальное количество контактов в базе:', initialCount);
401
402 // Трекер обработанных ID в этой сессии
403 const processedInSession = new Set();
404
405 let newContacts = 0;
406 let updatedContacts = 0;
407 let scrollAttempts = 0;
408 let previousChatCount = 0;
409 let noNewChatsCount = 0;
410 const maxNoNewChatsAttempts = 3; // Если 3 раза подряд не появились новые чаты - останавливаемся
411
412 while (!shouldStop) {
413 // Проверка на паузу
414 while (isPaused && !shouldStop) {
415 updateStatus('Пауза сбора базы', `Новых: ${newContacts}, Обновлено: ${updatedContacts}`);
416 await sleep(500);
417 }
418
419 if (shouldStop) {
420 console.log('Сбор базы остановлен пользователем');
421 break;
422 }
423
424 // Получаем текущие чаты
425 const currentChats = getAllChats();
426 console.log(`Попытка ${scrollAttempts + 1}: найдено ${currentChats.length} чатов в DOM`);
427
428 // Обрабатываем каждый чат
429 for (const chatElement of currentChats) {
430 const chatId = extractChatId(chatElement);
431
432 if (!chatId) {
433 console.log('Не удалось извлечь ID чата, пропускаем');
434 continue;
435 }
436
437 // Пропускаем, если уже обработали в этой сессии
438 if (processedInSession.has(chatId)) {
439 continue;
440 }
441
442 // Проверяем, есть ли уже этот контакт в базе
443 const existsInDb = db.contacts[chatId] !== undefined;
444
445 // В режиме обновления - пропускаем контакты, которых нет в базе
446 if (updateExisting && !existsInDb) {
447 console.log(`Режим обновления: контакт ${chatId} не в базе, пропускаем`);
448 continue;
449 }
450
451 // В режиме полного сбора - пропускаем контакты, которые уже есть
452 if (!updateExisting && existsInDb) {
453 console.log(`Режим полного сбора: контакт ${chatId} уже в базе, пропускаем`);
454 continue;
455 }
456
457 // Получаем данные чата
458 const chatData = getChatDataFromElement(chatElement);
459
460 if (existsInDb) {
461 // Обновляем существующий контакт
462 db.contacts[chatId].name = chatData.name;
463 db.contacts[chatId].lastMessageDate = chatData.lastMessageDate;
464 updatedContacts++;
465 console.log(`Обновлен контакт: ${chatData.name} (${chatId})`);
466 } else {
467 // Добавляем новый контакт
468 addContactToDatabase(db, chatId, chatData);
469 newContacts++;
470 console.log(`Добавлен контакт: ${chatData.name} (${chatId})`);
471 }
472
473 // Отмечаем как обработанный в этой сессии
474 processedInSession.add(chatId);
475
476 // Проверка тестового режима - останавливаемся после первого собранного контакта
477 if (testMode && (newContacts >= 1 || updatedContacts >= 1)) {
478 console.log('Тестовый режим: собран 1 контакт, останавливаем сбор');
479 shouldStop = true;
480 break;
481 }
482 }
483
484 // Обновляем статус
485 updateStatus(
486 'Сбор базы контактов...',
487 `Новых: ${newContacts} | Обновлено: ${updatedContacts} | Попыток скролла: ${scrollAttempts}`
488 );
489
490 // Если тестовый режим и уже собрали контакт - выходим
491 if (testMode && (newContacts >= 1 || updatedContacts >= 1)) {
492 break;
493 }
494
495 // Проверяем, появились ли новые чаты после скролла
496 if (currentChats.length === previousChatCount) {
497 noNewChatsCount++;
498 console.log(`Новые чаты не появились (${noNewChatsCount}/${maxNoNewChatsAttempts})`);
499
500 if (noNewChatsCount >= maxNoNewChatsAttempts) {
501 console.log('Достигнут конец списка чатов (новые чаты не подгружаются)');
502 break;
503 }
504
505 // Скроллим список чатов для подгрузки новых
506 console.log('Нет новых чатов, скроллим для подгрузки...');
507 await scrollChatList();
508 continue;
509 } else {
510 noNewChatsCount = 0; // Сбрасываем счетчик, если появились новые чаты
511 }
512
513 previousChatCount = currentChats.length;
514
515 // Скроллим для подгрузки следующей порции
516 const scrollSuccess = await scrollChatList();
517 if (!scrollSuccess) {
518 console.error('Ошибка скролла, останавливаем сбор');
519 break;
520 }
521
522 scrollAttempts++;
523
524 // Сохраняем базу каждые 50 контактов
525 if ((newContacts + updatedContacts) % 50 === 0 && (newContacts + updatedContacts) > 0) {
526 await saveContactsDatabase(db);
527 console.log('Промежуточное сохранение базы данных');
528 }
529 }
530
531 // Финальное сохранение базы
532 await saveContactsDatabase(db);
533
534 const finalCount = Object.keys(db.contacts).length;
535 console.log('=== СБОР БАЗЫ ЗАВЕРШЕН ===');
536 console.log('Всего контактов в базе:', finalCount);
537 console.log('Было:', initialCount, '| Стало:', finalCount, '| Добавлено:', newContacts, '| Обновлено:', updatedContacts);
538
539 const statusMessage = testMode
540 ? `Тестовый сбор завершен! Собрано: ${newContacts + updatedContacts} контакт`
541 : `Всего в базе: ${finalCount} контактов | Добавлено: ${newContacts} | Обновлено: ${updatedContacts}`;
542
543 updateStatus('Сбор базы завершен!', statusMessage);
544
545 // Сбрасываем флаги
546 shouldStop = false;
547
548 return {
549 total: finalCount,
550 added: newContacts,
551 updated: updatedContacts
552 };
553 }
554
555 // Функция для создания модального окна
556 function createModal() {
557 const modalHTML = `
558 <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;">
559 <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);">
560 <h2 style="margin: 0 0 20px 0; color: #333; font-size: 24px;">Массовая рассылка</h2>
561
562 <!-- Выбор режима рассылки -->
563 <div style="margin-bottom: 25px; padding: 20px; background: #f9f9f9; border-radius: 8px; border: 1px solid #e0e0e0;">
564 <h3 style="margin: 0 0 15px 0; color: #555; font-size: 18px;">🎯 Режим рассылки</h3>
565 <div style="display: flex; flex-direction: column; gap: 12px;">
566 <label style="display: flex; align-items: center; cursor: pointer; padding: 12px; background: white; border-radius: 6px; border: 2px solid #ddd; transition: all 0.2s;">
567 <input type="radio" name="sendingMode" value="database" checked style="width: 18px; height: 18px; margin-right: 12px; cursor: pointer;">
568 <div>
569 <div style="color: #333; font-weight: 600; font-size: 14px;">📊 По базе данных</div>
570 <div style="color: #777; font-size: 12px; margin-top: 4px;">Отправка по собранной базе контактов с фильтром по дате</div>
571 </div>
572 </label>
573 <label style="display: flex; align-items: center; cursor: pointer; padding: 12px; background: white; border-radius: 6px; border: 2px solid #ddd; transition: all 0.2s;">
574 <input type="radio" name="sendingMode" value="visible" style="width: 18px; height: 18px; margin-right: 12px; cursor: pointer;">
575 <div>
576 <div style="color: #333; font-weight: 600; font-size: 14px;">👁️ По видимым чатам</div>
577 <div style="color: #777; font-size: 12px; margin-top: 4px;">Отправка только по чатам, видимым в списке (с учетом фильтров)</div>
578 </div>
579 </label>
580 </div>
581 </div>
582
583 <!-- Блок управления базой данных -->
584 <div style="margin-bottom: 25px; padding: 20px; background: #f0f8ff; border-radius: 8px; border: 1px solid #d0e8ff;">
585 <h3 style="margin: 0 0 15px 0; color: #0066cc; font-size: 18px;">📊 Управление базой контактов</h3>
586
587 <div id="dbStatsBlock" style="margin-bottom: 15px; padding: 12px; background: white; border-radius: 6px; font-size: 13px; color: #333;">
588 Загрузка статистики...
589 </div>
590
591 <div style="display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 15px;">
592 <button id="collectDbButton" style="flex: 1; min-width: 140px; padding: 10px 16px; background: #4caf50; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">📥 Собрать базу</button>
593 <button id="updateDbButton" style="flex: 1; min-width: 140px; padding: 10px 16px; background: #2196f3; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">🔄 Обновить базу</button>
594 <button id="viewDbButton" style="flex: 1; min-width: 140px; padding: 10px 16px; background: #ff9800; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">👁️ Просмотр базы</button>
595 <button id="emergencyStopButton" style="flex: 1; min-width: 140px; padding: 10px 16px; background: #d32f2f; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">🚨 Экстренная остановка</button>
596 <button id="pauseDbButton" style="display: none; flex: 1; min-width: 140px; padding: 10px 16px; background: #ff9800; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">⏸️ Пауза</button>
597 <button id="stopDbButton" style="display: none; flex: 1; min-width: 140px; padding: 10px 16px; background: #f44336; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 600;">⏹️ Стоп</button>
598 </div>
599
600 <div style="display: flex; align-items: center; gap: 10px;">
601 <input type="checkbox" id="testModeCollect" style="width: 16px; height: 16px; cursor: pointer;">
602 <label for="testModeCollect" style="color: #555; font-size: 13px; cursor: pointer;">Тестовый режим (собрать только 1 контакт)</label>
603 </div>
604 </div>
605
606 <div style="margin-bottom: 20px;">
607 <label style="display: block; margin-bottom: 8px; color: #555; font-weight: 600;">Текст сообщения:</label>
608 <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>
609 </div>
610
611 <div style="margin-bottom: 25px;">
612 <label style="display: block; margin-bottom: 8px; color: #555; font-weight: 600;">Дата последнего сообщения (до какой даты отправляли):</label>
613 <input type="date" id="filterDate" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
614 <small style="color: #777; display: block; margin-top: 5px;">Будут обработаны чаты с датой последнего сообщения до указанной включительно</small>
615 </div>
616
617 <div style="margin-bottom: 20px;">
618 <label style="display: flex; align-items: center; cursor: pointer;">
619 <input type="checkbox" id="testMode" checked style="width: 18px; height: 18px; margin-right: 10px; cursor: pointer;">
620 <span style="color: #555; font-weight: 600;">Тестовый режим (отправить только 1 сообщение)</span>
621 </label>
622 <small style="color: #ff6600; display: block; margin-top: 5px; margin-left: 28px;">⚠️ Рекомендуется для первого запуска</small>
623 </div>
624
625 <div id="statusBlock" style="display: none; margin-bottom: 20px; padding: 15px; background: #f0f8ff; border-radius: 6px; border-left: 4px solid #0066cc;">
626 <div style="font-weight: 600; color: #0066cc; margin-bottom: 5px;">Статус:</div>
627 <div id="statusText" style="color: #333;">Готов к запуску</div>
628 <div id="progressText" style="color: #666; margin-top: 5px; font-size: 13px;"></div>
629 </div>
630
631 <div style="display: flex; gap: 10px; justify-content: flex-end;">
632 <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>
633 <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>
634 <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>
635 <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>
636 </div>
637 </div>
638 </div>
639 `;
640
641 document.body.insertAdjacentHTML('beforeend', modalHTML);
642 console.log('Модальное окно создано');
643
644 // Обработчики событий
645 document.getElementById('closeButton').addEventListener('click', closeModal);
646 document.getElementById('startButton').addEventListener('click', () => {
647 console.log('!!! КЛИК ПО КНОПКЕ ЗАПУСТИТЬ !!!');
648 startBulkSending();
649 });
650 document.getElementById('pauseButton').addEventListener('click', togglePause);
651 document.getElementById('stopButton').addEventListener('click', stopBulkSending);
652
653 // Обработчики для кнопок управления базой
654 document.getElementById('collectDbButton').addEventListener('click', async () => {
655 const testModeCollect = document.getElementById('testModeCollect').checked;
656 const confirmMessage = testModeCollect
657 ? 'Начать тестовый сбор базы контактов (только 1 контакт)?'
658 : 'Начать полный сбор базы контактов? Это может занять некоторое время.';
659
660 if (confirm(confirmMessage)) {
661 // Показываем кнопки паузы и остановки
662 document.getElementById('collectDbButton').style.display = 'none';
663 document.getElementById('updateDbButton').style.display = 'none';
664 document.getElementById('pauseDbButton').style.display = 'inline-block';
665 document.getElementById('stopDbButton').style.display = 'inline-block';
666
667 isRunning = true;
668 shouldStop = false;
669 isPaused = false;
670 await collectContactsDatabase(false, testModeCollect);
671 isRunning = false;
672
673 // Скрываем кнопки паузы и остановки
674 document.getElementById('collectDbButton').style.display = 'inline-block';
675 document.getElementById('updateDbButton').style.display = 'inline-block';
676 document.getElementById('pauseDbButton').style.display = 'none';
677 document.getElementById('stopDbButton').style.display = 'none';
678
679 await updateDatabaseStats();
680 }
681 });
682
683 document.getElementById('updateDbButton').addEventListener('click', async () => {
684 const testModeCollect = document.getElementById('testModeCollect').checked;
685 const confirmMessage = testModeCollect
686 ? 'Обновить базу контактов в тестовом режиме (только 1 контакт)?'
687 : 'Обновить существующую базу контактов?';
688
689 if (confirm(confirmMessage)) {
690 // Показываем кнопки паузы и остановки
691 document.getElementById('collectDbButton').style.display = 'none';
692 document.getElementById('updateDbButton').style.display = 'none';
693 document.getElementById('pauseDbButton').style.display = 'inline-block';
694 document.getElementById('stopDbButton').style.display = 'inline-block';
695
696 isRunning = true;
697 shouldStop = false;
698 isPaused = false;
699 await collectContactsDatabase(true, testModeCollect);
700 isRunning = false;
701
702 // Скрываем кнопки паузы и остановки
703 document.getElementById('collectDbButton').style.display = 'inline-block';
704 document.getElementById('updateDbButton').style.display = 'inline-block';
705 document.getElementById('pauseDbButton').style.display = 'none';
706 document.getElementById('stopDbButton').style.display = 'none';
707
708 await updateDatabaseStats();
709 }
710 });
711
712 // Обработчик паузы для сбора базы
713 document.getElementById('pauseDbButton').addEventListener('click', () => {
714 isPaused = !isPaused;
715 const pauseDbButton = document.getElementById('pauseDbButton');
716
717 if (isPaused) {
718 pauseDbButton.textContent = '▶️ Продолжить';
719 pauseDbButton.style.background = '#4caf50';
720 console.log('Сбор базы приостановлен');
721 } else {
722 pauseDbButton.textContent = '⏸️ Пауза';
723 pauseDbButton.style.background = '#ff9800';
724 console.log('Сбор базы возобновлен');
725 }
726 });
727
728 // Обработчик остановки для сбора базы
729 document.getElementById('stopDbButton').addEventListener('click', () => {
730 if (confirm('Вы уверены, что хотите остановить сбор базы?')) {
731 shouldStop = true;
732 isPaused = false;
733 console.log('Запрошена остановка сбора базы');
734 }
735 });
736
737 // Обработчик для кнопки просмотра базы
738 document.getElementById('viewDbButton').addEventListener('click', openViewDatabaseModal);
739
740 // Обработчик для кнопки экстренной остановки
741 document.getElementById('emergencyStopButton').addEventListener('click', async () => {
742 if (confirm('Экстренная остановка очистит все активные процессы рассылки. Продолжить?')) {
743 await clearSendingState();
744 shouldStop = true;
745 isPaused = false;
746 isRunning = false;
747 alert('Процесс остановлен! Все активные рассылки прекращены.');
748 console.log('Экстренная остановка выполнена');
749
750 // Обновляем интерфейс
751 updateStatus('Остановлено', 'Процесс прерван экстренной остановкой');
752 }
753 });
754
755 // Загружаем статистику при создании модального окна
756 updateDatabaseStats();
757 }
758
759 // Функция для обновления статистики базы данных в интерфейсе
760 async function updateDatabaseStats() {
761 const db = await loadContactsDatabase();
762 const stats = getDatabaseStats(db);
763 const statsBlock = document.getElementById('dbStatsBlock');
764
765 if (statsBlock) {
766 const lastUpdateText = stats.lastUpdate
767 ? new Date(stats.lastUpdate).toLocaleString('ru-RU')
768 : 'Никогда';
769
770 statsBlock.innerHTML = `
771 <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px;">
772 <div><strong>Всего контактов:</strong> ${stats.total}</div>
773 <div><strong>С датами:</strong> ${stats.withDates}</div>
774 <div><strong>Отправлено:</strong> ${stats.sent}</div>
775 <div><strong>Не отправлено:</strong> ${stats.notSent}</div>
776 </div>
777 <div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #e0e0e0; font-size: 12px; color: #999;">
778 Последнее обновление: ${lastUpdateText}
779 </div>
780 `;
781 }
782 }
783
784 // Функция для открытия модального окна
785 function openModal() {
786 const modal = document.getElementById('bulkSenderModal');
787 if (modal) {
788 modal.style.display = 'flex';
789 console.log('Модальное окно открыто');
790 }
791 }
792
793 // Функция для закрытия модального окна
794 function closeModal() {
795 const modal = document.getElementById('bulkSenderModal');
796 if (modal && !isRunning) {
797 modal.style.display = 'none';
798 console.log('Модальное окно закрыто');
799 } else if (isRunning) {
800 alert('Дождитесь завершения рассылки или нажмите "Стоп"');
801 }
802 }
803
804 // Функция для обновления статуса
805 function updateStatus(status, progress = '') {
806 const statusBlock = document.getElementById('statusBlock');
807 const statusText = document.getElementById('statusText');
808 const progressText = document.getElementById('progressText');
809
810 if (statusBlock && statusText) {
811 statusBlock.style.display = 'block';
812 statusText.textContent = status;
813 progressText.textContent = progress;
814 console.log('Статус:', status, progress);
815 }
816 }
817
818 // Функция для парсинга даты из текста
819 function parseDate(dateText) {
820 console.log('Парсинг даты:', dateText);
821
822 if (!dateText) {
823 console.log('Пустой текст даты');
824 return null;
825 }
826
827 const trimmed = dateText.trim();
828
829 // Если формат "10:29" (время) - берем текущую дату
830 if (trimmed.includes(':')) {
831 const today = new Date();
832 console.log('Распознано время, используем текущую дату:', today);
833 return today;
834 }
835
836 // Если формат "23.01" (день.месяц) или "31.12.2025" (день.месяц.год)
837 if (trimmed.includes('.')) {
838 const parts = trimmed.split('.');
839 if (parts.length === 2) {
840 // Формат "23.01"
841 const day = parseInt(parts[0]);
842 const month = parseInt(parts[1]) - 1; // Месяцы с 0
843 const currentYear = new Date().getFullYear();
844
845 if (!isNaN(day) && !isNaN(month)) {
846 const date = new Date(currentYear, month, day);
847 console.log('Распознанная дата (день.месяц):', date);
848 return date;
849 }
850 } else if (parts.length === 3) {
851 // Формат "31.12.2025"
852 const day = parseInt(parts[0]);
853 const month = parseInt(parts[1]) - 1; // Месяцы с 0
854 const year = parseInt(parts[2]);
855
856 if (!isNaN(day) && !isNaN(month) && !isNaN(year)) {
857 const date = new Date(year, month, day);
858 console.log('Распознанная дата (день.месяц.год):', date);
859 return date;
860 }
861 }
862 }
863
864 // Если дата в формате "24 января" или "21 июня 2025"
865 const months = {
866 'января': 0, 'февраля': 1, 'марта': 2, 'апреля': 3,
867 'мая': 4, 'июня': 5, 'июля': 6, 'августа': 7,
868 'сентября': 8, 'октября': 9, 'ноября': 10, 'декабря': 11
869 };
870
871 const parts = trimmed.split(' ');
872
873 // Формат "21 июня 2025"
874 if (parts.length === 3) {
875 const day = parseInt(parts[0]);
876 const month = months[parts[1].toLowerCase()];
877 const year = parseInt(parts[2]);
878
879 if (!isNaN(day) && !isNaN(month) && !isNaN(year)) {
880 const date = new Date(year, month, day);
881 console.log('Распознанная дата (день месяц год):', date);
882 return date;
883 }
884 }
885
886 // Формат "24 января"
887 if (parts.length === 2) {
888 const day = parseInt(parts[0]);
889 const month = months[parts[1].toLowerCase()];
890
891 if (!isNaN(day) && !isNaN(month)) {
892 const currentYear = new Date().getFullYear();
893 const date = new Date(currentYear, month, day);
894 console.log('Распознанная дата (день месяц):', date);
895 return date;
896 }
897 }
898
899 console.log('Не удалось распознать дату');
900 return null;
901 }
902
903 // Функция для получения всех чатов
904 function getAllChats() {
905 // Сначала проверяем, есть ли результаты поиска (класс index_chat_EHlBq)
906 const searchChats = document.querySelectorAll('.index_chat_EHlBq');
907
908 if (searchChats.length > 0) {
909 // Если есть результаты поиска, используем их
910 console.log('Найдены результаты поиска:', searchChats.length);
911 return Array.from(searchChats);
912 }
913
914 // Если поиска нет, используем обычные чаты
915 const chats = document.querySelectorAll('.index_chat_4fr82');
916 // Фильтруем только видимые чаты (учитываем фильтр поиска)
917 const visibleChats = Array.from(chats).filter(chat => {
918 const style = window.getComputedStyle(chat);
919 // Проверяем, что чат не скрыт (убрали проверку высоты, т.к. чаты виртуализированы)
920 return style.display !== 'none' &&
921 style.visibility !== 'hidden' &&
922 style.opacity !== '0' &&
923 chat.offsetParent !== null; // Проверяем offsetParent для фильтрации
924 });
925 console.log('Найдено чатов:', visibleChats.length, '(всего в DOM:', chats.length, ')');
926 return visibleChats;
927 }
928
929 // Функция для клика по чату
930 async function clickChat(chat) {
931 // Пробуем оба селектора для имени чата (обычный и поисковый)
932 const chatName = chat.querySelector('.index_chatTitle_TiXTq')?.textContent?.trim() ||
933 chat.querySelector('.index_chatTitle_x9txX')?.textContent ||
934 'Неизвестно';
935 console.log('Клик по чату:', chatName);
936 chat.click();
937 await sleep(2000); // Ждем загрузки чата
938 }
939
940 // Функция для открытия чата по ID (через прямую ссылку в новой вкладке)
941 async function openChatById(chatId) {
942 console.log('Открытие чата по ID в новой вкладке:', chatId);
943
944 // Формируем прямую ссылку на чат
945 const chatUrl = `https://seller.ozon.ru/app/messenger/?group=customers_v2&id=${chatId}`;
946 console.log('URL чата:', chatUrl);
947
948 // Открываем чат в новой вкладке
949 await GM.openInTab(chatUrl, false); // false = открыть в активной вкладке (на переднем плане)
950
951 // Ждем загрузки новой вкладки
952 await sleep(5000);
953
954 return true;
955 }
956
957 // Функция для получения даты последнего сообщения в открытом чате
958 function getLastMessageDate() {
959 const dateElements = document.querySelectorAll('.om_1_n4');
960 if (dateElements.length > 0) {
961 // Берем последнюю дату (последнее сообщение)
962 const lastDateElement = dateElements[dateElements.length - 1];
963 const dateText = lastDateElement.textContent.trim();
964 console.log('Дата последнего сообщения:', dateText);
965 return parseDate(dateText);
966 }
967 console.log('Элемент даты не найден');
968 return null;
969 }
970
971 // Функция для отправки сообщения
972 async function sendMessage(messageText) {
973 console.log('Отправка сообщения:', messageText);
974
975 // Находим текстовое поле
976 const textarea = document.querySelector('.om_17_a4');
977 if (!textarea) {
978 console.error('Текстовое поле не найдено');
979 return false;
980 }
981
982 // Вставляем текст
983 textarea.value = messageText;
984 textarea.dispatchEvent(new Event('input', { bubbles: true }));
985 console.log('Текст вставлен в поле');
986
987 // Ждем 1 секунду
988 await sleep(1000);
989
990 // Находим кнопку отправки (вторая кнопка с классом om_17_a8)
991 const sendButtons = document.querySelectorAll('.om_17_a8');
992 if (sendButtons.length < 2) {
993 console.error('Кнопка отправки не найдена');
994 return false;
995 }
996
997 // Кликаем на вторую кнопку (первая - это прикрепление файла)
998 const sendButton = sendButtons[1];
999 sendButton.click();
1000 console.log('Сообщение отправлено (клик выполнен)');
1001
1002 // Ждем 1 секунду после отправки
1003 await sleep(1000);
1004
1005 return true;
1006 }
1007
1008 // Функция задержки
1009 function sleep(ms) {
1010 return new Promise(resolve => setTimeout(resolve, ms));
1011 }
1012
1013 // Функция переключения паузы
1014 function togglePause() {
1015 isPaused = !isPaused;
1016 const pauseButton = document.getElementById('pauseButton');
1017
1018 if (isPaused) {
1019 pauseButton.textContent = '▶️ Продолжить';
1020 pauseButton.style.background = '#4caf50';
1021 console.log('Рассылка приостановлена');
1022 } else {
1023 pauseButton.textContent = '⏸️ Пауза';
1024 pauseButton.style.background = '#ff9800';
1025 console.log('Рассылка возобновлена');
1026 }
1027 }
1028
1029 // Функция остановки рассылки
1030 function stopBulkSending() {
1031 if (confirm('Вы уверены, что хотите остановить рассылку?')) {
1032 shouldStop = true;
1033 isPaused = false;
1034 console.log('Запрошена остановка рассылки');
1035 }
1036 }
1037
1038 // Основная функция рассылки
1039 async function startBulkSending() {
1040 console.log('=== ФУНКЦИЯ startBulkSending ВЫЗВАНА ===');
1041
1042 const messageText = document.getElementById('messageText').value.trim();
1043 const filterDateInput = document.getElementById('filterDate').value;
1044 const testModeCheckbox = document.getElementById('testMode');
1045 const isTestMode = testModeCheckbox ? testModeCheckbox.checked : false;
1046
1047 // Получаем выбранный режим рассылки
1048 const sendingMode = document.querySelector('input[name="sendingMode"]:checked')?.value || 'database';
1049
1050 console.log('Текст сообщения:', messageText);
1051 console.log('Дата фильтра:', filterDateInput);
1052 console.log('Тестовый режим:', isTestMode);
1053 console.log('Режим рассылки:', sendingMode);
1054
1055 if (!messageText) {
1056 alert('Введите текст сообщения');
1057 console.log('Остановка: нет текста сообщения');
1058 return;
1059 }
1060
1061 if (!filterDateInput) {
1062 alert('Выберите дату фильтра');
1063 console.log('Остановка: нет даты фильтра');
1064 return;
1065 }
1066
1067 // Выбираем функцию в зависимости от режима
1068 if (sendingMode === 'visible') {
1069 await startBulkSendingByVisibleChats(messageText, filterDateInput, isTestMode);
1070 } else {
1071 await startBulkSendingByDatabase(messageText, filterDateInput, isTestMode);
1072 }
1073 }
1074
1075 // Функция рассылки по базе данных
1076 async function startBulkSendingByDatabase(messageText, filterDateInput, isTestMode) {
1077 console.log('=== РАССЫЛКА ПО БАЗЕ ДАННЫХ ===');
1078
1079 // Загружаем базу данных
1080 const db = await loadContactsDatabase();
1081 const totalContacts = Object.keys(db.contacts).length;
1082
1083 if (totalContacts === 0) {
1084 alert('База контактов пуста! Сначала соберите базу контактов с помощью кнопки "Собрать базу".');
1085 return;
1086 }
1087
1088 const filterDate = new Date(filterDateInput);
1089 filterDate.setHours(23, 59, 59, 999); // Устанавливаем конец дня для корректного сравнения
1090
1091 console.log('Всего контактов в базе:', totalContacts);
1092 console.log('Фильтр по дате:', filterDate);
1093 console.log('Тестовый режим:', isTestMode ? 'ДА (только 1 сообщение)' : 'НЕТ');
1094
1095 // Фильтруем контакты по дате
1096 const contactsToSend = Object.values(db.contacts).filter(contact => {
1097 if (!contact.lastMessageDate) return false;
1098 const contactDate = new Date(contact.lastMessageDate);
1099 return contactDate <= filterDate;
1100 });
1101
1102 console.log('Контактов подходящих по дате:', contactsToSend.length);
1103
1104 if (contactsToSend.length === 0) {
1105 alert('Нет контактов, подходящих по указанной дате.');
1106 return;
1107 }
1108
1109 // Показываем статистику фильтрации
1110 const filterStats = `Всего в базе: ${totalContacts} | Подходящих по фильтру: ${contactsToSend.length}`;
1111 console.log(filterStats);
1112
1113 // Сохраняем состояние рассылки
1114 const sendingState = {
1115 isActive: true,
1116 messageText: messageText,
1117 filterDate: filterDate.toISOString(), // Сохраняем дату фильтра
1118 testMode: isTestMode,
1119 contactsList: contactsToSend,
1120 totalContacts: contactsToSend.length,
1121 processed: 0,
1122 sent: 0,
1123 skipped: 0,
1124 currentContactId: contactsToSend[0].id
1125 };
1126
1127 await saveSendingState(sendingState);
1128 console.log('Состояние рассылки сохранено, открываем первый чат');
1129
1130 // Открываем первый чат
1131 await openChatById(contactsToSend[0].id);
1132 }
1133
1134 // Функция рассылки по видимым чатам
1135 async function startBulkSendingByVisibleChats(messageText, filterDateInput, isTestMode) {
1136 console.log('=== РАССЫЛКА ПО ВИДИМЫМ ЧАТАМ ===');
1137
1138 const filterDate = new Date(filterDateInput);
1139 filterDate.setHours(23, 59, 59, 999);
1140
1141 console.log('Фильтр по дате:', filterDate);
1142 console.log('Тестовый режим:', isTestMode ? 'ДА (только 1 сообщение)' : 'НЕТ');
1143
1144 // Меняем интерфейс
1145 isRunning = true;
1146 isPaused = false;
1147 shouldStop = false;
1148
1149 const testModeCheckbox = document.getElementById('testMode');
1150 document.getElementById('startButton').style.display = 'none';
1151 document.getElementById('pauseButton').style.display = 'inline-block';
1152 document.getElementById('stopButton').style.display = 'inline-block';
1153 document.getElementById('messageText').disabled = true;
1154 document.getElementById('filterDate').disabled = true;
1155 if (testModeCheckbox) testModeCheckbox.disabled = true;
1156
1157 // Блокируем радио-кнопки режима
1158 document.querySelectorAll('input[name="sendingMode"]').forEach(radio => radio.disabled = true);
1159
1160 updateStatus(
1161 'Запуск рассылки по видимым чатам...',
1162 'Подготовка к рассылке...'
1163 );
1164
1165 let processed = 0;
1166 let sent = 0;
1167 let skipped = 0;
1168
1169 // Трекер обработанных чатов (по ID, чтобы не обрабатывать дважды)
1170 const processedChatIds = new Set();
1171
1172 // Счетчик попыток без новых чатов
1173 let noNewChatsCount = 0;
1174 const maxNoNewChatsAttempts = 3;
1175
1176 // Счетчик обработанных чатов с момента последнего скролла
1177 let chatsProcessedSinceScroll = 0;
1178 const scrollAfterChats = 10; // Скроллим каждые 10 обработанных чатов
1179
1180 while (!shouldStop) {
1181 // Проверка на паузу
1182 while (isPaused && !shouldStop) {
1183 updateStatus('Пауза', `Обработано: ${processed}, Отправлено: ${sent}, Пропущено: ${skipped}`);
1184 await sleep(500);
1185 }
1186
1187 if (shouldStop) {
1188 updateStatus('Остановлено пользователем', `Обработано: ${processed}, Отправлено: ${sent}, Пропущено: ${skipped}`);
1189 break;
1190 }
1191
1192 // Проверка тестового режима
1193 if (isTestMode && sent >= 1) {
1194 console.log('Тестовый режим: достигнут лимит в 1 сообщение, останавливаем рассылку');
1195 shouldStop = true;
1196 break;
1197 }
1198
1199 // Получаем текущие видимые чаты
1200 const currentChats = getAllChats();
1201 console.log('Найдено видимых чатов в DOM:', currentChats.length);
1202
1203 // Фильтруем только те чаты, которые еще не обработали
1204 const newChats = currentChats.filter(chat => {
1205 const chatId = extractChatId(chat);
1206 return chatId && !processedChatIds.has(chatId);
1207 });
1208
1209 console.log('Новых необработанных чатов:', newChats.length);
1210
1211 // Если нет новых чатов - увеличиваем счетчик
1212 if (newChats.length === 0) {
1213 noNewChatsCount++;
1214 console.log(`Новые чаты не появились (${noNewChatsCount}/${maxNoNewChatsAttempts})`);
1215
1216 if (noNewChatsCount >= maxNoNewChatsAttempts) {
1217 console.log('Достигнут конец списка чатов (новые чаты не подгружаются)');
1218 break;
1219 }
1220
1221 // Скроллим список чатов для подгрузки новых
1222 console.log('Нет новых чатов, скроллим для подгрузки...');
1223 await scrollChatList();
1224 continue;
1225 } else {
1226 noNewChatsCount = 0; // Сбрасываем счетчик, если появились новые чаты
1227 }
1228
1229 // Обрабатываем новые чаты
1230 for (const chat of newChats) {
1231 // Проверка на паузу
1232 while (isPaused && !shouldStop) {
1233 updateStatus('Пауза', `Обработано: ${processed}, Отправлено: ${sent}, Пропущено: ${skipped}`);
1234 await sleep(500);
1235 }
1236
1237 // Проверка на остановку
1238 if (shouldStop) {
1239 break;
1240 }
1241
1242 // Проверка тестового режима
1243 if (isTestMode && sent >= 1) {
1244 console.log('Тестовый режим: достигнут лимит в 1 сообщение, останавливаем рассылку');
1245 shouldStop = true;
1246 break;
1247 }
1248
1249 const chatId = extractChatId(chat);
1250 const chatName = chat.querySelector('.index_chatTitle_TiXTq')?.textContent ||
1251 chat.querySelector('.index_chatTitle_x9txX')?.textContent ||
1252 'Неизвестно';
1253
1254 // Отмечаем чат как обработанный
1255 processedChatIds.add(chatId);
1256
1257 updateStatus(
1258 `Обработка чата: ${chatName}`,
1259 `Обработано: ${processed}, Отправлено: ${sent}, Пропущено: ${skipped}`
1260 );
1261
1262 console.log(`[${processed + 1}] Обрабатываем чат: ${chatName} (${chatId})`);
1263
1264 // Кликаем по чату
1265 await clickChat(chat);
1266
1267 // Получаем дату последнего сообщения
1268 const lastMessageDate = getLastMessageDate();
1269
1270 if (!lastMessageDate) {
1271 console.log('Не удалось получить дату, пропускаем чат');
1272 skipped++;
1273 processed++;
1274 chatsProcessedSinceScroll++;
1275 continue;
1276 }
1277
1278 // Проверяем дату перед отправкой - сравниваем реальную дату из чата с фильтром
1279 let shouldSend = false;
1280 if (lastMessageDate && filterDate) {
1281 console.log('Сравнение дат:');
1282 console.log(' Дата из чата:', lastMessageDate.toLocaleDateString('ru-RU'));
1283 console.log(' Дата фильтра:', filterDate.toLocaleDateString('ru-RU'));
1284 console.log(' Дата из чата <= Дата фильтра?', lastMessageDate <= filterDate);
1285
1286 // Отправляем только если дата последнего сообщения <= дате фильтра
1287 shouldSend = lastMessageDate <= filterDate;
1288 } else {
1289 console.log('Не удалось получить дату или фильтр, пропускаем отправку');
1290 shouldSend = false;
1291 }
1292
1293 // Отправляем сообщение только если дата подходит
1294 let success = false;
1295 if (shouldSend) {
1296 console.log('✅ Дата подходит, отправляем сообщение');
1297 success = await sendMessage(messageText);
1298 } else {
1299 console.log('⏭️ Пропускаем отправку (дата не подходит)');
1300 success = false;
1301 }
1302
1303 if (success) {
1304 sent++;
1305 console.log(`✅ Сообщение отправлено в чат: ${chatName}`);
1306
1307 // Обновляем базу данных
1308 const db = await loadContactsDatabase();
1309 if (db.contacts[chatId]) {
1310 db.contacts[chatId].lastSentDate = new Date().toISOString();
1311 db.contacts[chatId].messageCount = (db.contacts[chatId].messageCount || 0) + 1;
1312 await saveContactsDatabase(db);
1313 }
1314 } else {
1315 console.error('❌ Ошибка отправки или дата не подходит');
1316 skipped++;
1317 }
1318
1319 processed++;
1320 chatsProcessedSinceScroll++;
1321
1322 // Скроллим список чатов каждые N обработанных чатов для подгрузки новых
1323 if (chatsProcessedSinceScroll >= scrollAfterChats) {
1324 console.log(`Обработано ${chatsProcessedSinceScroll} чатов, скроллим для подгрузки новых...`);
1325 await scrollChatList();
1326 chatsProcessedSinceScroll = 0;
1327 }
1328
1329 // Пауза между чатами
1330 await sleep(1000);
1331 }
1332
1333 // Если остановка запрошена - выходим
1334 if (shouldStop) {
1335 break;
1336 }
1337
1338 // Скроллим список чатов для подгрузки следующей порции
1339 console.log('Закончили обработку текущей порции, скроллим для подгрузки новых...');
1340 await scrollChatList();
1341 }
1342
1343 // Завершение
1344 isRunning = false;
1345 isPaused = false;
1346
1347 document.getElementById('startButton').style.display = 'inline-block';
1348 document.getElementById('pauseButton').style.display = 'none';
1349 document.getElementById('stopButton').style.display = 'none';
1350 document.getElementById('messageText').disabled = false;
1351 document.getElementById('filterDate').disabled = false;
1352 if (testModeCheckbox) testModeCheckbox.disabled = false;
1353
1354 // Разблокируем радио-кнопки режима
1355 document.querySelectorAll('input[name="sendingMode"]').forEach(radio => radio.disabled = false);
1356
1357 updateStatus(
1358 'Рассылка завершена!',
1359 `Всего обработано: ${processed} | Отправлено: ${sent} | Пропущено: ${skipped}`
1360 );
1361
1362 console.log('=== РАССЫЛКА ЗАВЕРШЕНА ===');
1363 console.log('Обработано:', processed);
1364 console.log('Отправлено:', sent);
1365 console.log('Пропущено:', skipped);
1366 }
1367
1368 // Функция для создания плавающей кнопки
1369 function createFloatingButton() {
1370 // Проверяем, не создана ли уже кнопка
1371 if (document.getElementById('bulkSenderFloatingBtn')) {
1372 return;
1373 }
1374
1375 const button = document.createElement('button');
1376 button.id = 'bulkSenderFloatingBtn';
1377 button.textContent = '📧 Рассылка';
1378 button.style.cssText = `
1379 position: fixed;
1380 bottom: 20px;
1381 right: 20px;
1382 padding: 15px 25px;
1383 background: #0066cc;
1384 color: white;
1385 border: none;
1386 border-radius: 50px;
1387 cursor: pointer;
1388 font-size: 16px;
1389 font-weight: 600;
1390 box-shadow: 0 4px 12px rgba(0, 102, 204, 0.4);
1391 z-index: 9999;
1392 transition: all 0.3s;
1393 `;
1394
1395 button.addEventListener('mouseenter', () => {
1396 button.style.background = '#0052a3';
1397 button.style.transform = 'scale(1.05)';
1398 button.style.boxShadow = '0 6px 16px rgba(0, 102, 204, 0.6)';
1399 });
1400
1401 button.addEventListener('mouseleave', () => {
1402 button.style.background = '#0066cc';
1403 button.style.transform = 'scale(1)';
1404 button.style.boxShadow = '0 4px 12px rgba(0, 102, 204, 0.4)';
1405 });
1406
1407 button.addEventListener('click', openModal);
1408
1409 document.body.appendChild(button);
1410 console.log('Плавающая кнопка создана');
1411 }
1412
1413 // Функция мониторинга прогресса рассылки в главной вкладке
1414 async function monitorSendingProgress() {
1415 console.log('Запуск мониторинга прогресса рассылки');
1416
1417 let lastProcessed = -1;
1418
1419 const monitorInterval = setInterval(async () => {
1420 const currentState = await loadSendingState();
1421
1422 if (!currentState || !currentState.isActive) {
1423 console.log('Рассылка завершена или остановлена, останавливаем мониторинг');
1424 clearInterval(monitorInterval);
1425
1426 // Обновляем интерфейс
1427 updateStatus('Рассылка завершена!', 'Проверьте результаты');
1428 await updateDatabaseStats();
1429
1430 return;
1431 }
1432
1433 // Обновляем статус только если изменился прогресс
1434 if (currentState.processed !== lastProcessed) {
1435 lastProcessed = currentState.processed;
1436
1437 const currentContact = currentState.contactsList[currentState.processed - 1];
1438 const contactName = currentContact ? currentContact.name : 'Неизвестно';
1439
1440 updateStatus(
1441 `Обработка: ${contactName}`,
1442 `Обработано: ${currentState.processed}/${currentState.totalContacts} | Отправлено: ${currentState.sent} | Пропущено: ${currentState.skipped}`
1443 );
1444
1445 console.log('Обновлен прогресс:', currentState.processed, '/', currentState.totalContacts);
1446
1447 // Если обработали все контакты, открываем следующий
1448 if (currentState.processed < currentState.totalContacts) {
1449 const nextContact = currentState.contactsList[currentState.processed];
1450 if (nextContact) {
1451 console.log('Открываем следующий чат из главной вкладки:', nextContact.name);
1452 currentState.currentContactId = nextContact.id;
1453 await saveSendingState(currentState);
1454 await openChatById(nextContact.id);
1455 }
1456 }
1457 }
1458 }, 1000); // Проверяем каждую секунду
1459 }
1460
1461 // Функция для создания фильтра по дате в списке чатов
1462 function createDateFilter() {
1463 // Проверяем, не создан ли уже фильтр
1464 if (document.getElementById('dateFilterContainer')) {
1465 return;
1466 }
1467
1468 // Ищем контейнер с поиском
1469 const searchContainer = document.querySelector('.om_1_f0')?.parentElement;
1470 if (!searchContainer) {
1471 console.log('Контейнер для фильтра не найден');
1472 return;
1473 }
1474
1475 // Создаем контейнер для фильтра
1476 const filterContainer = document.createElement('div');
1477 filterContainer.id = 'dateFilterContainer';
1478 filterContainer.style.cssText = `
1479 padding: 15px;
1480 background: #f5f5f5;
1481 border-bottom: 1px solid #ddd;
1482 display: flex;
1483 gap: 10px;
1484 align-items: center;
1485 flex-wrap: wrap;
1486 `;
1487
1488 filterContainer.innerHTML = `
1489 <label style="font-size: 13px; font-weight: 600; color: #555;">Фильтр по дате последнего сообщения:</label>
1490 <input type="date" id="dateFilterFrom" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
1491 <span style="color: #555;">—</span>
1492 <input type="date" id="dateFilterTo" style="width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;">
1493 <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>
1494 <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>
1495 `;
1496
1497 // Вставляем фильтр перед списком чатов
1498 const chatList = document.querySelector('.om_1_f0');
1499 if (chatList && chatList.parentElement) {
1500 chatList.parentElement.insertBefore(filterContainer, chatList);
1501 console.log('Фильтр по дате создан');
1502
1503 // Обработчики событий
1504 document.getElementById('applyDateFilter').addEventListener('click', applyDateFilter);
1505 document.getElementById('clearDateFilter').addEventListener('click', clearDateFilter);
1506 }
1507 }
1508
1509 // Функция применения фильтра по дате
1510 async function applyDateFilter() {
1511 const dateFrom = document.getElementById('dateFilterFrom').value;
1512 const dateTo = document.getElementById('dateFilterTo').value;
1513
1514 if (!dateFrom || !dateTo) {
1515 alert('Выберите обе даты для фильтрации');
1516 return;
1517 }
1518
1519 const filterFrom = new Date(dateFrom);
1520 const filterTo = new Date(dateTo);
1521
1522 console.log('Применение фильтра по дате:', filterFrom, '-', filterTo);
1523
1524 // Показываем индикатор загрузки
1525 const applyButton = document.getElementById('applyDateFilter');
1526 const originalText = applyButton.textContent;
1527 applyButton.textContent = 'Фильтрация...';
1528 applyButton.disabled = true;
1529
1530 // Получаем все чаты
1531 const allChats = document.querySelectorAll('.index_chat_4fr82');
1532 let visibleCount = 0;
1533 let hiddenCount = 0;
1534
1535 allChats.forEach(chat => {
1536 // Ищем дату в самом элементе чата (не открывая его)
1537 const dateElement = chat.querySelector('.index_chatDate_z4mNc, .index_chatDate_WJ-\\+mb');
1538
1539 if (dateElement) {
1540 const dateText = dateElement.textContent.trim();
1541 const chatDate = parseDate(dateText);
1542
1543 if (chatDate) {
1544 // Проверяем, попадает ли дата в диапазон
1545 if (chatDate >= filterFrom && chatDate <= filterTo) {
1546 // Дата подходит - оставляем чат видимым
1547 chat.style.display = 'grid';
1548 visibleCount++;
1549 console.log('Чат подходит:', chat.querySelector('.index_chatTitle_TiXTq')?.textContent, chatDate);
1550 } else {
1551 // Дата не подходит - скрываем чат
1552 chat.style.display = 'none';
1553 hiddenCount++;
1554 }
1555 } else {
1556 // Не удалось распознать дату - оставляем видимым
1557 chat.style.display = 'grid';
1558 visibleCount++;
1559 }
1560 } else {
1561 // Нет даты - оставляем видимым
1562 chat.style.display = 'grid';
1563 visibleCount++;
1564 }
1565 });
1566
1567 // Восстанавливаем кнопку
1568 applyButton.textContent = originalText;
1569 applyButton.disabled = false;
1570
1571 // Показываем кнопку сброса
1572 document.getElementById('clearDateFilter').style.display = 'inline-block';
1573
1574 console.log(`Фильтр применен. Показано: ${visibleCount}, Скрыто: ${hiddenCount}`);
1575 alert(`Фильтр применен!\nПоказано чатов: ${visibleCount}\nСкрыто чатов: ${hiddenCount}`);
1576 }
1577
1578 // Функция сброса фильтра
1579 function clearDateFilter() {
1580 // Показываем все чаты
1581 const allChats = document.querySelectorAll('.index_chat_4fr82');
1582 allChats.forEach(chat => {
1583 chat.style.display = 'grid';
1584 });
1585
1586 // Очищаем поля
1587 document.getElementById('dateFilterFrom').value = '';
1588 document.getElementById('dateFilterTo').value = '';
1589
1590 // Скрываем кнопку сброса
1591 document.getElementById('clearDateFilter').style.display = 'none';
1592
1593 console.log('Фильтр сброшен');
1594 }
1595
1596 // Инициализация
1597 async function init() {
1598 console.log('Инициализация расширения');
1599
1600 // Проверяем, что мы на странице мессенджера с покупателями
1601 if (!window.location.href.includes('group=customers_v2')) {
1602 console.log('Не на странице чатов с покупателями, расширение не активно');
1603 return;
1604 }
1605
1606 console.log('Страница подходит, создаем интерфейс');
1607
1608 // Проверяем, есть ли сохраненное состояние рассылки
1609 const savedState = await loadSendingState();
1610
1611 if (savedState && savedState.isActive) {
1612 console.log('Обнаружено активное состояние рассылки, продолжаем автоматически');
1613
1614 // Проверяем, это рабочая вкладка или вкладка для отправки
1615 const urlParams = new URLSearchParams(window.location.search);
1616 const chatId = urlParams.get('id');
1617
1618 if (chatId && chatId === savedState.currentContactId) {
1619 console.log('Это вкладка для отправки сообщения, ID совпадает:', chatId);
1620
1621 // Ждем загрузки страницы
1622 console.log('Ждем загрузки страницы 5 секунд...');
1623 await sleep(5000);
1624 console.log('Ожидание завершено, начинаем обработку');
1625
1626 // Получаем дату последнего сообщения в этом чате
1627 const lastMessageDate = getLastMessageDate();
1628 console.log('Дата последнего сообщения в чате:', lastMessageDate);
1629
1630 // Находим текущий контакт в списке
1631 const currentContact = savedState.contactsList.find(c => c.id === savedState.currentContactId);
1632
1633 // Получаем дату фильтра из состояния
1634 // Нужно сохранить дату фильтра в состоянии при запуске
1635 const filterDate = savedState.filterDate ? new Date(savedState.filterDate) : null;
1636 console.log('Дата фильтра из состояния:', filterDate);
1637
1638 // Проверяем дату перед отправкой - сравниваем реальную дату из чата с фильтром
1639 let shouldSend = false;
1640 if (lastMessageDate && filterDate) {
1641 console.log('Сравнение дат:');
1642 console.log(' Дата из чата:', lastMessageDate.toLocaleDateString('ru-RU'));
1643 console.log(' Дата фильтра:', filterDate.toLocaleDateString('ru-RU'));
1644 console.log(' Дата из чата <= Дата фильтра?', lastMessageDate <= filterDate);
1645
1646 // Отправляем только если дата последнего сообщения <= дате фильтра
1647 shouldSend = lastMessageDate <= filterDate;
1648 } else {
1649 console.log('Не удалось получить дату или фильтр, пропускаем отправку');
1650 shouldSend = false;
1651 }
1652
1653 // Отправляем сообщение только если дата подходит
1654 let success = false;
1655 if (shouldSend) {
1656 console.log('✅ Дата подходит, отправляем сообщение');
1657 success = await sendMessage(savedState.messageText);
1658 } else {
1659 console.log('⏭️ Пропускаем отправку (дата не подходит)');
1660 success = false;
1661 }
1662
1663 if (success) {
1664 console.log('✅ Сообщение отправлено');
1665
1666 // Обновляем базу данных
1667 const db = await loadContactsDatabase();
1668 if (db.contacts[savedState.currentContactId]) {
1669 db.contacts[savedState.currentContactId].lastSentDate = new Date().toISOString();
1670 db.contacts[savedState.currentContactId].messageCount = (db.contacts[savedState.currentContactId].messageCount || 0) + 1;
1671 await saveContactsDatabase(db);
1672 }
1673
1674 savedState.sent++;
1675 } else {
1676 console.error('❌ Ошибка отправки или дата не подходит');
1677 savedState.skipped++;
1678 }
1679
1680 savedState.processed++;
1681
1682 // Проверяем, нужно ли завершать
1683 if (savedState.testMode && savedState.sent >= 1) {
1684 console.log('Тестовый режим: достигнут лимит, завершаем');
1685 await clearSendingState();
1686 alert(`Тестовая рассылка завершена!\nОтправлено: ${savedState.sent} сообщение`);
1687 window.close(); // Закрываем вкладку
1688 return;
1689 }
1690
1691 if (savedState.processed >= savedState.totalContacts) {
1692 console.log('Все контакты обработаны, завершаем');
1693 await clearSendingState();
1694 alert(`Рассылка завершена!\nОбработано: ${savedState.processed}\nОтправлено: ${savedState.sent}\nПропущено: ${savedState.skipped}`);
1695 window.close(); // Закрываем вкладку
1696 return;
1697 }
1698
1699 // Сохраняем обновленное состояние
1700 await saveSendingState(savedState);
1701
1702 // Закрываем текущую вкладку
1703 console.log('Закрываем текущую вкладку');
1704 window.close();
1705
1706 return; // Не создаем интерфейс
1707 } else {
1708 console.log('Это главная вкладка, ожидаем результатов от рабочей вкладки');
1709
1710 // Создаем интерфейс и показываем прогресс
1711 createFloatingButton();
1712 createModal();
1713 openModal();
1714
1715 // Запускаем мониторинг состояния
1716 monitorSendingProgress();
1717
1718 return;
1719 }
1720 }
1721
1722 // Создаем плавающую кнопку сразу
1723 createFloatingButton();
1724 createModal();
1725
1726 // Ждем загрузки списка чатов и создаем фильтр
1727 setTimeout(() => {
1728 createDateFilter();
1729 }, 2000);
1730
1731 console.log('Интерфейс создан');
1732 }
1733
1734 // Запуск при загрузке страницы
1735 if (document.readyState === 'loading') {
1736 document.addEventListener('DOMContentLoaded', init);
1737 } else {
1738 init();
1739 }
1740})();