Last active
March 31, 2024 16:14
-
-
Save jim60105/1654a245f3ed4e9707a2bd655a1ecffc to your computer and use it in GitHub Desktop.
Twitch: 自動拍手機器
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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