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.4
  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 moreBtn = container.querySelector('div[role="button"]');
  97. if (!moreBtn) return;
  98.  
  99. // 建立封鎖按鈕
  100. let blockBtn = document.createElement('button');
  101. blockBtn.className = 'tm-block-user-btn';
  102. blockBtn.title = '封鎖用戶';
  103. blockBtn.style.marginLeft = '8px';
  104. blockBtn.style.background = 'none';
  105. blockBtn.style.border = 'none';
  106. blockBtn.style.cursor = 'pointer';
  107. blockBtn.style.fontSize = '18px';
  108. blockBtn.style.color = '#d00';
  109. blockBtn.innerHTML = '🚫';
  110.  
  111. blockBtn.onclick = function(e) {
  112. e.stopPropagation();
  113. if (confirm(`確定要封鎖 @${username} 嗎?\n(此用戶所有推文將被隱藏)`)) {
  114. let users = getBlockedUsers();
  115. if (!users.includes(username)) {
  116. users.push(username);
  117. setBlockedUsers(users);
  118. alert(`已封鎖 @${username}!`);
  119. filterPosts();
  120. }
  121. }
  122. };
  123.  
  124. // 插入在「更多」按鈕之後
  125. moreBtn.parentNode.insertBefore(blockBtn, moreBtn.nextSibling);
  126. });
  127. }
  128.  
  129. // observer 只監控新節點
  130. const observer = new MutationObserver(mutations => {
  131. let needFilter = false;
  132. for (const m of mutations) {
  133. if (m.addedNodes && m.addedNodes.length > 0) {
  134. needFilter = true;
  135. break;
  136. }
  137. }
  138. if (needFilter) {
  139. filterPosts();
  140. insertBlockButtons();
  141. }
  142. });
  143. observer.observe(document.body, { childList: true, subtree: true });
  144.  
  145. // 初始執行一次
  146. filterPosts();
  147. insertBlockButtons();
  148.  
  149. // 新增關鍵字
  150. GM_registerMenuCommand('新增關鍵字', () => {
  151. let input = prompt('請輸入要新增的關鍵字(可用半形或全形逗號分隔,一次可多個):');
  152. if (input !== null) {
  153. let arr = input.split(/,|,/).map(s => s.trim()).filter(Boolean);
  154. let keywords = getKeywords();
  155. let newKeywords = [...keywords];
  156. arr.forEach(k => {
  157. if (!newKeywords.includes(k)) newKeywords.push(k);
  158. });
  159. setKeywords(newKeywords);
  160. alert('已新增關鍵字!');
  161. location.reload();
  162. }
  163. });
  164.  
  165. // 關鍵字清單與單獨刪除
  166. GM_registerMenuCommand('關鍵字清單/刪除', () => {
  167. let keywords = getKeywords();
  168. if (keywords.length === 0) {
  169. alert('目前沒有設定任何關鍵字。');
  170. return;
  171. }
  172. let msg = '目前關鍵字如下:\n';
  173. keywords.forEach((k, i) => {
  174. msg += `${i+1}. ${k}\n`;
  175. });
  176. msg += '\n請輸入要刪除的關鍵字編號(可多個,用逗號分隔),或留空取消:';
  177. let input = prompt(msg, '');
  178. if (input !== null && input.trim() !== '') {
  179. let idxArr = input.split(/,|,/).map(s => parseInt(s.trim(), 10) - 1).filter(i => !isNaN(i) && i >= 0 && i < keywords.length);
  180. if (idxArr.length > 0) {
  181. let newKeywords = keywords.filter((k, i) => !idxArr.includes(i));
  182. setKeywords(newKeywords);
  183. alert('已刪除指定關鍵字!');
  184. location.reload();
  185. }
  186. }
  187. });
  188.  
  189. // 清除所有關鍵字
  190. GM_registerMenuCommand('清除所有關鍵字', () => {
  191. setKeywords([]);
  192. alert('已清除所有關鍵字!');
  193. location.reload();
  194. });
  195.  
  196. // 封鎖名單管理
  197. GM_registerMenuCommand('封鎖名單管理', () => {
  198. let users = getBlockedUsers();
  199. if (users.length === 0) {
  200. alert('目前沒有封鎖任何用戶。');
  201. return;
  202. }
  203. let msg = '目前封鎖用戶如下:\n';
  204. users.forEach((u, i) => {
  205. msg += `${i+1}. @${u}\n`;
  206. });
  207. msg += '\n請輸入要解除封鎖的用戶編號(可多個,用逗號分隔),或留空取消:';
  208. let input = prompt(msg, '');
  209. if (input !== null && input.trim() !== '') {
  210. let idxArr = input.split(/,|,/).map(s => parseInt(s.trim(), 10) - 1).filter(i => !isNaN(i) && i >= 0 && i < users.length);
  211. if (idxArr.length > 0) {
  212. let newUsers = users.filter((u, i) => !idxArr.includes(i));
  213. setBlockedUsers(newUsers);
  214. alert('已解除指定用戶封鎖!');
  215. location.reload();
  216. }
  217. }
  218. });
  219.  
  220. // 清除所有封鎖用戶
  221. GM_registerMenuCommand('清除所有封鎖用戶', () => {
  222. setBlockedUsers([]);
  223. alert('已清除所有封鎖用戶!');
  224. location.reload();
  225. });
  226.  
  227. })();