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-2auto-msg-1.2JavaScript
// ==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