检测下
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 | [](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 |
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 |
--------------------------------------------------------------------------------