Threads 关键字过滤推文

只要推文主体、标签、用户名等任一区块命中关键字,整则推文一起隐藏。支援关键字新增、清单、单独删除。支援快速封锁、清单、单独删除。

当前为 2025-04-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Keyword-based Tweet Filtering for Threads
  3. // @name:zh-TW Threads 關鍵字過濾推文
  4. // @name:zh-CN Threads 关键字过滤推文
  5. // @namespace http://tampermonkey.net/
  6. // @version 3.5
  7. // @description If any part of a post—such as the main content, hashtags, or username—matches a keyword, the entire post will be hidden. Supports adding keywords, viewing the list, and deleting them individually. Quick block is also supported, along with blocklist viewing and individual removal.
  8. // @description:zh-TW 只要推文主體、標籤、用戶名等任一區塊命中關鍵字,整則推文一起隱藏。支援關鍵字新增、清單、單獨刪除。支援快速封鎖、清單、單獨刪除。
  9. // @description:zh-CN 只要推文主体、标签、用户名等任一区块命中关键字,整则推文一起隐藏。支援关键字新增、清单、单独删除。支援快速封锁、清单、单独删除。
  10. // @author chatgpt
  11. // @match https://www.threads.net/*
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. // 關鍵字相關
  22. function getKeywords() {
  23. return GM_getValue('keywords', []);
  24. }
  25. function setKeywords(keywords) {
  26. GM_setValue('keywords', keywords);
  27. }
  28.  
  29. // 封鎖用戶相關
  30. function getBlockedUsers() {
  31. return GM_getValue('blockedUsers', []);
  32. }
  33. function setBlockedUsers(users) {
  34. GM_setValue('blockedUsers', users);
  35. }
  36.  
  37. // 取得所有推文主容器
  38. function getAllPostContainers() {
  39. return document.querySelectorAll('div[data-pressable-container][class*=" "]');
  40. }
  41.  
  42. // 在推文主容器下,找所有可能含有文字的區塊
  43. function getAllTextBlocks(container) {
  44. return container.querySelectorAll('span[dir="auto"]:not([translate="no"]), a[role="link"], span, div');
  45. }
  46.  
  47. // 取得用戶名稱(Threads 通常在 a[href^="/@"] 內)
  48. function getUsername(container) {
  49. let a = container.querySelector('a[href^="/@"]');
  50. if (a) {
  51. let username = a.getAttribute('href').replace('/', '').replace('@', '');
  52. return username;
  53. }
  54. return null;
  55. }
  56.  
  57. // 過濾推文
  58. function filterPosts() {
  59. let keywords = getKeywords();
  60. let blockedUsers = getBlockedUsers();
  61. let containers = getAllPostContainers();
  62. containers.forEach(container => {
  63. let blocks = getAllTextBlocks(container);
  64. let matched = false;
  65. // 關鍵字過濾
  66. blocks.forEach(block => {
  67. let text = (block.innerText || block.textContent || "").trim();
  68. if (text && keywords.some(keyword => keyword && text.includes(keyword))) {
  69. matched = true;
  70. }
  71. });
  72. // 封鎖用戶過濾
  73. let username = getUsername(container);
  74. if (username && blockedUsers.includes(username)) {
  75. matched = true;
  76. }
  77. if (matched) {
  78. container.style.display = 'none';
  79. } else {
  80. container.style.display = '';
  81. }
  82. });
  83. }
  84.  
  85. // 插入封鎖用戶按鈕(插在「分享」按鈕右邊)
  86. function insertBlockButtons() {
  87. let containers = getAllPostContainers();
  88. let blockedUsers = getBlockedUsers();
  89. containers.forEach(container => {
  90. // 避免重複插入
  91. if (container.querySelector('.tm-block-user-btn')) return;
  92. let username = getUsername(container);
  93. if (!username) return;
  94.  
  95. // 找到「分享」按鈕
  96. let shareBtn = container.querySelector('div[role="button"] svg[aria-label="分享"]');
  97. if (!shareBtn) return;
  98. let shareBtnDiv = shareBtn.closest('div[role="button"]');
  99. if (!shareBtnDiv) return;
  100.  
  101. // 建立封鎖按鈕
  102. let blockBtn = document.createElement('button');
  103. blockBtn.className = 'tm-block-user-btn';
  104. blockBtn.title = '封鎖用戶';
  105. blockBtn.style.marginLeft = '8px';
  106. blockBtn.style.background = 'none';
  107. blockBtn.style.border = 'none';
  108. blockBtn.style.cursor = 'pointer';
  109. blockBtn.style.fontSize = '18px';
  110. blockBtn.style.color = '#d00';
  111. blockBtn.innerHTML = '🚫';
  112.  
  113. blockBtn.onclick = function(e) {
  114. e.stopPropagation();
  115. if (confirm(`確定要封鎖 @${username} 嗎?\n(此用戶所有推文將被隱藏)`)) {
  116. let users = getBlockedUsers();
  117. if (!users.includes(username)) {
  118. users.push(username);
  119. setBlockedUsers(users);
  120. alert(`已封鎖 @${username}!`);
  121. filterPosts();
  122. }
  123. }
  124. };
  125.  
  126. // 插入在「分享」按鈕之後
  127. shareBtnDiv.parentNode.insertBefore(blockBtn, shareBtnDiv.nextSibling);
  128. });
  129. }
  130.  
  131. // observer 只監控新節點
  132. const observer = new MutationObserver(mutations => {
  133. let needFilter = false;
  134. for (const m of mutations) {
  135. if (m.addedNodes && m.addedNodes.length > 0) {
  136. needFilter = true;
  137. break;
  138. }
  139. }
  140. if (needFilter) {
  141. filterPosts();
  142. insertBlockButtons();
  143. }
  144. });
  145. observer.observe(document.body, { childList: true, subtree: true });
  146.  
  147. // 初始執行一次
  148. filterPosts();
  149. insertBlockButtons();
  150.  
  151. // 新增關鍵字
  152. GM_registerMenuCommand('新增關鍵字', () => {
  153. let input = prompt('請輸入要新增的關鍵字(可用半形或全形逗號分隔,一次可多個):');
  154. if (input !== null) {
  155. let arr = input.split(/,|,/).map(s => s.trim()).filter(Boolean);
  156. let keywords = getKeywords();
  157. let newKeywords = [...keywords];
  158. arr.forEach(k => {
  159. if (!newKeywords.includes(k)) newKeywords.push(k);
  160. });
  161. setKeywords(newKeywords);
  162. alert('已新增關鍵字!');
  163. location.reload();
  164. }
  165. });
  166.  
  167. // 關鍵字清單與單獨刪除
  168. GM_registerMenuCommand('關鍵字清單/刪除', () => {
  169. let keywords = getKeywords();
  170. if (keywords.length === 0) {
  171. alert('目前沒有設定任何關鍵字。');
  172. return;
  173. }
  174. let msg = '目前關鍵字如下:\n';
  175. keywords.forEach((k, i) => {
  176. msg += `${i+1}. ${k}\n`;
  177. });
  178. msg += '\n請輸入要刪除的關鍵字編號(可多個,用逗號分隔),或留空取消:';
  179. let input = prompt(msg, '');
  180. if (input !== null && input.trim() !== '') {
  181. let idxArr = input.split(/,|,/).map(s => parseInt(s.trim(), 10) - 1).filter(i => !isNaN(i) && i >= 0 && i < keywords.length);
  182. if (idxArr.length > 0) {
  183. let newKeywords = keywords.filter((k, i) => !idxArr.includes(i));
  184. setKeywords(newKeywords);
  185. alert('已刪除指定關鍵字!');
  186. location.reload();
  187. }
  188. }
  189. });
  190.  
  191. // 清除所有關鍵字
  192. GM_registerMenuCommand('清除所有關鍵字', () => {
  193. setKeywords([]);
  194. alert('已清除所有關鍵字!');
  195. location.reload();
  196. });
  197.  
  198. // 封鎖名單管理
  199. GM_registerMenuCommand('封鎖名單管理', () => {
  200. let users = getBlockedUsers();
  201. if (users.length === 0) {
  202. alert('目前沒有封鎖任何用戶。');
  203. return;
  204. }
  205. let msg = '目前封鎖用戶如下:\n';
  206. users.forEach((u, i) => {
  207. msg += `${i+1}. @${u}\n`;
  208. });
  209. msg += '\n請輸入要解除封鎖的用戶編號(可多個,用逗號分隔),或留空取消:';
  210. let input = prompt(msg, '');
  211. if (input !== null && input.trim() !== '') {
  212. let idxArr = input.split(/,|,/).map(s => parseInt(s.trim(), 10) - 1).filter(i => !isNaN(i) && i >= 0 && i < users.length);
  213. if (idxArr.length > 0) {
  214. let newUsers = users.filter((u, i) => !idxArr.includes(i));
  215. setBlockedUsers(newUsers);
  216. alert('已解除指定用戶封鎖!');
  217. location.reload();
  218. }
  219. }
  220. });
  221.  
  222. // 清除所有封鎖用戶
  223. GM_registerMenuCommand('清除所有封鎖用戶', () => {
  224. setBlockedUsers([]);
  225. alert('已清除所有封鎖用戶!');
  226. location.reload();
  227. });
  228.  
  229. })();