Skip to content

Instantly share code, notes, and snippets.

@jim60105
Last active March 31, 2024 16:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jim60105/1654a245f3ed4e9707a2bd655a1ecffc to your computer and use it in GitHub Desktop.
Save jim60105/1654a245f3ed4e9707a2bd655a1ecffc to your computer and use it in GitHub Desktop.
Twitch: 自動拍手機器
// ==UserScript==
// @name Twitch: Automatic clapping machine
// @name:zh Twitch: 自動拍手機器
// @version 0.1.0
// @description 在其它人拍手時自動跟著一起拍
// @author 琳(jim60105)
// @match https://www.twitch.tv/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=www.twitch.tv
// @license GPL3
// ==/UserScript==
(function () {
'use strict';
/**
* 注意: 這個腳本只能在 Twitch 的直播聊天室使用
*
* 若聊天室在背景、處於捲動狀態、或側欄隱藏,Twitch 可能會(也可能不會)更新聊天室,此時腳本可能會失效
* 此腳本會在頁面載入的「5」秒後注入,此時聊天室必須處於可用狀態 (完成載入且側欄不能是隱藏)
* 若是注入失敗,請打開聊天室並 F5 重新整理頁面
*
* 要使用或偵測貼圖,可填入貼圖名稱且前方留一個空格,例如「 PopNemo」
* 若你有該貼圖它就能自動轉換成貼圖,請小心使用
*/
// --- 設定區塊 ---
/**
* 要偵測的觸發字串
* 這是一個文字陣列,這些字串偵測到時就會記數觸發
* 可以輸入多個不同頻道的會員拍手貼圖做為偵測字串
*/
const stringToDetect = ['👏👏👏'];
/**
* 要發出去的字串
*/
const stringToReply = '👏✨👏✨👏✨👏✨👏';
// 範例條件說明:
// 偵測到「4」次字串才觸發
// (同一則訊息內重覆比對時只會計算一次)
// 在「1.5」秒內重覆被偵測到也只計算一次
// 偵測間隔不得超過「10」秒,超過的話就重新計算
// 自動發話後至少等待「120」秒後才會再次自動發話
/**
* 要偵測的次數
*/
const triggerCount = 4;
/**
* 每次間隔不得超過的秒數
*/
const triggerBetweenSeconds = 10;
/**
* 自動發話後至少等待的秒數
*/
const minTimeout = 120;
/**
* 在這個秒數內重覆偵測到觸發字串,至多只會計算一次
* (這是用來避免當視窗由背景來到前景時,聊天記錄一口氣噴出來造成誤觸發)
*/
const throttle = 1.5;
// --- 設定區塊結束 ---
let lastDetectTime = new Date(null);
let currentDetectCount = 0;
let lastTriggerTime = new Date(null);
setTimeout(() => {
onAppend(
document.querySelector(
'[data-test-selector="chat-scrollable-area__message-container"]'
),
function (added) {
added.forEach((node) => {
console.debug('Messages node: ', node);
const text = GetMessage(node);
if (!text) return;
if (!DetectMatch(text)) return;
if (!CheckTriggerCount()) return;
if (!CheckTimeout()) return;
SendMessage(stringToReply);
});
}
);
}, 5000);
function onAppend(elem, f) {
if (!elem) return;
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (m) {
if (m.addedNodes.length) {
f(m.addedNodes);
}
});
});
observer.observe(elem, { childList: true });
}
function GetMessage(node) {
const messageNode = node.querySelector('[data-a-target="chat-line-message-body"]');
if (!messageNode) return '';
let text = '';
messageNode.childNodes.forEach((element) => {
if (element.classList.contains('text-fragment')) {
text += element.textContent;
} else if (element.classList.contains('chat-line__message--emote-button')) {
const img = element.querySelector('img');
text += img.getAttribute('alt');
}
});
console.debug('Message: ', text);
return text;
}
function DetectMatch(text) {
let match = false;
stringToDetect.forEach((p) => {
match |= text.includes(p);
});
if (!match) return false;
console.debug(`Matched!`);
if (lastDetectTime.valueOf() + throttle * 1000 >= Date.now()) {
console.debug('Throttle detected');
return false;
}
if (lastDetectTime.valueOf() + triggerBetweenSeconds * 1000 < Date.now()) {
currentDetectCount = 1;
console.debug('Over max trigger seconds. Reset detect count to 1.');
} else {
currentDetectCount++;
}
lastDetectTime = Date.now();
console.debug(`Count: ${currentDetectCount}`);
return true;
}
function CheckTriggerCount() {
const shouldTrigger = currentDetectCount >= triggerCount;
if (shouldTrigger) console.debug('Triggered!');
return shouldTrigger;
}
function CheckTimeout() {
const isInTimeout = lastTriggerTime.valueOf() + minTimeout * 1000 > Date.now();
if (isInTimeout) console.debug('Still waiting for minTimeout');
return !isInTimeout;
}
function SendMessage(message) {
try {
const input = document
.querySelector('[data-a-target="chat-input"]')
?.querySelector('[data-a-target="chat-input-text"]');
if (!input) {
console.warn('Cannot find input element');
return;
}
const data = new DataTransfer();
data.setData('text/plain', message);
input.dispatchEvent(
new ClipboardEvent('paste', { bubbles: true, clipboardData: data })
);
setTimeout(() => {
const button = document.querySelector('[data-a-target="chat-send-button"]');
// HTMLCollection not array
button.click();
console.log(`[${new Date().toISOString()}]自動發話觸發: ${message}`);
}, 500);
} finally {
lastTriggerTime = Date.now();
currentDetectCount = 0;
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment