Rudimentary error reporting

This commit is contained in:
Natty 2024-05-17 14:25:19 +02:00
parent 5ae5c2f8d6
commit 1ea9d65571
No known key found for this signature in database
GPG key ID: BF6CB659ADEE60EC

View file

@ -18,10 +18,13 @@ async function apiRequestWithHeaders(url, options = null) {
throw new Error(`Error fetching ${url}: ${response.status} ${response.statusText}`); throw new Error(`Error fetching ${url}: ${response.status} ${response.statusText}`);
}) })
.then(async response => ({ headers: response.headers, body: await response.json() })) .then(async response => ({ response: { headers: response.headers, body: await response.json(), error: undefined }}))
.catch(error => { .catch(error => {
console.error(`Error fetching ${url}: ${error}`); console.error(`Error fetching ${url}:`, error);
return null; return {
response: undefined,
error: `Error fetching ${url}: ${error}`
};
}); });
} }
@ -32,7 +35,16 @@ async function apiRequestWithHeaders(url, options = null) {
*/ */
async function apiRequest(url, options = null) { async function apiRequest(url, options = null) {
const reply = await apiRequestWithHeaders(url, options); const reply = await apiRequestWithHeaders(url, options);
return reply?.body;
if (!reply.response) {
return {
error: reply.error
};
}
return {
response: reply.response.body
};
} }
function Handle(name, instance) { function Handle(name, instance) {
@ -80,7 +92,7 @@ Handle.prototype.webFinger = async function () {
resource: `acct:${this}` resource: `acct:${this}`
}); });
let webFinger = await apiRequest(defaultWebfingerUrl); let { response: webFinger } = await apiRequest(defaultWebfingerUrl);
if (!webFinger) { if (!webFinger) {
const contentTypeXrd = "application/xrd+xml"; const contentTypeXrd = "application/xrd+xml";
@ -104,7 +116,7 @@ Handle.prototype.webFinger = async function () {
?.getAttribute("template"); ?.getAttribute("template");
if (webfingerTemplate) if (webfingerTemplate)
webFinger = await apiRequest(webfingerTemplate.replace("{uri}", `acct:${this}`)); webFinger = (await apiRequest(webfingerTemplate.replace("{uri}", `acct:${this}`)))?.response;
} }
if (!webFinger) if (!webFinger)
@ -204,7 +216,7 @@ class ApiClient {
} }
let url = `https://${instance}/.well-known/nodeinfo`; let url = `https://${instance}/.well-known/nodeinfo`;
let nodeInfo = await apiRequest(url); let { response: nodeInfo } = await apiRequest(url);
if (!nodeInfo || !Array.isArray(nodeInfo.links)) { if (!nodeInfo || !Array.isArray(nodeInfo.links)) {
const client = new MastodonApiClient(instance, true); const client = new MastodonApiClient(instance, true);
@ -226,11 +238,11 @@ class ApiClient {
return client; return client;
} }
let apiResponse = await apiRequest(apiLink.href); let { response: apiResponse } = await apiRequest(apiLink.href);
if (!apiResponse) { if (!apiResponse) {
// Guess from API endpoints // Guess from API endpoints
const misskeyMeta = await apiRequest(`https://${instance}/api/meta`, { const { response: misskeyMeta } = await apiRequest(`https://${instance}/api/meta`, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
@ -270,7 +282,7 @@ class ApiClient {
return client; return client;
} }
let features = apiResponse?.metadata?.features; let features = apiResponse?.metadata?.["features"];
if (Array.isArray(features) && features.includes("pleroma_api")) { if (Array.isArray(features) && features.includes("pleroma_api")) {
const has_emoji_reacts = features.includes("pleroma_emoji_reactions"); const has_emoji_reacts = features.includes("pleroma_emoji_reactions");
const client = new PleromaApiClient(instance, has_emoji_reacts); const client = new PleromaApiClient(instance, has_emoji_reacts);
@ -286,14 +298,14 @@ class ApiClient {
/** /**
* @param {Handle} handle * @param {Handle} handle
* *
* @returns {Promise<FediUser>} * @returns {Promise<{ response: FediUser, error: undefined } | { response: undefined, error: string }>}
*/ */
async getUserIdFromHandle(handle){ throw new Error("Not implemented"); } async getUserIdFromHandle(handle){ throw new Error("Not implemented"); }
/** /**
* @param {FediUser} user * @param {FediUser} user
* *
* @returns {Promise<Note[]>} * @returns {Promise<{ response: Note[], error: undefined } | { response: undefined, error: string }>}
*/ */
async getNotes(user){ throw new Error("Not implemented"); } async getNotes(user){ throw new Error("Not implemented"); }
@ -427,9 +439,13 @@ class MastodonApiClient extends ApiClient {
let remaining = targetCount; let remaining = targetCount;
let data = []; let data = [];
while (remaining > 0 && nextUrl !== null) { while (remaining > 0 && nextUrl !== null) {
const reply = await apiRequestWithHeaders(nextUrl); const { response: reply, error } = await apiRequestWithHeaders(nextUrl);
if (reply?.body === null) { if (reply?.body == null || error) {
console.error(`Error while gathering entries. Returning incomplete data!`); console.error(`Error while gathering entries. Returning incomplete data!`);
if (data.length === 0) {
return { error };
}
break; break;
} }
nextUrl = MastodonApiClient.getNextPage(reply.headers); nextUrl = MastodonApiClient.getNextPage(reply.headers);
@ -443,37 +459,45 @@ class MastodonApiClient extends ApiClient {
break; break;
} }
return data.length === 0 ? null : data.flat(); return { response: data.length === 0 ? null : data.flat() };
} }
async getUserIdFromHandle(handle) { async getUserIdFromHandle(handle) {
const url = `https://${this._instance}/api/v1/accounts/lookup?acct=${handle.baseHandle}`; const url = `https://${this._instance}/api/v1/accounts/lookup?acct=${handle.baseHandle}`;
let response = await apiRequest(url, null); let { response } = await apiRequest(url, null);
if (!response) { if (!response) {
const url = `https://${this._instance}/api/v1/accounts/lookup?acct=${handle}`; const url = `https://${this._instance}/api/v1/accounts/lookup?acct=${handle}`;
response = await apiRequest(url, null);
const { response: res, error } = (await apiRequest(url, null));
if (error) {
return { error };
} }
if (!response) { response = res;
return null; }
if (typeof response !== "object") {
return { error: `Could not parse user lookup response from server, invalid object: ${response}` };
} }
return { return {
response: {
id: response.id, id: response.id,
avatar: response.avatar, avatar: response.avatar,
bot: response.bot, bot: response.bot,
name: response["display_name"], name: response["display_name"],
handle: handle, handle: handle,
}
}; };
} }
async getNotes(user) { async getNotes(user) {
const url = `https://${this._instance}/api/v1/accounts/${user.id}/statuses?exclude_replies=true&exclude_reblogs=true&limit=${this._API_LIMIT_SMALL}`; const url = `https://${this._instance}/api/v1/accounts/${user.id}/statuses?exclude_replies=true&exclude_reblogs=true&limit=${this._API_LIMIT_SMALL}`;
const response = await MastodonApiClient.apiRequestPaged(url, this._CNT_NOTES, this._API_LIMIT_SMALL, true); const { response, error} = await MastodonApiClient.apiRequestPaged(url, this._CNT_NOTES, this._API_LIMIT_SMALL, true);
if (!response) { if (error) {
return null; return { error };
} }
if (response?.some(note => note?.["pleroma"]?.["emoji_reactions"]?.length)) { if (response?.some(note => note?.["pleroma"]?.["emoji_reactions"]?.length)) {
@ -482,7 +506,8 @@ class MastodonApiClient extends ApiClient {
this._flavor = MastodonFlavor.FEDIBIRD; this._flavor = MastodonFlavor.FEDIBIRD;
} }
return response.map(note => ({ return {
response: response.map(note => ({
id: note.id, id: note.id,
replies: note["replies_count"] || 0, replies: note["replies_count"] || 0,
renotes: note["reblogs_count"] || 0, renotes: note["reblogs_count"] || 0,
@ -490,7 +515,8 @@ class MastodonApiClient extends ApiClient {
extra_reacts: note?.["emoji_reactions"]?.length > 0 || note?.["pleroma"]?.["emoji_reactions"]?.length > 0, extra_reacts: note?.["emoji_reactions"]?.length > 0 || note?.["pleroma"]?.["emoji_reactions"]?.length > 0,
instance: this._instance, instance: this._instance,
author: user author: user
})); }))
};
} }
async getRenotes(note) { async getRenotes(note) {
@ -513,7 +539,7 @@ class MastodonApiClient extends ApiClient {
async getReplies(noteIn) { async getReplies(noteIn) {
// The context endpoint has no limit parameter or pages // The context endpoint has no limit parameter or pages
const url = `https://${this._instance}/api/v1/statuses/${noteIn.id}/context`; const url = `https://${this._instance}/api/v1/statuses/${noteIn.id}/context`;
const response = await apiRequest(url); const response = (await apiRequest(url)).response;
if (!response) { if (!response) {
return null; return null;
@ -549,7 +575,7 @@ class MastodonApiClient extends ApiClient {
async getFavs(note) { async getFavs(note) {
const url = `https://${this._instance}/api/v1/statuses/${note.id}/favourited_by?limit=${this._API_LIMIT}`; const url = `https://${this._instance}/api/v1/statuses/${note.id}/favourited_by?limit=${this._API_LIMIT}`;
const response = await MastodonApiClient.apiRequestPaged(url, this._CNT_FAVS, this._API_LIMIT); const response = (await MastodonApiClient.apiRequestPaged(url, this._CNT_FAVS, this._API_LIMIT)).response;
if (!response) { if (!response) {
return null; return null;
@ -592,7 +618,7 @@ class PleromaApiClient extends MastodonApiClient {
// The documentation doesn't specify the hardcoded limit, so just use the lowest known one // The documentation doesn't specify the hardcoded limit, so just use the lowest known one
const url = `https://${this._instance}/api/v1/pleroma/statuses/${note.id}/reactions?limit=${this._API_LIMIT_SMALL}`; const url = `https://${this._instance}/api/v1/pleroma/statuses/${note.id}/reactions?limit=${this._API_LIMIT_SMALL}`;
const response = await MastodonApiClient.apiRequestPaged(url, this._CNT_FAVS, this._API_LIMIT_SMALL) ?? []; const response = (await MastodonApiClient.apiRequestPaged(url, this._CNT_FAVS, this._API_LIMIT_SMALL)).response ?? [];
/** /**
* @type {Map<string, FediUser>} * @type {Map<string, FediUser>}
@ -642,7 +668,7 @@ class FedibirdApiClient extends MastodonApiClient {
// Could not locate documentation for Fedibird API, so just use the lowest known limit // Could not locate documentation for Fedibird API, so just use the lowest known limit
const url = `https://${this._instance}/api/v1/statuses/${note.id}/emoji_reactioned_by?limit=${this._API_LIMIT_SMALL}`; const url = `https://${this._instance}/api/v1/statuses/${note.id}/emoji_reactioned_by?limit=${this._API_LIMIT_SMALL}`;
const response = await MastodonApiClient.apiRequestPaged(url, this._CNT_FAVS, this._API_LIMIT_SMALL) ?? []; const response = (await MastodonApiClient.apiRequestPaged(url, this._CNT_FAVS, this._API_LIMIT_SMALL)).response ?? [];
for (const reaction of response) { for (const reaction of response) {
let account = reaction["account"]; let account = reaction["account"];
@ -682,7 +708,7 @@ class MisskeyApiClient extends ApiClient {
async getUserIdFromHandle(handle) { async getUserIdFromHandle(handle) {
const lookupUrl = `https://${this._instance}/api/users/search-by-username-and-host`; const lookupUrl = `https://${this._instance}/api/users/search-by-username-and-host`;
const lookup = await apiRequest(lookupUrl, { const lookup = (await apiRequest(lookupUrl, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
@ -691,7 +717,7 @@ class MisskeyApiClient extends ApiClient {
username: handle.name, username: handle.name,
host: null host: null
} }
}); })).response;
let id = null; let id = null;
@ -707,7 +733,7 @@ class MisskeyApiClient extends ApiClient {
} }
const url = `https://${this._instance}/api/users/show`; const url = `https://${this._instance}/api/users/show`;
const response = await apiRequest(url, { const { response, error } = await apiRequest(url, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
@ -718,22 +744,24 @@ class MisskeyApiClient extends ApiClient {
} }
}); });
if (!response) { if (error) {
return null; return { error };
} }
return { return {
response: {
id: response.id, id: response.id,
avatar: response["avatarUrl"], avatar: response["avatarUrl"],
bot: response["isBot"], bot: response["isBot"],
name: response["name"], name: response["name"],
handle: handle, handle: handle,
}
}; };
} }
async getNotes(user) { async getNotes(user) {
const url = `https://${this._instance}/api/users/notes`; const url = `https://${this._instance}/api/users/notes`;
const response = await apiRequest(url, { const { response, error } = await apiRequest(url, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
@ -748,11 +776,12 @@ class MisskeyApiClient extends ApiClient {
} }
}); });
if (!response) { if (error) {
return null; return { error };
} }
return response.map(note => ({ return {
response: response.map(note => ({
id: note.id, id: note.id,
replies: note["repliesCount"], replies: note["repliesCount"],
renotes: note["renoteCount"], renotes: note["renoteCount"],
@ -760,12 +789,13 @@ class MisskeyApiClient extends ApiClient {
extra_reacts: false, extra_reacts: false,
instance: this._instance, instance: this._instance,
author: user author: user
})); }))
};
} }
async getRenotes(note) { async getRenotes(note) {
const url = `https://${this._instance}/api/notes/renotes`; const url = `https://${this._instance}/api/notes/renotes`;
const response = await apiRequest(url, { const response = (await apiRequest(url, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
@ -774,7 +804,7 @@ class MisskeyApiClient extends ApiClient {
noteId: note.id, noteId: note.id,
limit: this._CNT_RENOTES, limit: this._CNT_RENOTES,
} }
}); })).response;
if (!response) { if (!response) {
return null; return null;
@ -791,7 +821,7 @@ class MisskeyApiClient extends ApiClient {
async getReplies(note) { async getReplies(note) {
const url = `https://${this._instance}/api/notes/replies`; const url = `https://${this._instance}/api/notes/replies`;
const response = await apiRequest(url, { const response = (await apiRequest(url, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
@ -800,7 +830,7 @@ class MisskeyApiClient extends ApiClient {
noteId: note.id, noteId: note.id,
limit: this._CNT_REPLIES, limit: this._CNT_REPLIES,
} }
}); })).response;
if (!response) { if (!response) {
return null; return null;
@ -829,7 +859,7 @@ class MisskeyApiClient extends ApiClient {
async getFavs(note) { async getFavs(note) {
const url = `https://${this._instance}/api/notes/reactions`; const url = `https://${this._instance}/api/notes/reactions`;
const response = await apiRequest(url, { const response = (await apiRequest(url, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
@ -838,7 +868,7 @@ class MisskeyApiClient extends ApiClient {
noteId: note.id, noteId: note.id,
limit: this._CNT_FAVS, limit: this._CNT_FAVS,
} }
}); })).response;
if (!response) { if (!response) {
return null; return null;
@ -921,10 +951,10 @@ async function circleMain() {
progress.innerText = "Fetching your user..."; progress.innerText = "Fetching your user...";
const user = await client.getUserIdFromHandle(selfUser); const { response: user, error: userError } = await client.getUserIdFromHandle(selfUser);
if (!user) { if (userError) {
alert("Something went horribly wrong, couldn't fetch your user."); alert(`Something went horribly wrong, couldn't fetch your user:\n\n${userError}`);
fediHandle.disabled = false; fediHandle.disabled = false;
for (const radio of backend) { for (const radio of backend) {
radio.disabled = false; radio.disabled = false;
@ -937,10 +967,10 @@ async function circleMain() {
progress.innerText = "Fetching your latest posts..."; progress.innerText = "Fetching your latest posts...";
const notes = await client.getNotes(user); const { response: notes, error: noteError } = await client.getNotes(user);
if (!notes) { if (noteError) {
alert("Something went horribly wrong, couldn't fetch your notes."); alert(`Something went horribly wrong, couldn't fetch your notes:\n\n${noteError}`);
return; return;
} }