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