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, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

Advertisement:

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();
  };

})();