├── .gitignore ├── README.md ├── background.js ├── images ├── icon-128.png ├── icon-16.png ├── icon-32.png └── icon-48.png ├── manifest.json ├── misc ├── demo.png ├── how-to-block.png └── product-demo.png ├── popup ├── normalize.css ├── popup.css ├── popup.html └── popup.js ├── scripts ├── content.js └── injected.js └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 背景 2 | 3 | 实在受不了推文下无休无止的回复引流的黄推了,里面甚至涉及诈骗等灰产。 大家小心 4 | 5 | ## 设计宗旨 6 | 7 | 1. 隐私优先。本插件纯客户端代码,无服务端 api 或前端监控等设计 8 | 9 | ## 如何自定义【关键词】 10 | 11 | 1. download zip 包,并解压 12 | 13 | 2. 在 https://github.com/slarkvan/Block-Pornographic-Replies/blob/ae8615b5c140a73da1c5585ecef664421ec9409a/scripts/injected.js#L18 处里增加或删除关键词 14 | 15 | 3. 进入 [chrome://extensions/](chrome://extensions/),打开「开发者模式」,把项目拖进去即可(如果之前有插件了,删掉即可) 16 | 17 | ## 使用方式 18 | 19 | 1. ~~进入 [chrome://extensions/](chrome://extensions/),打开「开发者模式」~~ 20 | 21 | 2. ~~把代码包下载解压拖到里面即可~~ 22 | 23 | 目前已进入 Chrome web Store。[插件下载地址✨✨✨](https://chrome.google.com/webstore/detail/block-pornographic-replie/cdmilcmdgajfnplkcpgdckdgjadkgkhn) 24 | 25 | ## 效果 26 | 27 | 目前一定得**自己装了插件才有用。且只针对推文回复下的黄推**。插件目前有 3 种功能: 28 | 29 | ### 1.标记推文回复下的黄推引流,并在自身浏览时隐藏 30 | 31 | Image 32 | 33 | ### 2. 一键批量屏蔽 34 | 35 | Image 36 | 37 | ### 3. 单个屏蔽 38 | 39 | Image 40 | 41 | 具体可以拿**立党**的推文 检测下 42 | 43 | ## 说明 44 | 45 | 原理采用的是内置的「关键词匹配」检测是否是黄推,然后进行浏览器样式上的隐藏,识别准确率大约 90%,会有少少部分的错判和漏判。可以通过点击「移出列表」按钮加入信任白名单。支持批量屏蔽,单个屏蔽 46 | 47 | 以下用户是**不进行**色情检测的 48 | 49 | - 你关注的人 50 | - 你主动「移出列表」,加入白名单的人 51 | 52 | ## 后续计划 53 | 54 | - [ ] 增加 on/off 配置 55 | - [x] 增加一键 block 功能 56 | - [ ] 适配 firefox 浏览器 57 | - [ ] 进入 Store 58 | - [ ] 增加对搜索的检测 59 | - [ ] 其他 60 | 61 | ## 欢迎 PR 62 | 63 | 1. 想 PR 前,可以先在 discuss or issue 大概说下要做的内容 64 | 65 | ## Star History 66 | 67 | [![Star History Chart](https://api.star-history.com/svg?repos=slarkvan/Block-Pornographic-Replies&type=Date)](https://star-history.com/#slarkvan/Block-Pornographic-Replies&Date) 68 | 69 | ## License 70 | 71 | MIT 72 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onConnect.addListener(function (port) { 2 | if (port.name === "sync-porn-list") { 3 | port.onMessage.addListener(function (msg) { 4 | if (msg.messageType === "syncPornList") { 5 | const list = msg.list; 6 | chrome.action.setBadgeText({ text: String(list.length) }); 7 | } 8 | }); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slarkvan/Block-Pornographic-Replies/8cf79c5c4c2e502e2f0bc97cac5e3f56d29188b5/images/icon-128.png -------------------------------------------------------------------------------- /images/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slarkvan/Block-Pornographic-Replies/8cf79c5c4c2e502e2f0bc97cac5e3f56d29188b5/images/icon-16.png -------------------------------------------------------------------------------- /images/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slarkvan/Block-Pornographic-Replies/8cf79c5c4c2e502e2f0bc97cac5e3f56d29188b5/images/icon-32.png -------------------------------------------------------------------------------- /images/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slarkvan/Block-Pornographic-Replies/8cf79c5c4c2e502e2f0bc97cac5e3f56d29188b5/images/icon-48.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Block Pornographic Replies", 4 | "description": "屏蔽推特回复下的黄推。Block pornographic replies below the tweet.", 5 | "version": "2.6", 6 | "icons": { 7 | "16": "images/icon-16.png", 8 | "32": "images/icon-32.png", 9 | "48": "images/icon-48.png", 10 | "128": "images/icon-128.png" 11 | }, 12 | "background": { 13 | "service_worker": "background.js" 14 | }, 15 | "action": { 16 | "default_popup": "popup/popup.html", 17 | "default_icon": { 18 | "16": "images/icon-16.png", 19 | "32": "images/icon-32.png", 20 | "48": "images/icon-48.png", 21 | "128": "images/icon-128.png" 22 | } 23 | }, 24 | "host_permissions": ["*://*.twitter.com/*"], 25 | "web_accessible_resources": [ 26 | { 27 | "resources": ["scripts/injected.js"], 28 | "matches": ["https://*.twitter.com/*"] 29 | } 30 | ], 31 | "commands": { 32 | "_execute_action": { 33 | "suggested_key": { 34 | "default": "Ctrl+B", 35 | "mac": "Command+B" 36 | } 37 | } 38 | }, 39 | "content_scripts": [ 40 | { 41 | "js": ["scripts/content.js"], 42 | "css": ["style.css"], 43 | "matches": ["https://*.twitter.com/*"], 44 | "run_at": "document_start" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /misc/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slarkvan/Block-Pornographic-Replies/8cf79c5c4c2e502e2f0bc97cac5e3f56d29188b5/misc/demo.png -------------------------------------------------------------------------------- /misc/how-to-block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slarkvan/Block-Pornographic-Replies/8cf79c5c4c2e502e2f0bc97cac5e3f56d29188b5/misc/how-to-block.png -------------------------------------------------------------------------------- /misc/product-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slarkvan/Block-Pornographic-Replies/8cf79c5c4c2e502e2f0bc97cac5e3f56d29188b5/misc/product-demo.png -------------------------------------------------------------------------------- /popup/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { 178 | /* 1 */ 179 | overflow: visible; 180 | } 181 | 182 | /** 183 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 184 | * 1. Remove the inheritance of text transform in Firefox. 185 | */ 186 | 187 | button, 188 | select { 189 | /* 1 */ 190 | text-transform: none; 191 | } 192 | 193 | /** 194 | * Correct the inability to style clickable types in iOS and Safari. 195 | */ 196 | 197 | button, 198 | [type="button"], 199 | [type="reset"], 200 | [type="submit"] { 201 | -webkit-appearance: button; 202 | } 203 | 204 | /** 205 | * Remove the inner border and padding in Firefox. 206 | */ 207 | 208 | button::-moz-focus-inner, 209 | [type="button"]::-moz-focus-inner, 210 | [type="reset"]::-moz-focus-inner, 211 | [type="submit"]::-moz-focus-inner { 212 | border-style: none; 213 | padding: 0; 214 | } 215 | 216 | /** 217 | * Restore the focus styles unset by the previous rule. 218 | */ 219 | 220 | button:-moz-focusring, 221 | [type="button"]:-moz-focusring, 222 | [type="reset"]:-moz-focusring, 223 | [type="submit"]:-moz-focusring { 224 | outline: 1px dotted ButtonText; 225 | } 226 | 227 | /** 228 | * Correct the padding in Firefox. 229 | */ 230 | 231 | fieldset { 232 | padding: 0.35em 0.75em 0.625em; 233 | } 234 | 235 | /** 236 | * 1. Correct the text wrapping in Edge and IE. 237 | * 2. Correct the color inheritance from `fieldset` elements in IE. 238 | * 3. Remove the padding so developers are not caught out when they zero out 239 | * `fieldset` elements in all browsers. 240 | */ 241 | 242 | legend { 243 | box-sizing: border-box; /* 1 */ 244 | color: inherit; /* 2 */ 245 | display: table; /* 1 */ 246 | max-width: 100%; /* 1 */ 247 | padding: 0; /* 3 */ 248 | white-space: normal; /* 1 */ 249 | } 250 | 251 | /** 252 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 253 | */ 254 | 255 | progress { 256 | vertical-align: baseline; 257 | } 258 | 259 | /** 260 | * Remove the default vertical scrollbar in IE 10+. 261 | */ 262 | 263 | textarea { 264 | overflow: auto; 265 | } 266 | 267 | /** 268 | * 1. Add the correct box sizing in IE 10. 269 | * 2. Remove the padding in IE 10. 270 | */ 271 | 272 | [type="checkbox"], 273 | [type="radio"] { 274 | box-sizing: border-box; /* 1 */ 275 | padding: 0; /* 2 */ 276 | } 277 | 278 | /** 279 | * Correct the cursor style of increment and decrement buttons in Chrome. 280 | */ 281 | 282 | [type="number"]::-webkit-inner-spin-button, 283 | [type="number"]::-webkit-outer-spin-button { 284 | height: auto; 285 | } 286 | 287 | /** 288 | * 1. Correct the odd appearance in Chrome and Safari. 289 | * 2. Correct the outline style in Safari. 290 | */ 291 | 292 | [type="search"] { 293 | -webkit-appearance: textfield; /* 1 */ 294 | outline-offset: -2px; /* 2 */ 295 | } 296 | 297 | /** 298 | * Remove the inner padding in Chrome and Safari on macOS. 299 | */ 300 | 301 | [type="search"]::-webkit-search-decoration { 302 | -webkit-appearance: none; 303 | } 304 | 305 | /** 306 | * 1. Correct the inability to style clickable types in iOS and Safari. 307 | * 2. Change font properties to `inherit` in Safari. 308 | */ 309 | 310 | ::-webkit-file-upload-button { 311 | -webkit-appearance: button; /* 1 */ 312 | font: inherit; /* 2 */ 313 | } 314 | 315 | /* Interactive 316 | ========================================================================== */ 317 | 318 | /* 319 | * Add the correct display in Edge, IE 10+, and Firefox. 320 | */ 321 | 322 | details { 323 | display: block; 324 | } 325 | 326 | /* 327 | * Add the correct display in all browsers. 328 | */ 329 | 330 | summary { 331 | display: list-item; 332 | } 333 | 334 | /* Misc 335 | ========================================================================== */ 336 | 337 | /** 338 | * Add the correct display in IE 10+. 339 | */ 340 | 341 | template { 342 | display: none; 343 | } 344 | 345 | /** 346 | * Add the correct display in IE 10. 347 | */ 348 | 349 | [hidden] { 350 | display: none; 351 | } 352 | -------------------------------------------------------------------------------- /popup/popup.css: -------------------------------------------------------------------------------- 1 | .block-porn-body { 2 | display: block; 3 | min-width: 600px; 4 | padding: 20px; 5 | } 6 | 7 | .raw-button { 8 | border-radius: 4px; 9 | color: white; 10 | outline: none; 11 | padding: 8px 12px; 12 | text-align: center; 13 | text-decoration: none; 14 | display: inline-block; 15 | font-size: 16px; 16 | transition-duration: 0.4s; 17 | cursor: pointer; 18 | border-radius: 8px; 19 | appearance: none; 20 | border: 1px solid transparent; 21 | border-shadow: 0 2px 0 rgba(255, 38, 5, 0.06); 22 | } 23 | 24 | .mr-10 { 25 | margin-right: 10px; 26 | } 27 | 28 | .primary-button { 29 | background-color: #1677ff; 30 | } 31 | 32 | .primary-button:hover { 33 | background-color: #4096ff; 34 | } 35 | 36 | .secondary-button { 37 | padding: 6px 8px; 38 | font-size: 14px; 39 | background-color: #fff; 40 | border-color: #d9d9d9; 41 | color: rgba(0, 0, 0, 0.88); 42 | } 43 | 44 | .small-button { 45 | padding: 6px 8px; 46 | font-size: 14px; 47 | } 48 | 49 | .secondary-button:hover { 50 | color: #4096ff; 51 | border-color: #4096ff; 52 | } 53 | 54 | .danger-button { 55 | background-color: #ff4d4f; 56 | } 57 | 58 | .danger-button:hover { 59 | background-color: #ff7875; 60 | } 61 | 62 | .mw-88 { 63 | width: 88px; 64 | } 65 | 66 | #block-porn-user-list { 67 | margin-top: 16px; 68 | 69 | min-height: 240px; 70 | max-height: 480px; 71 | overflow-y: scroll; 72 | } 73 | 74 | .user-item { 75 | display: flex; 76 | justify-content: flex-start; 77 | align-items: flex-start; 78 | padding-right: 8px; 79 | margin-top: 8px; 80 | } 81 | 82 | .avatar-wrap { 83 | max-width: 40px; 84 | min-width: 40px; 85 | max-height: 40px; 86 | min-height: 40px; 87 | margin-right: 6px; 88 | border-radius: 50%; 89 | overflow: hidden; 90 | } 91 | 92 | .avatar { 93 | width: 100%; 94 | height: 100%; 95 | object-fit: cover; 96 | } 97 | 98 | .mw-88 { 99 | min-width: 88px; 100 | } 101 | 102 | .user-content { 103 | display: flex; 104 | flex-direction: column; 105 | word-break: break-all; 106 | justify-content: flex-start; 107 | font-size: 13px; 108 | line-height: 1.45; 109 | color: rgba(0, 0, 0, 0.88); 110 | 111 | margin-bottom: 8px; 112 | } 113 | 114 | .buttons { 115 | display: flex; 116 | flex-direction: column; 117 | margin-left: 12px; 118 | gap: 12px; 119 | } 120 | 121 | .mt-2 { 122 | margin-top: 2px; 123 | } 124 | -------------------------------------------------------------------------------- /popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

被标记的黄推列表(0

16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /popup/popup.js: -------------------------------------------------------------------------------- 1 | function extractContent(str) { 2 | if (typeof str !== "string" || str.length === 0) return; 3 | const content = str.substring(str.indexOf(" ") + 1); 4 | return content; 5 | } 6 | 7 | function getTaggedReason(field) { 8 | switch (field) { 9 | case "full_text": 10 | return "发言"; 11 | case "description": 12 | return "简介"; 13 | case "name": 14 | case "screen_name": 15 | return "昵称"; 16 | default: 17 | return "未知"; 18 | } 19 | } 20 | 21 | function renderUserList(list, callback, callback2) { 22 | const blockUserList = document.getElementById("block-porn-user-list"); 23 | const countNode = document.getElementById("count"); 24 | 25 | list.forEach(function (user) { 26 | const itemNode = document.createElement("div"); 27 | itemNode.classList.add("user-item"); 28 | 29 | const hiddenInput = document.createElement("input"); 30 | hiddenInput.type = "hidden"; 31 | hiddenInput.value = user.restId; 32 | hiddenInput.classList.add("hidden-porn-user-id"); 33 | 34 | const imgWrapNode = document.createElement("div"); 35 | imgWrapNode.classList.add("avatar-wrap"); 36 | const avatarNode = document.createElement("img"); 37 | avatarNode.classList.add("avatar"); 38 | avatarNode.setAttribute("src", user.avatar); 39 | imgWrapNode.appendChild(avatarNode); 40 | 41 | const contentNode = document.createElement("div"); 42 | contentNode.classList.add("user-content"); 43 | contentNode.innerHTML = ` 44 |
${user.name}@${user.screen_name}
45 |
简介: ${user.description}
46 |
发言: ${extractContent(user.full_text)}
47 |
标记原因: ${getTaggedReason(user.field)}
48 | `; 49 | 50 | const releaseNode = document.createElement("div"); 51 | releaseNode.classList.add("raw-button", "secondary-button", "mw-88"); 52 | releaseNode.textContent = "移出列表"; 53 | releaseNode.addEventListener("click", function () { 54 | // send msg to content.js 55 | itemNode.remove(); 56 | countNode.textContent = Number(countNode.textContent) - 1; 57 | callback({ 58 | restId: user.restId, 59 | screen_name: user.screen_name, 60 | }); 61 | }); 62 | 63 | const blockNode = document.createElement("div"); 64 | blockNode.classList.add("raw-button", "danger-button", "small-button", "mw-88"); 65 | blockNode.textContent = "屏蔽"; 66 | blockNode.addEventListener("click", function () { 67 | // send msg to content.js 68 | itemNode.remove(); 69 | countNode.textContent = Number(countNode.textContent) - 1; 70 | callback2({ 71 | restId: user.restId, 72 | screen_name: user.screen_name, 73 | }); 74 | }); 75 | 76 | const buttonsNode = document.createElement("div"); 77 | buttonsNode.classList.add("buttons"); 78 | buttonsNode.appendChild(releaseNode); 79 | buttonsNode.appendChild(blockNode); 80 | 81 | countNode.textContent = list.length; 82 | 83 | itemNode.appendChild(hiddenInput); 84 | if (user.avatar) { 85 | itemNode.appendChild(imgWrapNode); 86 | } 87 | itemNode.appendChild(contentNode); 88 | itemNode.appendChild(buttonsNode); 89 | blockUserList.appendChild(itemNode); 90 | }); 91 | } 92 | 93 | function blockHandler() { 94 | const hiddenInputs = Array.from(document.getElementsByClassName("hidden-porn-user-id")); 95 | const hiddenIds = hiddenInputs.map((input) => input.value); 96 | 97 | if (hiddenIds.length === 0) { 98 | alert("请先获取屏蔽列表"); 99 | return; 100 | } 101 | 102 | const blockUserList = document.getElementById("block-porn-user-list"); 103 | blockUserList.innerHTML = "正在屏蔽中....3 秒后刷新页面"; 104 | 105 | chrome.tabs.query({ currentWindow: true, active: true }, function (tabs) { 106 | var activeTab = tabs[0]; 107 | chrome.tabs.sendMessage(activeTab.id, { messageType: "batchBlockPornUserList", userIds: hiddenIds }); 108 | }); 109 | } 110 | 111 | function getLatestPornListHandler() { 112 | chrome.tabs.query({ currentWindow: true, active: true }, function (tabs) { 113 | var activeTab = tabs[0]; 114 | chrome.tabs.sendMessage(activeTab.id, { messageType: "getLatestPornList" }, function (response) { 115 | renderUserList( 116 | response.list, 117 | (user) => { 118 | chrome.tabs.sendMessage(activeTab.id, { messageType: "setTargetUserFree", user }); 119 | }, 120 | (user) => { 121 | chrome.tabs.sendMessage(activeTab.id, { messageType: "blockOneUser", user }); 122 | } 123 | ); 124 | }); 125 | }); 126 | } 127 | 128 | function resetHandler() { 129 | const blockUserList = document.getElementById("block-porn-user-list"); 130 | blockUserList.innerHTML = "已重置,刷新页面后生效"; 131 | chrome.tabs.query({ currentWindow: true, active: true }, function (tabs) { 132 | var activeTab = tabs[0]; 133 | chrome.tabs.sendMessage(activeTab.id, { messageType: "resetApp" }); 134 | }); 135 | } 136 | 137 | document.addEventListener("DOMContentLoaded", function () { 138 | document.getElementById("block-porn-refresh-btn").addEventListener("click", getLatestPornListHandler); 139 | document.getElementById("block-porn-block-btn").addEventListener("click", blockHandler); 140 | document.getElementById("block-porn-reset-btn").addEventListener("click", resetHandler); 141 | }); 142 | -------------------------------------------------------------------------------- /scripts/content.js: -------------------------------------------------------------------------------- 1 | function checkUserIsPorn(name) { 2 | const responser = localStorage.getItem("twitter_responser_porn_list") 3 | ? JSON.parse(localStorage.getItem("twitter_responser_porn_list")) 4 | : []; 5 | 6 | const user = responser.find((item) => { 7 | return item.screen_name === name; 8 | }); 9 | 10 | if (user) { 11 | return [user.isPorn, user]; 12 | } 13 | 14 | return [false, null]; 15 | } 16 | 17 | // "UserAvatar-Container-xx" => 'xx' 18 | function extractUsername(str) { 19 | const re = /-([^-]*)$/; // Regex to match the substring after the last hyphen 20 | 21 | const match = str.match(re); 22 | 23 | if (match) { 24 | return match[1]; 25 | } else { 26 | console.log("No match found"); 27 | } 28 | } 29 | 30 | function nodeHandler(node, way) { 31 | if (node.nodeName === "DIV" && node.getAttribute("data-testid") === "cellInnerDiv") { 32 | if (node && node.getAttribute("data-user-tag") === "porn") return; 33 | 34 | const descendants = node.querySelectorAll('div[data-testid^="UserAvatar-Container"]'); 35 | const firstElement = descendants[0]; 36 | if (firstElement && firstElement.getAttribute("data-testid")) { 37 | const name = extractUsername(firstElement.getAttribute("data-testid")); 38 | const [isPorn, user] = checkUserIsPorn(name); 39 | if (isPorn) { 40 | if (node && node.classList && !node.classList.contains("blocked-of-porn")) { 41 | node.classList.add("blocked-of-porn"); 42 | node.setAttribute("data-user-tag", "porn"); 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | function watchDomChange() { 50 | const targetNode = document; 51 | const config = { attributes: true, childList: true, subtree: true, characterData: true }; 52 | 53 | const observer = new MutationObserver(function (mutations) { 54 | mutations.forEach(function (mutation) { 55 | if (mutation.type === "childList") { 56 | mutation.addedNodes.forEach(function (node) { 57 | nodeHandler(node, "childList"); 58 | }); 59 | } 60 | 61 | if (mutation.type === "attributes") { 62 | const node = mutation.target; 63 | nodeHandler(node, "attributes"); 64 | } 65 | }); 66 | }); 67 | 68 | // 开始监测目标节点变化 69 | observer.observe(targetNode, config); 70 | } 71 | 72 | function injectScript() { 73 | const s = document.createElement("script"); 74 | s.src = chrome.runtime.getURL("scripts/injected.js"); 75 | s.onload = async function () { 76 | this.remove(); 77 | }; 78 | (document.head || document.documentElement).appendChild(s); 79 | } 80 | 81 | function main() { 82 | watchDomChange(); 83 | injectScript(); 84 | } 85 | 86 | // Uncaught Error: Extension context invalidated. 87 | // 但是似乎不影响功能 88 | try { 89 | main(); 90 | } catch (error) { 91 | console.log("main error", error); 92 | } 93 | 94 | function syncData(port) { 95 | try { 96 | const list = getLatestPornList(); 97 | port.postMessage({ messageType: "syncPornList", list: list }); 98 | setTimeout(() => { 99 | syncData(port); 100 | }, 1000); 101 | } catch (error) { 102 | console.log("syncData error", error); 103 | } 104 | } 105 | 106 | try { 107 | const port = chrome.runtime.connect({ name: "sync-porn-list" }); 108 | if (port) { 109 | port.onDisconnect.addListener((port) => { 110 | console.log("Disconnected from port", port); 111 | }); 112 | syncData(port); 113 | } 114 | } catch (e) { 115 | console.log("connect error", e); 116 | } 117 | 118 | chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) { 119 | if (message.messageType === "getLatestPornList") { 120 | const list = getLatestPornList(); 121 | sendResponse({ list }); 122 | } 123 | 124 | if (message.messageType === "batchBlockPornUserList") { 125 | const userIds = message.userIds; 126 | // TODO: 目前尝试一起请求,待观察 twitter 的政策,看后续是否模拟人工操作 127 | batchBlockPornUserList(userIds); 128 | } 129 | 130 | if (message.messageType === "setTargetUserFree") { 131 | const user = message.user; 132 | removeUserFromPornList(user); 133 | addUserIntoWhiteList(user); 134 | } 135 | 136 | if (message.messageType === "resetApp") { 137 | resetApp(); 138 | } 139 | 140 | if (message.messageType === "blockOneUser") { 141 | const user = message.user; 142 | if (user.restId) { 143 | batchBlockPornUserList([user.restId]); 144 | } 145 | } 146 | }); 147 | 148 | function getCookie(key) { 149 | const cookies = document.cookie.split(";"); 150 | for (let i = 0; i < cookies.length; i++) { 151 | const cookie = cookies[i].trim(); 152 | if (cookie.startsWith(key + "=")) { 153 | return cookie.substring(key.length + 1); 154 | } 155 | } 156 | return null; 157 | } 158 | 159 | function blockTargetUser(userId) { 160 | const csrfToken = getCookie("ct0"); 161 | return fetch("https://twitter.com/i/api/1.1/blocks/create.json", { 162 | headers: { 163 | // `authorization` 是 main.js 里的写死的参数,不区分用户,不清楚 Twitter 是否会定期更改 164 | authorization: 165 | "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA", 166 | "content-type": "application/x-www-form-urlencoded", 167 | "x-csrf-token": csrfToken, 168 | "x-twitter-active-user": "yes", 169 | "x-twitter-auth-type": "OAuth2Session", 170 | }, 171 | body: `user_id=${userId}`, 172 | method: "POST", 173 | }); 174 | } 175 | 176 | function blockAjax() { 177 | return blockTargetUser("3269947842"); 178 | } 179 | 180 | function getLatestPornList() { 181 | return localStorage.getItem("twitter_responser_porn_list") 182 | ? JSON.parse(localStorage.getItem("twitter_responser_porn_list")) 183 | : []; 184 | } 185 | 186 | function batchBlockPornUserList(userIds) { 187 | userIds.forEach((userId) => { 188 | blockTargetUser(userId).then((r) => { 189 | // remove from porn list 190 | removeUserFromPornList({ restId: userId }); 191 | }); 192 | }); 193 | } 194 | 195 | function removeUserFromPornList(user) { 196 | const { restId } = user; 197 | const list = localStorage.getItem("twitter_responser_porn_list") 198 | ? JSON.parse(localStorage.getItem("twitter_responser_porn_list")) 199 | : []; 200 | const idx = list.findIndex((item) => item.restId === restId); 201 | list.splice(idx, 1); 202 | localStorage.setItem("twitter_responser_porn_list", JSON.stringify(list)); 203 | } 204 | 205 | function addUserIntoWhiteList(user) { 206 | const { restId } = user; 207 | const list = localStorage.getItem("twitter_responser_whitelist") 208 | ? JSON.parse(localStorage.getItem("twitter_responser_whitelist")) 209 | : []; 210 | const idx = list.findIndex((item) => item.restId === restId); 211 | if (idx === -1) { 212 | list.push(user); 213 | } 214 | localStorage.setItem("twitter_responser_whitelist", JSON.stringify(list)); 215 | } 216 | 217 | function resetApp() { 218 | localStorage.removeItem("twitter_responser_porn_list"); 219 | } 220 | -------------------------------------------------------------------------------- /scripts/injected.js: -------------------------------------------------------------------------------- 1 | function mergeAndUnique(arr1, arr2) { 2 | const arr = [...arr1, ...arr2]; 3 | 4 | const uniqueArr = [...new Set(arr.map((item) => item.screen_name))]; 5 | 6 | const result = uniqueArr.map((name) => { 7 | const item1 = arr1.find((item) => item.screen_name === name); 8 | const item2 = arr2.find((item) => item.screen_name === name); 9 | return { ...item1, ...item2 }; 10 | }); 11 | 12 | return result; 13 | } 14 | 15 | // excute once 16 | const words = (function () { 17 | const keywordsString = 18 | "#代孕|#侮辱|#妈妈|#抚摸|#磕头|#秘书|#蒙眼|10天1cm|18禁|amateur|anal|av|a片|gay片|g点|g片|h动漫|h动画|porn|sm|telegram下载|tg下载|tg:|xing伴侣|yin荡|➕✈️|➕电报|一ye情|一ye欢|一夜情|一夜欢|万人斩|万艾可|三件套|三级|三陪|下体|不谈情|不走进生活|丝袜|丝诱|两性知识|中学老师一枚|主页私信|乖乖粉|买春|乱交|乱伦|乱奸|乳交|乳头|乳房|乳晕|乳沟|乳爆|乳神|互相倾诉一下|互相倾诉下|互相认识一下|互相认识下|亚情|人体摄影|人兽|人妻|人皮面具|从前面捅|从后面捅|代孕机构|伟哥|伦图|伦理片|伦理电影|体位|体制内老师|体制秘书|体奸|体质秘书|作爱|供卵|做爱|偷拍|偷欢|偷窥|催情药|催情辅助用品|入驻平台|全国可飞|全裸|兽交|兽奸|兽性|兽欲|内射|写真|凌辱|几吧|出轨|前凸后翘|加微信|加我主页|助勃|助孕|劲爆内容|勃起|包二奶|包选性别|千人斩|单亲|卖淫|印度三哥|厕奴|原味内衣|去衣|双乳|双峰|双性恋|双效款|双臀|反差|发情|发泄|发浪|发生关系|口交|口令|口射|口活|口淫|口爆|叫床|可以互相认识|可约|吃精|各种姿势|同城|同房|后庭|后穴|吞精|听话水|听话狗|听话的来|听话的狗|听话的🐕|听话🐕|吸精|呻吟|咪咪|哟啪|唯一 telegram|唯一 tg|唯一telegram|唯一tg|喜欢刺激|喜踩踏|喷水机|喷精|四房色播|国产AV|在主页|在编中学老师|在编小学教师|在编教师|在职中学老师|在职小学教师|在职教师|坐脸|增大|增粗|壮阳|处女|处男|多人轮|多人运动|大乳|大波|天然补品|套弄|女M|女S|女m|女s|女主人|女优|女公关|女奴|女王|女私教|女空姐|女郎|奶子|奸情|好嫩|好痒|妓女|妖娆|婊|婬|媚外|嫖娼|嫖客|嫩B|嫩b|嫩女|嫩比|嫩穴|嫩逼|学生妹|实战|客户反馈看媒体|寂寞女|寂寞男|密穴|寻m|寻s|射爽|射精|射颜|小xue|小姐姐一枚|小学教师一枚|小学语文老师|小电影|小穴|小视频|小逼|少修正|少儿不宜|少妇|少男少女|屁眼|屁股|巨乳|巨奶|巨屌|希爱力|干你|干死|干穴|年龄要求|幼b|幼交|幼女|幼师|幼比|幼男|幼逼|应召|延时|开苞|强j|强制up主|强奸|强暴|御姐资源|微密圈|必利劲|忠诚的狗狗|忠诚的🐕|快感|思想开放|性交|性伴|性器|性奴|性息|性愛|性感|性技|性服务|性欲|性爱|性生活|性瘾|性癖|性福|性虎|性虐|性行为|性运动|性饥渴|性骚扰|情欲|情色|情趣|惹火身材|懂的来|成人小说|成人文学|成人杂志|成人游戏|成人片|成人用品|成人电影|成人网站|成人论坛|成年小说|成年文学|成年杂志|成年游戏|成年片|成年用品|成年电影|成年网站|成年论坛|手淫|扌由插|打桩|打炮|扮狗|扮🐕|找个狗|找个🐕|找狗狗|找🐕|抓胸|投资孩子最好尝试|护士|抽一插|抽插|抽送|拔出来|招妓|招鸡|拳交|按摩上门|按摩棒|捆绑|捏弄|换妻|换媳|换脸|换装|接推广|推油|揉乳|插b|插你|插我|插暴|插比|插进|插逼|插阴|援交|援助交际|摸奶|摸胸|撩拔|操我|操死|操烂|操肏|操逼|操黑|放尿|无修正|无码|日烂|日逼|春宫|春药|暴乳|暴奸|暴干|暴操|暴淫|暴肏|暴艹|暴草|暴露|有点寂寞|有码|来主页|来场性|极品美女|欠干|欧美bt|欲仙欲死|欲女|欲望|欲火|死逼|母奸|每日大赛|每日疯狂大赛|洗精|流出|流淫|浪叫|浪女|浪妇|浪逼|淫书|淫乱|淫乳|淫亵|淫兽|淫叫|淫声|淫女|淫妇|淫妻|淫姐|淫威|淫娃|淫媚|淫师|淫情|淫教师|淫样|淫母|淫水|淫河|淫浪|淫液|淫照|淫片|淫电影|淫秽|淫穴|淫糜|淫肉|淫色|淫荡|淫蕩|淫虐|淫虫|淫贱|淫赶|淫靡|淫騷|淫魔|深喉|滚一滚|滚床单|滥交|漏乳|潮吹|潮喷|激情|火辣|炮友|熟女|熟妇|熟母|爆乳|爆操|爆肏|爆艹|爆草|爱液|爱爱|爽死我了|爽片|狂插|狂操|狼友|猛男|猥亵|瑜伽老师|瑟瑟|生殖器|男m|男s|男优|男公关|男奴|白嫩|白虎|百人斩|盗撮|直男醇|相奸|看主页|看我主页|砲友|破处|确定下单|福利视频|福利资源|私信主人|私信主页|私信告诉我|私信女|私信领福利|秘唇|穴口|穴图|粉嫩|粉穴|精卵|精子|精液|素人|素质男|素质约|约啪|约炮|约跑|线下|结婚|绿奴|绿帽|美乳|美女上门|美女图片|美女斗地主|美女裸体|美妇|美幼|美穴|美腿|美逼|羞羞|羞辱|群交|老司机|聊性|联系方式:|联系方式:|联系电报|肉体|肉具|肉唇|肉弹|肉棍|肉棒|肉欲|肉洞|肉穴|肉缝|肉茎|肉逼|肏你|肏死|肛交|肛洞|肛门|肥臀|肥逼|背德|胸推|胸部|脚交|脱光|脱内裤|脱衣|脱裤|腋毛女|自慰|自拍|舔脚|舔阴|舞女|色b|色区|色即是空|色妹妹|色小说|色情|色欲|色比|色狼|色猫|色电影|色界|色盟|色色|色视频|色诱|色逼|艳情|艳照|艳舞|艹死|艾力达|草死|荡女|荡妇|菊穴|菊花|菊门|萌妹资源|萝莉资源|蓝P|薄码|虎骑|蜜液|蜜穴|被干|被插|被操|裙底|裤袜|裸体照片|裸照|裸聊|裸舞|裸陪|裸露|要射了|视频美女|视频聊|视频资源|认证|试管|诱奸|诱惑|调教|谜奸药|豪乳|赤裸|足交|足控|踩头|踩背|车震|轮奸|轮操|轮暴|迷奸|迷幻药|迷幻藥|迷情水|迷情粉|迷情药|迷昏口|迷昏药|迷昏藥|迷药|迷藥|迷魂药|迷魂藥|迷魂香|逼奸|酒瓶插入|酥痒|释放|释欲|金马胶囊|针孔|针对所有男性问题|铃木麻|长期m|长期s|长期固定|长期的m|长期的s|门槛|阳具|阴b|阴唇|阴囊|阴户|阴核|阴比|阴毛|阴精|阴茎|阴蒂|阴逼|阴道|阴部|阴阜|阿姨|附近加我电报|陰唇|陰戶|陰核|陰道|集体淫|需要的主页简介|露b|靠谱狗|靠谱的狗|靠谱的🐕|靠谱🐕|鞭打|领取福利|颜射|风骚|食精|骚b|骚嘴|骚女|骚妇|骚水|骚浪|骚穴|骚货|骚贱贱|骚逼|高潮|鬼畜抄|魅惑|鸡吧|鸡奸|鸡巴|黄片|黄网|黄色网站|黑丝|黑逼|龟头|🐶🐶|🔞|🫦🫦🫦"; 19 | const words = keywordsString.split("|"); 20 | return words; 21 | })(); 22 | 23 | // GPT 给出的关键词,我啥也不知道 24 | function isPornography(str) { 25 | return words.some((s) => str.includes(s)); 26 | } 27 | 28 | function parseTwitterResponserInfo(response) { 29 | const entries = response.data.threaded_conversation_with_injections_v2.instructions[0].entries; 30 | const conversationEntries = entries.filter((entry) => entry.entryId.includes("conversationthread-")); 31 | const resultList = conversationEntries.map((entry) => { 32 | const result = entry.content.items[0].item.itemContent.tweet_results.result; 33 | return result; 34 | }); 35 | 36 | const userInfo = resultList 37 | .map((result) => { 38 | // "TweetWithVisibilityResults" | "Tweet" 39 | if (result.__typename !== "Tweet") return; 40 | 41 | const full_text = result.legacy.full_text; 42 | const following = result.core.user_results.result.legacy.following; 43 | const description = result.core.user_results.result.legacy.description; 44 | const name = result.core.user_results.result.legacy.name; 45 | const screen_name = result.core.user_results.result.legacy.screen_name; 46 | const avatar = result.core.user_results.result.legacy.profile_image_url_https; 47 | const restId = result.core.user_results.result.rest_id; 48 | 49 | let isPorn = false; 50 | let field = ""; 51 | if (isPornography(full_text)) { 52 | isPorn = true; 53 | field = "full_text"; 54 | } else if (isPornography(description)) { 55 | isPorn = true; 56 | field = "description"; 57 | } else if (isPornography(name)) { 58 | isPorn = true; 59 | field = "name"; 60 | } else if (isPornography(screen_name)) { 61 | isPorn = true; 62 | field = "screen_name"; 63 | } 64 | 65 | // whitelist 66 | const whiteList = localStorage.getItem("twitter_responser_whitelist") 67 | ? JSON.parse(localStorage.getItem("twitter_responser_whitelist")) 68 | : []; 69 | const matchedWhiteList = whiteList.some((item) => item.screen_name === screen_name); 70 | if (matchedWhiteList) { 71 | isPorn = false; 72 | } 73 | 74 | // `user` who you are `following` 75 | if (following) { 76 | isPorn = false; 77 | } 78 | 79 | const user = { 80 | full_text, 81 | description, 82 | name, 83 | screen_name, 84 | isPorn, 85 | field, 86 | restId, 87 | avatar, 88 | }; 89 | 90 | return user; 91 | }) 92 | .filter(Boolean); 93 | 94 | return userInfo; 95 | } 96 | 97 | function hijackXHR() { 98 | const XHR = XMLHttpRequest.prototype; 99 | const open = XHR.open; 100 | const send = XHR.send; 101 | const setRequestHeader = XHR.setRequestHeader; 102 | 103 | XHR.open = function () { 104 | return open.apply(this, arguments); 105 | }; 106 | 107 | XHR.setRequestHeader = function () { 108 | return setRequestHeader.apply(this, arguments); 109 | }; 110 | 111 | XHR.send = function () { 112 | this.addEventListener("load", function () { 113 | const url = this.responseURL; 114 | try { 115 | if (this.responseType != "blob") { 116 | let responseBody; 117 | if (this.responseType === "" || this.responseType === "text") { 118 | responseBody = JSON.parse(this.responseText); 119 | } else { 120 | responseBody = this.response; 121 | } 122 | 123 | // only hijack TweetDetail API 124 | if (url.includes("TweetDetail")) { 125 | const responserInfo = parseTwitterResponserInfo(responseBody); 126 | 127 | const pornList = responserInfo.filter((item) => item.isPorn); 128 | 129 | let list = localStorage.getItem("twitter_responser_porn_list") 130 | ? JSON.parse(localStorage.getItem("twitter_responser_porn_list")) 131 | : []; 132 | 133 | if (list.length > 5000) { 134 | // 防止数据过大 135 | list = []; 136 | } 137 | const newList = mergeAndUnique(list, pornList); 138 | localStorage.setItem("twitter_responser_porn_list", JSON.stringify(newList)); 139 | } 140 | } 141 | } catch (err) { 142 | console.debug("Error reading or processing response.", err); 143 | } 144 | }); 145 | 146 | return send.apply(this, arguments); 147 | }; 148 | } 149 | hijackXHR(); 150 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | .blocked-of-porn { 2 | opacity: 0; 3 | height: 0; 4 | } 5 | --------------------------------------------------------------------------------