BlueskyAPI (#5)

Completed the BlueskyAPI

Reviewed-on: #5
This commit is contained in:
CatAClock 2025-04-29 22:08:19 +00:00
parent 32065edbd5
commit 876f9707c6
12 changed files with 440 additions and 124 deletions

View file

@ -6,6 +6,8 @@
<link rel="icon" href="../Icons/favicon.ico" />
<link rel="stylesheet" href="../CSS/mail.css">
<script type="module" src="../JS/mail.js"></script>
<!-- Dependenci -->
<script language="JavaScript" type="text/javascript" src="https://kjur.github.io/jsrsasign/jsrsasign-latest-all-min.js"></script>
</head>
<body style="margin: 0px; text-align: center;">
@ -14,9 +16,7 @@
<section style="position: absolute; width: 100%; height: 100%" class="Notifications"></section>
<header style="position: relative; z-index: 1;">
<h1>Mail</h1>
<p class="Login"><em>Login</em></p>
<p class="Logout" style="visibility: hidden;"><em>Logout</em></p>
<p onclick="history.back()"><b>Back</b></p>
<p onclick="history.back()"><b>OK</b></p>
</header>
</body>
</html>

View file

@ -6,15 +6,22 @@
<link rel="icon" href="../Icons/favicon.ico" />
<script type="module" src="../JS/setting.js"></script>
<!-- Dependenci -->
<script language="JavaScript" type="text/javascript" src="https://kjur.github.io/jsrsasign/jsrsasign-latest-all-min.js"></script>
</head>
<body style="margin: 0px; text-align: center;">
<header>
<h1>Setting</h1>
</header>
<p>Just go back. It ain't ready yet...</p>
<p class="Local">Toggle Local</p>
<p class="Remote">Toggle Remote</p>
<p class="Login Mastodon"><em>Login to Mastodon</em></p>
<input type="text" minlength="6" class="WebInput Mastodon" required />
<p class="Logout Mastodon" style="visibility: hidden;"><em>Logout of Mastodon</em></p>
<p class="Login Bluesky"><em>Login to Bluesky</em></p>
<input type="text" minlength="6" class="WebInput Bluesky" required />
<p class="Logout Bluesky" style="visibility: hidden;"><em>Logout of Bluesky</em></p>
<p onclick="history.back()"><b>OK</b></p>
</body>
</html>

View file

@ -1,20 +0,0 @@
export const Scopes = "read write follow push";
export async function GetPublicTimeline(Local = false, Remote = false) {
let Timeline;
if (Local == true && Remote == true) {
console.error("Don't set both Local and Remote timelines to true.");
}
if (Local == true) {
Timeline = await fetch("https://wetdry.world/api/v1/timelines/public?limit=12&local=true")
.then((response) => response.json());
} else if (Remote == true) {
Timeline = await fetch("https://wetdry.world/api/v1/timelines/public?limit=12&remote=true")
.then((response) => response.json());
} else {
Timeline = await fetch("https://wetdry.world/api/v1/timelines/public?limit=12")
.then((response) => response.json());
}
return Timeline;
}

165
JS/BlueskyAPI.js Normal file
View file

@ -0,0 +1,165 @@
export async function GetBlueskyDID(PDS, Handle) {
let request = fetch(PDS + "/xrpc/com.atproto.identity.resolveDid?handle=" + Handle, { method: "GET"})
.then((response) => response.json());
return request;
}
// Component 1/4
export async function GetPDSWellKnown() {
let Data = await fetch("https://bsky.social/.well-known/oauth-authorization-server", {method: "GET"})
.then((response) => response.json());
return Data;
}
// 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, ChallengeCode) {
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: ChallengeCode, state: State, login_hint: "crowdedgames.group" }), headers: {"Content-Type": "application/x-www-form-urlencoded"}});
}
export async function AuthRequest(TokenEndpoint, ChallengeVerifier, code, DPoP) {
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: ChallengeVerifier}), headers: { "DPoP": DPoP, "Content-Type": "application/x-www-form-urlencoded"}})
.then((response) => response.json());
}
// Component 4/4
export async function ClientDPoP(POSTorGET, RequestURL, DPoPNonce) {
let KeyPair = await crypto.subtle.generateKey({name: "ECDSA", namedCurve: "P-256"}, true, ["sign", "verify"]);
// Header
var Header = {typ: "dpop+jwt", alg: "ES256", jwk:
await crypto.subtle.exportKey("jwk", KeyPair.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 = DPoPNonce;
var sHeader = JSON.stringify(Header);
var sPayload = JSON.stringify(Payload);
var JWT = KJUR.jws.JWS.sign("ES256", sHeader, sPayload,
await crypto.subtle.exportKey("jwk", KeyPair.privateKey)
.then(function(response) {
delete response["key_ops"];
delete response["ext"];
delete response["alg"];
return response})
);
return JWT;
}
// So far does nothing? Don't touch :3
export async function AssertionJWT() {
let KeyPair = await crypto.subtle.generateKey({name: "ECDSA", namedCurve: "P-256"}, true, ["sign", "verify"]);
// Header
var Header = {alg: "HS256", kid: await crypto.subtle.exportKey("jwk", KeyPair.publicKey).then(function(response) {return response})};
// Payload
var Payload = {};
Payload.iss = "https://fedi.crowdedgames.group/oauth/client-metadata.json";
Payload.sub = "https://fedi.crowdedgames.group/oauth/client-metadata.json";
// Payload.aud
Payload.jti = crypto.randomUUID();
Payload.iat = Math.floor(new Date(Date.now()).getTime() / 1000);
var sHeader = JSON.stringify(Header);
var sPayload = JSON.stringify(Payload);
var JWT = KJUR.jws.JWS.sign("HS256", sHeader, sPayload, "838383");
}
export async function HandleAuthorization(BlueskyPKCEverifer, BlueskyPKCEchallenge, BlueskyNonce) {
// Declare Variables
let KeyPair = await crypto.subtle.generateKey({name: "ECDSA", namedCurve: "P-256"}, true, ["sign", "verify"]);
let WellKnown = await GetPDSWellKnown();
let PAREndpoint = WellKnown.pushed_authorization_request_endpoint;
let State = crypto.randomUUID();
let PKCEverifier = await CreatePKCECodeVerifier();
let PKCEchallenge = await CreatePKCECodeChallenge(PKCEverifier);
// 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"));
let ExportedKey1 = await crypto.subtle.exportKey("raw", KeyPair.publicKey);
// Save these things.
document.cookie = BlueskyPKCEverifer + "=" + PKCEverifier + ";samesite=strict;path=/;expires=Fri, 31 Dec 9999 23:59:59 GMT;";
document.cookie = BlueskyPKCEchallenge + "=" + PKCEchallenge + ";samesite=strict;path=/;expires=Fri, 31 Dec 9999 23:59:59 GMT;";
document.cookie = BlueskyNonce + "=" + nonce + ";samesite=strict;path=/;expires=Fri, 31 Dec 9999 23:59:59 GMT;";
// Don't remove the keys. They are important.
// if (document.cookie.split("; ").find((row) => row.startsWith(BlueskyPublicKey + "="))?.split("=").length == 1 || document.cookie.split("; ").find((row) => row.startsWith(BlueskyPrivateKey + "="))?.split("=").length == 1) {
// document.cookie = BlueskyPublicKey + "=" + ExportedKey1 + ";samesite=strict;path=/;expires=Fri, 31 Dec 9999 23:59:59 GMT;";
// }
// Now we need to authenticate. Make sure the State stays the same throughout this whole process :]
document.location.href = "https://bsky.social/oauth/authorize?client_id=https://fedi.crowdedgames.group/oauth/client-metadata.json&request_uri=" + body.request_uri;
}
export async function GainTokens(PKCEcodeName, NonceName, AccessToken, RefreshToken) {
// 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) && document.cookie.split("; ").find((row) => row.startsWith(PKCEcodeName + "="))?.split("=").length > 1 && document.location.href.split("code=").length > 1) && document.cookie.split("; ").find((row) => row.startsWith(AccessToken + "="))?.split("=").length == 1) {
// Create varaibles, be aware of waits because of internet.
let DPoP = await ClientDPoP("POST", "https://bsky.social/oauth/token", document.cookie.split("; ").find((row) => row.startsWith(NonceName + "="))?.split("=")[1]);
let PKCE = document.cookie.split("; ").find((row) => row.startsWith(PKCEcodeName + "="))?.split("=")[1];
let code = document.location.href.split("code=")[1];
// Authentication
let Auth = await AuthRequest("https://bsky.social/oauth/token", PKCE, code, DPoP);
// Save the tokens and be done
document.cookie = AccessToken + "=" + Auth.access_token + ";samesite=strict;path=/;expires=Fri, 31 Dec 9999 23:59:59 GMT;";
document.cookie = RefreshToken + "=" + Auth.refresh_token + ";samesite=strict;path=/;expires=Fri, 31 Dec 9999 23:59:59 GMT;";
}
}
// 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;
}

101
JS/MastodonAPI.js Normal file
View file

@ -0,0 +1,101 @@
export const Scopes = "read write follow push";
let Origin = document.location.href;
// Gets the public timeline.
export async function GetPublicTimeline(Local = false, Remote = false, Website) {
let Timeline;
if (Local == true && Remote == true) {
console.error("Don't set both Local and Remote timelines to true.");
return Timeline;
}
if (Local == true) {
Timeline = await fetch(Website + "/api/v1/timelines/public?limit=12&local=true")
.then((response) => response.json());
} else if (Remote == true) {
Timeline = await fetch(Website + "/api/v1/timelines/public?limit=12&remote=true")
.then((response) => response.json());
} else {
Timeline = await fetch(Website + "/api/v1/timelines/public?limit=12")
.then((response) => response.json());
}
return Timeline;
}
// Gets the favorites of a user that you have access to.
export async function GetFavorites(Website, MastodonAccessToken, MastodonTokenType) {
let Favorites;
// Check for a token.
if (document.cookie.split("; ").find((row) => row.startsWith(MastodonAccessToken + "="))?.split("=").length > 1) {
// Get the varaibles that are stored in cookies.
let AccessToken = document.cookie.split("; ").find((row) => row.startsWith(MastodonAccessToken + "="))?.split("=")[1];
let TokenType = document.cookie.split("; ").find((row) => row.startsWith(MastodonTokenType + "="))?.split("=")[1];
Favorites = await fetch(Website + "/api/v1/favourites", {method: "GET", headers: {"Authorization": TokenType + " " + AccessToken}})
.then((response) => response.json());
}
return Favorites;
}
// Gets the bookmarks of a user that you have access to.
export async function GetBookmarks(Website, MastodonAccessToken, MastodonTokenType) {
let Bookmarks;
// Check for a token.
if (document.cookie.split("; ").find((row) => row.startsWith(MastodonAccessToken + "="))?.split("=").length > 1) {
// Get the varaibles that are stored in cookies.
let AccessToken = document.cookie.split("; ").find((row) => row.startsWith(MastodonAccessToken + "="))?.split("=")[1];
let TokenType = document.cookie.split("; ").find((row) => row.startsWith(MastodonTokenType + "="))?.split("=")[1];
Bookmarks = await fetch(Website + "/api/v1/bookmarks", {method: "GET", headers: {"Authorization": TokenType + " " + AccessToken}})
.then((response) => response.json());
}
return Bookmarks;
}
// Gets the notifications of a user that you have access to.
export async function GetNotifications(Website, MastodonAccessToken, MastodonTokenType) {
let Notifications;
// Check for a token.
if (document.cookie.split("; ").find((row) => row.startsWith(MastodonAccessToken + "="))?.split("=").length > 1) {
// Get the varaibles that are stored in cookies.
let AccessToken = document.cookie.split("; ").find((row) => row.startsWith(MastodonAccessToken + "="))?.split("=")[1];
let TokenType = document.cookie.split("; ").find((row) => row.startsWith(MastodonTokenType + "="))?.split("=")[1];
Notifications = await fetch(Website + "/api/v1/notifications", {method: "GET", headers: {"Authorization": TokenType + " " + AccessToken}})
.then((response) => response.json());
}
return Notifications;
}
// The first step to using the app.
export async function HandleAuthentication(Website, CookieClientID, CookieClientSecret) {
// See if the user is smart enough to put https.
let InstanceData = "";
// 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;
}
// Registering the app.
InstanceData = await fetch(Website + "/api/v1/apps?client_name=Channel Viewer&redirect_uris=" + Origin + "&scopes=" + Scopes, {method: "POST"})
.then((response) => response.json());
// Save the client stuff as cookies.
document.cookie = CookieClientID + "=" + InstanceData.client_id + ";samesite=strict;path=/;expires=Fri, 31 Dec 9999 23:59:59 GMT;";
document.cookie = CookieClientSecret + "=" + InstanceData.client_secret + ";samesite=strict;path=/;expires=Fri, 31 Dec 9999 23:59:59 GMT;";
// Now authenticate the app.
document.location.href = Website + "/oauth/authorize?client_id=" + InstanceData.client_id + "&redirect_uri=" + Origin + "&response_type=code&scope=" + Scopes;
}
// This specific functino goes after HandleAuthentication for when login happens.
export async function GainToken(Website, CookieClientID, CookieClientSecret, CookieAccessToken, CookieTokenType) {
// check if you both have a code and have a current authentication.
if (document.location.href.split("code=").length > 1 && document.cookie.split("; ").find((row) => row.startsWith(CookieClientID + "="))?.split("=").length > 1) {
let code = document.location.href.split("code=")[1];
let ClientID = document.cookie.split("; ").find((row) => row.startsWith(CookieClientID + "="))?.split("=")[1];
let ClientSecret = document.cookie.split("; ").find((row) => row.startsWith(CookieClientSecret + "="))?.split("=")[1];
let AuthenticationToken = await fetch(Website + "/oauth/token?client_id=" + ClientID + "&client_secret=" + ClientSecret + "&redirect_uri=" + Origin + "&grant_type=authorization_code&code=" + code, {method: "POST"})
.then((response) => response.json());
// Cookify These
document.cookie = CookieAccessToken + "=" + AuthenticationToken.access_token + ";samesite=strict;path=/;expires=Fri, 31 Dec 9999 23:59:59 GMT;";
document.cookie = CookieTokenType + "=" + AuthenticationToken.token_type + ";samesite=strict;path=/;expires=Fri, 31 Dec 9999 23:59:59 GMT;";
}
}

0
JS/TumblrAPI.js Normal file
View file

View file

@ -1,4 +1,4 @@
import * as ActivityPub from "./ActivityPub.js";
import * as MastodonAPI from "./MastodonAPI.js";
// GLOBAL VARS
// fuck you. I see why website developers use divs so fucking often.
@ -18,6 +18,9 @@ const ButtonSound = new Audio("Audio/button-305770.mp3");
const WarningClick = new Audio("Audio/button-pressed-38129.mp3");
const BackgroundMusic = new Audio("Audio/soft-piano-music-312509.mp3");
// Websites
let MastodonWebsite = "https://wetdry.world";
// Update a timer
function UpdateTime() {
let TimeNow = new Date();
@ -98,7 +101,7 @@ ArrowsButton[0].onclick = (event) => {
PosterContainerUpdate();
}
// ActivityPub integration
// MastodonAPI integration
async function PosterContainerUpdate() {
// Cookies for the public timelines
let LocalCookie = document.cookie.split("; ").find((row) => row.startsWith("Local="))?.split("=")[1];
@ -106,7 +109,7 @@ async function PosterContainerUpdate() {
let RemoteCookie = document.cookie.split("; ").find((row) => row.startsWith("Remote="))?.split("=")[1];
var RemoteTrue = (RemoteCookie === 'true');
let Timeline = await ActivityPub.GetPublicTimeline(LocalTrue, RemoteTrue);
let Timeline = await MastodonAPI.GetPublicTimeline(LocalTrue, RemoteTrue, MastodonWebsite);
let Content = [];
let Users = [];
for (let i in Timeline) {

View file

@ -1,113 +1,57 @@
import * as ActivityPub from "./ActivityPub.js";
import * as MastodonAPI from "./MastodonAPI.js";
import * as BlueskyAPI from "./BlueskyAPI.js";
import * as TumblrAPI from "./TumblrAPI.js";
let LoginButton = document.getElementsByClassName("Login")[0];
let LogoutButton = document.getElementsByClassName("Logout")[0];
let Origin = window.location.origin + "/HTML/mail"
// MAKE SURE THESE ARE SYNCED!
// Mastodon
let MastodonWebsite = "https://wetdry.world";
let MastodonClientID = "mastodon_client_id";
let MastodonClientSecret = "mastodon_client_secret";
let MastodonAccessToken = "mastodon_access_token";
let MastodonTokenType = "mastodon_token_type";
LoginButton.onclick = (event) => {
HandleAuthentication();
}
LogoutButton.onclick = (event) => {
console.log("Does nothing so far.");
}
async function HandleAuthentication() {
let InstanceData = await fetch("https://wetdry.world/api/v1/apps?client_name=Channel Viewer&redirect_uris=" + Origin + "&scopes=" + ActivityPub.Scopes, {method: "POST"})
.then((response) => response.json());
// Save the client stuff as cookies (STRICTLY THIS SITE!).
document.cookie = "client_id=" + InstanceData.client_id + ";samesite=strict;path=/;expires=Thu, 01 Jan 9999 00:00:00 GMT;";
document.cookie = "client_secret=" + InstanceData.client_secret + ";samesite=strict;path=/;expires=Thu, 01 Jan 9999 00:00:00 GMT;";
// Now authenticate the app.
let InstanceInfo = await fetch("https://wetdry.world/api/v1/instance", {method: "GET"})
.then((response) => response.json());
document.location.href = "https://wetdry.world/oauth/authorize?client_id=" + InstanceData.client_id + "&redirect_uri=" + Origin + "&response_type=code&scope=" + ActivityPub.Scopes;
}
// When the website starts, check to see if you actually went and got a code.
async function HandleLogin() {
if (document.location.href.split("code=").length > 1 && document.cookie.split("; ").find((row) => row.startsWith("access_token="))?.split("=") > 1) {
let code = document.location.href.split("code=")[1];
let ClientID = document.cookie.split("; ").find((row) => row.startsWith("client_id="))?.split("=")[1];
let ClientSecret = document.cookie.split("; ").find((row) => row.startsWith("client_secret="))?.split("=")[1];
let AuthenticationToken = await fetch("https://wetdry.world/oauth/token?client_id=" + ClientID + "&client_secret=" + ClientSecret + "&redirect_uri=" + Origin + "&grant_type=authorization_code&code=" + code, {method: "POST"})
.then((response) => response.json());
// Cookify These
document.cookie = "access_token=" + AuthenticationToken.access_token + ";samesite=strict;path=/;expires=Thu, 01 Jan 9999 00:00:00 GMT;";
document.cookie = "token_type=" + AuthenticationToken.token_type + ";samesite=strict;path=/;expires=Thu, 01 Jan 9999 00:00:00 GMT;";
}
}
function CheckLogin() {
// Check for a token.
if (document.cookie.split("; ").find((row) => row.startsWith("access_token="))?.split("=").length > 1) {
// Swap the buttons
LoginButton.remove();
LogoutButton.setAttribute("style", "");
}
}
// Bluesky
let BlueskyApp = "https://bsky.app";
let BlueskyPDS = "https://bsky.social";
let BlueskyPKCEverifer = "bluesky_pkce_verifier";
let BlueskyPKCEchallenge = "bluesky_pkce_challenge";
let BlueskyPublicKey = "bluesky_public_key";
let BlueskyNonce = "bluesky_nonce";
// Below is the thing it populates if you login.
async function PopulateFavorites() {
// Check for a token.
if (document.cookie.split("; ").find((row) => row.startsWith("access_token="))?.split("=").length > 1) {
// Get the varaibles that are stored in cookies.
let AccessToken = document.cookie.split("; ").find((row) => row.startsWith("access_token="))?.split("=")[1];
let TokenType = document.cookie.split("; ").find((row) => row.startsWith("token_type="))?.split("=")[1];
let Favorites = await fetch("https://wetdry.world/api/v1/favourites", {method: "GET", headers: {"Authorization": TokenType + " " + AccessToken}})
.then((response) => response.json());
let FavoritesArea = document.getElementsByClassName("Favorites")[0];
// Populate the favorites area.
for (let i in Favorites) {
FavoritesArea.innerHTML += "<article style='top:" + getRandomArbitrary(0, 84) + "%; left: " + getRandomArbitrary(0, 84) + "%;' class='Favorite'></article>";
FavoritesArea.getElementsByClassName("Favorite")[i].innerHTML = Favorites[i].content;
}
let Favorites = await MastodonAPI.GetFavorites(MastodonWebsite, MastodonAccessToken, MastodonTokenType);
let FavoritesArea = document.getElementsByClassName("Favorites")[0];
// Populate the favorites area.
for (let i in Favorites) {
FavoritesArea.innerHTML += "<article style='top:" + getRandomArbitrary(0, 84) + "%; left: " + getRandomArbitrary(0, 84) + "%;' class='Favorite'></article>";
FavoritesArea.getElementsByClassName("Favorite")[i].innerHTML = Favorites[i].content;
}
}
async function PopulateBookmarks() {
// Check for a token.
if (document.cookie.split("; ").find((row) => row.startsWith("access_token="))?.split("=").length > 1) {
// Get the varaibles that are stored in cookies.
let AccessToken = document.cookie.split("; ").find((row) => row.startsWith("access_token="))?.split("=")[1];
let TokenType = document.cookie.split("; ").find((row) => row.startsWith("token_type="))?.split("=")[1];
let Bookmarks = await fetch("https://wetdry.world/api/v1/bookmarks", {method: "GET", headers: {"Authorization": TokenType + " " + AccessToken}})
.then((response) => response.json());
let BookmarksArea = document.getElementsByClassName("Bookmarks")[0];
// Populate the Bookmarks area.
for (let i in Bookmarks) {
BookmarksArea.innerHTML += "<article style='top:" + getRandomArbitrary(0, 84) + "%; left: " + getRandomArbitrary(0, 84) + "%;' class='Bookmark'></article>";
BookmarksArea.getElementsByClassName("Bookmark")[i].innerHTML = Bookmarks[i].content;
}
let Bookmarks = await MastodonAPI.GetBookmarks(MastodonWebsite, MastodonAccessToken, MastodonTokenType);
let BookmarksArea = document.getElementsByClassName("Bookmarks")[0];
// Populate the Bookmarks area.
for (let i in Bookmarks) {
BookmarksArea.innerHTML += "<article style='top:" + getRandomArbitrary(0, 84) + "%; left: " + getRandomArbitrary(0, 84) + "%;' class='Bookmark'></article>";
BookmarksArea.getElementsByClassName("Bookmark")[i].innerHTML = Bookmarks[i].content;
}
}
async function PopulateNotifications() {
// Check for a token.
if (document.cookie.split("; ").find((row) => row.startsWith("access_token="))?.split("=").length > 1) {
// Get the varaibles that are stored in cookies.
let AccessToken = document.cookie.split("; ").find((row) => row.startsWith("access_token="))?.split("=")[1];
let TokenType = document.cookie.split("; ").find((row) => row.startsWith("token_type="))?.split("=")[1];
let Notifications = await fetch("https://wetdry.world/api/v1/notifications", {method: "GET", headers: {"Authorization": TokenType + " " + AccessToken}})
.then((response) => response.json());
console.log(Notifications);
let NotificationsArea = document.getElementsByClassName("Notifications")[0];
// Populate the Conversations area.
for (let i in Notifications) {
NotificationsArea.innerHTML += "<article style='top:" + getRandomArbitrary(0, 84) + "%; left: " + getRandomArbitrary(0, 84) + "%;' class='Notification'></article>";
NotificationsArea.getElementsByClassName("Notification")[i].innerHTML = Notifications[i].status.content;
}
let Notifications = await MastodonAPI.GetNotifications(MastodonWebsite, MastodonAccessToken, MastodonTokenType);
let NotificationsArea = document.getElementsByClassName("Notifications")[0];
// Populate the Conversations area.
for (let i in Notifications) {
NotificationsArea.innerHTML += "<article style='top:" + getRandomArbitrary(0, 84) + "%; left: " + getRandomArbitrary(0, 84) + "%;' class='Notification'></article>";
NotificationsArea.getElementsByClassName("Notification")[i].innerHTML = Notifications[i].status.content;
}
}
// Runs on website start.
// Remove traces of "login".
CheckLogin();
// Authentication.
HandleLogin();
// Populate the areas.
PopulateFavorites();
PopulateBookmarks();

View file

@ -1,10 +1,44 @@
import * as MastodonAPI from "./MastodonAPI.js";
import * as BlueskyAPI from "./BlueskyAPI.js";
import * as TumblrAPI from "./TumblrAPI.js";
// Settings buttons
let LocalButton = document.getElementsByClassName("Local")[0];
let RemoteButton = document.getElementsByClassName("Remote")[0];
let MastodonLoginButton = document.getElementsByClassName("Login Mastodon")[0];
let MastodonWebInput = document.getElementsByClassName("WebInput Mastodon")[0];
let MastodonLogoutButton = document.getElementsByClassName("Logout Mastodon")[0];
let BlueskyLoginButton = document.getElementsByClassName("Login Bluesky")[0];
let BlueskyWebInput = document.getElementsByClassName("WebInput Bluesky")[0];
let BlueskyLogoutButton = document.getElementsByClassName("Logout Bluesky")[0];
// MAKE SURE THESE ARE SYNCED!
// Mastodon
let MastodonWebsite = "https://wetdry.world";
let MastodonClientID = "mastodon_client_id";
let MastodonClientSecret = "mastodon_client_secret";
let MastodonAccessToken = "mastodon_access_token";
let MastodonTokenType = "mastodon_token_type";
// Bluesky
let BlueskyApp = "https://bsky.app";
let BlueskyPDS = "https://bsky.social";
let BlueskyPKCEverifer = "bluesky_pkce_verifier";
let BlueskyPKCEchallenge = "bluesky_pkce_challenge";
let BlueskyPublicKey = "bluesky_public_key";
let BlueskyNonce = "bluesky_nonce";
let BlueskyAccessToken = "bluesky_access_token";
let BlueskyRefreshToken = "bluesky_refresh_token";
// Tumblr
let TumblrWebsite = "https://www.tumblr.com";
// Change weather the timelines are public or remote
LocalButton.onclick = (event) => {
// Toggle the cookie
if (document.cookie.split(";").some((item) => item.trim().startsWith("Local="))) {
document.cookie = "Local=true;samesite=strict;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT;";
document.cookie = "Local=true;samesite=strict;path=/;expires=expires=0000-01-01;";
} else {
document.cookie = "Local=true;samesite=strict;path=/;";
}
@ -13,8 +47,65 @@ LocalButton.onclick = (event) => {
RemoteButton.onclick = (event) => {
// Toggle the cookie
if (document.cookie.split(";").some((item) => item.trim().startsWith("Remote="))) {
document.cookie = "Remote=true;samesite=strict;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT;";
document.cookie = "Remote=true;samesite=strict;path=/;expires=expires=0000-01-01;";
} else {
document.cookie = "Remote=true;samesite=strict;path=/;";
}
}
// Mastodon buttons
MastodonLoginButton.onclick = (event) => {
if (MastodonWebInput != "") {
MastodonAPI.HandleAuthentication(MastodonWebsite, MastodonClientID, MastodonClientSecret);
}
}
MastodonLogoutButton.onclick = (event) => {
document.cookie = MastodonClientID + "=nothing;" + ";samesite=strict;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT;";
document.cookie = MastodonClientSecret + "=nothing;" + ";samesite=strict;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT;";
document.cookie = MastodonAccessToken + "=nothing;" + ";samesite=strict;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT;";
document.cookie = MastodonTokenType + "=nothing;" + ";samesite=strict;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT;";
document.location.href = document.location.href;
}
// Bluesky Buttons
BlueskyLoginButton.onclick = (event) => {
if (BlueskyWebInput != "") {
BlueskyAPI.HandleAuthorization(BlueskyPKCEverifer, BlueskyPKCEchallenge, BlueskyNonce);
}
}
BlueskyLogoutButton.onclick = (event) => {
document.cookie = BlueskyPKCEverifer + "=nothing;" + ";samesite=strict;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT;";
document.cookie = BlueskyPKCEchallenge + "=nothing;" + ";samesite=strict;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT;";
document.cookie = BlueskyNonce + "=nothing;" + ";samesite=strict;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT;";
document.location.href = document.location.href;
}
// if an access token is found, login.
function CheckLogin() {
// Check for a mastodon token.
if (document.cookie.split("; ").find((row) => row.startsWith(MastodonAccessToken + "="))?.split("=").length > 1 && document.location.href.split("code=").length == 1) {
// Swap the buttons
MastodonLoginButton.remove();
MastodonWebInput.remove();
MastodonLogoutButton.setAttribute("style", "");
} else {
// Auto log in
MastodonAPI.GainToken(MastodonWebsite, MastodonClientID, MastodonClientSecret, MastodonAccessToken, MastodonTokenType);
}
if ((document.location.href.split("state=").length == 1 && document.location.href.split("iss=").length == 1 && document.location.href.split("code=").length == 1) && document.cookie.split("; ").find((row) => row.startsWith(BlueskyAccessToken + "="))?.split("=").length > 1) {
// Swap the buttons
BlueskyLoginButton.remove();
BlueskyWebInput.remove();
BlueskyLogoutButton.setAttribute("style", "");
} else {
// Auto log in
BlueskyAPI.GainTokens(BlueskyPKCEverifer, BlueskyNonce, BlueskyAccessToken, BlueskyRefreshToken);
}
// Check for a bluesky token.
}
// Runs on website start.
// Remove traces of "login".
CheckLogin();

View file

@ -5,4 +5,7 @@ A frontend to accessing my account on the fediverse
## technologies
- node on a bare debian install.
- npm and npx to execute it.
- currently only uses the Mastodon V1 API.
- Uses MastodonAPI, BlueskyAPI, and TumblrAPI.
- Your local machine can also run it (with the proper dependencies; take a look at the docker file).
Quick launch server without docker: `npx http-server /home/<HomeDirectory>/Documents/Fedi.CrowdedGames.Group --port 4000 --cors`

3
oauth/README.md Normal file
View file

@ -0,0 +1,3 @@
# Bluesky stuff.
Yeah.

View file

@ -0,0 +1,19 @@
{
"client_id": "https://fedi.crowdedgames.group/oauth/client-metadata.json",
"application_type": "web",
"client_name": "Channel Viewer",
"client_uri": "https://fedi.crowdedgames.group",
"dpop_bound_access_tokens": true,
"grant_types": [
"authorization_code",
"refresh_token"
],
"redirect_uris": [
"https://fedi.crowdedgames.group/HTML/setting.html"
],
"response_types": [
"code"
],
"token_endpoint_auth_method": "none",
"scope": "atproto transition:generic"
}