Reddit Auth (*.reddit.com only)

Handle authentication for Reddit, and provide some helper functions for cookies and tokens.

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.greasyfork.org/scripts/575868/1812157/Reddit%20Auth%20%28%2Aredditcom%20only%29.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        Reddit Auth (*.reddit.com only)
// @namespace   https://greasyfork.org/en/users/1574555-littux
// @version     1.0.1
// @description Handle authentication for Reddit, and provide some helper functions for cookies and tokens.
// @author      littux
// @match       https://*.reddit.com/*
// @grant       GM_xmlhttpRequest
// @connect     www.reddit.com
// @icon        https://b.thumbs.redditmedia.com/7GVLmrH9CdZeqXceSEWkmL8_DSUKRGUfwMxnUNh8D8A.png
// @license     GPL-3.0-only
// @run-at      document-start
// ==/UserScript==

unsafeWindow.__littuxUserscripts__ ??= {};
const userScripts = unsafeWindow.__littuxUserscripts__;

const gmFetch = (options) => {
	return new Promise((resolve, reject) => {
		GM_xmlhttpRequest({
			...options,
			onload: (res) => resolve(res),
			onerror: (err) => reject(err)
		});
	});
}

/** "ping" a URL to get its cookies by pretending to fetch an image */
const pingURL = (url) => {
	return new Promise((resolve) => {
		const img = new Image();
		img.onload = () => resolve();
		img.onerror = () => resolve();
		img.src = url + (url.includes("?") ? "&" : "?") + "v=" + Date.now();
	});
}

const getCookie = async (name) => {
	if (typeof window.cookieStore?.get === "function") {
		return (await cookieStore.get(name))?.value ?? null;
	}
	const cookie = "; " + document.cookie + ";";
	const searchPattern = "; " + name + "=";
	const patternStartIndex = cookie.indexOf(searchPattern);
	if (patternStartIndex === -1) return null;

	const valueStartIndex = patternStartIndex + searchPattern.length;

	return decodeURIComponent(
		cookie.substring(
			valueStartIndex, cookie.indexOf(";", valueStartIndex)
		)
	);
};

const getCookiePing = async (name, pingSrc) => {
	let cookieData = await getCookie(name);
	if (!cookieData) {
		await pingURL(pingSrc);
		cookieData = await getCookie(name);
		if (!cookieData) throw new Error("Failed getting cookie '" + name + "' by fetching " + pingSrc);
	}
	return cookieData;
}


const _getToken = async () => {
	userScripts.loid ??= await getCookie("loid");
	userScripts.tokenKey ??= "rAPI:accessToken~"+userScripts.loid, userScripts.expiryKey ??= "rAPI:accessTokenExpiry~"+userScripts.loid; // Seperate tokens for different users

	let token = localStorage.getItem(userScripts.tokenKey);
	let expiry = Number(localStorage.getItem(userScripts.expiryKey));
	if (token && expiry > Date.now()) return token;

	try {
		const response = await gmFetch({
			anonymous: false,
			method: "POST",
			url: "https://www.reddit.com/svc/shreddit/token",
			headers: {
				"Content-Type": "application/json",
				"origin": "https://www.reddit.com" // 403 if this isn't included
			},
			data: JSON.stringify({ csrf_token: await getCookiePing("csrf_token", "https://sh.reddit.com/404") })
		});

		if (response.status === 200) {
			const data = JSON.parse(response.responseText);
			token = data.token;
			expiry = data.expires;
		} else {
			alert(`Error "${response.status}" while attempting to fetch authentication headers (redditNativeTranslations)`);
		}
	} catch (e) {
		console.debug("Authentication token fetch error:", e);
	}

	// Save to cache
	localStorage.setItem(userScripts.tokenKey, token);
	localStorage.setItem(userScripts.expiryKey, expiry.toString());
	console.log("Saved new auth token to cache.");
	return token;
};

const getToken = async () => {
	if (userScripts.pendingTokenPromise) return userScripts.pendingTokenPromise;
	return userScripts.pendingTokenPromise = _getToken().finally(() => {
		userScripts.pendingTokenPromise = null;
	});
};