YT👏Boost chat

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

  1. // ==UserScript==
  2. // @name YT👏Boost chat
  3. // @version 1.4.5
  4. // @description 在其它人拍手時自動跟著一起拍
  5. // @author 琳(jim60105)
  6. // @match https://www.youtube.com/live_chat*
  7. // @icon https://www.youtube.com/favicon.ico
  8. // @license GPL3
  9. // @namespace https://gf.qytechs.cn/users/4839
  10. // ==/UserScript==
  11. /*
  12. * 原腳本作者
  13. https://xn--jgy.tw/Livestream/my-vtuber-dd-life/
  14. https://gist.github.com/jim60105/43b2c53bb59fb588e351982c1a14e273
  15. * YouTube Boost Chat導致聊天室元素變更,修改後搭配使用
  16. https://gf.qytechs.cn/zh-TW/scripts/520755/discussions/271546#comment-555456
  17. */
  18. (function () {
  19. 'use strict';
  20.  
  21. /**
  22. * 注意: 這個腳本只能在 Youtube 的直播聊天室使用
  23. *
  24. * 若聊天室不是在前景,Youtube 可能會停止更新聊天室,導致功能停止
  25. *
  26. * 聊天室由背景來到前景時,或是捲動停住後回到最下方時,有可能因為訊息一口氣噴出來而直接觸發
  27. * 請調整下方的 throttle 數值,以避免這種情況
  28. * 訊息過多時預設的1.5秒可能會不夠,但設定太高會影響偵測判定
  29. * 訊息過多時建議直接F5重整,不要讓它一直跑
  30. *
  31. * 要使用或偵測 Youtube 貼圖/會員貼圖,可填入像是這種格式 :_右サイリウム::_おんぷちゃん::_ハート:
  32. * 若你有使用貼圖的權限,它就能自動轉換成貼圖,請小心使用
  33. */
  34.  
  35. // --- 設定區塊 ---
  36. /**
  37. * 要偵測的觸發字串
  38. * 這是一個文字陣列,這些字串偵測到時就會記數觸發
  39. * 可以輸入多個不同頻道的會員拍手貼圖做為偵測字串
  40. */
  41. const stringToDetect = [
  42. ':clapping_hands::clapping_hands::clapping_hands:', // 這是三個拍手表符(👏👏👏)
  43. ':washhands::washhands::washhands:',
  44. ];
  45. const stringToReply = '👏👏👏';
  46.  
  47. // 範例條件說明:
  48. // 偵測到「4」次字串才觸發
  49. // (同一則訊息內重覆比對時只會計算一次)
  50. // 在「1.5」秒內重覆被偵測到也只計算一次
  51. // 偵測間隔不得超過「10」秒,超過的話就重新計算
  52. // 自動發話後至少等待「120」秒後才會再次自動發話
  53. /**
  54. * 要偵測的次數
  55. */
  56. const triggerCount = 2;
  57. /**
  58. * 每次間隔不得超過的秒數
  59. */
  60. const triggerBetweenSeconds = 20;
  61. /**
  62. * 自動發話後至少等待的秒數
  63. */
  64. const minTimeout = 120;
  65. /**
  66. * 在這個秒數內重覆偵測到觸發字串,至多只會計算一次
  67. * (這是用來避免當視窗由背景來到前景時,聊天記錄一口氣噴出來造成誤觸發)
  68. */
  69. const throttle = 0.5;
  70. // --- 設定區塊結束 ---
  71.  
  72. let lastDetectTime = new Date(null);
  73. let currentDetectCount = 0;
  74. let lastTriggerTime = new Date(null);
  75.  
  76. if (window.location.pathname.startsWith('/embed')) return;
  77.  
  78. if (
  79. typeof ytInitialData !== 'undefined' &&
  80. ytInitialData.continuationContents?.liveChatContinuation?.isReplay
  81. ) {
  82. console.debug('Replay mode, exit.');
  83. return;
  84. }
  85.  
  86. onAppend(
  87. document
  88. .getElementsByTagName('yt-live-chat-item-list-renderer')[0]
  89. ?.querySelector('.bst-message-list'),
  90. function (added) {
  91. added.forEach((node) => {
  92. console.debug('Messages node: ', node);
  93.  
  94. const text = GetMessage(node);
  95. if (!text) return;
  96.  
  97. console.log('拍手:文字', text)
  98.  
  99. if (!DetectMatch(text)) return;
  100.  
  101. console.log('拍手:文字已匹配', text)
  102.  
  103. if (!CheckTriggerCount()) return;
  104.  
  105. console.log('拍手:觸發數通過', text)
  106.  
  107. if (!CheckTimeout()) return;
  108.  
  109. console.log('拍手:等待時間通過', text)
  110.  
  111. SendMessage(stringToReply);
  112.  
  113. console.log('拍手:已發送', stringToReply)
  114.  
  115. });
  116. }
  117. );
  118.  
  119. function onAppend(elem, f) {
  120. if (!elem) return;
  121. var observer = new MutationObserver(function (mutations) {
  122. mutations.forEach(function (m) {
  123. if (m.addedNodes.length) {
  124. f(m.addedNodes);
  125. }
  126. });
  127. });
  128. observer.observe(elem, { childList: true });
  129. }
  130.  
  131. function GetMessage(node) {
  132. const messageNode = node.querySelector('.bst-message-body');
  133. if (!messageNode) return '';
  134.  
  135. let text = messageNode.innerText;
  136.  
  137. const emojis = messageNode.getElementsByTagName('img');
  138. for (const emojiNode of emojis) {
  139. text += emojiNode.getAttribute('shared-tooltip-text', 1);
  140. }
  141. console.debug('Message: ', text);
  142. return text;
  143. }
  144.  
  145. function DetectMatch(text) {
  146. let match = false;
  147. stringToDetect.forEach((p) => {
  148. match |= text.includes(p);
  149. });
  150.  
  151. if (!match) return false;
  152.  
  153. console.debug(`Matched!`);
  154.  
  155. if (lastDetectTime.valueOf() + throttle * 1000 >= Date.now()) {
  156. console.debug('Throttle detected');
  157. return false;
  158. }
  159.  
  160. if (lastDetectTime.valueOf() + triggerBetweenSeconds * 1000 < Date.now()) {
  161. currentDetectCount = 1;
  162. console.debug('Over max trigger seconds. Reset detect count to 1.');
  163. } else {
  164. currentDetectCount++;
  165. }
  166.  
  167. lastDetectTime = Date.now();
  168. console.debug(`Count: ${currentDetectCount}`);
  169. return true;
  170. }
  171.  
  172. function CheckTriggerCount() {
  173. const shouldTrigger = currentDetectCount >= triggerCount;
  174. if (shouldTrigger) console.debug('Triggered!');
  175. return shouldTrigger;
  176. }
  177.  
  178. function CheckTimeout() {
  179. const isInTimeout = lastTriggerTime.valueOf() + minTimeout * 1000 > Date.now();
  180. if (isInTimeout) console.debug('Still waiting for minTimeout');
  181. return !isInTimeout;
  182. }
  183.  
  184. function SendMessage(message) {
  185. try {
  186. const input = document
  187. .querySelector('yt-live-chat-text-input-field-renderer[class]')
  188. ?.querySelector('#input');
  189.  
  190. if (!input) {
  191. console.warn('Cannot find input element');
  192. console.warn('可能是訂閱者專屬模式?');
  193. return;
  194. }
  195.  
  196. const data = new DataTransfer();
  197. data.setData('text/plain', message);
  198. input.dispatchEvent(
  199. new ClipboardEvent('paste', { bubbles: true, clipboardData: data })
  200. );
  201. try {
  202. document.querySelector('yt-live-chat-text-input-field-renderer[class]').polymerController.onInputChange();
  203. } catch (e) { }
  204. setTimeout(() => {
  205. // Youtube is 💩 that they're reusing the same ID
  206. const buttons = document.querySelectorAll('#send-button');
  207. // Click any buttons under #send-button
  208. buttons.forEach((b) => {
  209. const _buttons = b.getElementsByTagName('button');
  210. // HTMLCollection not array
  211. Array.from(_buttons).forEach((_b) => {
  212. _b.click();
  213. });
  214. });
  215. console.log(`[${new Date().toISOString()}]自動發話觸發: ${message}`);
  216. }, 500);
  217. } finally {
  218. lastTriggerTime = Date.now();
  219. currentDetectCount = 0;
  220. }
  221. }
  222. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址