LinkedIn Cleaner

Remove all ads, promoted posts, suggested people, and sidebar noise from LinkedIn. Requires a valid license key.

在您安装前,Greasy Fork 希望您知道此脚本声明其包含了一些负面功能。这些功能也许会使脚本作者获利,而不能给您带来任何直接的金钱收益。

您只有在付费后才能使用脚本的全部功能。Greasy Fork 未参与到支付的流程,因此无法验证您是否获得了有价值的东西,亦无法帮助您申请退款。 脚本作者的说明: Requires a paid license key ($3.99 AUD/month or $29.99 AUD/year) from linkedinfilter.com. A free trial is not available.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         LinkedIn Cleaner
// @namespace    https://linkedinfilter.com
// @version      2.1.0
// @description  Remove all ads, promoted posts, suggested people, and sidebar noise from LinkedIn. Requires a valid license key.
// @author       LinkedIn Cleaner
// @homepageURL  https://linkedinfilter.com
// @supportURL   https://linkedinfilter.com
// @license      Proprietary
// @antifeature  payment Requires a paid license key ($3.99 AUD/month or $29.99 AUD/year) from linkedinfilter.com. A free trial is not available.
// @match        *://*.linkedin.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @connect      linkedincleaner.replit.app
// @connect      linkedinfilter.com
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  const VERIFY_URL = 'https://linkedincleaner.replit.app/api/licenses/verify';

  /* ----------------------------------------------------------
     LICENSE KEY STORAGE
     Uses GM_getValue/GM_setValue if available (Tampermonkey),
     falls back to localStorage for Safari Userscripts.
     ---------------------------------------------------------- */
  const KEY_STORAGE_KEY    = 'linkedin_cleaner_license_key';
  const LAST_CHECK_KEY     = 'linkedin_cleaner_last_check';
  const LAST_VALID_KEY     = 'linkedin_cleaner_last_valid';
  const DEVICE_ID_KEY      = 'linkedin_cleaner_device_id';

  function getStoredKey() {
    try { return GM_getValue(KEY_STORAGE_KEY, null); } catch (_) {}
    try { return localStorage.getItem(KEY_STORAGE_KEY); } catch (_) {}
    return null;
  }

  function setStoredKey(key) {
    try { GM_setValue(KEY_STORAGE_KEY, key); return; } catch (_) {}
    try { localStorage.setItem(KEY_STORAGE_KEY, key); } catch (_) {}
  }

  function deleteStoredKey() {
    try { GM_deleteValue(KEY_STORAGE_KEY); } catch (_) {}
    try { GM_deleteValue(LAST_CHECK_KEY); } catch (_) {}
    try { GM_deleteValue(LAST_VALID_KEY); } catch (_) {}
    try { localStorage.removeItem(KEY_STORAGE_KEY); } catch (_) {}
    // Note: device ID is intentionally kept so the same device can re-register after a new key is entered
  }

  function getOrCreateDeviceId() {
    let id;
    try { id = GM_getValue(DEVICE_ID_KEY, null); } catch (_) {}
    if (!id) { try { id = localStorage.getItem(DEVICE_ID_KEY); } catch (_) {} }
    if (!id) {
      id = crypto.randomUUID();
      try { GM_setValue(DEVICE_ID_KEY, id); } catch (_) {}
      try { localStorage.setItem(DEVICE_ID_KEY, id); } catch (_) {}
    }
    return id;
  }

  function getLastCheck() {
    try { return Number(GM_getValue(LAST_CHECK_KEY, 0)); } catch (_) {}
    try { return Number(localStorage.getItem(LAST_CHECK_KEY) || 0); } catch (_) {}
    return 0;
  }

  function setLastCheck(ts, valid) {
    try { GM_setValue(LAST_CHECK_KEY, String(ts)); GM_setValue(LAST_VALID_KEY, valid ? '1' : '0'); return; } catch (_) {}
    try { localStorage.setItem(LAST_CHECK_KEY, String(ts)); localStorage.setItem(LAST_VALID_KEY, valid ? '1' : '0'); } catch (_) {}
  }

  function getLastValid() {
    try { return GM_getValue(LAST_VALID_KEY, '0') === '1'; } catch (_) {}
    try { return localStorage.getItem(LAST_VALID_KEY) === '1'; } catch (_) {}
    return false;
  }

  /* ----------------------------------------------------------
     VERIFY LICENSE KEY AGAINST THE SERVER
     Verified once on first entry, then cached indefinitely.
     The user can call linkedinCleanerSetKey() to reset and re-verify.
     ---------------------------------------------------------- */
  const CHECK_INTERVAL_MS = Infinity; // verify once, trust forever

  function verifyKey(key, deviceId) {
    const body = JSON.stringify({ key, deviceId, platform: 'userscript' });
    return new Promise((resolve) => {
      const done = (valid, status) => resolve({ valid, status });

      // Use GM_xmlhttpRequest if available (bypasses CORS in Tampermonkey)
      if (typeof GM_xmlhttpRequest !== 'undefined') {
        GM_xmlhttpRequest({
          method: 'POST',
          url: VERIFY_URL,
          headers: { 'Content-Type': 'application/json' },
          data: body,
          timeout: 8000,
          onload(r) {
            try {
              const data = JSON.parse(r.responseText);
              done(data.valid === true, data.status);
            } catch (_) { done(false, 'error'); }
          },
          onerror() { done(false, 'network_error'); },
          ontimeout() { done(false, 'network_error'); },
        });
        return;
      }

      // Fallback: native fetch (works in Safari Userscripts)
      fetch(VERIFY_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body,
        signal: AbortSignal.timeout(8000),
      })
        .then((r) => r.json())
        .then((data) => done(data.valid === true, data.status))
        .catch(() => done(false, 'network_error'));
    });
  }

  /* ----------------------------------------------------------
     PROMPT FOR LICENSE KEY (shown when key is missing or invalid)
     ---------------------------------------------------------- */
  function promptForKey(message) {
    return new Promise((resolve) => {
      const overlay = document.createElement('div');
      overlay.id = 'lc-overlay';
      overlay.style.cssText = `
        position:fixed;top:0;left:0;width:100%;height:100%;z-index:999999;
        background:rgba(0,0,0,.55);display:flex;align-items:center;
        justify-content:center;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
      `;

      overlay.innerHTML = `
        <div style="background:#fff;border-radius:14px;padding:32px 28px;max-width:420px;width:90%;box-shadow:0 8px 40px rgba(0,0,0,.25)">
          <div style="font-size:20px;font-weight:700;color:#1a1a1a;margin-bottom:6px">LinkedIn Cleaner</div>
          <div style="font-size:13px;color:${message ? '#c0392b' : '#555'};margin-bottom:20px;line-height:1.5">
            ${message || 'Enter your license key to activate ad and suggestion removal.'}
          </div>
          <input id="lc-key-input" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
            style="width:100%;padding:10px 12px;border:2px solid #ddd;border-radius:8px;font-size:13px;font-family:monospace;margin-bottom:16px;outline:none"
          />
          <div style="display:flex;gap:10px">
            <button id="lc-submit" style="flex:1;background:#0a66c2;color:#fff;border:none;border-radius:8px;padding:11px;font-size:14px;font-weight:600;cursor:pointer">
              Activate
            </button>
            <button id="lc-cancel" style="padding:11px 16px;background:#f0f0f0;color:#444;border:none;border-radius:8px;font-size:14px;cursor:pointer">
              Cancel
            </button>
          </div>
          <div style="margin-top:14px;font-size:11px;color:#999;text-align:center">
            No key yet? <a href="#" id="lc-buy" style="color:#0a66c2">Get a subscription</a>
          </div>
        </div>
      `;

      document.body.appendChild(overlay);
      const input = overlay.querySelector('#lc-key-input');
      input.focus();

      overlay.querySelector('#lc-submit').addEventListener('click', () => {
        const val = input.value.trim();
        overlay.remove();
        resolve(val || null);
      });

      overlay.querySelector('#lc-cancel').addEventListener('click', () => {
        overlay.remove();
        resolve(null);
      });

      overlay.querySelector('#lc-buy').addEventListener('click', (e) => {
        e.preventDefault();
        window.open('https://1cffa241-a031-4b1a-ac9a-07a7a32ed051-00-1la2kydxl1j2a.spock.replit.dev/', '_blank');
      });

      input.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') overlay.querySelector('#lc-submit').click();
        if (e.key === 'Escape') overlay.querySelector('#lc-cancel').click();
      });
    });
  }

  /* ----------------------------------------------------------
     SMALL STATUS BANNER (shown briefly after activation)
     ---------------------------------------------------------- */
  function showBanner(message, type) {
    const existing = document.getElementById('lc-banner');
    if (existing) existing.remove();

    const banner = document.createElement('div');
    banner.id = 'lc-banner';
    const bg = type === 'success' ? '#2e7d32' : type === 'error' ? '#c0392b' : '#0a66c2';
    banner.style.cssText = `
      position:fixed;bottom:24px;right:24px;z-index:999999;
      background:${bg};color:#fff;padding:12px 18px;
      border-radius:10px;font-size:13px;font-weight:500;
      font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
      box-shadow:0 4px 16px rgba(0,0,0,.25);
      animation:lc-slide-in .25s ease;
    `;
    banner.textContent = message;
    document.body.appendChild(banner);
    setTimeout(() => banner?.remove(), 3500);
  }

  /* ----------------------------------------------------------
     LINKEDIN AD / SUGGESTION REMOVAL
     ---------------------------------------------------------- */

  const STATIC_SELECTORS = [
    '[data-ad-banner]', '.ad-banner-container', '.ad-banner', '.ad-slot', '.ads-container',
    'section[aria-label="Add to your feed"]', 'section[aria-label="Grow your network"]',
    'section[aria-label*="Learning"]', 'section[aria-label*="Pages you might like"]',
    'section[aria-label*="Discover more"]', 'section[aria-label*="Newsletter"]',
    'section[aria-label*="Events you might like"]',
    'section[aria-label*="Jobs you might be interested in"]',
    '.premium-upsell-link', '.premium-upsell-module', '[data-test-premium-module]',
    'section[aria-label*="Premium"]', '.feed-follows-module',
    '[data-finite-scroll-hotspot="pymk"]', '.discovery-modules',
    /* ---- Messaging sponsored / InMail ads ---- */
    '.msg-sponsored-card', '.msg-overlay-bubble-item--sponsored',
    '.msg-conversation-listitem--sponsored', '.msg-s-message-list__event--sponsored',
    '[data-control-name="sponsored_message"]',
  ];

  const PROMOTED_PATTERNS = [/^promoted$/i, /^sponsored$/i, /^promoted\s*·/i, /^sponsored\s*·/i];

  const SUGGESTION_LABEL_PATTERNS = [
    /people you may know/i, /add to your feed/i, /grow your network/i,
    /pages you might like/i, /events you might like/i, /learning/i, /newsletter/i,
    /jobs you might be interested/i, /premium/i, /suggested/i, /discover more/i,
    /you might also like/i,
  ];

  function hide(el) {
    if (el && el instanceof HTMLElement && el.style.display !== 'none')
      el.style.setProperty('display', 'none', 'important');
  }

  function hideFeedItem(el) {
    let node = el;
    while (node && node !== document.body) {
      if (
        (node.tagName === 'LI' && node.classList.contains('occludable-update')) ||
        node.classList.contains('feed-shared-update-v2') ||
        (node.tagName === 'LI' && node.dataset.id)
      ) { hide(node); return; }
      node = node.parentElement;
    }
    hide(el);
  }

  function cleanLinkedIn() {
    STATIC_SELECTORS.forEach((s) => { try { document.querySelectorAll(s).forEach(hide); } catch (_) {} });

    document.querySelectorAll(
      '.update-components-actor__sub-description span[aria-hidden="true"],' +
      '.feed-shared-actor__sub-description span[aria-hidden="true"],' +
      '.update-components-actor__description span[aria-hidden="true"]'
    ).forEach((span) => {
      if (PROMOTED_PATTERNS.some((re) => re.test(span.textContent.trim()))) hideFeedItem(span);
    });
    document.querySelectorAll(
      '.feed-shared-actor__description--promoted,[data-control-name="promoted_entity"]'
    ).forEach((el) => hideFeedItem(el));

    document.querySelectorAll('section[aria-label],div[aria-label]').forEach((el) => {
      const label = el.getAttribute('aria-label') || '';
      if (SUGGESTION_LABEL_PATTERNS.some((re) => re.test(label))) hide(el);
    });

    document.querySelectorAll(
      'li.occludable-update,.scaffold-finite-scroll__content > div > ul > li'
    ).forEach((item) => {
      if (
        item.querySelector('.discover-entity-type-pill') ||
        item.querySelector('[data-finite-scroll-hotspot="pymk"]') ||
        item.querySelector('.feed-follows-module')
      ) { hide(item); return; }
      const label = item.getAttribute('aria-label') || '';
      if (SUGGESTION_LABEL_PATTERNS.some((re) => re.test(label))) hide(item);
    });

    document.querySelectorAll(
      '.scaffold-layout__aside .artdeco-card,aside .artdeco-card'
    ).forEach((card) => {
      const heading = card.querySelector('h2,h3,[aria-label]');
      if (!heading) return;
      const text = (heading.textContent || heading.getAttribute('aria-label') || '').trim();
      if (SUGGESTION_LABEL_PATTERNS.some((re) => re.test(text))) hide(card);
    });

    // Messaging sponsored items — conversation list + message thread
    ['.msg-conversation-listitem', '.msg-selectable-entity',
     '.msg-overlay-list-bubble-item', '.msg-s-message-list__event'].forEach((sel) => {
      try {
        document.querySelectorAll(sel).forEach((item) => {
          const sponsored = Array.from(item.querySelectorAll('span,p,small,strong')).some(
            (el) => el.children.length === 0 && /^sponsored$/i.test((el.textContent || '').trim()),
          );
          if (sponsored) hide(item);
        });
      } catch (_) {}
    });
  }

  let debounceTimer = null;
  const observer = new MutationObserver(() => {
    clearTimeout(debounceTimer);
    debounceTimer = setTimeout(cleanLinkedIn, 150);
  });

  function startCleaning() {
    observer.observe(document.body, { childList: true, subtree: true });
    cleanLinkedIn();
    setTimeout(cleanLinkedIn, 1000);
    setTimeout(cleanLinkedIn, 3000);
  }

  /* ----------------------------------------------------------
     MAIN — LICENSE CHECK FLOW
     ---------------------------------------------------------- */

  async function main() {
    let key = getStoredKey();
    const deviceId = getOrCreateDeviceId();

    if (!key) {
      key = await promptForKey(null);
      if (!key) return;
      setStoredKey(key);
    }

    const now = Date.now();
    const lastCheck = getLastCheck();
    const needsCheck = now - lastCheck > CHECK_INTERVAL_MS;

    let valid;

    if (!needsCheck) {
      valid = getLastValid();
    } else {
      const result = await verifyKey(key, deviceId);
      valid = result.valid;
      setLastCheck(now, valid);

      if (!valid && result.status === 'device_limit_reached') {
        showBanner(
          'This key is already activated on 2 devices. Purchase an additional license for more devices.',
          'error'
        );
        return;
      }
    }

    if (valid) {
      startCleaning();
      if (needsCheck) showBanner('LinkedIn Cleaner is active', 'success');
    } else {
      deleteStoredKey();
      const newKey = await promptForKey(
        'Your license key is invalid or your subscription has expired. ' +
        'Please enter a valid key or renew your subscription.'
      );
      if (newKey) {
        setStoredKey(newKey);
        const { valid: recheck, status: recheckStatus } = await verifyKey(newKey, deviceId);
        setLastCheck(Date.now(), recheck);
        if (recheck) {
          startCleaning();
          showBanner('LinkedIn Cleaner is active', 'success');
        } else if (recheckStatus === 'device_limit_reached') {
          deleteStoredKey();
          showBanner('This key is already activated on 2 devices. Purchase an additional license.', 'error');
        } else {
          deleteStoredKey();
          showBanner('Key not recognised. Check your subscription.', 'error');
        }
      }
    }
  }

  main();

  /* ----------------------------------------------------------
     GLOBAL HELPER — lets users re-enter their key from console
     ---------------------------------------------------------- */
  window.linkedinCleanerSetKey = function () {
    deleteStoredKey();
    main();
  };

})();