Facebook cleaner

This script hides sponsored posts (ads) on Facebook, making your feed cleaner and free from distractions.

  1. // ==UserScript==
  2. // @name Facebook cleaner
  3. // @namespace https://lukaszmical.pl/
  4. // @version 0.2.1
  5. // @description This script hides sponsored posts (ads) on Facebook, making your feed cleaner and free from distractions.
  6. // @author Łukasz Micał
  7. // @match https://*.facebook.com/*
  8. // @grant GM_registerMenuCommand
  9. // @grant GM_unregisterMenuCommand
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @grant window.onurlchange
  13. // @icon https://www.google.com/s2/favicons?sz=64&domain=facebook.com
  14. // ==/UserScript==
  15.  
  16. // css:apps/facebook-cleaner/src/style/style-debug.css
  17. const style_debug_default =
  18. '[data-fcc="@"]{position:relative;}[data-fcc="@"]:before{display:block;content:attr(data-fcc-reason);position:absolute;top:0;left:50%;border-radius:16px;padding:8px;background-color:var(--card-background);transform:translateX(-50%);color:var(--primary-text);z-index:999;border:1px solid var(--primary-button-background)}';
  19.  
  20. // css:apps/facebook-cleaner/src/style/style.css
  21. const style_default =
  22. '[data-fcc="@"]{display:none !important;}[data-fcc="@"][data-fcc-type="@"]{display:block !important;max-height:24px;overflow:hidden;margin-bottom:16px;padding-top:24px;border-radius:16px;box-sizing:border-box;position:relative;background-color:var(--card-background);}[data-fcc="@"][data-fcc-type="@"] *{max-height:2px;}[data-fcc="@"][data-fcc-type="@"]:before{display:block;content:attr(title);position:absolute;top:4px;left:50%;transform:translateX(-50%);color:var(--primary-text);}';
  23.  
  24. // libs/share/src/ui/GlobalStyle.ts
  25. const GlobalStyle = class {
  26. static addStyle(key, styles) {
  27. const style =
  28. document.getElementById(key) ||
  29. (function () {
  30. const style2 = document.createElement('style');
  31. style2.id = key;
  32. document.head.appendChild(style2);
  33. return style2;
  34. })();
  35. style.textContent = styles;
  36. }
  37. };
  38.  
  39. // libs/share/src/utils/urlChangeEvent.ts
  40. function activateUrlChangeEvents() {
  41. if (!window.onurlchange) {
  42. const dispatchUrlChangeEvent = function () {
  43. window.dispatchEvent(new CustomEvent('urlchange'));
  44. };
  45. window.addEventListener('popstate', dispatchUrlChangeEvent);
  46. const originalPushState = history.pushState;
  47. history.pushState = function (...args) {
  48. originalPushState.apply(this, args);
  49. dispatchUrlChangeEvent();
  50. };
  51. const originalReplaceState = history.replaceState;
  52. history.replaceState = function (...args) {
  53. originalReplaceState.apply(this, args);
  54. dispatchUrlChangeEvent();
  55. };
  56. }
  57. }
  58.  
  59. // libs/share/src/ui/Observer.ts
  60. const Observer = class {
  61. start(element, callback, options) {
  62. this.stop();
  63. this.observer = new MutationObserver(callback);
  64. this.observer.observe(
  65. element,
  66. options || {
  67. attributeOldValue: true,
  68. attributes: true,
  69. characterData: true,
  70. characterDataOldValue: true,
  71. childList: true,
  72. subtree: true,
  73. }
  74. );
  75. }
  76.  
  77. stop() {
  78. if (this.observer) {
  79. this.observer.disconnect();
  80. }
  81. }
  82. };
  83.  
  84. // apps/facebook-cleaner/src/dictionary/locales.ts
  85. const locales = {
  86. id: {
  87. hiddenPost: 'Postingan tersembunyi',
  88. feed: 'Postingan Kabar Beranda',
  89. follow: 'Ikuti',
  90. join: 'Gabung',
  91. reels: 'Reels',
  92. sponsored: 'Bersponsor',
  93. },
  94. cs: {
  95. hiddenPost: 'Skryt\xE9 p\u0159\xEDsp\u011Bvky',
  96. feed: 'P\u0159\xEDsp\u011Bvku v kan\xE1lu vybran\xFDch p\u0159\xEDsp\u011Bvk\u016F',
  97. follow: 'Sledovat',
  98. join: 'P\u0159idat se',
  99. reels: 'Reels',
  100. sponsored: 'Sponzorov\xE1no',
  101. },
  102. de: {
  103. hiddenPost: 'Versteckte Beitr\xE4ge',
  104. feed: 'News Feed-Beitr\xE4ge',
  105. follow: 'Folgen',
  106. join: 'Beitreten',
  107. reels: 'Reels',
  108. sponsored: 'Anzeige',
  109. },
  110. en: {
  111. hiddenPost: 'Hidden posts',
  112. feed: 'News Feed posts',
  113. follow: 'Follow',
  114. join: 'Join',
  115. reels: 'Reels',
  116. sponsored: 'Sponsored',
  117. },
  118. es: {
  119. hiddenPost: 'Publicaciones ocultas',
  120. feed: 'Publicaciones de la secci\xF3n de noticias',
  121. follow: 'Seguir',
  122. join: 'Unirte',
  123. reels: 'Reels',
  124. sponsored: 'Publicidad',
  125. },
  126. fr: {
  127. hiddenPost: 'Messages masqu\xE9s',
  128. feed: 'Nouvelles publications du fil d\u2019actualit\xE9',
  129. follow: 'Suivre',
  130. join: 'Rejoindre',
  131. reels: 'Reels',
  132. sponsored: 'Sponsoris\xE9',
  133. },
  134. it: {
  135. hiddenPost: 'Post nascosti',
  136. feed: 'Post della sezione Notizie',
  137. follow: 'Segui',
  138. join: 'Iscriviti',
  139. reels: 'Reels',
  140. sponsored: 'Sponsorizzato',
  141. },
  142. pl: {
  143. hiddenPost: 'Ukryte posty',
  144. feed: 'Posty w Aktualno\u015Bciach',
  145. follow: 'Obserwuj',
  146. join: 'Do\u0142\u0105cz',
  147. reels: 'Rolki',
  148. sponsored: 'Sponsorowane',
  149. },
  150. pt: {
  151. hiddenPost: 'Postagens ocultas',
  152. feed: 'Publica\xE7\xF5es do Feed de Not\xEDcias',
  153. follow: 'Seguir',
  154. join: 'Participar',
  155. reels: 'Reels',
  156. sponsored: 'Patrocinado',
  157. },
  158. sk: {
  159. hiddenPost: 'Skryt\xE9 pr\xEDspevky',
  160. feed: 'Pr\xEDspevky v Novink\xE1ch',
  161. follow: 'Sledova\u0165',
  162. join: 'Prida\u0165 sa',
  163. reels: 'Reels',
  164. sponsored: 'Sponzorovan\xE9',
  165. },
  166. sl: {
  167. hiddenPost: 'Skrite objave',
  168. feed: 'Objave v viru novic',
  169. follow: 'Sledi',
  170. join: 'Pridru\u017Ei se',
  171. reels: 'Interaktivni videi',
  172. sponsored: 'Sponzorirano',
  173. },
  174. szl: {
  175. hiddenPost: 'Skryte posty',
  176. feed: 'Posty w Aktualno\u015Bciach',
  177. follow: 'Obserwuj',
  178. join: 'Do\u0142\u0105cz',
  179. reels: 'Rolki',
  180. sponsored: 'Szp\u014Dnzorowane',
  181. },
  182. tr: {
  183. hiddenPost: 'Gizli g\xF6nderiler',
  184. feed: 'Haber Kayna\u011F\u0131 g\xF6nderileri',
  185. follow: 'Takip Et',
  186. join: 'Kat\u0131l',
  187. reels: 'Reels',
  188. sponsored: 'Sponsorlu',
  189. },
  190. uk: {
  191. hiddenPost:
  192. '\u041F\u0440\u0438\u0445\u043E\u0432\u0430\u043D\u0456 \u043F\u043E\u0441\u0442\u0438/Prykhovani posty',
  193. feed: '\u0414\u043E\u043F\u0438\u0441\u0438 \u0437\u0456 \u0441\u0442\u0440\u0456\u0447\u043A\u0438 \u043D\u043E\u0432\u0438\u043D',
  194. follow: '\u0421\u0442\u0435\u0436\u0438\u0442\u0438',
  195. join: '\u041F\u0440\u0438\u0454\u0434\u043D\u0430\u0442\u0438\u0441\u044F',
  196. reels: '\u0412\u0456\u0434\u0435\u043E Reels',
  197. sponsored: '\u0420\u0435\u043A\u043B\u0430\u043C\u0430',
  198. },
  199. 'zh-Hans': {
  200. hiddenPost: '\u9690\u85CF\u5E16\u5B50',
  201. feed: '\u52A8\u6001\u6D88\u606F\u5E16\u5B50',
  202. follow: '\u5173\u6CE8',
  203. join: '\u52A0\u5165',
  204. reels: 'Reels',
  205. sponsored: '\u8D5E\u52A9\u5185\u5BB9',
  206. },
  207. 'zh-Hant': {
  208. hiddenPost: '\u96B1\u85CF\u8CBC\u6587',
  209. feed: '\u52D5\u614B\u6D88\u606F\u5E16\u5B50',
  210. follow: '\u8FFD\u8E64',
  211. join: '\u52A0\u5165',
  212. reels: 'Reels',
  213. sponsored: '\u8D0A\u52A9',
  214. },
  215. };
  216. const languages = Object.keys(locales);
  217.  
  218. // apps/facebook-cleaner/src/dictionary/Dictionary.ts
  219. const Dictionary = class {
  220. constructor() {
  221. this.lang = this.detectLanguage();
  222. this.dictionary = this.getDictionary();
  223. }
  224.  
  225. getFeedLabel() {
  226. return this.dictionary.feed;
  227. }
  228.  
  229. getFollowLabel() {
  230. return this.dictionary.follow;
  231. }
  232.  
  233. getJoinLabel() {
  234. return this.dictionary.join;
  235. }
  236.  
  237. getReelsLabel() {
  238. return this.dictionary.reels;
  239. }
  240.  
  241. getSponsoredLabel() {
  242. return this.dictionary.sponsored;
  243. }
  244.  
  245. hiddenPostLabel(count) {
  246. return `${this.dictionary.hiddenPost} (${count})`;
  247. }
  248.  
  249. detectLanguage() {
  250. const pageLang = window.document.documentElement.lang;
  251. if (pageLang && languages.includes(pageLang)) {
  252. return pageLang;
  253. }
  254. return void 0;
  255. }
  256.  
  257. getDictionary() {
  258. if (this.lang) {
  259. return locales[this.lang];
  260. }
  261. return void 0;
  262. }
  263. };
  264.  
  265. // apps/facebook-cleaner/src/services/ElementDetector.ts
  266. const ElementDetector = class {
  267. constructor() {
  268. this.dictionary = new Dictionary();
  269. }
  270.  
  271. getElement(root, query, text) {
  272. return this.getElements(root, query, text)[0];
  273. }
  274.  
  275. getElements(root, query, text) {
  276. return [...root.querySelectorAll(query)].filter((element) => {
  277. if (!text) {
  278. return true;
  279. }
  280. return element.textContent.includes(text);
  281. });
  282. }
  283.  
  284. getFeedElement() {
  285. const [feedHeader] = this.getElements(
  286. document,
  287. 'h3.html-h3',
  288. this.dictionary.getFeedLabel()
  289. );
  290. if (!feedHeader) {
  291. return void 0;
  292. }
  293. return feedHeader.parentElement.lastElementChild;
  294. }
  295. };
  296.  
  297. // apps/facebook-cleaner/src/services/UserSettings.ts
  298. const settingsMenuLabels = {
  299. ['fcc-hide-reels' /* HideReels */]: 'reels',
  300. ['fcc-hide-sponsored' /* HideSponsored */]: 'sponsored posts',
  301. ['fcc-hide-suggested-groups' /* HideSuggestedGroups */]: 'suggested groups',
  302. ['fcc-hide-suggested-profiles' /* HideSuggestedProfiles */]:
  303. 'suggested profiles',
  304. };
  305. const UserSettings = class {
  306. constructor() {
  307. this.setting = {
  308. ['fcc-hide-reels' /* HideReels */]: true,
  309. ['fcc-hide-sponsored' /* HideSponsored */]: true,
  310. ['fcc-hide-suggested-groups' /* HideSuggestedGroups */]: true,
  311. ['fcc-hide-suggested-profiles' /* HideSuggestedProfiles */]: true,
  312. };
  313. this.setting = this.readSettings();
  314. this.updateMenu();
  315. }
  316.  
  317. getSettings() {
  318. return { ...this.setting };
  319. }
  320.  
  321. readSettings() {
  322. return Object.fromEntries(
  323. Object.entries(this.setting).map(([key, defaultValue]) => [
  324. key,
  325. GM_getValue(key, defaultValue),
  326. ])
  327. );
  328. }
  329.  
  330. setSettingValue(id, value) {
  331. this.setting[id] = value;
  332. GM_setValue(id, value);
  333. this.updateMenu();
  334. }
  335.  
  336. settingLabel(id) {
  337. return [
  338. this.setting[id] ? 'Show' : 'Hide',
  339. settingsMenuLabels[id],
  340. 'in feed news',
  341. ].join(' ');
  342. }
  343.  
  344. updateMenu() {
  345. Object.keys(this.setting).forEach((id) => GM_unregisterMenuCommand(id));
  346. Object.entries(this.setting).forEach(([id, value]) =>
  347. GM_registerMenuCommand(
  348. this.settingLabel(id),
  349. () => this.setSettingValue(id, !value),
  350. {
  351. id,
  352. autoClose: true,
  353. }
  354. )
  355. );
  356. }
  357. };
  358.  
  359. // apps/facebook-cleaner/src/services/BannedPost.ts
  360. const BannedPost = class {
  361. constructor() {
  362. this.detector = new ElementDetector();
  363. this.dictionary = new Dictionary();
  364. }
  365.  
  366. filter(posts, settings) {
  367. return posts.filter((post) => {
  368. if (post.dataset.fcc) {
  369. return true;
  370. }
  371. const query = '[data-ad-rendering-role="profile_name"] [role="button"]';
  372. if (
  373. settings['fcc-hide-suggested-profiles' /* HideSuggestedProfiles */] &&
  374. this.detector.getElement(post, query, this.dictionary.getFollowLabel())
  375. ) {
  376. post.dataset.fccReason = 'follow' /* Follow */;
  377. return true;
  378. }
  379. if (
  380. settings['fcc-hide-suggested-groups' /* HideSuggestedGroups */] &&
  381. this.detector.getElement(post, query, this.dictionary.getJoinLabel())
  382. ) {
  383. post.dataset.fccReason = 'join' /* Join */;
  384. return true;
  385. }
  386. if (
  387. settings['fcc-hide-reels' /* HideReels */] &&
  388. this.detector.getElement(
  389. post,
  390. '[role="button"]',
  391. this.dictionary.getReelsLabel()
  392. )
  393. ) {
  394. post.dataset.fccReason = 'reels' /* Reels */;
  395. return true;
  396. }
  397. if (
  398. settings['fcc-hide-sponsored' /* HideSponsored */] &&
  399. this.detector.getElement(post, 'a[href*="ads/about"]')
  400. ) {
  401. post.dataset.fccReason = 'sponsored-link' /* SponsoredLink */;
  402. return true;
  403. }
  404. if (
  405. settings['fcc-hide-sponsored' /* HideSponsored */] &&
  406. this.detector.getElement(
  407. post,
  408. 'a[attributionsrc] [aria-labelledby]',
  409. this.dictionary.getSponsoredLabel()
  410. )
  411. ) {
  412. post.dataset.fccReason = 'sponsored-label' /* SponsoredLabel */;
  413. return true;
  414. }
  415. const items = this.detector.getElements(
  416. post,
  417. 'a[attributionsrc] [aria-labelledby]'
  418. );
  419. if (
  420. settings['fcc-hide-sponsored' /* HideSponsored */] &&
  421. items.some(this.isSponsoredElement.bind(this))
  422. ) {
  423. post.dataset.fccReason =
  424. 'sponsored-hidden-label' /* SponsoredHiddenLabel */;
  425. return true;
  426. }
  427. return false;
  428. });
  429. }
  430.  
  431. hide(post) {
  432. post.dataset.fcc = '@';
  433. post.dataset.fccType = '';
  434. }
  435.  
  436. isSponsoredElement(element) {
  437. const sponsoredLabel = this.dictionary.getSponsoredLabel();
  438. const items = [...element.firstElementChild.children].filter((i) =>
  439. sponsoredLabel.includes(i.innerText)
  440. );
  441. if (items.length < sponsoredLabel.length) {
  442. return false;
  443. }
  444. const elementLabel = items
  445. .map((item) => {
  446. const styles = getComputedStyle(item);
  447. return {
  448. isVisible: styles.position !== 'absolute',
  449. order: Number(styles.order),
  450. text: item.innerText,
  451. };
  452. })
  453. .filter((item) => item.isVisible)
  454. .sort((a, b) => a.order - b.order)
  455. .map((item) => item.text)
  456. .join('');
  457. return elementLabel.includes(sponsoredLabel);
  458. }
  459.  
  460. showHiddenPostGroups() {
  461. const hiddenPosts = document.querySelectorAll('[data-fcc="@"]');
  462. const hiddenPostsCount = (post) => {
  463. const nextPost = post.nextElementSibling;
  464. if (nextPost && nextPost.dataset.fcc) {
  465. return 1 + hiddenPostsCount(nextPost);
  466. }
  467. return 0;
  468. };
  469. [...hiddenPosts].forEach((post) => {
  470. const prevPost = post.previousElementSibling;
  471. if (!prevPost || (prevPost && !prevPost.dataset.fcc)) {
  472. const count = 1 + hiddenPostsCount(post);
  473. post.dataset.fccType = '@';
  474. post.title = this.dictionary.hiddenPostLabel(count);
  475. }
  476. });
  477. }
  478. };
  479.  
  480. // apps/facebook-cleaner/src/services/FeedCleaner.ts
  481. const FeedCleaner = class {
  482. constructor() {
  483. this.bannedPostDetector = new BannedPost();
  484. this.settings = new UserSettings();
  485. }
  486.  
  487. cleanFeed(feedElement) {
  488. const posts = [...feedElement.children];
  489. const bannedPosts = this.bannedPostDetector.filter(
  490. posts,
  491. this.settings.getSettings()
  492. );
  493. bannedPosts.forEach((post) => {
  494. this.bannedPostDetector.hide(post);
  495. });
  496. this.bannedPostDetector.showHiddenPostGroups();
  497. }
  498. };
  499.  
  500. // apps/facebook-cleaner/src/services/FacebookCleaner.ts
  501. const FacebookCleaner = class {
  502. constructor() {
  503. this.detector = new ElementDetector();
  504. this.feedCleaner = new FeedCleaner();
  505. this.feedElement = void 0;
  506. this.observer = new Observer();
  507. this.initEvents();
  508. }
  509.  
  510. run() {
  511. this.initFeedElement();
  512. this.initObserver();
  513. this.feedListUpdated();
  514. }
  515.  
  516. feedListUpdated() {
  517. if (this.isValidElement(this.feedElement)) {
  518. this.feedCleaner.cleanFeed(this.feedElement);
  519. }
  520. }
  521.  
  522. initEvents() {
  523. window.addEventListener('urlchange', () => {
  524. this.run();
  525. window.setTimeout(this.run.bind(this), 2e3);
  526. window.setTimeout(this.run.bind(this), 5e3);
  527. });
  528. window.setInterval(this.run.bind(this), 20 * 1e3);
  529. }
  530.  
  531. initFeedElement() {
  532. if (!this.isValidElement(this.feedElement)) {
  533. this.feedElement = this.detector.getFeedElement();
  534. }
  535. }
  536.  
  537. initObserver() {
  538. if (!this.isValidElement(this.feedElement)) {
  539. return this.observer.stop();
  540. }
  541. if (!this.feedElement.dataset.fccReady) {
  542. this.feedElement.dataset.fccReady = '1';
  543. this.observer.start(this.feedElement, this.feedListUpdated.bind(this), {
  544. childList: true,
  545. subtree: true,
  546. });
  547. }
  548. }
  549.  
  550. isValidElement(element) {
  551. return element && element.isConnected;
  552. }
  553. };
  554.  
  555. // apps/facebook-cleaner/src/main.ts
  556. activateUrlChangeEvents();
  557. const isDebug = false;
  558. GlobalStyle.addStyle(
  559. 'fcc-style',
  560. isDebug ? style_debug_default : style_default
  561. );
  562. const service = new FacebookCleaner();
  563. service.run();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址