From 456bd86379625529bc917edac11e726e09db8251 Mon Sep 17 00:00:00 2001 From: CatAClock Date: Fri, 23 May 2025 12:10:21 -0700 Subject: [PATCH] organization, commenting, and fixing facets to make multiple work --- JS/BlueskyAPI.js | 57 +++++++++++++-------- JS/MastodonAPI.js | 8 ++- JS/expanded.js | 125 ++++++++++++++++++++++++++-------------------- JS/post.js | 6 +-- 4 files changed, 115 insertions(+), 81 deletions(-) diff --git a/JS/BlueskyAPI.js b/JS/BlueskyAPI.js index da94bff..4b5716a 100644 --- a/JS/BlueskyAPI.js +++ b/JS/BlueskyAPI.js @@ -1,5 +1,8 @@ import * as Variables from "./Variables.js"; + +// Getters +// This gets the timeline. The cursor is a time in Z form. export async function GetTimeline(Cursor) { if (localStorage.getItem(Variables.BlueskyAccessToken) == null) { console.log("No access token!"); @@ -26,6 +29,7 @@ export async function GetTimeline(Cursor) { return body; } +// This gets the post. If there are multiple URIs, they must be within an array. export async function GetPosts(URIs) { if (localStorage.getItem(Variables.BlueskyAccessToken) == null) { console.log("No access token!"); @@ -52,6 +56,27 @@ export async function GetBlob(DID, CID) { 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) { + let RequestBody = { + "repo": Repo, + "collection": Collection, + "rkey": RKey + } + let DPoP = await ClientDPoPPDS("GET", localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.getRecord?repo=" + Repo + "&collection=" + Collection + "&rkey=" + RKey); + let request = fetch(localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.getRecord?repo=" + Repo + "&collection=" + Collection + "&rkey=" + RKey, { method: "GET", 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) { + await HandleError(body, header); + body = await DeleteRecord(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) { if (localStorage.getItem(Variables.BlueskyAccessToken) == null) { console.log("No access token!"); @@ -72,6 +97,7 @@ export async function CreatePost(DID, Text) { return body; } +// Creates a reply post. The RootID is always the first post, the ReplyID is the post you are replying to. export async function CreateReplyPost(DID, Text, ReplyID, RootID) { if (localStorage.getItem(Variables.BlueskyAccessToken) == null) { console.log("No access token!"); @@ -102,7 +128,8 @@ export async function CreateReplyPost(DID, Text, ReplyID, RootID) { return body; } -export async function SetThreadGate(DID, Post, VisibilitySettings) { +// 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 (localStorage.getItem(Variables.BlueskyAccessToken) == null) { console.log("No access token!"); return ""; @@ -117,7 +144,8 @@ export async function SetThreadGate(DID, Post, VisibilitySettings) { return body; } -export async function SendLike(DID, RefURI, RefCID) { +// Create a like and send it to the server. +export async function CreateLike(DID, RefURI, RefCID) { if (localStorage.getItem(Variables.BlueskyAccessToken) == null) { console.log("No access token!"); return ""; @@ -142,7 +170,8 @@ export async function SendLike(DID, RefURI, RefCID) { return body; } -export async function SendRepost(DID, RefURI, RefCID) { +// 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 ""; @@ -167,24 +196,7 @@ export async function SendRepost(DID, RefURI, RefCID) { return body; } -export async function GetRecord(Repo, Collection, RKey) { - let RequestBody = { - "repo": Repo, - "collection": Collection, - "rkey": RKey - } - let DPoP = await ClientDPoPPDS("GET", localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.getRecord?repo=" + Repo + "&collection=" + Collection + "&rkey=" + RKey); - let request = fetch(localStorage.getItem(Variables.BlueskyPDS) + "/xrpc/com.atproto.repo.getRecord?repo=" + Repo + "&collection=" + Collection + "&rkey=" + RKey, { method: "GET", 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) { - await HandleError(body, header); - body = await DeleteRecord(Repo, Collection, RKey); - } - return body; -} - +// Creates a record. Universal way of making things. export async function CreateRecord(Repo, Collection, Record, RKey) { let RequestBody = { "repo": Repo, @@ -206,6 +218,8 @@ export async function CreateRecord(Repo, Collection, Record, RKey) { return body; } +// Deleters +// Removes a record. Can be a like, a repost, a post, etc. export async function DeleteRecord(Repo, Collection, RKey) { let RequestBody = { "repo": Repo, @@ -224,6 +238,7 @@ export async function DeleteRecord(Repo, Collection, RKey) { return body; } +// Things to get this to work. // Component 1/4 export async function GetPDSWellKnown() { return await fetch("https://bsky.social/.well-known/oauth-authorization-server", {method: "GET"}) diff --git a/JS/MastodonAPI.js b/JS/MastodonAPI.js index 4950e41..a136993 100644 --- a/JS/MastodonAPI.js +++ b/JS/MastodonAPI.js @@ -2,6 +2,7 @@ import * as Variables from "./Variables.js"; export const Scopes = "read write follow push"; +// Getters // Gets the public timeline. export async function GetPublicTimeline(Local = false, Remote = false, Website) { // Variables can be found in `setting.js` @@ -77,6 +78,7 @@ export async function GetNotifications() { .then((response) => response.json()); } +// A status is just a post. It gets it. export async function GetStatus(ID) { if (localStorage.getItem(Variables.MastodonAccessToken) == null) { console.log("No access token!"); @@ -88,6 +90,7 @@ export async function GetStatus(ID) { .then((response) => response.json()); } +// Creators // Make a status export async function CreateStatus(Text, Visibility = "public") { if (localStorage.getItem(Variables.MastodonAccessToken) == null) { @@ -133,7 +136,7 @@ export async function CreateReplyStatus(Text, Visibility = "public", ReplyID) { return body; } -export async function SendFavorite(ID, IsFavorited) { +export async function CreateFavorite(ID, IsFavorited) { if (localStorage.getItem(Variables.MastodonAccessToken) == null) { console.log("No access token!"); return ""; @@ -148,7 +151,7 @@ export async function SendFavorite(ID, IsFavorited) { } } -export async function SendReblog(ID, IsReblogged) { +export async function CreateReblog(ID, IsReblogged) { if (localStorage.getItem(Variables.MastodonAccessToken) == null) { console.log("No access token!"); return ""; @@ -163,6 +166,7 @@ export async function SendReblog(ID, IsReblogged) { } } +// Things to make this work // The first step to using the app. export async function HandleAuthentication(Website) { // See if the user is smart enough to put https. diff --git a/JS/expanded.js b/JS/expanded.js index 3423ec1..7544d1a 100644 --- a/JS/expanded.js +++ b/JS/expanded.js @@ -21,18 +21,18 @@ GetPost(); // Button stuff Favorite.onclick = (event) => { if (website == "Mastodon") { - MastodonAPI.SendFavorite(post.id, post.favourited); + MastodonAPI.CreateFavorite(post.id, post.favourited); } else if (website == "Bluesky") { - BlueskyAPI.SendLike(localStorage.getItem(Variables.BlueskyDID), post.post.uri, post.post.cid); + BlueskyAPI.CreateLike(localStorage.getItem(Variables.BlueskyDID), post.post.uri, post.post.cid); } SetFavorite(); } Boost.onclick = (event) => { if (website == "Mastodon") { - MastodonAPI.SendReblog(post.id, post.reblogged); + MastodonAPI.CreateReblog(post.id, post.reblogged); } else if (website == "Bluesky") { - BlueskyAPI.SendRepost(localStorage.getItem(Variables.BlueskyDID), post.post.uri, post.post.cid); + BlueskyAPI.CreateRepost(localStorage.getItem(Variables.BlueskyDID), post.post.uri, post.post.cid); } SetBoost(); } @@ -50,7 +50,7 @@ async function GetPost() { document.getElementsByClassName("Handle Regular")[0].innerHTML = post.reblog.account.username + " ( R: " + post.account.username + " )"; if (post.reblog.media_attachments.length != 0) { for (let i of post.reblog.media_attachments) { - await CreateMedia(i, document.getElementsByClassName("Images Regular")[0]); + await ApplyMedia(i, document.getElementsByClassName("Images Regular")[0]); } } } else { @@ -59,7 +59,7 @@ async function GetPost() { // Show the image if it exists. if (post.media_attachments.length != 0) { for (let i of post.media_attachments) { - await CreateMedia(i, document.getElementsByClassName("Images Regular")[0]); + await ApplyMedia(i, document.getElementsByClassName("Images Regular")[0]); } } } @@ -72,38 +72,22 @@ async function GetPost() { if (post.in_reply_to_id != null) { var AnotherPost = await MastodonAPI.GetStatus(post.in_reply_to_id); document.getElementsByClassName("Origin Parent")[0].innerHTML = website; - if (AnotherPost.reblog != null) { - document.getElementsByClassName("PostText Parent")[0].innerHTML = AnotherPost.reblog.content; - if (AnotherPost.reblog.media_attachments.length != 0) { - for (let i of AnotherPost.reblog.media_attachments) { - await CreateMedia(i, document.getElementsByClassName("Images Parent")[0]); - } - } - } else { - document.getElementsByClassName("PostText Parent")[0].innerHTML = AnotherPost.content; - if (AnotherPost.media_attachments.length != 0) { - for (let i of AnotherPost.media_attachments) { - await CreateMedia(i, document.getElementsByClassName("Images Parent")[0]); - } + document.getElementsByClassName("Handle Parent")[0].innerHTML = AnotherPost.account.username; + document.getElementsByClassName("PostText Parent")[0].innerHTML = AnotherPost.content; + if (AnotherPost.media_attachments.length != 0) { + for (let i of AnotherPost.media_attachments) { + await ApplyMedia(i, document.getElementsByClassName("Images Parent")[0]); } } // Now time to see if there are any grandparents if (AnotherPost.in_reply_to_id != null) { var AnotherAnotherPost = await MastodonAPI.GetStatus(AnotherPost.in_reply_to_id); document.getElementsByClassName("Origin GrandParent")[0].innerHTML = website; - if (AnotherAnotherPost.reblog != null) { - document.getElementsByClassName("PostText GrandParent")[0].innerHTML = AnotherAnotherPost.reblog.content; - if (AnotherAnotherPost.reblog.media_attachments.length != 0) { - for (let i of AnotherAnotherPost.reblog.media_attachments) { - await CreateMedia(i, document.getElementsByClassName("Images GrandParent")[0]); - } - } - } else { - document.getElementsByClassName("PostText GrandParent")[0].innerHTML = AnotherAnotherPost.content; - if (AnotherAnotherPost.media_attachments.length != 0) { - for (let i of AnotherAnotherPost.media_attachments) { - await CreateMedia(i, document.getElementsByClassName("Images GrandParent")[0]); - } + document.getElementsByClassName("Handle GrandParent")[0].innerHTML = AnotherAnotherPost.account.username; + document.getElementsByClassName("PostText GrandParent")[0].innerHTML = AnotherAnotherPost.content; + if (AnotherAnotherPost.media_attachments.length != 0) { + for (let i of AnotherAnotherPost.media_attachments) { + await ApplyMedia(i, document.getElementsByClassName("Images GrandParent")[0]); } } } @@ -116,27 +100,13 @@ async function GetPost() { document.getElementsByClassName("Handle Regular")[0].innerHTML = post.post.author.handle; } // Text. This will be modified later. - var Text = post.post.record.text; - // Check for facets. Facets are things that change what the text does or looks like. - if (post.post.record.hasOwnProperty("facets")) { - for (let i of post.post.record.facets) { - if (i.features[0].$type == "app.bsky.richtext.facet#link") { - var EmojiRegex = /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu; - var EmojiObjects = Text.match(EmojiRegex); - var SubtractNumber = 0; - if (EmojiObjects != null) { - SubtractNumber = EmojiObjects.length * 2; - } - Text = Text.substring(0, i.index.byteStart - SubtractNumber) + "" + Text.substring(i.index.byteStart - SubtractNumber, i.index.byteEnd - SubtractNumber) + "" + Text.substring(i.index.byteEnd - SubtractNumber, Text.length - 1); - } - } - } + var Text = ApplyFacets(post.post.record, post.post.record.text); // Place the text. Text = Text.replace(/\r?\n|\r/g, "
"); document.getElementsByClassName("PostText Regular")[0].innerHTML = Text; // Show the image if it exists. if (post.post.record.hasOwnProperty("embed")) { - await CreateMedia(post.post.record, document.getElementsByClassName("Images Regular")[0], post.post.author.did); + await ApplyMedia(post.post.record, document.getElementsByClassName("Images Regular")[0], post.post.author.did); } // We don't need to update the post with new information. The repos are seperate. // Set the texts. It's opposite because "setting" causes it to switch. @@ -155,11 +125,11 @@ async function GetPost() { var AnotherPost = await BlueskyAPI.GetRecord(post.reply.parent.uri.split("/")[2], post.reply.parent.uri.split("/")[3], post.reply.parent.uri.split("/")[4]); document.getElementsByClassName("Origin Parent")[0].innerHTML = website; document.getElementsByClassName("Handle Parent")[0].innerHTML = post.reply.parent.author.handle; - Text = AnotherPost.value.text; + Text = ApplyFacets(AnotherPost.value, AnotherPost.value.text); Text = Text.replace(/\r?\n|\r/g, "
"); document.getElementsByClassName("PostText Parent")[0].innerHTML = Text; if (AnotherPost.value.hasOwnProperty("embed")) { - await CreateMedia(AnotherPost.value, document.getElementsByClassName("Images Parent")[0], post.reply.parent.author.did); + await ApplyMedia(AnotherPost.value, document.getElementsByClassName("Images Parent")[0], post.reply.parent.author.did); } // Now time to see if there are any grandparents. @@ -167,11 +137,11 @@ async function GetPost() { var AnotherAnotherPost = await BlueskyAPI.GetRecord(AnotherPost.value.reply.parent.uri.split("/")[2], AnotherPost.value.reply.parent.uri.split("/")[3], AnotherPost.value.reply.parent.uri.split("/")[4]); document.getElementsByClassName("Origin GrandParent")[0].innerHTML = website; document.getElementsByClassName("Handle GrandParent")[0].innerHTML = post.reply.grandparentAuthor.handle; - Text = AnotherAnotherPost.value.text; + Text = ApplyFacets(AnotherAnotherPost.value, AnotherAnotherPost.value.text); Text = Text.replace(/\r?\n|\r/g, "
"); document.getElementsByClassName("PostText GrandParent")[0].innerHTML = Text; if (AnotherAnotherPost.value.hasOwnProperty("embed")) { - await CreateMedia(AnotherAnotherPost.value, document.getElementsByClassName("Images GrandParent")[0], post.reply.grandparentAuthor.did); + await ApplyMedia(AnotherAnotherPost.value, document.getElementsByClassName("Images GrandParent")[0], post.reply.grandparentAuthor.did); } } } @@ -180,7 +150,9 @@ async function GetPost() { } } -async function CreateMedia(Media, Element, Author = undefined) { +// Applyers. Essentially applies a thing to an operation. +// Finds any associated media and applies it to the post. +async function ApplyMedia(Media, Element, Author = undefined) { // Check to see if the image is on the same server. if (website == "Mastodon") { if (Media.type == "image") { @@ -199,8 +171,9 @@ async function CreateMedia(Media, Element, Author = undefined) { } else if (website == "Bluesky") { if (Media.embed.$type == "app.bsky.embed.record") { var Texty = await BlueskyAPI.GetPosts([Media.embed.record.uri]); - console.log(Texty); - Element.innerHTML += "

" + Texty.posts[0].record.text + "


"; + var Text = ApplyFacets(Texty.posts[0].record, Texty.posts[0].record.text); + Text = Text.replace(/\r?\n|\r/g, "
"); + Element.innerHTML += "

" + Text + "


"; if (Texty.posts[0].record.embed.$type == "app.bsky.embed.images") { for (let i of Texty.posts[0].record.embed.images) { var Blobby = await BlueskyAPI.GetBlob(Texty.posts[0].author.did, i.image.ref.$link); @@ -227,6 +200,48 @@ async function CreateMedia(Media, Element, Author = undefined) { } } +// Applies the necessary facets to the text. +function ApplyFacets(record, text) { + var StringArray = []; + var SplitAreas = [0]; + var Hrefs = []; + var 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 + var EmojiRegex = /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu; + var EmojiObjects = TempText.match(EmojiRegex); + var 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 + for (let i = 0; i < StringArray.length; i += 2) { + TempText += StringArray[i] + "" + StringArray[i + 1] + ""; + } + // Last minute append. + TempText += StringArray[StringArray.length - 1]; + return TempText; + } else { + return text; + } +} + +// Setters function SetFavorite() { FavoriteFlipper = !(FavoriteFlipper); if (FavoriteFlipper == false) { diff --git a/JS/post.js b/JS/post.js index 6d19e07..7d80273 100644 --- a/JS/post.js +++ b/JS/post.js @@ -72,16 +72,16 @@ async function Post() { if (website == "Bluesky") { if (JSON.parse(localStorage.getItem("post")).hasOwnProperty("reply")) { let Post = await BlueskyAPI.CreateReplyPost(localStorage.getItem(Variables.BlueskyDID), Text, JSON.parse(localStorage.getItem("post")).post, JSON.parse(localStorage.getItem("post")).reply.root); - await BlueskyAPI.SetThreadGate(localStorage.getItem(Variables.BlueskyDID), Post.uri, TempVisible); + await BlueskyAPI.CreateThreadGate(localStorage.getItem(Variables.BlueskyDID), Post.uri, TempVisible); return; } else { let Post = await BlueskyAPI.CreateReplyPost(localStorage.getItem(Variables.BlueskyDID), Text, JSON.parse(localStorage.getItem("post")).post, JSON.parse(localStorage.getItem("post")).post); - await BlueskyAPI.SetThreadGate(localStorage.getItem(Variables.BlueskyDID), Post.uri, TempVisible); + await BlueskyAPI.CreateThreadGate(localStorage.getItem(Variables.BlueskyDID), Post.uri, TempVisible); return; } } else if (website == "All") { let Post = await BlueskyAPI.CreatePost(localStorage.getItem(Variables.BlueskyDID), Text); - await BlueskyAPI.SetThreadGate(localStorage.getItem(Variables.BlueskyDID), Post.uri, TempVisible); + await BlueskyAPI.CreateThreadGate(localStorage.getItem(Variables.BlueskyDID), Post.uri, TempVisible); } } // Youtube posting.