Last active
August 29, 2024 22:06
-
-
Save OutRite/ea13495bbed51ab96e85d32100142d0e to your computer and use it in GitHub Desktop.
Feature Gate Key
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Feature Gate Key | |
// @namespace 534 | |
// @match https://bsky.app/* | |
// @match https://main.bsky.dev/* | |
// @match https://*.onrender.com/* | |
// @grant GM_getValue | |
// @grant GM_setValue | |
// @version 1.0 | |
// @author 534 | |
// @description Allows you to enable feature gates, giving you access to secret features on Bluesky (and other sites, probably). | |
// @run-at document-start | |
// ==/UserScript== | |
/* | |
After you flip the feature switches you want to use, refresh. | |
If this doesn't work and you're using uBlock origin, add the following rule: | |
* https://featuregates.org/v1/initialize * allow | |
You can get a list of some of the feature gate names here: | |
https://github.com/bluesky-social/social-app/blob/main/src/lib/statsig/gates.ts | |
*/ | |
// These are some known names of gates in main, to make FGK more convenient. | |
var presets = [ | |
'debug_show_feedcontext', | |
'new_user_guided_tour', | |
'onboarding_minimum_interests', | |
'session_withproxy_fix', | |
'show_follow_back_label_v2', | |
'suggested_feeds_interstitial', | |
'video_debug', | |
'videos' | |
]; | |
function djb2hash(txt) { | |
/* http://www.cse.yorku.ca/~oz/hash.html | |
unsigned long | |
hash(unsigned char *str) | |
{ | |
unsigned long hash = 5381; | |
int c; | |
while (c = *str++) | |
hash = ((hash << 5) + hash) + c; / * hash * 33 + c * / | |
return hash; | |
}*/ | |
hash = 0; // diff IV. idk why | |
for (i=0; i<txt.length; i++) { | |
// this is - instead of + in statsig. idk why | |
hash = ((hash << 5) - hash) + txt.charCodeAt(i); | |
hash = hash & 0xFFFFFFFF; // 32-bit | |
} | |
return hash>>>0; // this makes it unsigned | |
} | |
unsafeWindow.origfetch = unsafeWindow.fetch; | |
function loadfetch() { | |
unsafeWindow.fetch = async function(a, b){ | |
console.log("fetch "+a) | |
if (a == "https://featuregates.org/v1/initialize") { | |
console.log("fetching from featuregates.org") | |
// First, we send the request so that we can get a list of all feature gates. | |
var resp = await unsafeWindow.origfetch(a, b); | |
console.log("featuregates.org success, generating hijack") | |
var text = await resp.text(); | |
// Next, we switch all feature gates to true. | |
var gates = JSON.parse(text); | |
var kgates = Object.keys(gates.feature_gates); | |
GM_setValue("gates", kgates); | |
for (i=0;i<kgates.length;i++) { | |
if (GM_getValue(kgates[i]+"_default", 0)===0) { | |
GM_setValue(kgates[i]+"_default", gates.feature_gates[kgates[i]].value); | |
} | |
// Set feature gates based on config | |
gates.feature_gates[kgates[i]].value = GM_getValue(kgates[i], GM_getValue(kgates[i]+"_default", false)) | |
} | |
var puvkeys = Object.keys(gates.prefetched_user_values); | |
for (i=0;i<puvkeys.length;i++) { | |
for (j=0;j<kgates.length;j++) { | |
gates.prefetched_user_values[puvkeys[i]].feature_gates[kgates[j]].value = GM_getValue(kgates[j], GM_getValue(kgates[j]+"_default", false)) | |
} | |
} | |
realtext = JSON.stringify(gates); | |
if (!unsafeWindow.fgk_ready) { | |
unsafeWindow.fgk_ready = true; | |
var button = document.createElement("button"); | |
button.innerText = "Open Feature Gate Key"; | |
button.style.position = "fixed"; | |
button.style['z-index'] = "9999"; | |
document.body.appendChild(button); // we need to do this first to render the button | |
// now we can move it to the bottom left | |
button.style.top = (document.body.clientHeight-button.clientHeight) + "px"; | |
button.addEventListener('pointerup', function (e) { | |
genwindow(); | |
e.stopPropagation(); | |
}); | |
} | |
return new Promise(resolve => { | |
resolve({ | |
ok: true, | |
status: 200, | |
text: async function () { | |
return new Promise(resolve => { | |
resolve(realtext); | |
}) | |
} | |
}); | |
}) | |
} else { | |
return unsafeWindow.origfetch(a, b); | |
} | |
} | |
} | |
setInterval(loadfetch,1000) | |
function generate_gatetext(gate) { | |
var mp = document.createElement("p"); | |
mp.fgk_gateid = gate; | |
var gatename = GM_getValue(gate+"_name", gate); | |
var default_state = GM_getValue(gate+"_default", false); | |
var state = GM_getValue(gate, default_state); | |
if (state != default_state) { | |
// Italicize non-default states, to indicate when a gate has been changed from its default option. | |
var ital = document.createElement("u"); | |
ital.innerText = gatename.toString(); | |
mp.append(ital, ": "+state.toString()+" "); | |
} else { | |
mp.append(gatename, ": "+state.toString()+" "); | |
} | |
/*var reset_link = document.createElement("span"); | |
reset_link.style.color = "#dd1010"; | |
reset_link.innerText = "(reset)"; | |
if (state != default_state) { | |
mp.append(reset_link); | |
}*/ | |
mp.addEventListener('pointerdown', function (e) { | |
GM_setValue(gate, !state); | |
// We want to regenerate the text so we have the silly underline and the updated boolean. this is probably unnecessary but it's fine | |
mp.parentNode.replaceChild(generate_gatetext(gate), mp); | |
e.stopPropagation(); | |
}); | |
return mp; | |
} | |
function addgatename(gatename) { | |
var gateid = djb2hash(gatename); | |
GM_setValue(gateid+"_name", gatename); | |
} | |
function genwindow() { | |
// Generates the feature switcher window | |
var windiv = document.createElement('div'); | |
// height: 400px; width: 400px;background: #00000060; | |
windiv.style.height = "400px"; | |
windiv.style.width = "400px"; | |
windiv.style.left = "0px"; | |
windiv.style.top = "0px"; | |
windiv.style.background = "#00000060"; // black w/ 96 alpha | |
windiv.style.color = "#fff"; | |
windiv.style['z-index'] = 9999999; | |
windiv.style.position = "fixed"; | |
// Generate the toolbar | |
var toolbar = document.createElement("div"); | |
toolbar.style.display = "flex"; | |
toolbar.style['justify-content'] = "space-between"; | |
toolbar.style['align-items'] = "center"; | |
toolbar.style.background = "linear-gradient(270deg, #0000bb90, #00004490)"; | |
toolbar.style.color = "#fff"; | |
toolbar.addEventListener('pointerdown', function (e) { | |
toolbar.mspos = [e.clientX-toolbar.parentElement.offsetLeft, e.clientY-toolbar.parentElement.offsetTop]; | |
toolbar.setPointerCapture(e.pointerId); | |
}); | |
toolbar.addEventListener('pointerup', function (e) { | |
toolbar.releasePointerCapture(e.pointerId); | |
}) | |
toolbar.addEventListener('pointermove', function (e) { | |
if (e.buttons==1) { | |
toolbar.parentElement.style.left = e.clientX - toolbar.mspos[0] + "px"; | |
toolbar.parentElement.style.top = e.clientY - toolbar.mspos[1] + "px"; // we don't need toString because in js int+str -> str | |
} | |
}) | |
var tbp1 = document.createElement("p"); | |
tbp1.innerText = "Feature Gates" | |
var tbp2 = document.createElement("p"); | |
var span = document.createElement("span"); | |
span.style.background = "#f00"; | |
span.style.color = "#fff"; | |
span.style['border-radius'] = "12px"; | |
span.style.border = "3px solid #f00"; | |
span.style['border-right'] = "7px solid #f00"; | |
span.style['border-left'] = "7px solid #f00"; | |
span.innerText = "X"; | |
span.addEventListener('pointerdown', function (e) { | |
windiv.parentElement.removeChild(windiv); | |
e.stopPropagation(); | |
}); | |
tbp2.append(span, " ") | |
toolbar.append(tbp1, tbp2) | |
// Generating the primary window content, with the feature toggles | |
var gates = GM_getValue("gates", []); | |
var gatecontents = document.createElement("div"); | |
gatecontents.style.overflow = "scroll"; | |
gatecontents.style.height = "349px"; | |
var nameinp = document.createElement("input"); | |
nameinp.type = "text"; | |
var button = document.createElement("button"); | |
button.addEventListener('pointerdown', function (e) { | |
var gatename = nameinp.value; | |
addgatename(gatename); | |
}); | |
button.innerText = "Populate name" | |
gatecontents.append(nameinp,button) | |
for (i=0; i<gates.length; i++) { | |
var gate = gates[i]; | |
var gatep = generate_gatetext(gate); | |
gatecontents.append(gatep); | |
} | |
// Putting it together | |
windiv.append(toolbar, gatecontents); | |
document.body.append(windiv); | |
} | |
for (i=0;i<presets.length;i++) { | |
addgatename(presets[i]); | |
} | |
unsafeWindow.genwindow = genwindow; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment