- Loads of bug fixes. - CSS improvements. - Tags added. - Optimized some things. - Some other oddities? I don't know :3 - Happiness increased by 2%.
637 lines
25 KiB
JavaScript
637 lines
25 KiB
JavaScript
import * as Variables from "./Variables.js";
|
|
|
|
let Token = localStorage.getItem(Variables.BlueskyAccessToken);
|
|
if (Token == null) {
|
|
console.error("No Bluesky access token!");
|
|
}
|
|
|
|
// Getters
|
|
// This gets the timeline. The cursor is a time in Z form.
|
|
export async function GetTimeline(Cursor) {
|
|
if (Token == null) {
|
|
return "";
|
|
}
|
|
let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/app.bsky.feed.getTimeline";
|
|
if (Cursor != "") {
|
|
FetchThing += "?cursor=" + Cursor;
|
|
}
|
|
let DPoP = await ClientDPoPPDS("GET", FetchThing);
|
|
let request = fetch(FetchThing, {method: "GET", headers: {"Authorization": "DPoP " + Token, "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) {
|
|
await HandleError(body, header);
|
|
body = await GetTimeline(Cursor);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
// Gets a "public" timeline (essentially a feed by bsky.app where you can discover posts).
|
|
export async function GetPublicTimeline(Cursor) {
|
|
if (Token == null) {
|
|
return "";
|
|
}
|
|
let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/app.bsky.feed.getFeed?feed=at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot";
|
|
if (Cursor != "") {
|
|
FetchThing += "&cursor=" + Cursor;
|
|
}
|
|
let DPoP = await ClientDPoPPDS("GET", FetchThing);
|
|
let request = fetch(FetchThing, {method: "GET", headers: {"Authorization": "DPoP " + Token, "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) {
|
|
await HandleError(body, header);
|
|
body = await GetPublicTimeline(Cursor);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
export async function GetProfile(DID) {
|
|
if (Token == null) {
|
|
return "";
|
|
}
|
|
let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/app.bsky.actor.getProfile?actor=" + DID;
|
|
let DPoP = await ClientDPoPPDS("GET", FetchThing);
|
|
let request = fetch(FetchThing, { method: "GET", headers: {"Authorization": "DPoP " + Token, "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) {
|
|
await HandleError(body, header);
|
|
body = await GetProfile(DID);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
export async function GetProfileFeed(DID) {
|
|
if (Token == null) {
|
|
return "";
|
|
}
|
|
let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/app.bsky.feed.getAuthorFeed?actor=" + DID;
|
|
let DPoP = await ClientDPoPPDS("GET", FetchThing);
|
|
let request = fetch(FetchThing, { method: "GET", headers: {"Authorization": "DPoP " + Token, "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) {
|
|
await HandleError(body, header);
|
|
body = await GetProfileFeed(DID);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
// This gets the post. If there are multiple URIs, they must be within an array.
|
|
export async function GetPosts(URIs) {
|
|
if (Token == null) {
|
|
return "";
|
|
}
|
|
let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/app.bsky.feed.getPosts?uris=" + URIs;
|
|
let DPoP = await ClientDPoPPDS("GET", FetchThing);
|
|
let request = fetch(FetchThing, { method: "GET", headers: {"Authorization": "DPoP " + Token, "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) {
|
|
await HandleError(body, header);
|
|
body = await GetPosts(URIs);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
// Get a blob (like an image or video). Authentication need not apply.
|
|
export async function GetBlob(DID, CID) {
|
|
let request = fetch("https://bsky.social/xrpc/com.atproto.sync.getBlob?did=" + DID + "&cid=" + CID, {method: "GET"});
|
|
let body = await request.then((response) => response.blob());
|
|
return body;
|
|
}
|
|
|
|
// Gets a record. The repo is the account DID, the collection is the type of record, and the key is the little bit at the end.
|
|
export async function GetRecord(Repo, Collection, RKey) {
|
|
if (Token == null) {
|
|
return "";
|
|
}
|
|
let RequestBody = {
|
|
"repo": Repo,
|
|
"collection": Collection,
|
|
"rkey": RKey
|
|
}
|
|
let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.getRecord?repo=" + Repo + "&collection=" + Collection + "&rkey=" + RKey;
|
|
let DPoP = await ClientDPoPPDS("GET", FetchThing);
|
|
let request = fetch(FetchThing, { method: "GET", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + Token, "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) {
|
|
await HandleError(body, header);
|
|
body = await GetRecord(Repo, Collection, RKey);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
// Creators
|
|
// This creates a post. Requires the DID of the account and the Text for the record.
|
|
export async function CreatePost(DID, Text, ContentWarning = undefined, Tags = [], ReplyID = undefined, RootID = undefined) {
|
|
if (Token == null) {
|
|
return "";
|
|
}
|
|
let Record = {
|
|
"$type": "app.bsky.feed.post",
|
|
"text": Text,
|
|
"createdAt": new Date(Date.now()).toISOString()
|
|
};
|
|
// Content warning stuff.
|
|
if (ContentWarning != undefined) {
|
|
Record.labels = {
|
|
"$type": "com.atproto.label.defs#selfLabels",
|
|
"values": [{
|
|
"val": ContentWarning
|
|
}]
|
|
};
|
|
}
|
|
// ReplyID and RootID simultaniously.
|
|
if (ReplyID != undefined && RootID != undefined) {
|
|
Record.reply = {
|
|
"parent": {
|
|
"uri": ReplyID.uri,
|
|
"cid": ReplyID.cid
|
|
},
|
|
"root": {
|
|
"uri": RootID.uri,
|
|
"cid": RootID.cid
|
|
}
|
|
};
|
|
}
|
|
// Tags
|
|
if (Tags.length != 0) {
|
|
Record.tags = Tags;
|
|
}
|
|
let body = await CreateRecord(DID, "app.bsky.feed.post", Record, undefined);
|
|
if (body.hasOwnProperty("error") && body.error == "InvalidRequest") {
|
|
let matches = body.message.match(/(\d+)/);
|
|
Record.text = Text.slice(0, matches[0] - 1);
|
|
body = await CreateRecord(DID, "app.bsky.feed.post", Record, undefined);
|
|
if (ReplyID == undefined && RootID == undefined) {
|
|
await CreatePost(DID, Text.slice(matches[0] - 1, Text.length), ContentWarning, Tags, body, body);
|
|
} else {
|
|
await CreatePost(DID, Text.slice(matches[0] - 1, Text.length), ContentWarning, Tags, body, RootID);
|
|
}
|
|
}
|
|
return body;
|
|
}
|
|
|
|
// Creates a Thread Gate for who can reply. Requires the account DID, a post, and a list of who is allowed.
|
|
export async function CreateThreadGate(DID, Post, VisibilitySettings) {
|
|
if (Token == null) {
|
|
return "";
|
|
}
|
|
let Record = {
|
|
"$type": "app.bsky.feed.threadgate",
|
|
"post": Post,
|
|
"allow": VisibilitySettings,
|
|
"createdAt": new Date(Date.now()).toISOString()
|
|
}
|
|
let body = CreateRecord(DID, "app.bsky.feed.threadgate", Record, undefined);
|
|
return body;
|
|
}
|
|
|
|
// Create a like and send it to the server.
|
|
export async function CreateLike(DID, RefURI, RefCID) {
|
|
if (Token == null) {
|
|
return "";
|
|
}
|
|
let StrongRef = {
|
|
"$type": "com.atproto.repo.strongRef",
|
|
"uri": RefURI,
|
|
"cid": RefCID
|
|
}
|
|
let Record = {
|
|
"$type": "app.bsky.feed.like",
|
|
"subject": StrongRef,
|
|
"createdAt": new Date(Date.now()).toISOString()
|
|
}
|
|
let body = await GetRecord(DID, "app.bsky.feed.like", RefURI.split("/")[RefURI.split("/").length - 1]);
|
|
// We might have a record. If we do, delete the thing. If we don't, create the thing.
|
|
if (body.hasOwnProperty("error") && body.error == "RecordNotFound") {
|
|
body = await CreateRecord(DID, "app.bsky.feed.like", Record, RefURI.split("/")[RefURI.split("/").length - 1]);
|
|
} else {
|
|
body = await DeleteRecord(DID, "app.bsky.feed.like", RefURI.split("/")[RefURI.split("/").length - 1]);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
export async function CreateFollow(DID, SubjectDID) {
|
|
if (Token == null) {
|
|
return "";
|
|
}
|
|
let Record = {
|
|
"$type": "app.bsky.graph.follow",
|
|
"subject": SubjectDID,
|
|
"createdAt": new Date(Date.now()).toISOString()
|
|
}
|
|
let body = await GetRecord(DID, "app.bsky.graph.follow", SubjectDID);
|
|
// We might have a record. If we do, delete the thing. If we don't, create the thing.
|
|
if (body.hasOwnProperty("error") && body.error == "RecordNotFound") {
|
|
body = await CreateRecord(DID, "app.bsky.graph.follow", Record, SubjectDID);
|
|
} else {
|
|
body = await DeleteRecord(DID, "app.bsky.graph.follow", SubjectDID);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
export async function CreateBlock(DID, SubjectDID) {
|
|
if (Token == null) {
|
|
return "";
|
|
}
|
|
let Record = {
|
|
"$type": "app.bsky.graph.block",
|
|
"subject": SubjectDID,
|
|
"createdAt": new Date(Date.now()).toISOString()
|
|
}
|
|
let body = await GetRecord(DID, "app.bsky.graph.block", SubjectDID);
|
|
// We might have a record. If we do, delete the thing. If we don't, create the thing.
|
|
if (body.hasOwnProperty("error") && body.error == "RecordNotFound") {
|
|
body = await CreateRecord(DID, "app.bsky.graph.block", Record, SubjectDID);
|
|
} else {
|
|
body = await DeleteRecord(DID, "app.bsky.graph.block", SubjectDID);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
// Create a repost and send it to the server.
|
|
export async function CreateRepost(DID, RefURI, RefCID) {
|
|
if (localStorage.getItem(Variables.BlueskyAccessToken) == null) {
|
|
console.log("No access token!");
|
|
return "";
|
|
}
|
|
let StrongRef = {
|
|
"$type": "com.atproto.repo.strongRef",
|
|
"uri": RefURI,
|
|
"cid": RefCID
|
|
}
|
|
let Record = {
|
|
"$type": "app.bsky.feed.repost",
|
|
"subject": StrongRef,
|
|
"createdAt": new Date(Date.now()).toISOString()
|
|
}
|
|
let body = await GetRecord(DID, "app.bsky.feed.repost", RefURI.split("/")[RefURI.split("/").length - 1]);
|
|
// We might have a record. If we do, delete the thing. If we don't, create the thing.
|
|
if (body.hasOwnProperty("error") && body.error == "RecordNotFound") {
|
|
body = await CreateRecord(DID, "app.bsky.feed.repost", Record, RefURI.split("/")[RefURI.split("/").length - 1]);
|
|
} else {
|
|
body = await DeleteRecord(DID, "app.bsky.feed.repost", RefURI.split("/")[RefURI.split("/").length - 1]);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
// Put record updates a record.
|
|
export async function PutRecord(Repo, Collection, Record, RKey) {
|
|
if (Token == null) {
|
|
return "";
|
|
}
|
|
let RequestBody = {
|
|
"repo": Repo,
|
|
"collection": Collection,
|
|
"record": Record,
|
|
"rkey": RKey
|
|
}
|
|
let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.putRecord";
|
|
let DPoP = await ClientDPoPPDS("POST", FetchThing);
|
|
let request = fetch(FetchThing, { body: JSON.stringify(RequestBody), method: "POST", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + Token, "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) {
|
|
await HandleError(body, header);
|
|
body = await PutRecord(Repo, Collection, Record, RKey);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
// Creates a record. Universal way of making things.
|
|
export async function CreateRecord(Repo, Collection, Record, RKey) {
|
|
if (Token == null) {
|
|
return "";
|
|
}
|
|
let RequestBody = {
|
|
"repo": Repo,
|
|
"collection": Collection,
|
|
"record": Record
|
|
}
|
|
if (RKey != undefined) {
|
|
RequestBody.rkey = RKey;
|
|
}
|
|
let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.createRecord";
|
|
let DPoP = await ClientDPoPPDS("POST", FetchThing);
|
|
let request = fetch(FetchThing, { body: JSON.stringify(RequestBody), method: "POST", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + Token, "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) {
|
|
await HandleError(body, header);
|
|
body = await CreateRecord(Repo, Collection, Record, RKey);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
// Applyers
|
|
// Currently this only applies to the link Facets.
|
|
export function ApplyFacets(record, text) {
|
|
let StringArray = [];
|
|
let SplitAreas = [0];
|
|
let Hrefs = [];
|
|
let TempText = "";
|
|
if (record.hasOwnProperty("facets")) {
|
|
// First, append split areas.
|
|
for (let i of record.facets) {
|
|
if (i.features[0].$type == "app.bsky.richtext.facet#link") {
|
|
SplitAreas.push(i.index.byteStart);
|
|
SplitAreas.push(i.index.byteEnd);
|
|
Hrefs.push(i.features[0].uri);
|
|
Hrefs.push("");
|
|
}
|
|
}
|
|
// Last minute append.
|
|
SplitAreas.push(text.length);
|
|
// Remove emoji regex
|
|
let EmojiRegex = /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu;
|
|
let EmojiObjects = text.match(EmojiRegex);
|
|
let SubtractNumber = 0;
|
|
if (EmojiObjects != null) {
|
|
SubtractNumber = EmojiObjects.length * 2;
|
|
}
|
|
// Now we split the string
|
|
for (let i = 1; i < SplitAreas.length; i++) {
|
|
StringArray.push(text.slice(SplitAreas[i - 1] - SubtractNumber, SplitAreas[i] - SubtractNumber));
|
|
}
|
|
// Finally, we append the string with <a>
|
|
for (let i = 1; i < StringArray.length; i += 2) {
|
|
TempText += StringArray[i - 1] + "<a href='" + Hrefs[i - 1] + "'>" + StringArray[i] + "</a>";
|
|
}
|
|
}
|
|
if (TempText == "") {
|
|
return text;
|
|
}
|
|
return TempText;
|
|
}
|
|
|
|
// Deleters
|
|
// Removes a record. Can be a like, a repost, a post, etc.
|
|
export async function DeleteRecord(Repo, Collection, RKey) {
|
|
if (Token == null) {
|
|
return "";
|
|
}
|
|
let RequestBody = {
|
|
"repo": Repo,
|
|
"collection": Collection,
|
|
"rkey": RKey
|
|
}
|
|
let FetchThing = localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.deleteRecord";
|
|
let DPoP = await ClientDPoPPDS("POST", FetchThing);
|
|
let request = fetch(FetchThing, { body: JSON.stringify(RequestBody), method: "POST", headers: {"Content-Type": "application/json", "Authorization": "DPoP " + Token, "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) {
|
|
await HandleError(body, header);
|
|
body = await DeleteRecord(Repo, Collection, RKey);
|
|
}
|
|
return body;
|
|
}
|
|
|
|
// Things to get this to work.
|
|
// Component 1/4
|
|
export async function GetPDSWellKnown() {
|
|
return fetch("https://bsky.social/.well-known/oauth-authorization-server", {method: "GET"});
|
|
}
|
|
|
|
// 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
|
|
let 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
|
|
let 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);
|
|
|
|
let sHeader = JSON.stringify(Header);
|
|
let sPayload = JSON.stringify(Payload);
|
|
let 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
|
|
let 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
|
|
let 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));
|
|
|
|
let sHeader = JSON.stringify(Header);
|
|
let sPayload = JSON.stringify(Payload);
|
|
let 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().then((response) => response.json());
|
|
|
|
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().then((response) => response.json());
|
|
|
|
// 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
|
|
await localStorage.setItem(Variables.BlueskyAccessToken, Auth.access_token);
|
|
await localStorage.setItem(Variables.BlueskyRefreshToken, Auth.refresh_token);
|
|
// That long string just gets the payload
|
|
// aud = PDS server we are communicating with; sub = user DID
|
|
await localStorage.setItem(Variables.BlueskyPDS, "https://" + KJUR.jws.JWS.readSafeJSONString(b64utoutf8(localStorage.getItem(Variables.BlueskyAccessToken).split(".")[1])).aud.split(":")[2]);
|
|
await 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().then((response) => response.json());
|
|
// Fake PAR request to get the nonce.
|
|
let State = crypto.randomUUID();
|
|
let PKCEverifier = await CreatePKCECodeVerifier();
|
|
let PKCEchallenge = await CreatePKCECodeChallenge(PKCEverifier);
|
|
let PAR = PARrequest(WellKnown.pushed_authorization_request_endpoint, State, PKCEchallenge);
|
|
let nonce = await PAR.then((response) => response.headers.get("dpop-nonce"));
|
|
// Save nonce
|
|
await localStorage.setItem(Variables.BlueskyNonce, nonce);
|
|
// 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 = await ReauthRequest(WellKnown.token_endpoint, Var, DPoP).then((response) => response.json());
|
|
// Save the tokens and be done
|
|
await localStorage.setItem(Variables.BlueskyAccessToken, Auth.access_token);
|
|
await localStorage.setItem(Variables.BlueskyRefreshToken, Auth.refresh_token);
|
|
}
|
|
|
|
async function HandleError(body, header) {
|
|
await localStorage.setItem(Variables.BlueskyNonce, header);
|
|
if (body.message != undefined && body.message.includes("claim timestamp check failed")) {
|
|
await RefreshTokens();
|
|
Token = localStorage.getItem(Variables.BlueskyAccessToken);
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|