diff --git a/packages/pl-api/lib/client.ts b/packages/pl-api/lib/client.ts index 3e6fc9514..c5a075ac8 100644 --- a/packages/pl-api/lib/client.ts +++ b/packages/pl-api/lib/client.ts @@ -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: { diff --git a/packages/pl-api/lib/entities/async-refresh.ts b/packages/pl-api/lib/entities/async-refresh.ts new file mode 100644 index 000000000..71291e32a --- /dev/null +++ b/packages/pl-api/lib/entities/async-refresh.ts @@ -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; + +export { asyncRefreshSchema, type AsyncRefresh }; diff --git a/packages/pl-api/lib/entities/index.ts b/packages/pl-api/lib/entities/index.ts index a2c84797c..37fec1e81 100644 --- a/packages/pl-api/lib/entities/index.ts +++ b/packages/pl-api/lib/entities/index.ts @@ -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'; diff --git a/packages/pl-api/lib/entities/instance.ts b/packages/pl-api/lib/entities/instance.ts index 9d1f7a232..13c836629 100644 --- a/packages/pl-api/lib/entities/instance.ts +++ b/packages/pl-api/lib/entities/instance.ts @@ -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 }; diff --git a/packages/pl-api/lib/request.ts b/packages/pl-api/lib/request.ts index ac89a221b..05b28eef2 100644 --- a/packages/pl-api/lib/request.ts +++ b/packages/pl-api/lib/request.ts @@ -30,6 +30,47 @@ const getNextLink = (response: Pick): string | null => const getPrevLink = (response: Pick): 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): AsyncRefreshHeader | null => { + const value = response.headers.get('mastodon-async-refresh'); + + if (!value) { + return null; + } + + const asyncRefreshHeader: Record = {}; + + 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> { 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, };