auto-msg-1.2

Pasted Oct 27, 2026, 5:28 PM1 file548 lines

Scan to open on your phone

Point a camera at the QR code - it opens this exact paste, no app needed.

https://codepastes.com/auto-msg-1-2
auto-msg-1.2
// ==UserScript==
// @name         Twitch Auto Message (Macro configurável)
// @namespace    https://tampermonkey.net/
// @version      2.0
// @description  Envia mensagens automáticas no chat da Twitch, uma caixa por mensagem, em ordem, com intervalo configurável
// @match        https://www.twitch.tv/*
// @match        https://twitch.tv/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

(function () {
  'use strict';

  console.log('[TAM] script carregado');

  const MIN_INTERVAL = 5; // segundos (trava de segurança)

  // ---------- Configuração persistente ----------
  const defaults = {
    messages: [
      { text: 'Mensagem 1', enabled: true },
      { text: 'Mensagem 2', enabled: true },
    ],
    interval: 300,
    jitter: 0,
    minimized: false,
    hidden: false,
    posX: null,
    posY: null,
  };
  let saved = {};
  try { saved = GM_getValue('cfg', {}) || {}; } catch (e) { console.warn('[TAM] GM_getValue falhou', e); }
  const cfg = Object.assign({}, defaults, saved);

  // Migra o formato antigo (texto único, uma mensagem por linha) para caixas separadas
  if (typeof cfg.messages === 'string') {
    cfg.messages = cfg.messages
      .split('\n').map((t) => t.trim()).filter(Boolean)
      .map((text) => ({ text, enabled: true }));
  }
  if (!Array.isArray(cfg.messages) || !cfg.messages.length) {
    cfg.messages = [{ text: '', enabled: true }];
  }

  const save = () => { try { GM_setValue('cfg', cfg); } catch (e) { console.warn('[TAM] GM_setValue falhou', e); } };

  // ---------- Estado ----------
  let running = false;
  let nextAt = 0;
  let periodMs = 1;
  let nextPos = 0; // posição (na lista completa) de onde a busca da próxima mensagem começa
  let tickId = null;
  let rowEls = [];

  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

  // Procura a próxima caixa ativa e preenchida, a partir de "from", em ordem circular
  function findNext(from) {
    const n = cfg.messages.length;
    for (let i = 0; i < n; i++) {
      const j = (from + i) % n;
      const m = cfg.messages[j];
      if (m.enabled && m.text.trim()) return j;
    }
    return -1;
  }

  // ---------- Envio da mensagem ----------
  function getEditor() {
    return (
      document.querySelector('[data-a-target="chat-input"]') ||
      document.querySelector('.chat-wysiwyg-input__editor') ||
      document.querySelector('div[role="textbox"][contenteditable="true"]')
    );
  }
  const editorText = (el) => (el.textContent || '').replace(/[\u200B\uFEFF]/g, '').trim();

  function clearEditor(el) {
    el.focus();
    document.execCommand('selectAll', false, null);
    document.execCommand('delete', false, null);
  }

  // Tenta inserir o texto no editor da Twitch (Slate) por 3 métodos diferentes
  async function typeInto(el, text) {
    if (editorText(el)) clearEditor(el);
    el.focus();

    // Método 1: evento de colar (funciona mesmo com a aba sem foco)
    try {
      const dt = new DataTransfer();
      dt.setData('text/plain', text);
      el.dispatchEvent(new ClipboardEvent('paste', { bubbles: true, cancelable: true, clipboardData: dt }));
    } catch (e) { console.warn('[TAM] paste falhou', e); }
    await sleep(300);
    if (editorText(el)) return 'paste';

    // Método 2: execCommand
    el.focus();
    document.execCommand('insertText', false, text);
    await sleep(300);
    if (editorText(el)) return 'execCommand';

    // Método 3: beforeinput (o Slate escuta esse evento)
    try {
      el.dispatchEvent(new InputEvent('beforeinput', {
        bubbles: true, cancelable: true, inputType: 'insertText', data: text,
      }));
    } catch (e) { console.warn('[TAM] beforeinput falhou', e); }
    await sleep(300);
    if (editorText(el)) return 'beforeinput';

    return null;
  }

  async function sendMessage(text) {
    const el = getEditor();
    if (!el) return 'no_editor';

    const method = await typeInto(el, text);
    console.log('[TAM] método de digitação que funcionou:', method);
    if (!method) return 'no_text';

    await sleep(400); // dá tempo do React habilitar o botão de enviar

    const btn = document.querySelector('[data-a-target="chat-send-button"]');
    if (btn && !btn.disabled && btn.getAttribute('aria-disabled') !== 'true') {
      btn.click();
    } else {
      const opts = { key: 'Enter', code: 'Enter', keyCode: 13, which: 13, bubbles: true, cancelable: true };
      el.dispatchEvent(new KeyboardEvent('keydown', opts));
      el.dispatchEvent(new KeyboardEvent('keyup', opts));
    }

    await sleep(700);
    return editorText(el) === '' ? 'ok' : 'not_sent';
  }

  function schedule() {
    const base = Math.max(MIN_INTERVAL, Number(cfg.interval) || 60) * 1000;
    const jit = cfg.jitter > 0 ? base * ((Math.random() * 2 - 1) * cfg.jitter) / 100 : 0;
    periodMs = Math.max(MIN_INTERVAL * 1000, base + jit);
    nextAt = Date.now() + periodMs;
  }

  async function sendByIndex(j) {
    const text = cfg.messages[j].text.trim().replace(/\s*\n\s*/g, ' ');
    const res = await sendMessage(text);
    const msgs = {
      ok: 'Enviada #' + (j + 1) + ': ' + text.slice(0, 28),
      no_editor: 'Erro: campo do chat não encontrado (recarregue a página)',
      no_text: 'Erro: o texto não entrou no campo do chat',
      not_sent: 'Erro: o texto entrou mas não foi enviado (slow mode? chat restrito?)',
    };
    setStatus(msgs[res] || res);
    console.log('[TAM] resultado do envio:', res);
    return res;
  }

  async function fire() {
    const j = findNext(nextPos);
    if (j < 0) return setStatus('Nenhuma mensagem ativa/preenchida');
    nextPos = (j + 1) % cfg.messages.length;
    await sendByIndex(j);
    highlight();
  }

  function formatTime(s) {
    const m = Math.floor(s / 60);
    const r = s % 60;
    return m > 0 ? m + ':' + String(r).padStart(2, '0') : s + 's';
  }

  function tick() {
    if (!running) return;
    const leftMs = Math.max(0, nextAt - Date.now());
    countdownEl.textContent = formatTime(Math.ceil(leftMs / 1000));
    progressFill.style.width = (100 - (leftMs / periodMs) * 100) + '%';
    if (Date.now() >= nextAt) {
      schedule();
      fire();
    }
  }

  function setRunningUI(on) {
    toggleBtn.textContent = on ? '⏹  Parar' : '▶  Iniciar';
    toggleBtn.className = 'tam-btn ' + (on ? 'tam-danger' : 'tam-primary');
    dotEl.className = 'tam-dot' + (on ? ' tam-on' : '');
    if (!on) {
      countdownEl.textContent = '--';
      progressFill.style.width = '0%';
    }
  }

  function start() {
    readInputs();
    if (findNext(0) < 0) return setStatus('Erro: preencha ao menos uma caixa');
    running = true;
    nextPos = 0;
    schedule();
    clearInterval(tickId);
    tickId = setInterval(tick, 500);
    setRunningUI(true);
    setStatus('Rodando...');
    tick();
    highlight();
  }

  function stop() {
    running = false;
    clearInterval(tickId);
    setRunningUI(false);
    setStatus('Parado');
    highlight();
  }

  // ---------- Interface (sem innerHTML, compatível com Trusted Types) ----------
  GM_addStyle(`
    #tam-panel{position:fixed;z-index:2147483647;width:340px;background:#18181b;color:#efeff1;
      border:1px solid #2f2f35;border-radius:12px;font:13px/1.4 "Inter","Segoe UI",Arial,sans-serif;
      box-shadow:0 12px 32px rgba(0,0,0,.55);overflow:hidden}
    #tam-panel *{box-sizing:border-box}
    #tam-panel .tam-head{display:flex;align-items:center;gap:8px;padding:10px 12px;cursor:move;user-select:none;
      background:linear-gradient(135deg,#9147ff,#6a2fd6);font-weight:700;font-size:13px}
    #tam-panel .tam-title{flex:1}
    #tam-panel .tam-dot{width:9px;height:9px;border-radius:50%;background:#6b6b73;box-shadow:0 0 0 2px rgba(255,255,255,.15)}
    #tam-panel .tam-dot.tam-on{background:#00e676;box-shadow:0 0 8px #00e676}
    #tam-panel .tam-min{background:rgba(255,255,255,.15);border:none;color:#fff;cursor:pointer;width:24px;height:24px;
      border-radius:6px;font-size:14px;line-height:1}
    #tam-panel .tam-min:hover{background:rgba(255,255,255,.28)}
    #tam-panel .tam-body{padding:12px;display:flex;flex-direction:column;gap:12px}
    #tam-panel .tam-hint{font-size:11px;color:#8b8b95}
    #tam-panel .tam-sec{display:flex;justify-content:space-between;align-items:center;font-size:11px;font-weight:700;
      letter-spacing:.06em;text-transform:uppercase;color:#adadb8}
    #tam-panel .tam-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto;padding-right:3px}
    #tam-panel .tam-list::-webkit-scrollbar{width:6px}
    #tam-panel .tam-list::-webkit-scrollbar-thumb{background:#3a3a3d;border-radius:3px}
    #tam-panel .tam-card{display:flex;gap:8px;align-items:flex-start;padding:8px;background:#1f1f23;
      border:1px solid #2f2f35;border-radius:10px;transition:border-color .15s,box-shadow .15s,opacity .15s}
    #tam-panel .tam-card.tam-off{opacity:.5}
    #tam-panel .tam-card.tam-next{border-color:#9147ff;box-shadow:0 0 0 1px #9147ff}
    #tam-panel .tam-side{display:flex;flex-direction:column;align-items:center;gap:8px;padding-top:2px}
    #tam-panel .tam-num{width:22px;height:22px;border-radius:50%;background:#2d1f4d;color:#bf94ff;
      font-weight:700;font-size:11px;display:flex;align-items:center;justify-content:center}
    #tam-panel .tam-switch{position:relative;width:28px;height:16px;display:inline-block;cursor:pointer}
    #tam-panel .tam-switch input{opacity:0;width:0;height:0;position:absolute}
    #tam-panel .tam-slider{position:absolute;inset:0;background:#3a3a3d;border-radius:16px;transition:.15s}
    #tam-panel .tam-slider::before{content:"";position:absolute;width:12px;height:12px;left:2px;top:2px;
      background:#fff;border-radius:50%;transition:.15s}
    #tam-panel .tam-switch input:checked + .tam-slider{background:#9147ff}
    #tam-panel .tam-switch input:checked + .tam-slider::before{transform:translateX(12px)}
    #tam-panel textarea,#tam-panel input[type=number]{background:#0e0e10;color:#efeff1;border:1px solid #333338;
      border-radius:8px;padding:7px 9px;font:13px "Inter","Segoe UI",Arial,sans-serif;width:100%;outline:none}
    #tam-panel textarea{resize:vertical;flex:1;min-width:0;min-height:44px}
    #tam-panel textarea:focus,#tam-panel input[type=number]:focus{border-color:#9147ff}
    #tam-panel .tam-actions{display:flex;flex-direction:column;gap:4px}
    #tam-panel .tam-icon{width:24px;height:22px;background:#2a2a30;color:#c8c8d0;border:none;border-radius:6px;
      cursor:pointer;font-size:10px;padding:0}
    #tam-panel .tam-icon:hover{background:#3a3a42;color:#fff}
    #tam-panel .tam-icon.tam-del:hover{background:#c0392b}
    #tam-panel .tam-add{background:transparent;color:#bf94ff;border:1px dashed #4a3a78;border-radius:10px;
      padding:8px;cursor:pointer;font-size:12px;font-weight:600}
    #tam-panel .tam-add:hover{background:#221a3a}
    #tam-panel .tam-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
    #tam-panel .tam-field{display:flex;flex-direction:column;gap:4px;font-size:11px;color:#adadb8}
    #tam-panel .tam-btns{display:flex;gap:8px}
    #tam-panel button{transition:background-color .2s ease,transform .15s ease,box-shadow .2s ease,
      border-color .2s ease,color .2s ease,opacity .2s ease}
    #tam-panel button:active{transform:scale(.96)}
    #tam-panel .tam-icon:hover,#tam-panel .tam-min:hover{transform:translateY(-1px)}
    #tam-panel .tam-btn{border:none;border-radius:10px;padding:10px 12px;cursor:pointer;font-weight:700;font-size:13px;
      color:#fff;box-shadow:0 2px 6px rgba(0,0,0,.25)}
    #tam-panel .tam-btn:hover{transform:translateY(-1px);box-shadow:0 6px 14px rgba(0,0,0,.35)}
    #tam-panel .tam-btn:active{transform:translateY(0) scale(.97);box-shadow:0 1px 3px rgba(0,0,0,.3)}
    #tam-panel .tam-primary{background:#9147ff;flex:1.4}
    #tam-panel .tam-primary:hover{background:#a368ff;box-shadow:0 6px 16px rgba(145,71,255,.45)}
    #tam-panel .tam-danger{background:#d6382b;flex:1.4}
    #tam-panel .tam-danger:hover{background:#e5493c;box-shadow:0 6px 16px rgba(214,56,43,.45)}
    #tam-panel .tam-ghost{background:#2a2a30;color:#efeff1;flex:1;font-weight:600}
    #tam-panel .tam-ghost:hover{background:#3a3a42}
    #tam-panel .tam-logo{width:18px;height:18px;fill:#fff;flex:none}
    #tam-panel kbd{background:#2a2a30;border:1px solid #3a3a3d;border-radius:4px;padding:0 5px;font:inherit;color:#efeff1}
    #tam-panel .tam-timer{background:#1f1f23;border:1px solid #2f2f35;border-radius:10px;padding:9px 10px}
    #tam-panel .tam-timer-row{display:flex;justify-content:space-between;align-items:center;font-size:12px;color:#adadb8}
    #tam-panel .tam-timer-row b{color:#fff;font-size:15px}
    #tam-panel .tam-bar{height:5px;background:#2f2f35;border-radius:3px;margin-top:7px;overflow:hidden}
    #tam-panel .tam-fill{height:100%;width:0;background:linear-gradient(90deg,#9147ff,#bf94ff);transition:width .5s linear}
    #tam-panel .tam-status{font-size:11px;color:#8b8b95;min-height:14px}
    #tam-panel .tam-status.tam-err{color:#ff7b72}
  `);

  function h(tag, props, children) {
    const el = document.createElement(tag);
    Object.entries(props || {}).forEach(([k, v]) => {
      if (k === 'text') el.textContent = v;
      else el.setAttribute(k, v);
    });
    (children || []).forEach((c) => el.append(c));
    return el;
  }

  // Logo da Twitch em SVG (criado com createElementNS, sem innerHTML)
  function twitchLogo() {
    const NS = 'http://www.w3.org/2000/svg';
    const svg = document.createElementNS(NS, 'svg');
    svg.setAttribute('viewBox', '0 0 24 24');
    svg.setAttribute('class', 'tam-logo');
    const p = document.createElementNS(NS, 'path');
    p.setAttribute('d', 'M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.43l-3 3v-3H6.857V1.714h13.714Z');
    svg.appendChild(p);
    return svg;
  }

  const HOTKEY = 'F2';

  const dotEl = h('span', { class: 'tam-dot' });
  const minBtn = h('button', { class: 'tam-min', title: 'Minimizar', text: '—' });
  const head = h('div', { class: 'tam-head' }, [
    twitchLogo(),
    dotEl,
    h('span', { class: 'tam-title', text: 'Auto Mensagem' }),
    minBtn,
  ]);

  const listEl = h('div', { class: 'tam-list' });
  const addBtn = h('button', { class: 'tam-add', text: '+ Adicionar mensagem' });
  const intervalEl = h('input', { type: 'number', min: String(MIN_INTERVAL) });
  const jitterEl = h('input', { type: 'number', min: '0', max: '90' });
  const toggleBtn = h('button', { class: 'tam-btn tam-primary', text: '▶  Iniciar' });
  const testBtn = h('button', { class: 'tam-btn tam-ghost', text: '✉  Testar' });
  const countdownEl = h('b', { text: '--' });
  const progressFill = h('div', { class: 'tam-fill' });
  const statusEl = h('div', { class: 'tam-status' });

  const bodyEl = h('div', { class: 'tam-body' }, [
    h('div', { class: 'tam-hint' }, [
      document.createTextNode('Enviadas da 1ª até a última e depois recomeçam. Esconder/mostrar painel: '),
      h('kbd', { text: HOTKEY }),
    ]),
    h('div', { class: 'tam-sec' }, [h('span', { text: 'Mensagens' })]),
    listEl,
    addBtn,
    h('div', { class: 'tam-grid' }, [
      h('label', { class: 'tam-field' }, [document.createTextNode('Intervalo (seg)'), intervalEl]),
      h('label', { class: 'tam-field' }, [document.createTextNode('Variação (%)'), jitterEl]),
    ]),
    h('div', { class: 'tam-timer' }, [
      h('div', { class: 'tam-timer-row' }, [h('span', { text: 'Próxima em' }), countdownEl]),
      h('div', { class: 'tam-bar' }, [progressFill]),
    ]),
    h('div', { class: 'tam-btns' }, [toggleBtn, testBtn]),
    statusEl,
  ]);

  const panel = h('div', { id: 'tam-panel' }, [head, bodyEl]);

  // Evita que atalhos de teclado da Twitch interceptem o que você digita no painel
  ['keydown', 'keyup', 'keypress'].forEach((ev) =>
    panel.addEventListener(ev, (e) => e.stopPropagation())
  );

  function setStatus(t) {
    statusEl.textContent = t;
    statusEl.className = 'tam-status' + (/^Erro/.test(t) ? ' tam-err' : '');
  }

  // Destaca a caixa que será enviada a seguir
  function highlight() {
    const upcoming = running ? findNext(nextPos) : -1;
    rowEls.forEach((row, i) => {
      const off = !cfg.messages[i].enabled;
      row.className = 'tam-card' + (i === upcoming ? ' tam-next' : '') + (off ? ' tam-off' : '');
    });
  }

  function move(i, dir) {
    const j = i + dir;
    if (j < 0 || j >= cfg.messages.length) return;
    const tmp = cfg.messages[i];
    cfg.messages[i] = cfg.messages[j];
    cfg.messages[j] = tmp;
    save();
    render();
  }

  function removeAt(i) {
    if (cfg.messages.length === 1) {
      cfg.messages[0] = { text: '', enabled: true };
    } else {
      cfg.messages.splice(i, 1);
    }
    nextPos = nextPos % cfg.messages.length;
    save();
    render();
  }

  function render() {
    listEl.textContent = '';
    rowEls = [];
    cfg.messages.forEach((m, i) => {
      const num = h('span', { class: 'tam-num', text: String(i + 1) });

      const chk = h('input', { type: 'checkbox' });
      chk.checked = m.enabled;
      chk.addEventListener('change', () => { m.enabled = chk.checked; save(); highlight(); });
      const sw = h('label', { class: 'tam-switch', title: 'Ativar/desativar esta mensagem' }, [
        chk, h('span', { class: 'tam-slider' }),
      ]);

      const ta = h('textarea', { rows: '2', placeholder: 'Mensagem ' + (i + 1) });
      ta.value = m.text;
      ta.addEventListener('input', () => { m.text = ta.value; save(); });

      const mk = (t, title, fn, extra) => {
        const b = h('button', { class: 'tam-icon' + (extra ? ' ' + extra : ''), text: t, title });
        b.addEventListener('click', fn);
        return b;
      };

      const row = h('div', { class: 'tam-card' }, [
        h('div', { class: 'tam-side' }, [num, sw]),
        ta,
        h('div', { class: 'tam-actions' }, [
          mk('▲', 'Subir', () => move(i, -1)),
          mk('▼', 'Descer', () => move(i, 1)),
          mk('✕', 'Remover', () => removeAt(i), 'tam-del'),
        ]),
      ]);
      listEl.appendChild(row);
      rowEls.push(row);
    });
    highlight();
  }

  function fillInputs() {
    intervalEl.value = cfg.interval;
    jitterEl.value = cfg.jitter;
    bodyEl.style.display = cfg.minimized ? 'none' : 'flex';
    panel.style.display = cfg.hidden ? 'none' : '';
    render();
  }

  function readInputs() {
    cfg.interval = Math.max(MIN_INTERVAL, parseInt(intervalEl.value, 10) || 60);
    cfg.jitter = Math.min(90, Math.max(0, parseInt(jitterEl.value, 10) || 0));
    intervalEl.value = cfg.interval;
    jitterEl.value = cfg.jitter;
    save();
  }

  function place() {
    let x = cfg.posX != null ? cfg.posX : window.innerWidth - 370;
    let y = cfg.posY != null ? cfg.posY : 80;
    x = Math.min(Math.max(0, x), Math.max(0, window.innerWidth - 120));
    y = Math.min(Math.max(0, y), Math.max(0, window.innerHeight - 40));
    panel.style.left = x + 'px';
    panel.style.top = y + 'px';
  }

  [intervalEl, jitterEl].forEach((el) => el.addEventListener('change', readInputs));
  toggleBtn.addEventListener('click', () => (running ? stop() : start()));
  testBtn.addEventListener('click', async () => {
    const j = findNext(0);
    if (j < 0) return setStatus('Erro: preencha ao menos uma caixa');
    setStatus('Testando...');
    await sendByIndex(j);
  });
  addBtn.addEventListener('click', () => {
    cfg.messages.push({ text: '', enabled: true });
    save();
    render();
    const tas = listEl.querySelectorAll('textarea');
    if (tas.length) {
      tas[tas.length - 1].focus();
      listEl.scrollTop = listEl.scrollHeight;
    }
  });
  minBtn.addEventListener('click', () => {
    cfg.minimized = !cfg.minimized;
    bodyEl.style.display = cfg.minimized ? 'none' : 'flex';
    save();
  });

  // Arrastar painel
  (function drag() {
    let sx, sy, ox, oy, dragging = false;
    head.addEventListener('mousedown', (e) => {
      if (e.target === minBtn) return;
      dragging = true;
      sx = e.clientX; sy = e.clientY;
      ox = panel.offsetLeft; oy = panel.offsetTop;
      e.preventDefault();
    });
    document.addEventListener('mousemove', (e) => {
      if (!dragging) return;
      panel.style.left = ox + e.clientX - sx + 'px';
      panel.style.top = oy + e.clientY - sy + 'px';
    });
    document.addEventListener('mouseup', () => {
      if (!dragging) return;
      dragging = false;
      cfg.posX = panel.offsetLeft;
      cfg.posY = panel.offsetTop;
      save();
    });
  })();

  // Atalho para esconder/mostrar o painel (o envio automático continua rodando escondido)
  document.addEventListener('keydown', (e) => {
    if (e.key !== HOTKEY || e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) return;
    e.preventDefault();
    cfg.hidden = !cfg.hidden;
    panel.style.display = cfg.hidden ? 'none' : '';
    save();
  }, true);

  // Garante que o painel exista (a Twitch é uma SPA e pode recriar o DOM)
  function ensurePanel() {
    if (!document.body) return;
    if (!document.getElementById('tam-panel')) {
      document.body.appendChild(panel);
      place();
      console.log('[TAM] painel adicionado à página');
    }
  }

  fillInputs();
  ensurePanel();
  setInterval(ensurePanel, 2000);

  // Atalho no menu do Tampermonkey para recuperar o painel se sumir
  try {
    GM_registerMenuCommand('Mostrar / resetar posição do painel', () => {
      cfg.posX = null;
      cfg.posY = null;
      cfg.minimized = false;
      cfg.hidden = false;
      save();
      fillInputs();
      ensurePanel();
      place();
    });
  } catch (e) { /* ignora */ }
})();

21,745 chars · codepastes.com