Fedi.CrowdedGames.Group/JS/BlueskyAPI.js
2025-05-05 23:55:56 +00:00

288 lines
13 KiB
JavaScript

import * as Variables from "./Variables.js";
export async function CreatePost(DID, Text) {
let PDS = localStorage.getItem(Variables.BlueskyPDS);
let Json = {
"$type": "app.bsky.feed.post",
"text": Text,
"createdAt": new Date(Date.now()).toISOString()
}
let RequestBody = {
"repo": DID,
"collection": "app.bsky.feed.post",
"record": Json
}
let DPoP = await ClientDPoPPDS("POST", PDS + "/xrpc/com.atproto.repo.createRecord");
let request = fetch(PDS + "/xrpc/com.atproto.repo.createRecord", { body: JSON.stringify(RequestBody), method: "POST", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + localStorage.getItem(Variables.BlueskyAccessToken), "DPoP": DPoP}});
let body = await request.then((response) => response.json());
let status = await request.then((response) => response.status);
let header = await request.then((response) => response.headers.get("dpop-nonce"));
if (status == 401) {
if (body.message.includes("DPoP nonce mismatch")) {
await localStorage.setItem(Variables.BlueskyNonce, header);
}
if (body.message.includes("claim timestamp check failed")) {
await RefreshTokens();
}
body = await CreatePost(DID, Text);
}
return body;
}
export async function SetThreadGate(DID, Post, VisibilitySettings) {
let PDS = localStorage.getItem(Variables.BlueskyPDS);
let Json = {
"$type": "app.bsky.feed.threadgate",
"post": Post,
"allow": VisibilitySettings,
"createdAt": new Date(Date.now()).toISOString()
}
let RequestBody = {
"repo": DID,
"collection": "app.bsky.feed.threadgate",
"record": Json,
"rkey": Post.split("/")[Post.split("/").length - 1]
}
let DPoP = await ClientDPoPPDS("POST", PDS + "/xrpc/com.atproto.repo.createRecord");
let request = fetch(PDS + "/xrpc/com.atproto.repo.createRecord", { body: JSON.stringify(RequestBody), method: "POST", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + localStorage.getItem(Variables.BlueskyAccessToken), "DPoP": DPoP}});
let body = await request.then((response) => response.json());
let status = await request.then((response) => response.status);
let header = await request.then((response) => response.headers.get("dpop-nonce"));
if (status == 401) {
if (body.message.includes("DPoP nonce mismatch")) {
await localStorage.setItem(Variables.BlueskyNonce, header);
}
if (body.message.includes("claim timestamp check failed")) {
await RefreshTokens();
}
body = await SetThreadGate(DID, Post, VisibilitySettings);
}
return body;
}
// Component 1/4
export async function GetPDSWellKnown() {
return await fetch("https://bsky.social/.well-known/oauth-authorization-server", {method: "GET"})
.then((response) => response.json());
}
// Component 2/4
// Many thanks to https://github.com/tonyxu-io/pkce-generator. It was the base for this code.
export async function CreatePKCECodeVerifier() {
// Generate some Numbers
let Numbers = new Uint8Array(32);
crypto.getRandomValues(Numbers);
// Generate a random string of characters.
let CodeVerifier = "";
for (let i in Numbers) {
CodeVerifier += String.fromCharCode(Numbers[i]);
}
// Put this random string into Base64URL format.
CodeVerifier = btoa(CodeVerifier).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
return CodeVerifier;
}
export async function CreatePKCECodeChallenge(CodeVerifier) {
// Generate a code challenge with the code verifier.
// This is done by first SHA256 encrypting the CodeVerifier, then putting the outputted string into Base64URL format.
let CodeChallenge = btoa(await sha256(CodeVerifier)).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
return CodeChallenge;
}
// Component 3/4
export async function PARrequest(PAREndpoint, State, Challenge) {
return fetch(PAREndpoint, {method: "POST", body: new URLSearchParams({ response_type: "code", code_challenge_method: "S256", scope: "atproto transition:generic", client_id: "https://fedi.crowdedgames.group/oauth/client-metadata.json", redirect_uri: "https://fedi.crowdedgames.group/HTML/setting.html", code_challenge: Challenge, state: State, login_hint: "crowdedgames.group" }), headers: {"Content-Type": "application/x-www-form-urlencoded"}});
}
async function AuthRequest(TokenEndpoint, code, DPoP, Verify) {
return fetch(TokenEndpoint, {method: "POST", body: new URLSearchParams({ grant_type: "authorization_code", code: code, client_id: "https://fedi.crowdedgames.group/oauth/client-metadata.json", redirect_uri: "https://fedi.crowdedgames.group/HTML/setting.html", code_verifier: Verify}), headers: { "DPoP": DPoP, "Content-Type": "application/x-www-form-urlencoded"}})
.then((response) => response.json());
}
async function ReauthRequest(TokenEndpoint, Token, DPoP) {
return fetch(TokenEndpoint, {method: "POST", body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: Token, client_id: "https://fedi.crowdedgames.group/oauth/client-metadata.json"}), headers: { "DPoP": DPoP, "Content-Type": "application/x-www-form-urlencoded"}});
}
// Component 4/4
export async function ClientDPoPToken(POSTorGET, RequestURL) {
let PublicKey = await crypto.subtle.importKey("jwk", JSON.parse(localStorage.getItem(Variables.BlueskyPublicKey)), {name: "ECDSA", namedCurve: "P-256"}, true, ["verify"]);
let PrivateKey = await crypto.subtle.importKey("jwk", JSON.parse(localStorage.getItem(Variables.BlueskyPrivateKey)), {name: "ECDSA", namedCurve: "P-256"}, true, ["sign"]);
// Header
var Header = {typ: "dpop+jwt", alg: "ES256", jwk:
await crypto.subtle.exportKey("jwk", PublicKey)
.then(function(response) {
delete response["key_ops"];
delete response["ext"];
delete response["alg"];
return response})
};
// Payload
var Payload = {};
Payload.iss = "https://fedi.crowdedgames.group/oauth/client-metadata.json";
Payload.jti = crypto.randomUUID();
Payload.htm = POSTorGET;
Payload.htu = RequestURL;
Payload.iat = Math.floor(new Date(Date.now()).getTime() / 1000);
Payload.nonce = localStorage.getItem(Variables.BlueskyNonce);
var sHeader = JSON.stringify(Header);
var sPayload = JSON.stringify(Payload);
var JWT = KJUR.jws.JWS.sign("ES256", sHeader, sPayload,
await crypto.subtle.exportKey("jwk", PrivateKey)
.then(function(response) {
delete response["key_ops"];
delete response["ext"];
delete response["alg"];
return response})
);
return JWT;
}
export async function ClientDPoPPDS(POSTorGET, RequestURL) {
let PublicKey = await crypto.subtle.importKey("jwk", JSON.parse(localStorage.getItem(Variables.BlueskyPublicKey)), {name: "ECDSA", namedCurve: "P-256"}, true, ["verify"]);
let PrivateKey = await crypto.subtle.importKey("jwk", JSON.parse(localStorage.getItem(Variables.BlueskyPrivateKey)), {name: "ECDSA", namedCurve: "P-256"}, true, ["sign"]);
// Header
var Header = {typ: "dpop+jwt", alg: "ES256", jwk:
await crypto.subtle.exportKey("jwk", PublicKey)
.then(function(response) {
delete response["key_ops"];
delete response["ext"];
delete response["alg"];
return response})
};
// Payload
var Payload = {};
Payload.iss = "https://fedi.crowdedgames.group/oauth/client-metadata.json";
Payload.jti = crypto.randomUUID();
Payload.htm = POSTorGET;
Payload.htu = RequestURL;
Payload.iat = Math.floor(new Date(Date.now()).getTime() / 1000);
Payload.nonce = localStorage.getItem(Variables.BlueskyNonce);
Payload.ath = await CreatePKCECodeChallenge(localStorage.getItem(Variables.BlueskyAccessToken));
var sHeader = JSON.stringify(Header);
var sPayload = JSON.stringify(Payload);
var JWT = KJUR.jws.JWS.sign("ES256", sHeader, sPayload,
await crypto.subtle.exportKey("jwk", PrivateKey)
.then(function(response) {
delete response["key_ops"];
delete response["ext"];
delete response["alg"];
return response})
);
return JWT;
}
export async function HandleAuthorization(Website) {
// Quickly check to see if it has something before :// so it doesn't screw the link.
if (Website.toLowerCase().split("://").length > 1) {
Website = "https://" + Website.split("://")[1];
} else {
Website = "https://" + Website;
}
// Declare Variables
let KeyPair = await crypto.subtle.generateKey({name: "ECDSA", namedCurve: "P-256"}, true, ["sign", "verify"]);
let WellKnown = await GetPDSWellKnown();
let State = crypto.randomUUID();
let PKCEverifier = await CreatePKCECodeVerifier();
let PKCEchallenge = await CreatePKCECodeChallenge(PKCEverifier);
// Save these
localStorage.setItem(Variables.BlueskyPKCEVerifier, PKCEverifier);
localStorage.setItem(Variables.BlueskyPKCEChallenge, PKCEchallenge);
// PAR request (beginning)
let PAR = PARrequest(WellKnown.pushed_authorization_request_endpoint, State, PKCEchallenge);
let body = await PAR.then((response) => response.json());
let nonce = await PAR.then((response) => response.headers.get("dpop-nonce"));
// Save nonce
localStorage.setItem(Variables.BlueskyNonce, nonce);
// Export keys
let ExportedKey1 = await crypto.subtle.exportKey("jwk", KeyPair.publicKey);
let ExportedKey2 = await crypto.subtle.exportKey("jwk", KeyPair.privateKey);
// Convert them into a good format.
localStorage.setItem(Variables.BlueskyPublicKey, JSON.stringify(ExportedKey1));
localStorage.setItem(Variables.BlueskyPrivateKey, JSON.stringify(ExportedKey2));
// Now we need to authenticate. Make sure the State stays the same throughout this whole process :]
document.location.href = Website + "/oauth/authorize?client_id=https://fedi.crowdedgames.group/oauth/client-metadata.json&request_uri=" + body.request_uri;
}
export async function GainTokens() {
let WellKnown = await GetPDSWellKnown();
// Check to see if something's a miss...
if ((document.location.href.split("state=").length > 1 && document.location.href.split("iss=").length > 1 && document.location.href.split("code=").length > 1) && localStorage.getItem(Variables.BlueskyPKCEVerifier) != null && localStorage.getItem(Variables.BlueskyAccessToken) == null) {
// Create varaibles, be aware of waits because of internet.
let DPoP = await ClientDPoPToken("POST", WellKnown.token_endpoint);
let code = document.location.href.split("code=")[1];
// Authentication
let Var = await localStorage.getItem(Variables.BlueskyPKCEVerifier);
let Auth = await AuthRequest(WellKnown.token_endpoint, code, DPoP, Var);
// Save the tokens and be done
localStorage.setItem(Variables.BlueskyAccessToken, Auth.access_token);
localStorage.setItem(Variables.BlueskyRefreshToken, Auth.refresh_token);
// That long string just gets the payload
// aud = PDS server we are communicating with; sub = user DID
localStorage.setItem(Variables.BlueskyPDS, "https://" + KJUR.jws.JWS.readSafeJSONString(b64utoutf8(localStorage.getItem(Variables.BlueskyAccessToken).split(".")[1])).aud.split(":")[2]);
localStorage.setItem(Variables.BlueskyDID, KJUR.jws.JWS.readSafeJSONString(b64utoutf8(localStorage.getItem(Variables.BlueskyAccessToken).split(".")[1])).sub);
}
}
// Refreshing tokens is an integral part of auth.
export async function RefreshTokens() {
let WellKnown = await GetPDSWellKnown();
// Create varaibles, be aware of waits because of internet.
let DPoP = await ClientDPoPToken("POST", WellKnown.token_endpoint);
// Token refresh
let Var = await localStorage.getItem(Variables.BlueskyRefreshToken);
let Auth = ReauthRequest(WellKnown.token_endpoint, Var, DPoP);
let body = await Auth.then((response) => response.json());
let header = await Auth.then((response) => response.headers.get("dpop-nonce"));
if (body.hasOwnProperty("error_description") && body.error_description.includes("DPoP nonce mismatch")) {
localStorage.setItem(Variables.BlueskyNonce, header);
DPoP = await ClientDPoPToken("POST", WellKnown.token_endpoint);
body = await ReauthRequest(WellKnown.token_endpoint, Var, DPoP).then((response) => response.json());
}
// Save the tokens and be done
localStorage.setItem(Variables.BlueskyAccessToken, body.access_token);
localStorage.setItem(Variables.BlueskyRefreshToken, body.refresh_token);
}
// Stolen from elsewhere.
// Firefox snippet; Slightly edited.
async function sha256(message) {
// encode as UTF-8
const MessageBuffer = new TextEncoder().encode(message);
// hash the message
const HashBuffer = await crypto.subtle.digest('SHA-256', MessageBuffer);
// convert ArrayBuffer to Array
const HashArray = Array.from(new Uint8Array(HashBuffer));
// convert this hashArray to a string
let string = "";
for (let i in HashArray) {
string += String.fromCharCode(HashArray[i]);
}
return string;
}
// Firefox snippet.
function ab2str(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
// Firefox snippet.
function str2ab(str) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i);
}
return buf;
}