Brand Analytics Comment Task Creator

Парсит посты с brandanalytics.ru, генерирует комментарии через AI и создаёт задачи на unu.im

Size

22.9 KB

Version

0.0.4

Created

Apr 9, 2026

Updated

7 days ago

1// ==UserScript==
2// @name		Brand Analytics Comment Task Creator
3// @description		Парсит посты с brandanalytics.ru, генерирует комментарии через AI и создаёт задачи на unu.im
4// @match		*://brandanalytics.ru/*
5// @match		*://unu.im/tasks/add*
6// @match		*://unu.im/tasks/pay/*
7// @grant		GM.getValue
8// @grant		GM.setValue
9// @grant		GM.deleteValue
10// @version		0.0.4
11// ==/UserScript==
12(function () {
13    // ─── CONSTANTS ───────────────────────────────────────────────────────────────
14    const STORAGE_KEY = 'ba_pending_task';
15    const PROMPT_KEY = 'ba_custom_prompt';
16    const UNU_URL = 'https://unu.im/tasks/add?tarif_id=21';
17
18    // ─── UTILS ───────────────────────────────────────────────────────────────────
19    function debounce(fn, delay) {
20        let timer;
21        return function (...args) {
22            clearTimeout(timer);
23            timer = setTimeout(() => fn.apply(this, args), delay);
24        };
25    }
26
27    // ─── BRANDANALYTICS PAGE ─────────────────────────────────────────────────────
28    function initBrandAnalytics() {
29        console.log('[BA-Tasks] Инициализация на brandanalytics.ru');
30        injectStyles();
31        observeModal();
32        observeToolbar();
33    }
34
35    function injectStyles() {
36        TM_addStyle(`
37      .ba-modal-block {
38        margin-top: 14px;
39        padding: 12px 14px;
40        background: #1a1a2e;
41        border-radius: 10px;
42        border: 1px solid #2d2d5e;
43        font-family: 'Segoe UI', Arial, sans-serif;
44      }
45      .ba-modal-block .ba-title {
46        font-size: 12px;
47        font-weight: 700;
48        color: #a78bfa;
49        letter-spacing: 0.4px;
50        margin-bottom: 10px;
51        text-transform: uppercase;
52      }
53      .ba-btn {
54        display: inline-flex;
55        align-items: center;
56        gap: 6px;
57        padding: 7px 16px;
58        border-radius: 7px;
59        border: none;
60        cursor: pointer;
61        font-size: 13px;
62        font-weight: 600;
63        transition: background 0.2s, opacity 0.2s;
64        font-family: 'Segoe UI', Arial, sans-serif;
65      }
66      .ba-btn-generate {
67        background: #a78bfa;
68        color: #fff;
69      }
70      .ba-btn-generate:hover { background: #7c3aed; }
71      .ba-btn-generate:disabled {
72        opacity: 0.5;
73        cursor: not-allowed;
74      }
75      .ba-btn-send {
76        background: #10b981;
77        color: #fff;
78        margin-top: 10px;
79        display: none;
80      }
81      .ba-btn-send:hover { background: #059669; }
82      .ba-btn-send.visible { display: inline-flex; }
83      .ba-loading-text {
84        font-size: 12px;
85        color: #a78bfa;
86        margin-left: 8px;
87        display: none;
88      }
89      .ba-comment-result {
90        margin-top: 10px;
91        background: #0f3460;
92        border-radius: 8px;
93        padding: 10px 12px;
94        font-size: 13px;
95        color: #f3f4f6;
96        line-height: 1.6;
97        display: none;
98        border: 1px solid #1e4080;
99        white-space: pre-wrap;
100      }
101      .ba-comment-result.visible { display: block; }
102      .ba-comment-label {
103        font-size: 11px;
104        color: #6b7280;
105        margin-bottom: 4px;
106      }
107
108      /* ── Settings button in toolbar ── */
109      #ba-settings-btn {
110        display: inline-flex;
111        align-items: center;
112        gap: 5px;
113        padding: 0 12px;
114        height: 32px;
115        border-radius: 6px;
116        border: none;
117        cursor: pointer;
118        font-size: 13px;
119        font-weight: 600;
120        background: #a78bfa;
121        color: #fff;
122        font-family: 'Segoe UI', Arial, sans-serif;
123        transition: background 0.2s;
124        margin-left: 8px;
125        vertical-align: middle;
126      }
127      #ba-settings-btn:hover { background: #7c3aed; }
128
129      /* ── Settings overlay ── */
130      #ba-settings-overlay {
131        display: none;
132        position: fixed;
133        inset: 0;
134        background: rgba(0,0,0,0.55);
135        z-index: 999998;
136      }
137      #ba-settings-overlay.visible { display: block; }
138      #ba-settings-modal {
139        position: fixed;
140        top: 50%;
141        left: 50%;
142        transform: translate(-50%, -50%);
143        z-index: 999999;
144        background: #1a1a2e;
145        border: 1px solid #2d2d5e;
146        border-radius: 14px;
147        width: 520px;
148        max-width: 95vw;
149        padding: 24px;
150        font-family: 'Segoe UI', Arial, sans-serif;
151        box-shadow: 0 8px 40px rgba(0,0,0,0.5);
152      }
153      #ba-settings-modal h2 {
154        margin: 0 0 6px 0;
155        font-size: 16px;
156        font-weight: 700;
157        color: #a78bfa;
158      }
159      #ba-settings-modal p.ba-hint {
160        margin: 0 0 14px 0;
161        font-size: 12px;
162        color: #6b7280;
163        line-height: 1.5;
164      }
165      #ba-settings-modal textarea {
166        width: 100%;
167        height: 220px;
168        background: #0f3460;
169        border: 1px solid #1e4080;
170        border-radius: 8px;
171        color: #f3f4f6;
172        font-size: 13px;
173        line-height: 1.6;
174        padding: 10px 12px;
175        resize: vertical;
176        font-family: 'Segoe UI', Arial, sans-serif;
177        box-sizing: border-box;
178      }
179      #ba-settings-modal textarea:focus { outline: 2px solid #a78bfa; }
180      .ba-settings-actions {
181        display: flex;
182        gap: 10px;
183        margin-top: 14px;
184        justify-content: flex-end;
185      }
186      #ba-settings-save {
187        background: #10b981;
188        color: #fff;
189        padding: 8px 20px;
190        border-radius: 7px;
191        border: none;
192        cursor: pointer;
193        font-size: 13px;
194        font-weight: 600;
195        font-family: 'Segoe UI', Arial, sans-serif;
196        transition: background 0.2s;
197      }
198      #ba-settings-save:hover { background: #059669; }
199      #ba-settings-cancel {
200        background: #374151;
201        color: #d1d5db;
202        padding: 8px 20px;
203        border-radius: 7px;
204        border: none;
205        cursor: pointer;
206        font-size: 13px;
207        font-weight: 600;
208        font-family: 'Segoe UI', Arial, sans-serif;
209        transition: background 0.2s;
210      }
211      #ba-settings-cancel:hover { background: #4b5563; }
212      #ba-settings-saved-msg {
213        font-size: 12px;
214        color: #10b981;
215        display: none;
216        align-self: center;
217      }
218    `);
219    }
220
221    function injectSettingsButton() {
222        const toolbar = document.querySelector('.toolbar_box');
223        if (!toolbar || document.getElementById('ba-settings-btn')) return;
224
225        const btn = document.createElement('button');
226        btn.id = 'ba-settings-btn';
227        btn.innerHTML = '⚙️ Настройки автокомментариев';
228        toolbar.appendChild(btn);
229
230        // Create overlay + modal
231        const overlay = document.createElement('div');
232        overlay.id = 'ba-settings-overlay';
233
234        const modal = document.createElement('div');
235        modal.id = 'ba-settings-modal';
236        modal.innerHTML = `
237      <h2>⚙️ Настройки автокомментариев</h2>
238      <p class="ba-hint">Введите свой промпт для генерации комментариев. Используйте <strong>{post_text}</strong> и <strong>{post_link}</strong> как плейсхолдеры — они будут заменены на реальные данные поста. Если поле пустое — используется промпт по умолчанию.</p>
239      <textarea id="ba-prompt-textarea" placeholder="Например: Ты маркетолог. Напиши живой комментарий к посту:\n\n{post_text}\n\nСсылка: {post_link}"></textarea>
240      <div class="ba-settings-actions">
241        <span id="ba-settings-saved-msg">✅ Сохранено!</span>
242        <button id="ba-settings-cancel">Отмена</button>
243        <button id="ba-settings-save">💾 Сохранить</button>
244      </div>
245    `;
246        overlay.appendChild(modal);
247        document.body.appendChild(overlay);
248
249        // Load saved prompt
250        GM.getValue(PROMPT_KEY, '').then(saved => {
251            document.getElementById('ba-prompt-textarea').value = saved;
252        });
253
254        btn.addEventListener('click', () => {
255            overlay.classList.add('visible');
256        });
257
258        overlay.addEventListener('click', (e) => {
259            if (e.target === overlay) overlay.classList.remove('visible');
260        });
261
262        document.getElementById('ba-settings-cancel').addEventListener('click', () => {
263            overlay.classList.remove('visible');
264        });
265
266        document.getElementById('ba-settings-save').addEventListener('click', async () => {
267            const val = document.getElementById('ba-prompt-textarea').value.trim();
268            await GM.setValue(PROMPT_KEY, val);
269            console.log('[BA-Tasks] Промпт сохранён');
270            const msg = document.getElementById('ba-settings-saved-msg');
271            msg.style.display = 'inline';
272            setTimeout(() => {
273                msg.style.display = 'none';
274                overlay.classList.remove('visible');
275            }, 1200);
276        });
277    }
278
279    function observeToolbar() {
280        const observer = new MutationObserver(debounce(() => {
281            injectSettingsButton();
282        }, 300));
283        observer.observe(document.body, { childList: true, subtree: true });
284        // Try immediately too
285        injectSettingsButton();
286    }
287
288    function observeModal() {
289        const observer = new MutationObserver(debounce(() => {
290            const modal = document.querySelector('#fulltext_dialog.modal-overlay');
291            if (!modal) return;
292
293            // Проверяем, не вставили ли уже блок
294            if (modal.querySelector('.ba-modal-block')) return;
295
296            // Ищем место для вставки — после .row_fulltext или перед .fulltext_dialog_switch
297            const insertTarget = modal.querySelector('.fulltext_dialog_switch');
298            if (!insertTarget) return;
299
300            const postText = extractModalText(modal);
301            const postLink = extractModalLink(modal);
302
303            if (!postText || !postLink) {
304                console.warn('[BA-Tasks] Не удалось извлечь текст или ссылку из модального окна');
305                return;
306            }
307
308            console.log('[BA-Tasks] Модальное окно открыто, вставляю блок. Ссылка:', postLink);
309            injectModalBlock(insertTarget, postText, postLink);
310        }, 300));
311
312        observer.observe(document.body, { childList: true, subtree: true });
313    }
314
315    function extractModalText(modal) {
316    // Полный текст из модалки — берём весь innerText блока msg_text
317        const msgText = modal.querySelector('.msg_text');
318        if (!msgText) return null;
319        return msgText.innerText.trim();
320    }
321
322    function extractModalLink(modal) {
323    // Ссылка из заголовка модалки
324        const linkEl = modal.querySelector('.msg_source.info_item a[href]');
325        if (linkEl) return linkEl.href;
326
327        // Запасной вариант — ссылка из "Оригинал сообщения"
328        const origEl = modal.querySelector('.row_original_msg a[href]');
329        if (origEl) return origEl.href;
330
331        return null;
332    }
333
334    function injectModalBlock(insertTarget, postText, postLink) {
335        const block = document.createElement('div');
336        block.className = 'ba-modal-block';
337        block.innerHTML = `
338      <div class="ba-title">🤖 Задача для фрилансера</div>
339      <button class="ba-btn ba-btn-generate" id="ba-gen-btn">✨ Сгенерировать комментарий</button>
340      <span class="ba-loading-text" id="ba-gen-loading">Генерирую...</span>
341      <div class="ba-comment-result" id="ba-gen-result">
342        <div class="ba-comment-label">Текст комментария:</div>
343        <div id="ba-gen-text"></div>
344      </div>
345      <button class="ba-btn ba-btn-send" id="ba-send-btn">📋 Отправить фрилансеру на unu.im</button>
346    `;
347
348        insertTarget.parentNode.insertBefore(block, insertTarget);
349
350        const genBtn = block.querySelector('#ba-gen-btn');
351        const sendBtn = block.querySelector('#ba-send-btn');
352        const loadingEl = block.querySelector('#ba-gen-loading');
353        const resultBox = block.querySelector('#ba-gen-result');
354        const textEl = block.querySelector('#ba-gen-text');
355
356        genBtn.addEventListener('click', async () => {
357            genBtn.disabled = true;
358            loadingEl.style.display = 'inline';
359            resultBox.classList.remove('visible');
360            sendBtn.classList.remove('visible');
361
362            console.log('[BA-Tasks] Генерирую комментарий для:', postLink);
363
364            try {
365                const comment = await generateComment(postText, postLink);
366                textEl.textContent = comment;
367                resultBox.classList.add('visible');
368                sendBtn.classList.add('visible');
369                sendBtn.dataset.comment = comment;
370                console.log('[BA-Tasks] Комментарий готов:', comment);
371            } catch (err) {
372                console.error('[BA-Tasks] Ошибка генерации:', err);
373                textEl.textContent = '❌ Ошибка генерации. Попробуйте ещё раз.';
374                resultBox.classList.add('visible');
375                genBtn.disabled = false;
376            } finally {
377                loadingEl.style.display = 'none';
378            }
379        });
380
381        sendBtn.addEventListener('click', async () => {
382            const comment = sendBtn.dataset.comment;
383            const taskData = { link: postLink, text: postText, comment };
384            console.log('[BA-Tasks] Сохраняю задачу и открываю unu.im:', taskData);
385            await GM.setValue(STORAGE_KEY, JSON.stringify(taskData));
386            window.open(UNU_URL, '_blank');
387        });
388    }
389
390    async function generateComment(postText, postLink) {
391        const customPrompt = await GM.getValue(PROMPT_KEY, '');
392
393        let prompt;
394        if (customPrompt) {
395            prompt = customPrompt
396                .replace('{post_text}', postText)
397                .replace('{post_link}', postLink);
398        } else {
399            prompt = `Ты — маркетолог. Тебе нужно написать естественный, живой комментарий от лица обычного пользователя к следующему посту из социальной сети.
400
401Пост: """
402${postText}
403"""
404
405Ссылка на пост: ${postLink}
406
407Требования к комментарию:
408- Комментарий должен быть коротким (1-3 предложения)
409- Звучать как живой человек, не как реклама
410- Быть релевантным теме поста
411- Можно упомянуть личный опыт
412- Не использовать слова "реклама", "промо", "партнёрский"
413- Писать только на русском языке
414
415Напиши только текст комментария, без кавычек и пояснений.`;
416        }
417
418        const result = await RM.aiCall(prompt);
419        return result.trim();
420    }
421
422    // ─── UNU.IM PAGE ─────────────────────────────────────────────────────────────
423    async function initUnu() {
424        console.log('[BA-Tasks] Инициализация на unu.im');
425        const url = window.location.href;
426
427        // Step 2: страница "Задача создана" — кликаем на ссылку оплатить
428        if (url.includes('/tasks/add')) {
429            const raw = await GM.getValue(STORAGE_KEY, null);
430            if (!raw) {
431                console.log('[BA-Tasks] Нет сохранённых данных задачи');
432                return;
433            }
434            let taskData;
435            try { taskData = JSON.parse(raw); } catch (e) {
436                console.error('[BA-Tasks] Ошибка парсинга данных задачи:', e);
437                return;
438            }
439
440            // Проверяем — это страница после создания (есть ссылка оплатить) или форма
441            const checkForPayLink = () => {
442                const payLink = document.querySelector('a[href*="/tasks/pay/"]');
443                if (payLink) {
444                    console.log('[BA-Tasks] Задача создана, кликаю на оплатить:', payLink.href);
445                    payLink.click();
446                    return true;
447                }
448                return false;
449            };
450
451            if (!checkForPayLink()) {
452                console.log('[BA-Tasks] Заполняю форму данными:', taskData);
453                setTimeout(() => {
454                    fillUnuForm(taskData);
455                    // После заполнения формы — автоматически сабмитим
456                    setTimeout(() => {
457                        const submitBtn = document.querySelector('button.bt-purle[type="submit"]');
458                        if (submitBtn) {
459                            console.log('[BA-Tasks] Кликаю Создать');
460                            submitBtn.click();
461                        }
462                    }, 600);
463                }, 1500);
464
465                // Наблюдаем за появлением ссылки оплатить после сабмита
466                const observer = new MutationObserver(debounce(() => {
467                    checkForPayLink();
468                }, 300));
469                observer.observe(document.body, { childList: true, subtree: true });
470            }
471        }
472
473        // Step 3: страница оплаты /tasks/pay/* — ставим 1 и жмём Оплатить
474        if (url.includes('/tasks/pay/')) {
475            console.log('[BA-Tasks] Страница оплаты, ставлю количество 1 и жму Оплатить');
476            setTimeout(() => {
477                const kolvoInput = document.querySelector('input[name="kolvo"]');
478                const payBtn = document.querySelector('button#buttonpay[type="submit"]');
479                if (!kolvoInput || !payBtn) {
480                    console.error('[BA-Tasks] Не найдены поля на странице оплаты');
481                    return;
482                }
483                kolvoInput.value = '1';
484                kolvoInput.dispatchEvent(new Event('input', { bubbles: true }));
485                kolvoInput.dispatchEvent(new Event('change', { bubbles: true }));
486                console.log('[BA-Tasks] Количество выполнений = 1, кликаю Оплатить');
487                payBtn.click();
488            }, 1500);
489        }
490    }
491
492    function fillUnuForm(taskData) {
493        const nameInput = document.querySelector('input[name="task_name"]');
494        const linkInput = document.querySelector('input[name="task_link"]');
495        const descrTextarea = document.querySelector('textarea[name="task_descr"]');
496        const reportTextarea = document.querySelector('textarea[name="task_need_for_report"]');
497
498        if (!nameInput || !linkInput || !descrTextarea) {
499            console.error('[BA-Tasks] Не найдены поля формы на unu.im');
500            return;
501        }
502
503        let sourceName = 'пост';
504        try {
505            const url = new URL(taskData.link);
506            sourceName = url.hostname.replace('www.', '');
507        } catch (e) {}
508
509        const taskTitle = `Написать комментарий к посту на ${sourceName}`;
510
511        const taskDescription = `Здравствуйте!
512
513Нужно оставить комментарий к посту по ссылке ниже.
514
515📌 Ссылка на пост: ${taskData.link}
516
517💬 Текст комментария (напишите именно этот текст):
518${taskData.comment}
519
520Важно:
521— Комментарий должен выглядеть как живой, от реального человека
522— Не копируйте дословно, можно немного перефразировать, сохраняя смысл
523— Аккаунт должен быть живым (не пустым)`;
524
525        const reportDescription = 'Скриншот оставленного комментария с видимым именем аккаунта и датой публикации.';
526
527        setNativeValue(nameInput, taskTitle);
528        setNativeValue(linkInput, taskData.link);
529        setNativeValue(descrTextarea, taskDescription);
530        if (reportTextarea) {
531            setNativeValue(reportTextarea, reportDescription);
532        }
533
534        showUnuNotification();
535        console.log('[BA-Tasks] Форма заполнена успешно');
536    }
537
538    function setNativeValue(el, value) {
539        el.focus();
540        el.value = value;
541        el.dispatchEvent(new Event('input', { bubbles: true }));
542        el.dispatchEvent(new Event('change', { bubbles: true }));
543        el.blur();
544    }
545
546    function showUnuNotification() {
547        TM_addStyle(`
548      #ba-unu-notice {
549        position: fixed;
550        top: 20px;
551        right: 20px;
552        z-index: 99999;
553        background: #10b981;
554        color: #fff;
555        border-radius: 10px;
556        padding: 14px 20px;
557        font-family: 'Segoe UI', Arial, sans-serif;
558        font-size: 14px;
559        font-weight: 600;
560        box-shadow: 0 4px 20px rgba(0,0,0,0.3);
561        animation: ba-slide-in 0.4s ease;
562      }
563      @keyframes ba-slide-in {
564        from { transform: translateX(120%); opacity: 0; }
565        to { transform: translateX(0); opacity: 1; }
566      }
567    `);
568        const notice = document.createElement('div');
569        notice.id = 'ba-unu-notice';
570        notice.textContent = '✅ Форма заполнена! Проверьте и нажмите «Создать»';
571        document.body.appendChild(notice);
572        setTimeout(() => notice.remove(), 5000);
573    }
574
575    // ─── INIT ─────────────────────────────────────────────────────────────────────
576    function init() {
577        const host = window.location.hostname;
578        if (host.includes('brandanalytics.ru')) {
579            initBrandAnalytics();
580        } else if (host.includes('unu.im')) {
581            initUnu();
582        }
583    }
584
585    if (document.readyState === 'loading') {
586        document.addEventListener('DOMContentLoaded', init);
587    } else {
588        init();
589    }
590})();
Brand Analytics Comment Task Creator | Robomonkey