mirror of
https://github.com/AMNatty/Mastodon-Circles.git
synced 2024-11-21 18:47:26 -07:00
Implement Mastodon request pagination
Now the same information depth can be processed on all client variants.
This commit is contained in:
parent
79e358ccb1
commit
473be10607
1 changed files with 98 additions and 12 deletions
110
create-circle.js
110
create-circle.js
|
@ -3,7 +3,7 @@
|
||||||
* @param {RequestInfo | URL} url
|
* @param {RequestInfo | URL} url
|
||||||
* @param {{ body?: any } & RequestInit?} options
|
* @param {{ body?: any } & RequestInit?} options
|
||||||
*/
|
*/
|
||||||
async function apiRequest(url, options = null)
|
async function apiRequestWithHeaders(url, options = null)
|
||||||
{
|
{
|
||||||
console.log(`Fetching :: ${url}`);
|
console.log(`Fetching :: ${url}`);
|
||||||
|
|
||||||
|
@ -19,13 +19,28 @@ async function apiRequest(url, options = null)
|
||||||
|
|
||||||
throw new Error(`Error fetching ${url}: ${response.status} ${response.statusText}`);
|
throw new Error(`Error fetching ${url}: ${response.status} ${response.statusText}`);
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => new Promise((resolve, reject) => {
|
||||||
|
response.json()
|
||||||
|
.then(rbody => resolve({headers: response.headers, body: rbody}))
|
||||||
|
.catch(error => reject(error))
|
||||||
|
}))
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error(`Error fetching ${url}: ${error}`);
|
console.error(`Error fetching ${url}: ${error}`);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {RequestInfo | URL} url
|
||||||
|
* @param {{ body?: any } & RequestInit?} options
|
||||||
|
*/
|
||||||
|
async function apiRequest(url, options = null)
|
||||||
|
{
|
||||||
|
const reply = await apiRequestWithHeaders(url, options);
|
||||||
|
return reply?.body;
|
||||||
|
}
|
||||||
|
|
||||||
function Handle(name, instance) {
|
function Handle(name, instance) {
|
||||||
let handleObj = Object.create(Handle.prototype);
|
let handleObj = Object.create(Handle.prototype);
|
||||||
handleObj.name = name;
|
handleObj.name = name;
|
||||||
|
@ -338,6 +353,74 @@ class MastodonApiClient extends ApiClient {
|
||||||
super(instance);
|
super(instance);
|
||||||
this._emoji_reacts = emoji_reacts;
|
this._emoji_reacts = emoji_reacts;
|
||||||
this._flavor = flavor;
|
this._flavor = flavor;
|
||||||
|
// Server-side hard limits on return items; varies per endpoint
|
||||||
|
this._API_LIMIT = 80;
|
||||||
|
this._API_LIMIT_SMALL = 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Headers} headers
|
||||||
|
* @return {URL?} request URL for next page or null
|
||||||
|
*/
|
||||||
|
static getNextPage(headers)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* https://docs.joinmastodon.org/api/guidelines/#pagination
|
||||||
|
*
|
||||||
|
* Not explicitly documented in the page linked above, but
|
||||||
|
* - the next page will automatically use the same limit as the original request
|
||||||
|
* (tested with Mastodon 4.2.1 and Akkoma 3.10.3)
|
||||||
|
* - the last page can sometimes still contain a next/prev link, but this "next" page
|
||||||
|
* will then be empty and not contain any Link header (e.g. Akkoma 3.10.3 with statuses)
|
||||||
|
* To save on API requests, we can check if less than expected were returned
|
||||||
|
*/
|
||||||
|
const links = headers.get("Link");
|
||||||
|
if (links === null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
for (const link of links.split(",")) {
|
||||||
|
const p = link.split(";").map(s => s.trim());
|
||||||
|
if (p.length == 2 && p[1] === 'rel="next"') {
|
||||||
|
// Remove enclosing angle brackets <...>
|
||||||
|
return p[0].substring(1, p[0].length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {RequestInfo | URL} url
|
||||||
|
* @param {number} targetCount how many entries to gather
|
||||||
|
* @param {number?} requestLimit how many entries a single request is expected to return.
|
||||||
|
* If set will be used to detect end of data early, without needing to request an empty page.
|
||||||
|
* @param {boolean} exactTarget if true, discard entries exceeding targetCount
|
||||||
|
*/
|
||||||
|
static async apiRequestPaged(url, targetCount, requestLimit = null, exactTarget = false)
|
||||||
|
{
|
||||||
|
console.log(`Fetching repeatedly (${targetCount} a ${requestLimit}) :: ${url}`);
|
||||||
|
|
||||||
|
let nextUrl = url;
|
||||||
|
let remaining = targetCount;
|
||||||
|
let data = [];
|
||||||
|
while (remaining > 0 && nextUrl !== null) {
|
||||||
|
const reply = await apiRequestWithHeaders(nextUrl);
|
||||||
|
if (reply?.body === null) {
|
||||||
|
console.error(`Error while gathering entries. Returning incomplete data!`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
nextUrl = MastodonApiClient.getNextPage(reply.headers);
|
||||||
|
let newdata = reply.body;
|
||||||
|
if (exactTarget && newdata.length > remaining)
|
||||||
|
newdata = newdata.slice(0, remaining);
|
||||||
|
|
||||||
|
data.push(newdata);
|
||||||
|
remaining -= newdata.length;
|
||||||
|
if (newdata.length < requestLimit)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.length === 0 ? null : data.flat();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserIdFromHandle(handle) {
|
async getUserIdFromHandle(handle) {
|
||||||
|
@ -363,8 +446,8 @@ class MastodonApiClient extends ApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNotes(user) {
|
async getNotes(user) {
|
||||||
const url = `https://${this._instance}/api/v1/accounts/${user.id}/statuses?exclude_replies=true&exclude_reblogs=true&limit=40`;
|
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 apiRequest(url, null);
|
const response = await MastodonApiClient.apiRequestPaged(url, this._CNT_NOTES, this._API_LIMIT_SMALL, true);
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -388,8 +471,8 @@ class MastodonApiClient extends ApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRenotes(note) {
|
async getRenotes(note) {
|
||||||
const url = `https://${this._instance}/api/v1/statuses/${note.id}/reblogged_by`;
|
const url = `https://${this._instance}/api/v1/statuses/${note.id}/reblogged_by?limit=${this._API_LIMIT}`;
|
||||||
const response = await apiRequest(url);
|
const response = await MastodonApiClient.apiRequestPaged(url, this._CNT_RENOTES, this._API_LIMIT);
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -405,6 +488,7 @@ class MastodonApiClient extends ApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getReplies(noteIn) {
|
async getReplies(noteIn) {
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
@ -441,8 +525,8 @@ class MastodonApiClient extends ApiClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFavs(note) {
|
async getFavs(note) {
|
||||||
const url = `https://${this._instance}/api/v1/statuses/${note.id}/favourited_by`;
|
const url = `https://${this._instance}/api/v1/statuses/${note.id}/favourited_by?limit=${this._API_LIMIT}`;
|
||||||
const response = await apiRequest(url);
|
const response = await MastodonApiClient.apiRequestPaged(url, this._CNT_FAVS, this._API_LIMIT);
|
||||||
|
|
||||||
if (!response) {
|
if (!response) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -483,8 +567,9 @@ class PleromaApiClient extends MastodonApiClient {
|
||||||
if (!this._emoji_reacts)
|
if (!this._emoji_reacts)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
const url = `https://${this._instance}/api/v1/pleroma/statuses/${note.id}/reactions`;
|
// The documentation doesn't specify the hardcoded limit, so just use the lowest known one
|
||||||
const response = await apiRequest(url) ?? [];
|
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) ?? [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {Map<string, FediUser>}
|
* @type {Map<string, FediUser>}
|
||||||
|
@ -532,8 +617,9 @@ class FedibirdApiClient extends MastodonApiClient {
|
||||||
*/
|
*/
|
||||||
let users = new Map();
|
let users = new Map();
|
||||||
|
|
||||||
const url = `https://${this._instance}/api/v1/statuses/${note.id}/emoji_reactioned_by`;
|
// Could not locate documentation for Fedibird API, so just use lowest known limit
|
||||||
const response = await apiRequest(url) ?? [];
|
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) ?? [];
|
||||||
|
|
||||||
for (const reaction of response) {
|
for (const reaction of response) {
|
||||||
let account = reaction["account"];
|
let account = reaction["account"];
|
||||||
|
|
Loading…
Reference in a new issue