Medium Member Bypass

Modern Medium GUI with multiple bypass services and fallback with availability checks, including custom domains and Freedium banner auto-close.

当前为 2025-01-07 提交的版本,查看 最新版本

// ==UserScript==
// @name         Medium Member Bypass
// @author       UniverseDev
// @license      GPL-3.0-or-later
// @namespace    http://tampermonkey.net/
// @version      13.9.2
// @description  Modern Medium GUI with multiple bypass services and fallback with availability checks, including custom domains and Freedium banner auto-close.
// @match        *://*.medium.com/*
// @match        *://*.betterprogramming.pub/*
// @match        *://*.towardsdatascience.com/*
// @match        https://freedium.cfd/*
// @match        https://readmedium.com/*
// @match        https://md.vern.cc/*
// @match        https://archive.is/*
// @match        https://archive.li/*
// @match        https://archive.vn/*
// @match        https://archive.ph/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      freedium.cfd
// @connect      readmedium.com
// @connect      md.vern.cc
// @connect      archive.is
// @connect      archive.li
// @connect      archive.vn
// @connect      archive.ph
// ==/UserScript==

(function() {
    'use strict';

    const SETTINGS_CLASS = 'medium-settings';
    const NOTIFICATION_CLASS = 'medium-notification';
    const MEMBER_DIV_SELECTOR = 'p.bf.b.bg.z.bk';
    const FREEDIUM_CLOSE_BUTTON_SELECTOR = '.close-button';
    const MEMBER_WALL_CHECK_SELECTOR = 'div.s.u.w.fg.fh.q';

    const getStoredValue = (key, defaultValue) => GM_getValue(key, defaultValue);
    const setStoredValue = (key, value) => GM_setValue(key, value);

    const MEDIUM_CUSTOM_DOMAINS = ['betterprogramming.pub', 'towardsdatascience.com'];

    const config = {
        bypassUrls: {
            freedium: 'https://freedium.cfd',
            readmedium: 'https://readmedium.com',
            libmedium: 'https://md.vern.cc/',
            archiveIs: 'https://archive.is/newest/',
            archiveLi: 'https://archive.li/newest/',
            archiveVn: 'https://archive.vn/newest/',
            archivePh: 'https://archive.ph/newest/',
        },
        currentBypassIndex: getStoredValue('currentBypassIndex', 0),
        memberOnlyDivSelector: MEMBER_DIV_SELECTOR,
        autoRedirectDelay: getStoredValue('redirectDelay', 5000),
        autoRedirectEnabled: getStoredValue('autoRedirect', true),
        darkModeEnabled: getStoredValue('darkModeEnabled', false),
        isBypassSession: getStoredValue('isBypassSession', false),
    };

    const isCurrentPageMediumDomain = () => window.location.hostname.endsWith('medium.com') || isCurrentPageMediumCustomDomain();
    const isCurrentPageMediumCustomDomain = () => MEDIUM_CUSTOM_DOMAINS.includes(window.location.hostname);

    let bypassServiceKeys = Object.keys(config.bypassUrls);
    let isCurrentlyRedirecting = false;

    const injectStyles = () => {
        const style = document.createElement('style');
        style.textContent = `
            .${SETTINGS_CLASS} {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                width: 360px;
                background-color: var(--background-color, white);
                box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
                border-radius: 16px;
                font-family: 'Arial', sans-serif;
                z-index: 10000;
                padding: 20px;
                display: none;
                color: var(--text-color, #333);
                cursor: grab;
                user-select: none;
                -webkit-user-select: none;
                -moz-user-select: none;
                -ms-user-user-select: none;
            }
            .${SETTINGS_CLASS}.dark {
                --background-color: #333;
                --text-color: white;
            }
            .medium-settings-header {
                font-size: 22px;
                font-weight: bold;
                margin-bottom: 20px;
                text-align: center;
            }
            .medium-settings-toggle {
                margin: 15px 0;
                display: flex;
                justify-content: space-between;
                align-items: center;
            }
            .medium-settings-toggle > span {
                flex-grow: 1;
            }
            .medium-settings-input {
                margin-left: 10px;
                padding: 8px 10px;
                border: 1px solid #ccc;
                border-radius: 8px;
                box-sizing: border-box;
            }
            .medium-settings-input#redirectDelay {
                width: 70px;
            }
            .medium-settings-input#bypassSelector {
                width: 120px;
                appearance: auto;
                -webkit-appearance: auto;
                -moz-appearance: auto;
                background-repeat: no-repeat;
                background-position: right 10px center;
            }
            .${SETTINGS_CLASS}.dark .medium-settings-input#bypassSelector {
                border-color: #666;
            }
            .medium-settings-button {
                background-color: var(--button-bg-color, #1a8917);
                color: var(--button-text-color, white);
                border: none;
                padding: 8px 14px;
                border-radius: 20px;
                cursor: pointer;
                font-weight: bold;
                transition: background-color 0.3s;
            }
            .medium-settings-button:hover {
                background-color: #155c11;
            }
            .${NOTIFICATION_CLASS} {
                position: fixed;
                bottom: 20px;
                right: 20px;
                background-color: #1a8917;
                color: white;
                padding: 15px;
                border-radius: 20px;
                box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
                font-family: 'Arial', sans-serif;
                z-index: 10000;
                opacity: 0;
                transform: translateY(20px);
                transition: all 0.3s ease;
            }
            .${NOTIFICATION_CLASS}.show {
                opacity: 1;
                transform: translateY(0);
            }
            .medium-settings-input:focus {
                outline: none;
                border-color: #1a8917;
                box-shadow: 0 0 5px rgba(26, 137, 23, 0.3);
            }

            .switch {
              position: relative;
              display: inline-block;
              width: 40px;
              height: 24px;
            }

            .switch input {
              opacity: 0;
              width: 0;
              height: 0;
            }

            .slider {
              position: absolute;
              cursor: pointer;
              top: 0;
              left: 0;
              right: 0;
              bottom: 0;
              background-color: #ccc;
              transition: .4s;
            }

            .slider:before {
              position: absolute;
              content: "";
              height: 16px;
              width: 16px;
              left: 4px;
              bottom: 4px;
              background-color: white;
              transition: .4s;
            }

            input:checked + .slider {
              background-color: #1a8917;
            }

            input:focus + .slider {
              box-shadow: 0 0 1px #1a8917;
            }

            input:checked + .slider:before {
              transform: translateX(16px);
            }

            .slider.round {
              border-radius: 34px;
            }

            .slider.round:before {
              border-radius: 50%;
            }
        `;
        document.head.appendChild(style);
    };

    const showStealthNotification = (message) => {
        const notification = document.createElement('div');
        notification.className = NOTIFICATION_CLASS;
        notification.textContent = message;
        document.body.appendChild(notification);

        setTimeout(() => notification.classList.add('show'), 50);
        setTimeout(() => {
            notification.classList.remove('show');
            setTimeout(() => notification.remove(), 300);
        }, 3000);
    };

    const getCurrentBypassServiceKey = () => {
        return bypassServiceKeys[config.currentBypassIndex % bypassServiceKeys.length];
    };

    const switchToNextBypassService = () => {
        config.currentBypassIndex++;
        setStoredValue('currentBypassIndex', config.currentBypassIndex);
        showStealthNotification(`Trying next bypass service: ${getCurrentBypassServiceKey()}`);
    };

    const checkServiceAvailability = async () => {
        const availabilityPromises = Object.entries(config.bypassUrls).map(async ([key, url]) => {
            try {
                const response = await fetch(url, { method: 'HEAD', mode: 'no-cors' });
                return { key, available: response.ok || response.type === 'opaque' };
            } catch (error) {
                console.error(`Service unavailable: ${key} - ${url}`, error);
                return { key, available: false };
            }
        });
        const results = await Promise.allSettled(availabilityPromises);
        return results.reduce((accumulator, result) => {
            if (result.status === 'fulfilled') {
                accumulator[result.value.key] = result.value.available;
            }
            return accumulator;
        }, {});
    };

    const attemptNextBypass = async (articleUrl, attemptNumber) => {
        switchToNextBypassService();
        const nextBypassServiceKey = getCurrentBypassServiceKey();
        if (nextBypassServiceKey) {
            attemptBypass(articleUrl, nextBypassServiceKey, attemptNumber + 1);
        } else {
            console.error("No more bypass services to try.");
            showStealthNotification("All bypass attempts failed.");
        }
    };

    const attemptBypass = async (articleUrl, bypassKey, attemptNumber = 1) => {
        const bypassUrlValue = config.bypassUrls[bypassKey];
        const serviceAvailability = await checkServiceAvailability();

        if (!serviceAvailability[bypassKey]) {
            showStealthNotification(`Service unavailable: ${bypassKey}`);
            return attemptNextBypass(articleUrl, attemptNumber);
        }

        try {
            let bypassUrl;
            const mediumURL = new URL(decodeURIComponent(articleUrl));
            let articlePathname = mediumURL.pathname;

            if (bypassKey === 'libmedium') {
                if (articlePathname.startsWith('/')) {
                    articlePathname = articlePathname.substring(1);
                }
                bypassUrl = `${bypassUrlValue}${articlePathname}`;
            } else if (bypassKey.startsWith('archive')) {
                bypassUrl = bypassUrlValue + articleUrl + '#bypass';
            } else {
                const bypassBaseURL = new URL(bypassUrlValue);
                bypassUrl = new URL(mediumURL.pathname, bypassBaseURL).href;
            }
            isCurrentlyRedirecting = true;
            window.location.href = bypassUrl;
        } catch (error) {
            console.error(`Error during bypass with ${bypassKey}:`, error);
            showStealthNotification(`Bypass failed with ${bypassKey}.`);
            attemptNextBypass(articleUrl, attemptNumber);
        }
    };

    const attachSettingsPanelListeners = (settingsContainer) => {
        settingsContainer.querySelector('#bypassSelector').addEventListener('change', (event) => {
            const selectedKey = event.target.value;
            config.currentBypassIndex = bypassServiceKeys.indexOf(selectedKey);
            setStoredValue('currentBypassIndex', config.currentBypassIndex);
            showStealthNotification(`Bypass service set to ${selectedKey}`);
        });

        settingsContainer.querySelector('#toggleRedirectCheckbox').addEventListener('change', () => {
            config.autoRedirectEnabled = settingsContainer.querySelector('#toggleRedirectCheckbox').checked;
            setStoredValue('autoRedirect', config.autoRedirectEnabled);
            showStealthNotification('Auto-Redirect toggled');
        });

        settingsContainer.querySelector('#toggleDarkModeCheckbox').addEventListener('change', () => {
            config.darkModeEnabled = settingsContainer.querySelector('#toggleDarkModeCheckbox').checked;
            setStoredValue('darkModeEnabled', config.darkModeEnabled);
            settingsContainer.classList.toggle('dark', config.darkModeEnabled);
            showStealthNotification('Dark Mode toggled');
        });

        settingsContainer.querySelector('#bypassNow').addEventListener('click', async () => {
            showStealthNotification('Attempting bypass...');
            const currentArticleUrl = encodeURIComponent(window.location.href);
            const selectedBypassService = getCurrentBypassServiceKey();
            setStoredValue('isBypassSession', true);
            await attemptBypass(currentArticleUrl, selectedBypassService);
        });

        settingsContainer.querySelector('#resetDefaults').addEventListener('click', () => {
            config.autoRedirectDelay = 5000;
            config.autoRedirectEnabled = true;
            config.darkModeEnabled = false;
            config.currentBypassIndex = 0;

            setStoredValue('redirectDelay', config.autoRedirectDelay);
            setStoredValue('autoRedirect', config.autoRedirectEnabled);
            setStoredValue('darkModeEnabled', config.darkModeEnabled);
            setStoredValue('currentBypassIndex', config.currentBypassIndex);

            settingsContainer.querySelector('#redirectDelay').value = config.autoRedirectDelay;
            settingsContainer.querySelector('#toggleRedirectCheckbox').checked = config.autoRedirectEnabled;
            settingsContainer.querySelector('#toggleDarkModeCheckbox').checked = config.darkModeEnabled;
            settingsContainer.querySelector('#bypassSelector').innerHTML = bypassServiceKeys.map((key, index) => `
                <option value="${key}" ${index === config.currentBypassIndex ? 'selected' : ''}>${key}</option>
            `).join('');
            settingsContainer.classList.remove('dark');
            showStealthNotification('Settings reset to defaults');
        });

        settingsContainer.querySelector('#saveSettings').addEventListener('click', () => {
            const newDelay = parseInt(settingsContainer.querySelector('#redirectDelay').value, 10);
            if (!isNaN(newDelay) && newDelay >= 0) {
                config.autoRedirectDelay = newDelay;
                setStoredValue('redirectDelay', newDelay);
                showStealthNotification('Settings saved');
            }
        });

        settingsContainer.querySelector('#closeSettings').addEventListener('click', () => {
            settingsContainer.style.display = 'none';
        });

        settingsContainer.querySelectorAll('.medium-settings-input').forEach(input => {
            input.addEventListener('mousedown', (event) => {
                event.preventDefault();
            });
        });
    };

    const showMediumSettingsPanel = () => {
        let existingPanel = document.querySelector(`.${SETTINGS_CLASS}`);
        if (existingPanel) {
            existingPanel.style.display = 'block';
            return;
        }

        const settingsContainer = document.createElement('div');
        settingsContainer.className = `${SETTINGS_CLASS} ${config.darkModeEnabled ? 'dark' : ''}`;
        settingsContainer.innerHTML = `
            <div class="medium-settings-header">Medium Settings</div>
            <div class="medium-settings-toggle">
                <span>Auto-Redirect</span>
                <label class="switch">
                    <input type="checkbox" id="toggleRedirectCheckbox" ${config.autoRedirectEnabled ? 'checked' : ''}>
                    <span class="slider round"></span>
                </label>
            </div>
            <div class="medium-settings-toggle">
                <span>Redirect Delay (ms)</span>
                <input type="number" class="medium-settings-input" id="redirectDelay" value="${config.autoRedirectDelay}" />
            </div>
            <div class="medium-settings-toggle">
                <span>Dark Mode</span>
                <label class="switch">
                    <input type="checkbox" id="toggleDarkModeCheckbox" ${config.darkModeEnabled ? 'checked' : ''}>
                    <span class="slider round"></span>
                </label>
            </div>
            <div class="medium-settings-toggle">
                <span>Bypass Service</span>
                <select id="bypassSelector" class="medium-settings-input">
                    ${bypassServiceKeys.map((key, index) => `
                        <option value="${key}" ${index === config.currentBypassIndex ? 'selected' : ''}>${key}</option>
                    `).join('')}
                </select>
            </div>
            <div class="medium-settings-toggle">
                <button class="medium-settings-button" id="bypassNow">Bypass Now</button>
            </div>
            <div class="medium-settings-toggle">
                <button class="medium-settings-button" id="resetDefaults">Reset to Default</button>
            </div>
            <div class="medium-settings-toggle">
                <button class="medium-settings-button" id="saveSettings">Save</button>
                <button class="medium-settings-button" id="closeSettings">Close</button>
            </div>
        `;

        attachSettingsPanelListeners(settingsContainer);

        let isDragging = false;
        let dragStartX, dragStartY;

        settingsContainer.addEventListener('mousedown', (event) => {
            isDragging = true;
            dragStartX = event.clientX - settingsContainer.offsetLeft;
            dragStartY = event.clientY - settingsContainer.offsetTop;
            settingsContainer.style.cursor = 'grabbing';
        });

        document.addEventListener('mousemove', (event) => {
            if (!isDragging) return;
            settingsContainer.style.left = `${event.clientX - dragStartX}px`;
            settingsContainer.style.top = `${event.clientY - dragStartY}px`;
        });

        document.addEventListener('mouseup', () => {
            isDragging = false;
            settingsContainer.style.cursor = 'grab';
        });

        document.body.appendChild(settingsContainer);
        settingsContainer.style.display = 'block';
    };

    const performAutoRedirect = async () => {
        isCurrentlyRedirecting = false;

        if (config.isBypassSession) {
            setStoredValue('isBypassSession', false);
            return;
        }

        if (config.autoRedirectEnabled && document.querySelector(MEMBER_WALL_CHECK_SELECTOR) && !isCurrentlyRedirecting) {
            const serviceAvailability = await checkServiceAvailability();
            let currentBypassKey = getCurrentBypassServiceKey();

            if (currentBypassKey && !serviceAvailability[currentBypassKey]) {
                showStealthNotification(`Current bypass service (${currentBypassKey}) is unavailable.`);
                switchToNextBypassService();
                const nextBypassKey = getCurrentBypassServiceKey();
                if (nextBypassKey) {
                    showStealthNotification(`Attempting bypass with ${nextBypassKey}...`);
                    setTimeout(async () => {
                        const currentArticleUrl = encodeURIComponent(window.location.href);
                        setStoredValue('isBypassSession', true);
                        await attemptBypass(currentArticleUrl, nextBypassKey);
                    }, config.autoRedirectDelay);
                } else {
                    showStealthNotification("No available bypass services to try.");
                }
                return;
            }

            if (currentBypassKey) {
                showStealthNotification(`Attempting bypass with ${currentBypassKey}...`);
                setTimeout(async () => {
                    const currentArticleUrl = encodeURIComponent(window.location.href);
                    setStoredValue('isBypassSession', true);
                    if (currentBypassKey.startsWith('archive')) {
                        removeBypassFragmentFromUrl();
                    }
                    await attemptBypass(currentArticleUrl, currentBypassKey);
                }, config.autoRedirectDelay);
            } else {
                showStealthNotification("No available bypass services to try.");
            }
        }
    };

    const removeBypassFragmentFromUrl = () => {
        const archiveDomains = ['archive.is', 'archive.li', 'archive.vn', 'archive.ph'];
        const currentDomain = window.location.hostname;

        if (archiveDomains.includes(currentDomain) && window.location.hash === '#bypass') {
            window.history.replaceState({}, document.title, window.location.pathname + window.location.search);
        }
    };

    const autoCloseFreediumBanner = () => {
        if (window.location.hostname === 'freedium.cfd') {
            window.addEventListener('load', () => {
                const closeButton = document.querySelector(FREEDIUM_CLOSE_BUTTON_SELECTOR);
                if (closeButton) {
                    closeButton.click();
                } else {
                    console.log('Freedium banner close button not found.');
                }
            });
        }
    };

    const initializeScript = () => {
        removeBypassFragmentFromUrl();
        injectStyles();
        autoCloseFreediumBanner();

        if (isCurrentPageMediumDomain()) {
            GM_registerMenuCommand('Open Medium Settings', showMediumSettingsPanel);
            performAutoRedirect();
        } else if (Object.values(config.bypassUrls).some((url) => window.location.href.startsWith(url) || bypassServiceKeys.some(key => key.startsWith('archive') && window.location.href.startsWith(config.bypassUrls[key])))) {
            isCurrentlyRedirecting = false;
        }
    };

    initializeScript();
})();

QingJ © 2025

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