Skip to content

Instantly share code, notes, and snippets.

@jim60105
Last active February 10, 2024 05:05
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jim60105/43b2c53bb59fb588e351982c1a14e273 to your computer and use it in GitHub Desktop.
Save jim60105/43b2c53bb59fb588e351982c1a14e273 to your computer and use it in GitHub Desktop.
Youtube: 自動拍手機器
// ==UserScript==
// @name Youtube: Automatic clapping machine
// @name:zh Youtube: 自動拍手機器
// @version 1.4.5
// @description 在其它人拍手時自動跟著一起拍
// @author 琳(jim60105)
// @match https://www.youtube.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @license GPL3
// ==/UserScript==
(function () {
'use strict';
/**
* 注意: 這個腳本只能在 Youtube 的直播聊天室使用
*
* 若聊天室不是在前景,Youtube 可能會停止更新聊天室,導致功能停止
*
* 聊天室由背景來到前景時,或是捲動停住後回到最下方時,有可能因為訊息一口氣噴出來而直接觸發
* 請調整下方的 throttle 數值,以避免這種情況
* 訊息過多時預設的1.5秒可能會不夠,但設定太高會影響偵測判定
* 訊息過多時建議直接F5重整,不要讓它一直跑
*
* 要使用或偵測 Youtube 貼圖/會員貼圖,可填入像是這種格式 :_右サイリウム::_おんぷちゃん::_ハート:
* 若你有使用貼圖的權限,它就能自動轉換成貼圖,請小心使用
*/
// --- 設定區塊 ---
/**
* 要偵測的觸發字串
* 這是一個文字陣列,這些字串偵測到時就會記數觸發
* 可以輸入多個不同頻道的會員拍手貼圖做為偵測字串
*/
const stringToDetect = [
':clapping_hands::clapping_hands::clapping_hands:', // 這是三個拍手表符(👏👏👏)
':_拍手8::_拍手8::_拍手8:', // 以下是其它頻道的三拍手會員貼圖
':_clap::_clap::_clap:',
':_yuriClap::_yuriClap::_yuriClap::_yuriClap:',
':_pachipachi::_pachipachi::_pachipachi:',
];
/**
* 要發出去的字串
*/
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);
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('#items'),
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);
});
}
);
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('#message');
if (!messageNode) return '';
let text = messageNode.innerText;
const emojis = messageNode.getElementsByTagName('img');
for (const emojiNode of emojis) {
text += emojiNode.getAttribute('shared-tooltip-text');
}
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
.getElementsByTagName('yt-live-chat-text-input-field-renderer')[0]
?.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 })
);
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;
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment