-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfbSkipAd.js
354 lines (334 loc) · 10.3 KB
/
fbSkipAd.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
// if e contains anything in whitelist, then ignore.
const whitelist = [];
// if e contains anything in blacklist, then hide.
// a[data-hovercard][href*="hc_ref=ADS"] from https://github.com/uBlockOrigin/uAssets/issues/233
// a[role="button"][target="_blank"] is used for good post now too.
const blacklist = [
"._m8c",
".uiStreamSponsoredLink",
'a[data-hovercard][href*="hc_ref=ADS"]',
'a[role="button"][rel~="noopener"][data-lynx-mode="async"]',
];
const sponsoredTexts = [
"Sponsored",
"مُموَّل", // Arabic
"赞助内容", // Chinese (Simplified)
"贊助", // Chinese (Traditional)
"Sponzorováno", // Czech
"Gesponsord", // Dutch
"May Sponsor", // Filipino
"Sponsorisé", // French
"Gesponsert", // German
"Χορηγούμενη", // Greek
"ממומן", // Hebrew
"प्रायोजित", // Hindi
"Bersponsor", // Indonesian
"Sponsorizzato", // Italian
"Sponsorowane", // Polish
"Patrocinado", // Portuguese (Brazil)
"Реклама", // Russian
"Sponzorované", // Slovak
"Publicidad", // Spanish
"ได้รับการสนับสนุน", // Thai
"Sponsorlu", // Turkish
"Được tài trợ", // Vietnamese
];
const possibleSponsoredTextQueries = [
'div[id^="feedsubtitle"] > :first-child',
'div[id^="feed_sub_title"] > :first-child',
'div[id^="feed__sub__title"] > :first-child',
'div[id^="feedlabel"] > :first-child',
'div[id^="fbfeed_sub_header_id"] > :nth-child(3)',
'div[data-testid$="storysub-title"] > :first-child',
'div[data-testid$="story-subtilte"] > :first-child',
'div[data-testid$="story--subtilte"] > :first-child',
'a[role="button"][aria-labelledby]',
'a[role="link"] > span[aria-labelledby]',
'div[data-testid*="subtitle"] > :first-child',
'div[data-testid*="label"] > :first-child',
];
/**
* Facebook uses various techniques to hide an element
* @param {Element} e
* @returns {boolean} true if this element is hidden; Thus a text inside this element is not visible to the users.
*/
function isHidden(e) {
const style = window.getComputedStyle(e);
if (
style.display === "none" ||
style.opacity === "0" ||
style.fontSize === "0px" ||
style.visibility === "hidden" ||
style.position === "absolute"
) {
return true;
}
return false;
}
/**
* Facebook uses various techniques to hide a text inside an element
* @param {Element} e
* @returns {string} a text hidden inside this DOM element; Returns an empty string if there is no hidden text.
*/
function getTextFromElement(e) {
return (e.innerText === "" ? e.dataset.content : e.innerText) || "";
}
/**
* For FB5, Facebook also hides a text directly inside a container element.
* @param {Element} e
* @returns {string} a text hidden inside this DOM element
*/
function getTextFromContainerElement(e) {
// we only need the data-content of a container element, or any direct text inside it
return (
e.dataset.content ||
Array.prototype.filter
.call(e.childNodes, (element) => {
return element.nodeType === Node.TEXT_NODE;
})
.map((element) => {
return element.textContent;
})
.join("")
);
}
/**
* Return a text inside this given DOM element that is visible to the users
* @param {Element} e
* @returns {string}
*/
function getVisibleText(e) {
if (isHidden(e)) {
// stop if this is hidden
return "";
}
const children = e.querySelectorAll(":scope > *");
if (children.length !== 0) {
// more level => recursive
return (
getTextFromContainerElement(e) +
Array.prototype.slice.call(children).map(getVisibleText).flat().join("")
);
}
// we have found the real text
return getTextFromElement(e);
}
/**
* Hide an element if this is a sponsored element
* @param {Element} e
* @returns {boolean} true if this is a sponsored element
*/
function hideIfSponsored(e) {
// ignore if matches the whitelist
if (
whitelist.some((query) => {
if (e.querySelector(query) !== null) {
e.dataset.blocked = "whitelist";
console.info(`Ignored (${query})`, [e]);
return true;
}
return false;
})
) {
return false; // ignored this element
}
// hide right away if macthces the blacklist
if (
blacklist.some((query) => {
if (e.querySelector(query) !== null) {
e.style.display = "none";
e.dataset.blocked = "blacklist";
console.info(`AD Blocked (${query})`, [e]);
return true;
}
return false;
})
) {
return true; // has ad
}
// Look through a list of possible locations of "Sponsored" tag, and see if it matches our list of `sponsoredTexts`
return possibleSponsoredTextQueries.some((query) => {
const result = e.querySelectorAll(query);
return [...result].some((t) => {
const visibleText = getVisibleText(t);
if (
sponsoredTexts.some(
(sponsoredText) => visibleText.indexOf(sponsoredText) !== -1
)
) {
e.style.display = "none";
e.dataset.blocked = "sponsored";
console.info(
`AD Blocked (query='${query}', visibleText='${visibleText}')`,
[e]
);
return true;
}
return false;
});
});
}
let feedObserver = null;
// wait for and observe FB5 feed element
function setFB5FeedObserver() {
// We are expecting to find a new feed div
const feed = document.querySelector(
"div[role=feed]:not([data-adblock-monitored])"
);
if (feed !== null) {
// check existing posts
feed
.querySelectorAll('div[data-pagelet^="FeedUnit_"]')
.forEach(hideIfSponsored);
const feedContainer = feed.parentNode;
// flag this feed as monitored
feed.dataset.adblockMonitored = true;
feedObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
// check if feed was reloaded without changing page
if (
mutation.target === feedContainer &&
mutation.addedNodes.length > 0
) {
feedObserver.disconnect();
// check again for the new feed. Since the DOM has just changed, we
// want to wait a bit and start looking for the new div after it was
// rendered. We put our method at the end of the current queue stack
setTimeout(setFB5FeedObserver, 0);
}
// new feed posts added
if (mutation.target === feed && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (
node.dataset.pagelet &&
node.dataset.pagelet.startsWith("FeedUnit_")
) {
hideIfSponsored(node);
}
});
}
});
});
// check for new feed posts
feedObserver.observe(feed, {
childList: true,
});
// check if the feed is replaced
feedObserver.observe(feedContainer, {
childList: true,
});
console.info("Monitoring feed updates", [feed]);
} else {
// no feed div was available yet in DOM. will check again
setTimeout(setFB5FeedObserver, 1000);
}
}
function onPageChange() {
let feed = document.getElementById("stream_pagelet");
if (feed !== null) {
// if the user change page to homepage
feed
.querySelectorAll('div[id^="hyperfeed_story_id_"]')
.forEach(hideIfSponsored);
feedObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.target.id.startsWith("hyperfeed_story_id_")) {
hideIfSponsored(mutation.target);
}
});
});
feedObserver.observe(feed, {
childList: true,
subtree: true,
});
console.info("Monitoring feed updates", [feed]);
return;
}
feed = document.getElementById("pagelet_group_");
if (feed !== null) {
// if the user change page to https://www.facebook.com/groups/*
feed.querySelectorAll('div[id^="mall_post_"]').forEach(hideIfSponsored);
feedObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
mutation.target
.querySelectorAll('div[id^="mall_post_"]')
.forEach(hideIfSponsored);
});
});
feedObserver.observe(feed, {
childList: true,
subtree: true,
});
console.info("Monitoring feed updates", [feed]);
return;
}
// FB5 design
// there's a feed div that we don't monitor yet
feed = document.querySelector("div[role=feed]:not([data-adblock-monitored])");
if (feed !== null) {
setFB5FeedObserver();
return;
}
// there's a feed loading placeholder
feed = document.getElementById("suspended-feed");
if (feed !== null) {
setFB5FeedObserver();
return;
}
// No new feed was detected
// Cleanup observer when there's no feed monitored left in DOM
if (
feedObserver !== null &&
document.querySelector("div[role=feed][data-adblock-monitored]") === null
) {
feedObserver.disconnect();
}
}
const fbObserver = new MutationObserver(onPageChange);
// wait for and observe FB5 page element
function setupFB5PageObserver() {
// We are expecting to find a page div
const pageDiv = document.querySelector(
"div[data-pagelet=root] div[data-pagelet=page]"
);
// make sure there's a page element
if (pageDiv !== null) {
// trigger first page initiation
onPageChange();
// Facebook uses ajax to load new content so
// we need to observe the container of the page
// for any page changes
fbObserver.observe(pageDiv.parentNode, {
childList: true,
});
console.info("Monitoring page changes", [pageDiv]);
} else {
// no page div was available yet in DOM. will check again
setTimeout(setupFB5PageObserver, 1000);
}
}
let fbContent = document.getElementsByClassName("fb_content")[0];
if (fbContent !== undefined) {
// Old Facebook design
// remove on first load
onPageChange();
// Facebook uses ajax to load new content so
// we need this to watch for page change
fbObserver.observe(fbContent, {
childList: true,
});
console.info("Monitoring page changes", [fbContent]);
} else if (document.getElementById("mount_0_0") !== null) {
// if it's FB5 design
setupFB5PageObserver();
}
// if we can't find ".fb_content", then it must be a mobile website.
// in that case, we don't need javascript to block ads
// cleanup
window.addEventListener("beforeunload", () => {
fbObserver.disconnect();
if (feedObserver !== null) {
feedObserver.disconnect();
}
fbContent = null;
});