pl-api: add mastodon 4.5 stuff

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-10-31 17:56:57 +01:00
parent 87e24cb219
commit a76ad6ccae
5 changed files with 116 additions and 2 deletions

View File

@ -25,6 +25,7 @@ import {
announcementSchema,
antennaSchema,
applicationSchema,
asyncRefreshSchema,
authorizationServerMetadataSchema,
backupSchema,
bookmarkFolderSchema,
@ -95,7 +96,7 @@ import {
} from './entities';
import { coerceObject, filteredArray } from './entities/utils';
import { AKKOMA, type Features, getFeatures, GOTOSOCIAL, ICESHRIMP_NET, MITRA, PIXELFED, PLEROMA } from './features';
import request, { getNextLink, getPrevLink, type RequestBody, type RequestMeta } from './request';
import request, { getAsyncRefreshHeader, getNextLink, getPrevLink, type RequestBody, type RequestMeta } from './request';
import { buildFullPath } from './utils/url';
import type {
@ -2477,7 +2478,9 @@ class PlApiClient {
getContext: async (statusId: string, params?: GetStatusContextParams) => {
const response = await this.request(`/api/v1/statuses/${statusId}/context`, { params });
return v.parse(contextSchema, response.json);
const asyncRefreshHeader = getAsyncRefreshHeader(response);
return { asyncRefreshHeader, ...v.parse(contextSchema, response.json) } ;
},
/**
@ -3890,6 +3893,19 @@ class PlApiClient {
},
};
/** Experimental async refreshes API methods */
public readonly asyncRefreshes = {
/**
* Get Status of Async Refresh
* @see {@link https://docs.joinmastodon.org/methods/async_refreshes/#show}
*/
show: async (id: string) => {
const response = await this.request(`/api/v1_alpha/async_refreshes/${id}`);
return v.parse(asyncRefreshSchema, response.json);
},
};
public readonly admin = {
/** Perform moderation actions with accounts. */
accounts: {

View File

@ -0,0 +1,20 @@
import * as v from 'valibot';
/**
* @category Schemas
* @see {@link https://docs.joinmastodon.org/entities/AsyncRefresh/}
*/
const asyncRefreshSchema = v.object({
async_refresh: v.object({
id: v.string(),
status: v.picklist(['running', 'finished']),
result_count: v.nullable(v.number()),
}),
});
/**
* @category Entity types
*/
type AsyncRefresh = v.InferOutput<typeof asyncRefreshSchema>;
export { asyncRefreshSchema, type AsyncRefresh };

View File

@ -23,6 +23,7 @@ export * from './announcement';
export * from './announcement-reaction';
export * from './antenna';
export * from './application';
export * from './async-refresh';
export * from './authorization-server-metadata';
export * from './backup';
export * from './bookmark-folder';

View File

@ -161,6 +161,23 @@ const configurationSchema = coerceObject({
translation: coerceObject({
enabled: v.fallback(v.boolean(), false),
}),
timelines_access: coerceObject({
...v.entriesFromList(
['hashtag_feeds', 'trending_link_feeds'],
coerceObject(
v.entriesFromList(
['local', 'remote'],
v.fallback(v.picklist(['public', 'authenticated', 'disabled']), 'public'),
),
),
),
live_feeds: coerceObject(
v.entriesFromList(
['local', 'bubble', 'remote', 'wrenched'],
v.fallback(v.picklist(['public', 'authenticated', 'disabled']), 'public'),
),
),
}),
urls: coerceObject({
streaming: v.fallback(v.optional(v.pipe(v.string(), v.url())), undefined),
status: v.fallback(v.optional(v.pipe(v.string(), v.url())), undefined),
@ -358,6 +375,23 @@ const instanceSchema = v.pipe(
};
}
if (data.pleroma?.metadata?.restrict_unauthenticated?.timelines) {
const timelines = data.pleroma.metadata.restrict_unauthenticated.timelines;
const features = data.pleroma.metadata.features || [];
if (!data.configuration) data.configuration = {};
data.configuration.timelines_access = {
...data.configuration.timelines_access,
live_feeds: {
bubble: timelines.bubble ? 'authenticated' : features.includes('bubble_timeline') ? 'public' : 'disabled',
local: timelines.local ? 'authenticated' : 'public',
remote: timelines.federated ? 'authenticated' : 'public',
wrenched: timelines.wrenched ? 'authenticated' : features.includes('pleroma:wrenched_timeline') ? 'public' : 'disabled',
},
};
}
if (data.domain) return { account_domain: data.domain, ...data, api_versions: apiVersions };
return { ...instanceV1ToV2(data), api_versions: apiVersions };

View File

@ -30,6 +30,47 @@ const getNextLink = (response: Pick<Response, 'headers'>): string | null =>
const getPrevLink = (response: Pick<Response, 'headers'>): string | null =>
getLinks(response).refs.find(link => link.rel.toLocaleLowerCase() === 'prev')?.uri || null;
interface AsyncRefreshHeader {
id: string;
retry: number;
}
const isAsyncRefreshHeader = (obj: object): obj is AsyncRefreshHeader =>
'id' in obj && 'retry' in obj;
const getAsyncRefreshHeader = (response: Pick<Response, 'headers'>): AsyncRefreshHeader | null => {
const value = response.headers.get('mastodon-async-refresh');
if (!value) {
return null;
}
const asyncRefreshHeader: Record<string, unknown> = {};
value.split(/,\s*/).forEach((pair) => {
const [key, val] = pair.split('=', 2);
let typedValue: string | number;
if (key && ['id', 'retry'].includes(key) && val) {
if (val.startsWith('"')) {
typedValue = val.slice(1, -1);
} else {
typedValue = parseInt(val);
}
asyncRefreshHeader[key] = typedValue;
}
});
if (isAsyncRefreshHeader(asyncRefreshHeader)) {
return asyncRefreshHeader;
}
return null;
};
interface RequestBody<Params = Record<string, any>> {
method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
body?: any;
@ -131,9 +172,11 @@ export {
type Response,
type RequestBody,
type RequestMeta,
type AsyncRefreshHeader,
getLinks,
getNextLink,
getPrevLink,
getAsyncRefreshHeader,
request,
request as default,
};