🏠 Home 

YT👏Boost chat

在其它人拍手時自動跟著一起拍

// ==UserScript==
// @name                YT👏Boost chat
// @version             1.4.5
// @description         在其它人拍手時自動跟著一起拍
// @author              琳(jim60105)
// @match               https://www.youtube.com/live_chat*
// @icon                https://www.youtube.com/favicon.ico
// @license             GPL3
// @namespace https://greasyfork.org/users/4839
// ==/UserScript==
/*
* 原腳本作者
https://xn--jgy.tw/Livestream/my-vtuber-dd-life/
https://gist.github.com/jim60105/43b2c53bb59fb588e351982c1a14e273
* YouTube Boost Chat導致聊天室元素變更,修改後搭配使用
https://greasyfork.org/zh-TW/scripts/520755/discussions/271546#comment-555456
*/
(function () {
'use strict';
/**
* 注意: 這個腳本只能在 Youtube 的直播聊天室使用
*
* 若聊天室不是在前景,Youtube 可能會停止更新聊天室,導致功能停止
*
* 聊天室由背景來到前景時,或是捲動停住後回到最下方時,有可能因為訊息一口氣噴出來而直接觸發
* 請調整下方的 throttle 數值,以避免這種情況
* 訊息過多時預設的1.5秒可能會不夠,但設定太高會影響偵測判定
* 訊息過多時建議直接F5重整,不要讓它一直跑
*
* 要使用或偵測 Youtube 貼圖/會員貼圖,可填入像是這種格式 :_右サイリウム::_おんぷちゃん::_ハート:
* 若你有使用貼圖的權限,它就能自動轉換成貼圖,請小心使用
*/
// --- 設定區塊 ---
/**
* 要偵測的觸發字串
* 這是一個文字陣列,這些字串偵測到時就會記數觸發
* 可以輸入多個不同頻道的會員拍手貼圖做為偵測字串
*/
const stringToDetect = [
':clapping_hands::clapping_hands::clapping_hands:', // 這是三個拍手表符(👏👏👏)
':washhands::washhands::washhands:',
];
const stringToReply = '👏👏👏';
// 範例條件說明:
// 偵測到「4」次字串才觸發
// (同一則訊息內重覆比對時只會計算一次)
// 在「1.5」秒內重覆被偵測到也只計算一次
// 偵測間隔不得超過「10」秒,超過的話就重新計算
// 自動發話後至少等待「120」秒後才會再次自動發話
/**
* 要偵測的次數
*/
const triggerCount = 2;
/**
* 每次間隔不得超過的秒數
*/
const triggerBetweenSeconds = 20;
/**
* 自動發話後至少等待的秒數
*/
const minTimeout = 120;
/**
* 在這個秒數內重覆偵測到觸發字串,至多只會計算一次
* (這是用來避免當視窗由背景來到前景時,聊天記錄一口氣噴出來造成誤觸發)
*/
const throttle = 0.5;
// --- 設定區塊結束 ---
let lastDetectTime = new Date(null);
let currentDetectCount = 0;
let lastTriggerTime = new Date(null);
if (window.location.pathname.startsWith('/embed')) return;
if (
typeof ytInitialData !== 'undefined' &&
ytInitialData.continuationContents?.liveChatContinuation?.isReplay
) {
console.debug('Replay mode, exit.');
return;
}
onAppend(
document
.getElementsByTagName('yt-live-chat-item-list-renderer')[0]
?.querySelector('.bst-message-list'),
function (added) {
added.forEach((node) => {
console.debug('Messages node: ', node);
const text = GetMessage(node);
if (!text) return;
console.log('拍手:文字', text)
if (!DetectMatch(text)) return;
console.log('拍手:文字已匹配', text)
if (!CheckTriggerCount()) return;
console.log('拍手:觸發數通過', text)
if (!CheckTimeout()) return;
console.log('拍手:等待時間通過', text)
SendMessage(stringToReply);
console.log('拍手:已發送', stringToReply)
});
}
);
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('.bst-message-body');
if (!messageNode) return '';
let text = messageNode.innerText;
const emojis = messageNode.getElementsByTagName('img');
for (const emojiNode of emojis) {
text += emojiNode.getAttribute('shared-tooltip-text', 1);
}
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('yt-live-chat-text-input-field-renderer[class]')
?.querySelector('#input');
if (!input) {
console.warn('Cannot find input element');
console.warn('可能是訂閱者專屬模式?');
return;
}
const data = new DataTransfer();
data.setData('text/plain', message);
input.dispatchEvent(
new ClipboardEvent('paste', { bubbles: true, clipboardData: data })
);
try {
document.querySelector('yt-live-chat-text-input-field-renderer[class]').polymerController.onInputChange();
} catch (e) { }
setTimeout(() => {
// Youtube is 💩 that they're reusing the same ID
const buttons = document.querySelectorAll('#send-button');
// Click any buttons under #send-button
buttons.forEach((b) => {
const _buttons = b.getElementsByTagName('button');
// HTMLCollection not array
Array.from(_buttons).forEach((_b) => {
_b.click();
});
});
console.log(`[${new Date().toISOString()}]自動發話觸發: ${message}`);
}, 500);
} finally {
lastTriggerTime = Date.now();
currentDetectCount = 0;
}
}
})();