// ==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 // @downloadURL none // ==/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) => ` `).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 = `
Medium Settings
Auto-Redirect
Redirect Delay (ms)
Dark Mode
Bypass Service
`; 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(); })();