// ==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();
})();