Scan to open on your phone
Point a camera at the QR code - it opens this exact paste, no app needed.
https://codepastes.com/twitch-auto-msgtwitch-auto-msg scriptJava
// ==UserScript==
// @name Twitch Auto Message (Macro configurável)
// @namespace https://tampermonkey.net/
// @version 1.3
// @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
// @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,
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 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;
nextAt = Date.now() + Math.max(MIN_INTERVAL * 1000, base + jit);
}
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 tick() {
if (!running) return;
const left = Math.max(0, Math.ceil((nextAt - Date.now()) / 1000));
countdownEl.textContent = left + 's';
if (Date.now() >= nextAt) {
schedule();
fire();
}
}
function start() {
readInputs();
if (findNext(0) < 0) return setStatus('Preencha ao menos uma caixa');
running = true;
nextPos = 0;
schedule();
clearInterval(tickId);
tickId = setInterval(tick, 500);
toggleBtn.textContent = '⏹ Parar';
toggleBtn.style.background = '#c0392b';
setStatus('Rodando...');
highlight();
}
function stop() {
running = false;
clearInterval(tickId);
toggleBtn.textContent = '▶ Iniciar';
toggleBtn.style.background = '#9147ff';
countdownEl.textContent = '--';
setStatus('Parado');
highlight();
}
// ---------- Interface (sem innerHTML, compatível com Trusted Types) ----------
function h(tag, props, children) {
const el = document.createElement(tag);
Object.entries(props || {}).forEach(([k, v]) => {
if (k === 'style') el.style.cssText = v;
else if (k === 'text') el.textContent = v;
else el.setAttribute(k, v);
});
(children || []).forEach((c) => el.append(c));
return el;
}
const labelCss = 'display:flex;flex-direction:column;gap:3px;font-size:12px;color:#adadb8;';
const fieldCss =
'background:#0e0e10;color:#efeff1;border:1px solid #3a3a3d;border-radius:4px;' +
'padding:5px;font:13px Arial,sans-serif;box-sizing:border-box;width:100%;';
const smallBtnCss =
'background:#26262c;color:#efeff1;border:1px solid #3a3a3d;border-radius:3px;' +
'cursor:pointer;font-size:10px;width:22px;height:20px;padding:0;line-height:1;';
const minBtn = h('button', {
title: 'Minimizar', text: '—',
style: 'background:none;border:none;color:#fff;cursor:pointer;font-size:14px;',
});
const head = h('div', {
style: 'display:flex;justify-content:space-between;align-items:center;padding:8px 10px;' +
'background:#9147ff;border-radius:7px 7px 0 0;cursor:move;font-weight:bold;user-select:none;',
}, [h('span', { text: '🤖 Auto Mensagem' }), minBtn]);
const listEl = h('div', {
style: 'display:flex;flex-direction:column;gap:6px;max-height:300px;overflow-y:auto;padding-right:2px;',
});
const addBtn = h('button', {
text: '+ Adicionar mensagem',
style: 'background:#26262c;color:#efeff1;border:1px dashed #5a5a60;border-radius:4px;' +
'padding:6px;cursor:pointer;font-size:12px;',
});
const intervalEl = h('input', { type: 'number', min: String(MIN_INTERVAL), style: fieldCss });
const jitterEl = h('input', { type: 'number', min: '0', max: '90', style: fieldCss });
const toggleBtn = h('button', {
text: '▶ Iniciar',
style: 'border:none;color:#fff;background:#9147ff;padding:8px;border-radius:4px;cursor:pointer;font-weight:bold;',
});
const testBtn = h('button', {
text: '✉ Testar envio agora',
style: 'background:#26262c;color:#efeff1;border:1px solid #3a3a3d;border-radius:4px;padding:6px;cursor:pointer;font-size:12px;',
});
const countdownEl = h('b', { text: '--' });
const statusEl = h('div', { style: 'font-size:11px;color:#adadb8;min-height:14px;' });
const bodyEl = h('div', { style: 'padding:10px;display:flex;flex-direction:column;gap:8px;' }, [
h('div', { style: 'font-size:11px;color:#adadb8;', text: 'As mensagens são enviadas da 1ª caixa até a última e depois recomeçam.' }),
listEl,
addBtn,
h('div', { style: 'display:flex;gap:8px;' }, [
h('label', { style: labelCss + 'flex:1;' }, [document.createTextNode('Intervalo (seg)'), intervalEl]),
h('label', { style: labelCss + 'flex:1;' }, [document.createTextNode('Variação (%)'), jitterEl]),
]),
toggleBtn,
testBtn,
h('div', {}, [document.createTextNode('Próxima em: '), countdownEl]),
statusEl,
]);
const panel = h('div', {
id: 'tam-panel',
style: 'position:fixed;z-index:2147483647;width:310px;background:#18181b;color:#efeff1;' +
'border:1px solid #3a3a3d;border-radius:8px;font:13px/1.4 Arial,sans-serif;' +
'box-shadow:0 4px 16px rgba(0,0,0,.5);',
}, [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; }
// Destaca a caixa que será enviada a seguir
function highlight() {
const upcoming = running ? findNext(nextPos) : -1;
rowEls.forEach((row, i) => {
row.style.borderColor = i === upcoming ? '#9147ff' : '#3a3a3d';
row.style.boxShadow = i === upcoming ? '0 0 0 1px #9147ff' : 'none';
});
}
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', { text: String(i + 1), style: 'font-weight:bold;color:#bf94ff;font-size:12px;' });
const chk = h('input', { type: 'checkbox', title: 'Ativar/desativar esta mensagem' });
chk.checked = m.enabled;
chk.addEventListener('change', () => { m.enabled = chk.checked; save(); highlight(); });
const ta = h('textarea', {
rows: '2', placeholder: 'Mensagem ' + (i + 1),
style: fieldCss + 'resize:vertical;flex:1;min-width:0;',
});
ta.value = m.text;
ta.addEventListener('input', () => { m.text = ta.value; save(); });
const mk = (t, title, fn) => {
const b = h('button', { text: t, title, style: smallBtnCss });
b.addEventListener('click', fn);
return b;
};
const row = h('div', {
style: 'display:flex;gap:5px;align-items:flex-start;padding:5px;border:1px solid #3a3a3d;border-radius:6px;',
}, [
h('div', { style: 'display:flex;flex-direction:column;align-items:center;gap:4px;padding-top:2px;' }, [num, chk]),
ta,
h('div', { style: 'display:flex;flex-direction:column;gap:2px;' }, [
mk('▲', 'Subir', () => move(i, -1)),
mk('▼', 'Descer', () => move(i, 1)),
mk('✕', 'Remover', () => removeAt(i)),
]),
]);
listEl.appendChild(row);
rowEls.push(row);
});
highlight();
}
function fillInputs() {
intervalEl.value = cfg.interval;
jitterEl.value = cfg.jitter;
bodyEl.style.display = cfg.minimized ? 'none' : 'flex';
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 - 340;
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('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();
});
})();
// 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;
save();
fillInputs();
ensurePanel();
place();
});
} catch (e) { /* ignora */ }
})();15,458 chars · codepastes.com