Массовая отправка сообщений ОТВЕТО

Автоматическая отправка сообщений по всем чатам с фильтрацией по дате

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})();