adds a "Random Location" option to the ControlD service redirect destinations picker
// ==UserScript==
// @name ControlD Random Services Location
// @namespace https://spin.rip/
// @version 1.1.1
// @description adds a "Random Location" option to the ControlD service redirect destinations picker
// @author spin
// @match https://controld.com/*
// @grant none
// @run-at document-idle
// @icon https://www.google.com/s2/favicons?sz=64&domain=controld.com
// @license GPL-3.0-only
// ==/UserScript==
(function () {
'use strict';
const RANDOM_VIA = '?';
const RANDOM_LABEL = 'Random Location';
const RANDOM_TEST_ID = 'services-country-RandomLocation';
const RANDOM_DEFAULT_TEST_ID = 'default-redirect-country-RandomLocation';
// controld's own svg for the random/redirect icon, scaled to 16x16 to match flag size
const RANDOM_SVG = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="display:block;">
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.834 10C22.882 4.325 17.946 0 12 0 5.373 0 0 5.373 0 12c0 3.073 1.155 5.877 3.056 8 .445.497.931.958 1.453 1.375A11.96 11.96 0 0 0 9 23.622V11l.02-.04c.044-1.157.162-2.25.337-3.252C10.207 7.9 11.092 8 12 8c.908 0 1.793-.1 2.643-.292.126.72.222 1.488.283 2.292h2.005a25.585 25.585 0 0 0-.365-2.899 11.99 11.99 0 0 0 2.925-1.726A9.969 9.969 0 0 1 21.8 10h2.034zm-16.4 6.9a11.991 11.991 0 0 0-2.925 1.725A9.96 9.96 0 0 1 2.049 13h4.968c.048 1.379.192 2.692.417 3.9zM6 20a9.997 9.997 0 0 1 1.903-1.124c.294 1.01.652 1.905 1.059 2.654A9.97 9.97 0 0 1 5.999 20zM12 6c.756 0 1.492-.084 2.2-.242a13.568 13.568 0 0 0-.51-1.474c-.393-.941-.809-1.572-1.169-1.937a1.533 1.533 0 0 0-.395-.308A.286.286 0 0 0 12 2c-.01 0-.048 0-.126.039-.086.042-.221.13-.395.308-.36.365-.776.996-1.168 1.937-.186.445-.357.938-.51 1.474C10.507 5.916 11.243 6 12 6zm3.039-3.53c.407.749.765 1.645 1.06 2.655A9.993 9.993 0 0 0 18 4a9.969 9.969 0 0 0-2.962-1.53zm-6.078 0c-.407.75-.765 1.645-1.06 2.655A9.994 9.994 0 0 1 6 4 9.97 9.97 0 0 1 8.96 2.47zM7.017 11c.048-1.379.192-2.692.417-3.899A11.992 11.992 0 0 1 4.51 5.375 9.96 9.96 0 0 0 2.049 11h4.968z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 12a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-8a2 2 0 0 0-2-2h-9zm7.942 1.058a.625.625 0 0 0-.884.884l.458.458c-1.338.14-2.56.85-3.342 1.964l-.384.548-.627-.892A3.865 3.865 0 0 0 13 14.375a.625.625 0 1 0 0 1.25c.852 0 1.65.415 2.14 1.113L16.026 18l-.886 1.262A2.615 2.615 0 0 1 13 20.375a.625.625 0 1 0 0 1.25c1.26 0 2.44-.614 3.163-1.645l.627-.892.384.548a4.675 4.675 0 0 0 3.342 1.964l-.458.458a.625.625 0 0 0 .884.884L22.884 21l-1.942-1.942a.625.625 0 0 0-.884.884l.388.388a3.425 3.425 0 0 1-2.249-1.412L17.553 18l.644-.918a3.425 3.425 0 0 1 2.249-1.412l-.388.388a.625.625 0 0 0 .884.884L22.884 15l-1.942-1.942z" fill="currentColor"/>
</svg>`;
// checkmark svg matching controld's style
const CHECKMARK_SVG = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="color: rgb(29, 191, 115);">
<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
</svg>`;
let observer = null;
// figure out which type of modal we're looking at
function detectModalContext(scrollList) {
if (scrollList.querySelector('button[data-testid^="services-country-"]')) {
return 'services';
}
if (scrollList.querySelector('button[data-testid^="default-redirect-country-"]')) {
return 'default';
}
return null;
}
function getCountryBtnSelector(context) {
if (context === 'default') return 'button[data-testid^="default-redirect-country-"]';
return 'button[data-testid^="services-country-"]';
}
function getRandomTestId(context) {
if (context === 'default') return RANDOM_DEFAULT_TEST_ID;
return RANDOM_TEST_ID;
}
// watches for the destinations modal to appear and injects the random option
function startObserver() {
if (observer) return;
observer = new MutationObserver(() => {
const modal = document.querySelector('.modal-dialog');
if (!modal) return;
const scrollList = modal.querySelector('.show-scrollbar');
if (!scrollList) return;
const context = detectModalContext(scrollList);
if (!context) return;
const selector = getCountryBtnSelector(context);
const randomTestId = getRandomTestId(context);
if (scrollList.querySelector(`[data-testid="${randomTestId}"]`)) return;
const allCountryBtns = scrollList.querySelectorAll(selector);
if (!allCountryBtns.length) return;
// prefer cloning a non-selected button so we don't inherit active/expanded
// styles (green text, missing chevron, etc). the selected button often has a
// different css class than the rest.
const firstBtn = allCountryBtns[0];
let referenceBtn = firstBtn;
if (allCountryBtns.length > 1) {
const firstClass = firstBtn.className;
const secondClass = allCountryBtns[1].className;
if (firstClass !== secondClass) {
referenceBtn = allCountryBtns[1];
}
}
injectRandomOption(scrollList, referenceBtn, firstBtn, context);
});
observer.observe(document.body, { childList: true, subtree: true });
}
function injectRandomOption(scrollList, referenceBtn, firstBtn, context) {
const randomTestId = getRandomTestId(context);
// clone a real button to inherit all css classes and structure
const clone = referenceBtn.cloneNode(true);
clone.setAttribute('data-testid', randomTestId);
clone.setAttribute('aria-label', `select ${RANDOM_LABEL}`);
// swap the flag <img> for the controld random svg icon
const img = clone.querySelector('img');
if (img) {
const iconWrapper = document.createElement('span');
iconWrapper.innerHTML = RANDOM_SVG;
// match the native flag color
iconWrapper.style.cssText = 'color: rgba(232, 239, 255, 0.6); display: flex; align-items: center;';
img.replaceWith(iconWrapper);
}
// remove dropdown chevron (img on services page, svg on profile options page)
clone.querySelectorAll('img').forEach(el => el.remove());
const chevronSvg = clone.querySelector('[data-testid="proxy-country-close"]');
if (chevronSvg) chevronSvg.remove();
// replace the country name text
const nameSpan = clone.querySelector('span[aria-label*="show tooltip"]');
if (nameSpan) {
nameSpan.textContent = RANDOM_LABEL;
nameSpan.setAttribute('aria-label', `show tooltip: ${RANDOM_LABEL}`);
}
// update the proxy-country data-testid on the inner div
const proxyDiv = clone.querySelector('[data-testid^="proxy-country-"]');
if (proxyDiv) {
proxyDiv.setAttribute('data-testid', 'proxy-country-RandomLocation');
}
// remove any existing checkmark
const checkmark = clone.querySelector('.right-svg');
if (checkmark) checkmark.remove();
const container = firstBtn.parentElement;
// deep clone to strip inherited react event handlers
const freshClone = clone.cloneNode(true);
freshClone.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
handleRandomSelect(freshClone, context);
});
container.insertBefore(freshClone, container.firstChild);
// the native list uses border-top on every item except the first to create
// separators. since we inserted above the first item, that item (now second)
// needs a border-top to maintain the separator pattern.
const firstBtnProxyDiv = firstBtn.querySelector('[data-testid^="proxy-country-"]');
if (firstBtnProxyDiv) {
firstBtnProxyDiv.style.borderTop = '1px solid var(--theme-ui-colors-white15, rgba(255, 255, 255, 0.15))';
}
// show checkmark if already set to random
checkCurrentVia(freshClone, context);
}
function getApiUrl(context) {
const profileId = getProfileId();
if (!profileId) return null;
if (context === 'default') {
return `https://api.controld.com/profiles/${profileId}/default`;
}
const serviceName = getServiceName();
if (!serviceName) return null;
return `https://api.controld.com/profiles/${profileId}/services/${serviceName}`;
}
async function checkCurrentVia(btnElement, context) {
const url = getApiUrl(context);
if (!url) return;
const token = getSessionToken();
if (!token) return;
try {
const resp = await fetch(url, {
headers: { 'Authorization': 'Bearer ' + token }
});
const data = await resp.json();
let isRandom = false;
if (context === 'default') {
isRandom = data?.body?.default?.via === RANDOM_VIA;
} else if (data?.body?.services) {
const serviceName = getServiceName();
isRandom = data.body.services.some(s => {
const pk = s.PK || '';
const name = (typeof s.name === 'string' ? s.name : '').toLowerCase();
const via = s.via || s.action?.via || '';
return (pk === serviceName || name === serviceName) && via === RANDOM_VIA;
});
}
if (isRandom) {
addCheckmark(btnElement);
}
} catch (err) {
// silently fail
}
}
function addCheckmark(btnElement) {
// avoid duplicates
if (btnElement.querySelector('.right-svg')) return;
// find the inner row div (same level as the flag+name container)
const proxyDiv = btnElement.querySelector('[data-testid^="proxy-country-"]');
if (!proxyDiv) return;
const check = document.createElement('div');
check.className = 'right-svg';
check.innerHTML = CHECKMARK_SVG;
proxyDiv.appendChild(check);
}
async function handleRandomSelect(btnElement, context) {
const url = getApiUrl(context);
if (!url) {
console.warn('[ControlD Random] could not determine api url from context');
return;
}
const token = getSessionToken();
if (!token) {
console.warn('[ControlD Random] no session token found');
return;
}
try {
const resp = await fetch(url, {
method: 'PUT',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json'
},
body: JSON.stringify({ status: 1, do: 3, via: RANDOM_VIA })
});
const data = await resp.json();
if (data.success) {
// remove checkmarks from all other items in the list
const modal = document.querySelector('.modal-dialog');
if (modal) {
modal.querySelectorAll('.right-svg').forEach(el => el.remove());
}
// add checkmark to our button
addCheckmark(btnElement);
// close modal
const closeBtn = document.querySelector(
'.modal-dialog button[data-testid="dialog-close-button"], ' +
'.modal-dialog button[data-testid="close-button"], ' +
'.modal-dialog button[aria-label*="Close"], ' +
'.modal-dialog button[aria-label*="close"]'
);
if (closeBtn) closeBtn.click();
// reload to sync the ui state
setTimeout(() => location.reload(), 300);
} else {
console.error('[ControlD Random] api error:', data.error?.message);
}
} catch (err) {
console.error('[ControlD Random] request failed:', err);
}
}
function getProfileId() {
const urlMatch = window.location.pathname.match(/profiles\/([^/]+)/);
return urlMatch?.[1] || null;
}
function getServiceName() {
let serviceName = window.__controld_random_serviceName || null;
if (!serviceName) {
serviceName = findServiceFromReact();
}
return serviceName;
}
function findServiceFromReact() {
const modal = document.querySelector('.modal-dialog');
if (!modal) return null;
const fiberKey = Object.keys(modal).find(k =>
k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$')
);
if (!fiberKey) return null;
let fiber = modal[fiberKey];
let depth = 0;
while (fiber && depth < 50) {
const props = fiber.memoizedProps || fiber.pendingProps;
if (props) {
if (props.servicePK || props.service?.PK || props.serviceId) {
return props.servicePK || props.service?.PK || props.serviceId;
}
if (typeof props.serviceName === 'string') {
return props.serviceName;
}
}
let state = fiber.memoizedState;
let stateDepth = 0;
while (state && stateDepth < 10) {
if (state.memoizedState && typeof state.memoizedState === 'object') {
const s = state.memoizedState;
if (s.servicePK || s.PK) return s.servicePK || s.PK;
}
state = state.next;
stateDepth++;
}
fiber = fiber.return;
depth++;
}
return null;
}
function getSessionToken() {
try {
const session = JSON.parse(localStorage.getItem('persist:session') || '{}');
return JSON.parse(session.sessionToken || '""') || null;
} catch {
return null;
}
}
// capture which service card was clicked before the modal opens
function interceptGlobeClicks() {
document.addEventListener('click', (e) => {
const btn = e.target.closest(
'button[data-testid^="proxy-list-button"], button[aria-label*="Open Proxy List"]'
);
if (btn) {
const card = btn.closest('[data-testid^="service-list-item-"]');
if (card) {
const name = card.getAttribute('data-testid').replace('service-list-item-', '');
if (name) window.__controld_random_serviceName = name;
}
}
}, true);
}
// also capture service name from fetch calls the app makes when opening the modal
function interceptServiceFetch() {
const origFetch = window.fetch;
window.fetch = function (...args) {
const url = typeof args[0] === 'string' ? args[0] : args[0]?.url;
if (url) {
const match = url.match(/\/profiles\/[^/]+\/services\/([^/?]+)$/);
if (match && match[1] !== 'categories') {
window.__controld_random_serviceName = match[1];
}
}
return origFetch.apply(this, args);
};
}
// handle spa navigation by watching for url changes
function watchNavigation() {
// patch pushState and replaceState so we can react to route changes
const origPush = history.pushState;
const origReplace = history.replaceState;
history.pushState = function () {
origPush.apply(this, arguments);
window.dispatchEvent(new Event('controld-nav'));
};
history.replaceState = function () {
origReplace.apply(this, arguments);
window.dispatchEvent(new Event('controld-nav'));
};
// also catch back/forward
window.addEventListener('popstate', () => {
window.dispatchEvent(new Event('controld-nav'));
});
// on any nav event, make sure the observer is running
window.addEventListener('controld-nav', () => {
// clear stale service name when navigating away
if (!window.location.pathname.includes('/services')) {
window.__controld_random_serviceName = null;
}
// (re-)start observer just in case
startObserver();
});
}
// init
interceptServiceFetch();
interceptGlobeClicks();
watchNavigation();
startObserver();
})();