Парсит посты с 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})();