Steam: Auto-submit Family View PIN

Remembers and automatically submits your Family View PIN

// ==UserScript==
// @name        Steam: Auto-submit Family View PIN
// @namespace   jycaplsilxrklkcsoamspgyiwilbprtd
// @description Remembers and automatically submits your Family View PIN
// @license     MIT License
// @match       https://steamcommunity.com/*
// @match       https://store.steampowered.com/*
// @version     1.2
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_deleteValue
// @run-at      document-start
// ==/UserScript==

/* jshint bitwise: true, curly: true, eqeqeq: true, esversion: 6,
funcscope: false, futurehostile: true, latedef: true, noarg: true,
nocomma: true, nonbsp: true, nonew: true, notypeof: false,
shadow: outer, singleGroups: true, strict: true, undef: true,
unused: true, varstmt: true, browser: true */
/* globals GM_getValue, GM_setValue, GM_deleteValue */

(function () {
	"use strict";

	// Double-check the hostname just so we're extra
	// sure we're not sending the PIN to other sites.
	// Greasemonkey can be tricked into running
	// scripts on different sites, like this:
	// http://example.org/?://steamcommunity.com/
	if (location.hostname !== "steamcommunity.com" && location.hostname !== "store.steampowered.com") {
		return;
	}


	// Start listening to the broadcast channel.
	let channelName = GM_getValue("channelName");

	// Create channel name if not yet generated
	if (!channelName) {
		const random = new Uint32Array(4);
		crypto.getRandomValues(random);

		channelName = random.reduce((acc, num) => acc + num.toString(36) + "-", "");

		GM_setValue("channelName", channelName);
	}

	// Add constant secret to further reduce the chance of collision
	channelName += "TtdrK77KvoOsXD6r";

	const abort = new AbortController();

	// Only reload after making sure we're on a page with a family PIN field
	let isFamilyViewPage = false;
	let doReloadLater = false;

	const channel = new BroadcastChannel(channelName);
	channel.onmessage = (event) => {
		if (event.data === "unlocked") {
			abort.abort();

			if (isFamilyViewPage) {
				location.reload();
			} else {
				doReloadLater = true;
			}
		}
	};


	window.addEventListener("DOMContentLoaded", () => {
		const form = document.getElementById("unlock_form");

		if (!form) {
			// No PIN field
			channel.close();
			return;
		}

		if (doReloadLater) {
			location.reload();
		} else {
			isFamilyViewPage = true;
		}


		let prefName = "pin";

		// Find account ID in the page
		const profileLink = document.querySelector("[data-miniprofile]");

		if (profileLink) {
			prefName += profileLink.dataset.miniprofile;
		}

		const pin = GM_getValue(prefName);


		if (!pin) {
			// We don't have a PIN so we can't auto-unlock yet.
			// Try to capture it for next time.
			const capturePIN = function () {
				GM_setValue(prefName, String(form.elements.pin.value));
			};

			form.addEventListener("submit", capturePIN, { capture: true, passive: true });

			if (form.elements.submit_btn) {
				form.elements.submit_btn.addEventListener("click", capturePIN, { capture: true, passive: true });
			}
		} else {
			const postData = new FormData();

			// Get session ID from cookies
			const sid = document.cookie.match(/[; ]sessionid=([^\s;]*)/);
			postData.set("pin", pin);
			postData.set("sessionid", sid[1]);

			// Send unlock request
			fetch("/parental/ajaxunlock", {
				method: "POST",
				mode: "same-origin",
				referrerPolicy: "strict-origin",
				credentials: "include",
				headers: { "Accept": "application/json" },
				body: postData,
				keepalive: true,
				signal: abort.signal
			})
				.then((response) => response.json())
				.then((json) => {
				if (json.success) {
					channel.postMessage("unlocked");
					location.reload();
				} else if ("success" in json && json.eresult === 15) {
					// We got an explicit failure reply.
					// success is false and eresult is 15 (AccessDenied).
					// Assume the PIN has changed and delete it.
					GM_deleteValue(prefName);
				}
			});
		}
	}, true);

})();