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.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
  };

})();