From 5e1ef0854842ca8a6993491723a01fe8951d8111 Mon Sep 17 00:00:00 2001 From: hazycora Date: Thu, 31 Aug 2023 10:58:55 -0500 Subject: [PATCH 1/4] Implement delegated domain check --- create-circle.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/create-circle.js b/create-circle.js index ca65ec8..8ce0d6d 100644 --- a/create-circle.js +++ b/create-circle.js @@ -21,6 +21,7 @@ async function apiRequest(url, options = null) /** * @typedef {{ + * handle: string, * name: string, * instance: string, * }} Handle @@ -189,7 +190,7 @@ class MastodonApiClient extends ApiClient { } async getUserIdFromHandle(handle) { - const url = `https://${this._instance}/api/v1/accounts/lookup?acct=${handle.name}@${handle.instance}`; + const url = `https://${this._instance}/api/v1/accounts/lookup?acct=${handle.handle}`; const response = await apiRequest(url, null); if (!response) { @@ -547,11 +548,25 @@ function parseHandle(fediHandle, fallbackInstance = "") { const [name, instance] = fediHandle.split("@", 2); return { + handle: fediHandle, name: name, instance: instance || fallbackInstance, }; } +/** + * @typedef @param {Handle} handle + * @returns {Promise} instance + */ +async function getDelegateInstance(handle) { + // We're checking webfinger to see which URL is for the user, + // since that may be on a different domain than the webfinger request + const response = await apiRequest(`https://${handle.instance}/.well-known/webfinger?resource=acct:${handle.handle}`) + const selfLink = response.links.find(link => link.rel == 'self') + const url = new URL(selfLink.href) + return url.hostname; +} + /** * @typedef {FediUser & {conStrength: number}} RatedUser */ @@ -566,6 +581,8 @@ async function circleMain() { let fediHandle = document.getElementById("txt_mastodon_handle"); const selfUser = parseHandle(fediHandle.value); + selfUser.instance = await getDelegateInstance(selfUser) + let form = document.getElementById("generateForm"); let backend = form.backend; for (const radio of backend) { From d3299af352d353d40f86bdce90a045af4aef4228 Mon Sep 17 00:00:00 2001 From: hazycora Date: Thu, 31 Aug 2023 12:07:23 -0500 Subject: [PATCH 2/4] handle inputs for non-delegated usernames as well --- create-circle.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/create-circle.js b/create-circle.js index 8ce0d6d..4420eff 100644 --- a/create-circle.js +++ b/create-circle.js @@ -556,15 +556,19 @@ function parseHandle(fediHandle, fallbackInstance = "") { /** * @typedef @param {Handle} handle - * @returns {Promise} instance + * @returns {Promise} */ -async function getDelegateInstance(handle) { +async function getDelegatedHandle(handle) { // We're checking webfinger to see which URL is for the user, // since that may be on a different domain than the webfinger request const response = await apiRequest(`https://${handle.instance}/.well-known/webfinger?resource=acct:${handle.handle}`) const selfLink = response.links.find(link => link.rel == 'self') const url = new URL(selfLink.href) - return url.hostname; + handle.instance = url.hostname; + // If a user inputs @h@social.besties.house but their handle is actually @h@besties.house, + // we want to use the latter + handle.handle = response.subject?.replace(/^acct:/, '') ?? handle.handle; + return handle } /** @@ -579,9 +583,8 @@ async function circleMain() { generateBtn.style.display = "none"; let fediHandle = document.getElementById("txt_mastodon_handle"); - const selfUser = parseHandle(fediHandle.value); - selfUser.instance = await getDelegateInstance(selfUser) + const selfUser = await getDelegatedHandle(parseHandle(fediHandle.value)); let form = document.getElementById("generateForm"); let backend = form.backend; From ea3e389d018a4b69a7b8479760a8f61921de1773 Mon Sep 17 00:00:00 2001 From: Natty Date: Thu, 31 Aug 2023 19:27:54 +0200 Subject: [PATCH 3/4] Turned Handle into a class and implemented WebFinger with a fall-back --- create-circle.js | 129 ++++++++++++++++++++++++++++++++++------------- image.js | 2 +- 2 files changed, 94 insertions(+), 37 deletions(-) diff --git a/create-circle.js b/create-circle.js index 4420eff..041823c 100644 --- a/create-circle.js +++ b/create-circle.js @@ -19,13 +19,89 @@ async function apiRequest(url, options = null) }); } +function Handle(name, instance) { + let handleObj = Object.create(Handle.prototype); + handleObj.name = name; + handleObj.instance = instance; + handleObj._baseInstance = null; + handleObj._apiInstance = null; + + return handleObj; +} + +Object.defineProperty(Handle.prototype, "baseInstance", { + get: function () { + return this._baseInstance || this.instance; + } +}); + +Object.defineProperty(Handle.prototype, "apiInstance", { + get: function () { + return this._apiInstance || this.instance; + } +}); + +Object.defineProperty(Handle.prototype, "baseHandle", { + get: function () { + return this.name + "@" + this.baseInstance; + } +}); + +Handle.prototype.toString = function () { + return this.name + "@" + this.instance; +}; + /** - * @typedef {{ - * handle: string, - * name: string, - * instance: string, - * }} Handle + * @returns {Promise} The handle WebFingered, or the original on fail */ +Handle.prototype.webFinger = async function () { + if (this._baseInstance) { + return this; + } + + let url = `https://${this.instance}/.well-known/webfinger?` + new URLSearchParams({ + resource: `acct:${this}` + }); + + let webFinger = await apiRequest(url); + + if (!webFinger) + return this; + + let acct = webFinger["subject"]; + + if (typeof acct !== "string") + return this; + + if (acct.startsWith("acct:")) { + acct = acct.substring("acct:".length); + } + + let baseHandle = parseHandle(acct); + baseHandle._baseInstance = baseHandle.instance; + baseHandle.instance = this.instance; + + const links = webFinger["links"]; + + if (!Array.isArray(links)) { + return baseHandle; + } + + const selfLink = links.find(link => link["rel"] === "self"); + if (!selfLink) { + return baseHandle; + } + + try { + const url = new URL(selfLink["href"]) + baseHandle._apiInstance = url.hostname; + } catch (e) { + console.error(`Error parsing WebFinger self link ${selfLink["href"]}: ${e}`); + } + + return baseHandle; +}; + /** * @typedef {{ @@ -190,7 +266,7 @@ class MastodonApiClient extends ApiClient { } async getUserIdFromHandle(handle) { - const url = `https://${this._instance}/api/v1/accounts/lookup?acct=${handle.handle}`; + const url = `https://${this._instance}/api/v1/accounts/lookup?acct=${handle.baseHandle}`; const response = await apiRequest(url, null); if (!response) { @@ -374,8 +450,11 @@ class MisskeyApiClient extends ApiClient { let id = null; for (const user of Array.isArray(lookup) ? lookup : []) { - if ((user["host"] === handle.instance || this._instance === handle.instance && user["host"] === null) - && user["username"] === handle.name) { + const isLocal = user?.["host"] === handle.instance || + user?.["host"] === handle.baseInstance || + this._instance === handle.apiInstance && user?.["host"] === null; + + if (isLocal && user?.["username"] === handle.name && user["id"]) { id = user["id"]; break; } @@ -547,28 +626,7 @@ function parseHandle(fediHandle, fallbackInstance = "") { fediHandle = fediHandle.replaceAll(" ", ""); const [name, instance] = fediHandle.split("@", 2); - return { - handle: fediHandle, - name: name, - instance: instance || fallbackInstance, - }; -} - -/** - * @typedef @param {Handle} handle - * @returns {Promise} - */ -async function getDelegatedHandle(handle) { - // We're checking webfinger to see which URL is for the user, - // since that may be on a different domain than the webfinger request - const response = await apiRequest(`https://${handle.instance}/.well-known/webfinger?resource=acct:${handle.handle}`) - const selfLink = response.links.find(link => link.rel == 'self') - const url = new URL(selfLink.href) - handle.instance = url.hostname; - // If a user inputs @h@social.besties.house but their handle is actually @h@besties.house, - // we want to use the latter - handle.handle = response.subject?.replace(/^acct:/, '') ?? handle.handle; - return handle + return new Handle(name, instance || fallbackInstance); } /** @@ -583,8 +641,7 @@ async function circleMain() { generateBtn.style.display = "none"; let fediHandle = document.getElementById("txt_mastodon_handle"); - - const selfUser = await getDelegatedHandle(parseHandle(fediHandle.value)); + const selfUser = await parseHandle(fediHandle.value).webFinger(); let form = document.getElementById("generateForm"); let backend = form.backend; @@ -597,17 +654,17 @@ async function circleMain() { let client; switch (backend.value) { case "mastodon": - client = new MastodonApiClient(selfUser.instance); + client = new MastodonApiClient(selfUser.apiInstance); break; case "pleroma": - client = new PleromaApiClient(selfUser.instance, true); + client = new PleromaApiClient(selfUser.apiInstance, true); break; case "misskey": - client = new MisskeyApiClient(selfUser.instance); + client = new MisskeyApiClient(selfUser.apiInstance); break; default: progress.innerText = "Detecting instance..."; - client = await ApiClient.getClient(selfUser.instance); + client = await ApiClient.getClient(selfUser.apiInstance); backend.value = client.getClientName(); break; } diff --git a/image.js b/image.js index 70a08f6..289cace 100644 --- a/image.js +++ b/image.js @@ -50,7 +50,7 @@ function render(users, selfUser) { centerX - radius[layerIndex], centerY - radius[layerIndex], radius[layerIndex], - "@" + users[userNum].handle.name + "@" + users[userNum].handle.instance + "@" + users[userNum].handle ); userNum++; From 6d80fa565b010fd2d2cb761c412de61d233a58b6 Mon Sep 17 00:00:00 2001 From: Natty Date: Thu, 31 Aug 2023 19:48:45 +0200 Subject: [PATCH 4/4] Gracefully fall-back to the tag handle for accounts that do not pass the first lookup --- create-circle.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/create-circle.js b/create-circle.js index 041823c..bbea8bc 100644 --- a/create-circle.js +++ b/create-circle.js @@ -12,6 +12,13 @@ async function apiRequest(url, options = null) } return await fetch(url, options ?? {}) + .then(response => { + if (response.ok) { + return response; + } + + throw new Error(`Error fetching ${url}: ${response.status} ${response.statusText}`); + }) .then(response => response.json()) .catch(error => { console.error(`Error fetching ${url}: ${error}`); @@ -267,7 +274,12 @@ class MastodonApiClient extends ApiClient { async getUserIdFromHandle(handle) { const url = `https://${this._instance}/api/v1/accounts/lookup?acct=${handle.baseHandle}`; - const response = await apiRequest(url, null); + let response = await apiRequest(url, null); + + if (!response) { + const url = `https://${this._instance}/api/v1/accounts/lookup?acct=${handle}`; + response = await apiRequest(url, null); + } if (!response) { return null;