Skip to content

Instantly share code, notes, and snippets.

@OutRite
Last active August 29, 2024 22:06
Show Gist options
  • Save OutRite/ea13495bbed51ab96e85d32100142d0e to your computer and use it in GitHub Desktop.
Save OutRite/ea13495bbed51ab96e85d32100142d0e to your computer and use it in GitHub Desktop.
Feature Gate Key
// ==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