Автоматическая отправка сообщений по всем чатам с фильтрацией по дате
Size
22.8 KB
Version
1.1.1
Created
Mar 13, 2026
Updated
8 days ago
1// ==UserScript==
2// @name Массовая отправка сообщений ОТВЕТО
3// @description Автоматическая отправка сообщений по всем чатам с фильтрацией по дате
4// @version 1.1.1
5// @match https://*.app.otveto.ru/*
6// @icon https://app.otveto.ru/favicon-32x32.png?v=47xjxmQnba
7// @grant GM.getValue
8// @grant GM.setValue
9// ==/UserScript==
10(function() {
11 'use strict';
12
13 // Утилита для debounce
14 function debounce(func, wait) {
15 let timeout;
16 return function executedFunction(...args) {
17 const later = () => {
18 clearTimeout(timeout);
19 func(...args);
20 };
21 clearTimeout(timeout);
22 timeout = setTimeout(later, wait);
23 };
24 }
25
26 // Парсинг даты из формата "16 апреля 2024" или "DD.MM.YYYY"
27 function parseRussianDate(dateStr) {
28 const months = {
29 'января': 0, 'февраля': 1, 'марта': 2, 'апреля': 3,
30 'мая': 4, 'июня': 5, 'июля': 6, 'августа': 7,
31 'сентября': 8, 'октября': 9, 'ноября': 10, 'декабря': 11
32 };
33
34 // Формат "16 апреля 2024"
35 const russianMatch = dateStr.match(/(\d+)\s+([а-яё]+)\s+(\d{4})/i);
36 if (russianMatch) {
37 const day = parseInt(russianMatch[1]);
38 const month = months[russianMatch[2].toLowerCase()];
39 const year = parseInt(russianMatch[3]);
40 return new Date(year, month, day);
41 }
42
43 // Формат "16 апреля" (без года - используем текущий год)
44 const russianMatchNoYear = dateStr.match(/(\d+)\s+([а-яё]+)$/i);
45 if (russianMatchNoYear) {
46 const day = parseInt(russianMatchNoYear[1]);
47 const month = months[russianMatchNoYear[2].toLowerCase()];
48 const currentYear = new Date().getFullYear();
49 return new Date(currentYear, month, day);
50 }
51
52 // Формат "DD.MM.YYYY"
53 const dotMatch = dateStr.match(/(\d{2})\.(\d{2})\.(\d{4})/);
54 if (dotMatch) {
55 return new Date(dotMatch[3], dotMatch[2] - 1, dotMatch[1]);
56 }
57
58 return null;
59 }
60
61 // Класс для управления массовой рассылкой
62 class MassSender {
63 constructor() {
64 this.isRunning = false;
65 this.stats = {
66 total: 0,
67 sent: 0,
68 skipped: 0,
69 errors: 0
70 };
71 this.config = {
72 message: '',
73 dateFilter: null
74 };
75 }
76
77 async loadState() {
78 const state = await GM.getValue('massSenderState', null);
79 if (state) {
80 this.stats = state.stats || this.stats;
81 this.config = state.config || this.config;
82 }
83 console.log('Загружено состояние:', this.stats, this.config);
84 }
85
86 async saveState() {
87 await GM.setValue('massSenderState', {
88 stats: this.stats,
89 config: this.config
90 });
91 console.log('Сохранено состояние:', this.stats);
92 }
93
94 async resetStats() {
95 this.stats = {
96 total: 0,
97 sent: 0,
98 skipped: 0,
99 errors: 0
100 };
101 await this.saveState();
102 this.updateStatsUI();
103 }
104
105 updateStatsUI() {
106 const statsEl = document.getElementById('mass-sender-stats');
107 if (statsEl) {
108 statsEl.innerHTML = `
109 <div style="font-size: 12px; color: #666;">
110 <div>Всего чатов: ${this.stats.total}</div>
111 <div style="color: #4caf50;">Отправлено: ${this.stats.sent}</div>
112 <div style="color: #ff9800;">Пропущено: ${this.stats.skipped}</div>
113 <div style="color: #f44336;">Ошибок: ${this.stats.errors}</div>
114 </div>
115 `;
116 }
117 }
118
119 // Получение всех чатов с прокруткой для ленивой загрузки
120 async getAllChats() {
121 console.log('Начинаем загрузку всех чатов...');
122 const chatsContainer = document.querySelector('.MuiTableBody-root');
123 if (!chatsContainer) {
124 console.error('Контейнер чатов не найден');
125 return [];
126 }
127
128 let previousCount = 0;
129 let stableCount = 0;
130 const maxStableIterations = 3;
131
132 // Прокручиваем до конца списка для загрузки всех чатов
133 while (stableCount < maxStableIterations) {
134 const chatRows = chatsContainer.querySelectorAll('tr.MuiTableRow-root.MuiTableRow-hover');
135 const currentCount = chatRows.length;
136
137 console.log(`Загружено чатов: ${currentCount}`);
138
139 if (currentCount === previousCount) {
140 stableCount++;
141 } else {
142 stableCount = 0;
143 }
144
145 previousCount = currentCount;
146
147 // Прокручиваем к последнему элементу
148 if (chatRows.length > 0) {
149 chatRows[chatRows.length - 1].scrollIntoView({ behavior: 'smooth', block: 'end' });
150 }
151
152 // Ждем загрузки новых элементов
153 await new Promise(resolve => setTimeout(resolve, 2000));
154 }
155
156 // Собираем все чаты
157 const chatRows = chatsContainer.querySelectorAll('tr.MuiTableRow-root.MuiTableRow-hover');
158 const chats = [];
159
160 chatRows.forEach((row, index) => {
161 const link = row.querySelector('a');
162 if (link) {
163 chats.push({
164 element: row,
165 link: link,
166 index: index
167 });
168 }
169 });
170
171 console.log(`Всего загружено чатов: ${chats.length}`);
172 this.stats.total = chats.length;
173 this.updateStatsUI();
174 await this.saveState();
175
176 return chats;
177 }
178
179 // Получение даты последнего сообщения в открытом чате
180 getLastMessageDate() {
181 const dateLables = document.querySelectorAll('[data-date-label]');
182 if (dateLables.length === 0) {
183 console.log('Сообщения не найдены в чате');
184 return null;
185 }
186
187 // Берем последнюю дату
188 const lastDateLabel = dateLables[dateLables.length - 1];
189 const dateStr = lastDateLabel.getAttribute('data-date-label');
190 console.log('Последняя дата сообщения:', dateStr);
191
192 return parseRussianDate(dateStr);
193 }
194
195 // Отправка сообщения в открытый чат
196 async sendMessage(message) {
197 console.log('Отправка сообщения:', message);
198
199 // Находим поле ввода
200 const textarea = document.querySelector('textarea[name="answer"]');
201 if (!textarea) {
202 console.error('Поле ввода не найдено');
203 return false;
204 }
205
206 // Вставляем текст
207 textarea.value = message;
208 textarea.dispatchEvent(new Event('input', { bubbles: true }));
209 textarea.dispatchEvent(new Event('change', { bubbles: true }));
210
211 // Ждем немного
212 await new Promise(resolve => setTimeout(resolve, 500));
213
214 // Находим кнопку отправки
215 const submitButton = document.querySelector('button[type="submit"]:not(.Mui-disabled)');
216 if (!submitButton) {
217 console.error('Кнопка отправки не найдена или неактивна');
218 return false;
219 }
220
221 // Кликаем на кнопку
222 submitButton.click();
223 console.log('Сообщение отправлено');
224
225 return true;
226 }
227
228 // Основной процесс рассылки
229 async startSending() {
230 if (this.isRunning) {
231 console.log('Рассылка уже запущена');
232 return;
233 }
234
235 if (!this.config.message) {
236 alert('Введите текст сообщения!');
237 return;
238 }
239
240 this.isRunning = true;
241 await this.resetStats();
242
243 console.log('Начинаем массовую рассылку...');
244 console.log('Сообщение:', this.config.message);
245 console.log('Дата фильтра:', this.config.dateFilter || 'не указана (отправка во все чаты)');
246
247 try {
248 // Получаем все чаты
249 const chats = await this.getAllChats();
250
251 // Обрабатываем каждый чат
252 for (let i = 0; i < chats.length; i++) {
253 if (!this.isRunning) {
254 console.log('Рассылка остановлена пользователем');
255 break;
256 }
257
258 console.log(`\n--- Обработка чата ${i + 1}/${chats.length} ---`);
259
260 // Кликаем на чат
261 chats[i].link.click();
262 console.log('Открыт чат');
263
264 // Если дата фильтра указана, проверяем дату последнего сообщения
265 if (this.config.dateFilter) {
266 // Ждем загрузки чата
267 await new Promise(resolve => setTimeout(resolve, 5000));
268
269 // Проверяем дату последнего сообщения
270 const lastMessageDate = this.getLastMessageDate();
271
272 if (!lastMessageDate) {
273 console.log('Не удалось получить дату последнего сообщения, пропускаем');
274 this.stats.skipped++;
275 this.updateStatsUI();
276 await this.saveState();
277 continue;
278 }
279
280 console.log('Дата последнего сообщения:', lastMessageDate);
281 console.log('Дата фильтра:', this.config.dateFilter);
282
283 // Проверяем, нужно ли отправлять сообщение
284 if (lastMessageDate >= this.config.dateFilter) {
285 console.log('Дата последнего сообщения позже фильтра, пропускаем');
286 this.stats.skipped++;
287 this.updateStatsUI();
288 await this.saveState();
289 continue;
290 }
291
292 console.log('Дата последнего сообщения раньше фильтра, отправляем');
293 } else {
294 // Без фильтра по дате - просто ждем немного для загрузки чата
295 await new Promise(resolve => setTimeout(resolve, 2000));
296 console.log('Фильтр по дате не указан, отправляем сообщение');
297 }
298
299 // Отправляем сообщение
300 const success = await this.sendMessage(this.config.message);
301
302 if (success) {
303 this.stats.sent++;
304 console.log('Сообщение успешно отправлено');
305 } else {
306 this.stats.errors++;
307 console.error('Ошибка при отправке сообщения');
308 }
309
310 this.updateStatsUI();
311 await this.saveState();
312
313 // Пауза между чатами
314 await new Promise(resolve => setTimeout(resolve, 2000));
315 }
316
317 console.log('\n=== Рассылка завершена ===');
318 console.log('Статистика:', this.stats);
319 alert(`Рассылка завершена!\n\nОтправлено: ${this.stats.sent}\nПропущено: ${this.stats.skipped}\nОшибок: ${this.stats.errors}`);
320
321 } catch (error) {
322 console.error('Ошибка при рассылке:', error);
323 alert('Произошла ошибка при рассылке: ' + error.message);
324 } finally {
325 this.isRunning = false;
326 }
327 }
328
329 stopSending() {
330 this.isRunning = false;
331 console.log('Остановка рассылки...');
332 }
333 }
334
335 // Создание UI
336 function createUI() {
337 console.log('Создание UI для массовой рассылки');
338
339 // Проверяем, что мы на странице чатов
340 if (!window.location.href.includes('/answers/chats/')) {
341 console.log('Не на странице чатов, UI не создается');
342 return;
343 }
344
345 // Проверяем, что UI еще не создан
346 if (document.getElementById('mass-sender-panel')) {
347 console.log('UI уже создан');
348 return;
349 }
350
351 const sender = new MassSender();
352 sender.loadState();
353
354 // Создаем кнопку для открытия панели
355 const openButton = document.createElement('button');
356 openButton.id = 'mass-sender-open-btn';
357 openButton.textContent = 'Массовая отправка';
358 openButton.style.cssText = `
359 position: fixed;
360 top: 20px;
361 right: 20px;
362 z-index: 10000;
363 padding: 12px 24px;
364 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
365 color: white;
366 border: none;
367 border-radius: 8px;
368 font-size: 14px;
369 font-weight: 600;
370 cursor: pointer;
371 box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
372 transition: all 0.3s ease;
373 `;
374
375 openButton.addEventListener('mouseenter', () => {
376 openButton.style.transform = 'translateY(-2px)';
377 openButton.style.boxShadow = '0 6px 20px rgba(102, 126, 234, 0.6)';
378 });
379
380 openButton.addEventListener('mouseleave', () => {
381 openButton.style.transform = 'translateY(0)';
382 openButton.style.boxShadow = '0 4px 15px rgba(102, 126, 234, 0.4)';
383 });
384
385 // Создаем панель настроек
386 const panel = document.createElement('div');
387 panel.id = 'mass-sender-panel';
388 panel.style.cssText = `
389 position: fixed;
390 top: 50%;
391 left: 50%;
392 transform: translate(-50%, -50%);
393 z-index: 10001;
394 background: white;
395 border-radius: 16px;
396 box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
397 padding: 30px;
398 width: 500px;
399 max-width: 90vw;
400 display: none;
401 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
402 `;
403
404 panel.innerHTML = `
405 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px;">
406 <h2 style="margin: 0; font-size: 24px; color: #333;">Массовая рассылка</h2>
407 <button id="mass-sender-close-btn" style="background: none; border: none; font-size: 28px; cursor: pointer; color: #999; line-height: 1;">×</button>
408 </div>
409
410 <div style="margin-bottom: 20px;">
411 <label style="display: block; margin-bottom: 8px; font-weight: 600; color: #555; font-size: 14px;">Текст сообщения:</label>
412 <textarea id="mass-sender-message" style="width: 100%; min-height: 120px; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; font-family: inherit; resize: vertical; box-sizing: border-box;" placeholder="Введите текст сообщения..."></textarea>
413 </div>
414
415 <div style="margin-bottom: 25px;">
416 <label style="display: block; margin-bottom: 8px; font-weight: 600; color: #555; font-size: 14px;">Отправлять, если последнее сообщение раньше (необязательно):</label>
417 <input type="date" id="mass-sender-date" style="width: 100%; padding: 12px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px; box-sizing: border-box;" />
418 <div style="font-size: 12px; color: #999; margin-top: 6px;">Оставьте пустым для отправки во все чаты без проверки даты (быстрее)</div>
419 </div>
420
421 <div id="mass-sender-stats" style="background: #f5f5f5; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
422 <div style="font-size: 12px; color: #666;">
423 <div>Всего чатов: 0</div>
424 <div style="color: #4caf50;">Отправлено: 0</div>
425 <div style="color: #ff9800;">Пропущено: 0</div>
426 <div style="color: #f44336;">Ошибок: 0</div>
427 </div>
428 </div>
429
430 <div style="display: flex; gap: 12px;">
431 <button id="mass-sender-start-btn" style="flex: 1; padding: 14px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.3s ease;">
432 Начать рассылку
433 </button>
434 <button id="mass-sender-stop-btn" style="flex: 1; padding: 14px; background: #f44336; color: white; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; display: none; transition: all 0.3s ease;">
435 Остановить
436 </button>
437 <button id="mass-sender-reset-btn" style="padding: 14px 20px; background: #ff9800; color: white; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.3s ease;">
438 Сброс
439 </button>
440 </div>
441 `;
442
443 // Создаем overlay
444 const overlay = document.createElement('div');
445 overlay.id = 'mass-sender-overlay';
446 overlay.style.cssText = `
447 position: fixed;
448 top: 0;
449 left: 0;
450 width: 100%;
451 height: 100%;
452 background: rgba(0, 0, 0, 0.5);
453 z-index: 10000;
454 display: none;
455 `;
456
457 document.body.appendChild(openButton);
458 document.body.appendChild(overlay);
459 document.body.appendChild(panel);
460
461 // Обработчики событий
462 openButton.addEventListener('click', () => {
463 panel.style.display = 'block';
464 overlay.style.display = 'block';
465 sender.updateStatsUI();
466 });
467
468 overlay.addEventListener('click', () => {
469 panel.style.display = 'none';
470 overlay.style.display = 'none';
471 });
472
473 panel.querySelector('#mass-sender-close-btn').addEventListener('click', () => {
474 panel.style.display = 'none';
475 overlay.style.display = 'none';
476 });
477
478 panel.querySelector('#mass-sender-start-btn').addEventListener('click', async () => {
479 const message = document.getElementById('mass-sender-message').value.trim();
480 const dateStr = document.getElementById('mass-sender-date').value;
481
482 if (!message) {
483 alert('Введите текст сообщения!');
484 return;
485 }
486
487 sender.config.message = message;
488 sender.config.dateFilter = dateStr ? new Date(dateStr) : null;
489
490 // Меняем кнопки
491 document.getElementById('mass-sender-start-btn').style.display = 'none';
492 document.getElementById('mass-sender-stop-btn').style.display = 'block';
493
494 await sender.startSending();
495
496 // Возвращаем кнопки
497 document.getElementById('mass-sender-start-btn').style.display = 'block';
498 document.getElementById('mass-sender-stop-btn').style.display = 'none';
499 });
500
501 panel.querySelector('#mass-sender-stop-btn').addEventListener('click', () => {
502 sender.stopSending();
503 document.getElementById('mass-sender-start-btn').style.display = 'block';
504 document.getElementById('mass-sender-stop-btn').style.display = 'none';
505 });
506
507 panel.querySelector('#mass-sender-reset-btn').addEventListener('click', async () => {
508 if (confirm('Сбросить статистику?')) {
509 await sender.resetStats();
510 }
511 });
512
513 console.log('UI создан успешно');
514 }
515
516 // Инициализация
517 function init() {
518 console.log('Инициализация расширения массовой рассылки ОТВЕТО');
519
520 // Ждем загрузки страницы
521 if (document.readyState === 'loading') {
522 document.addEventListener('DOMContentLoaded', createUI);
523 } else {
524 createUI();
525 }
526
527 // Отслеживаем изменения URL для SPA
528 let lastUrl = location.href;
529 const observer = new MutationObserver(debounce(() => {
530 const currentUrl = location.href;
531 if (currentUrl !== lastUrl) {
532 lastUrl = currentUrl;
533 console.log('URL изменился:', currentUrl);
534 setTimeout(createUI, 1000);
535 }
536 }, 500));
537
538 observer.observe(document.body, {
539 childList: true,
540 subtree: true
541 });
542 }
543
544 init();
545})();