pl-api: Mostly finish migration to valibot
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
6633b28645
commit
2f7e149f75
44 changed files with 457 additions and 394 deletions
|
@ -227,7 +227,7 @@ class PlApiClient {
|
|||
}
|
||||
}
|
||||
|
||||
#paginatedGet = async <T extends v.BaseSchema<any, any, any>>(input: URL | RequestInfo, body: RequestBody, schema: T): Promise<PaginatedResponse<v.InferOutput<T>>> => {
|
||||
#paginatedGet = async <T>(input: URL | RequestInfo, body: RequestBody, schema: v.BaseSchema<any, T, v.BaseIssue<unknown>>): Promise<PaginatedResponse<T>> => {
|
||||
const getMore = (input: string | null) => input ? async () => {
|
||||
const response = await this.request(input);
|
||||
|
||||
|
@ -2441,7 +2441,7 @@ class PlApiClient {
|
|||
const enqueue = (fn: () => any) => ws.readyState === WebSocket.CONNECTING ? queue.push(fn) : fn();
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const message = streamingEventSchema.parse(JSON.parse(event.data as string));
|
||||
const message = v.parse(streamingEventSchema, JSON.parse(event.data as string));
|
||||
|
||||
listeners.filter(({ listener, stream }) => (!stream || message.stream.includes(stream)) && listener(message));
|
||||
};
|
||||
|
@ -2687,7 +2687,7 @@ class PlApiClient {
|
|||
response = await this.request('/api/v1/instance');
|
||||
}
|
||||
|
||||
const instance = v.parse(instanceSchema.readonly(), response.json);
|
||||
const instance = v.parse(v.pipe(instanceSchema, v.readonly()), response.json);
|
||||
this.#setInstance(instance);
|
||||
|
||||
return instance;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import * as v from 'valibot';
|
||||
|
||||
import { directoryCategorySchema, directoryLanguageSchema, directoryServerSchema, directoryStatisticsPeriodSchema } from './entities';
|
||||
import { filteredArray } from './entities/utils';
|
||||
import request from './request';
|
||||
|
@ -23,25 +25,25 @@ class PlApiDirectoryClient {
|
|||
async getStatistics() {
|
||||
const response = await this.request('/statistics');
|
||||
|
||||
return filteredArray(directoryStatisticsPeriodSchema).parse(response.json);
|
||||
return v.parse(filteredArray(directoryStatisticsPeriodSchema), response.json);
|
||||
}
|
||||
|
||||
async getCategories(params?: Params) {
|
||||
const response = await this.request('/categories', { params });
|
||||
|
||||
return filteredArray(directoryCategorySchema).parse(response.json);
|
||||
return v.parse(filteredArray(directoryCategorySchema), response.json);
|
||||
}
|
||||
|
||||
async getLanguages(params?: Params) {
|
||||
const response = await this.request('/categories', { params });
|
||||
|
||||
return filteredArray(directoryLanguageSchema).parse(response.json);
|
||||
return v.parse(filteredArray(directoryLanguageSchema), response.json);
|
||||
}
|
||||
|
||||
async getServers(params?: Params) {
|
||||
const response = await this.request('/servers', { params });
|
||||
|
||||
return filteredArray(directoryServerSchema).parse(response.json);
|
||||
return v.parse(filteredArray(directoryServerSchema), response.json);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { coerceObject, dateSchema, filteredArray } from './utils';
|
|||
const filterBadges = (tags?: string[]) =>
|
||||
tags?.filter(tag => tag.startsWith('badge:')).map(tag => v.parse(roleSchema, { id: tag, name: tag.replace(/^badge:/, '') }));
|
||||
|
||||
const preprocessAccount = (account: any) => {
|
||||
const preprocessAccount = v.transform((account: any) => {
|
||||
if (!account?.acct) return null;
|
||||
|
||||
const username = account.username || account.acct.split('@')[0];
|
||||
|
@ -59,7 +59,7 @@ const preprocessAccount = (account: any) => {
|
|||
])), ...account.source }
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
const fieldSchema = v.object({
|
||||
name: v.string(),
|
||||
|
@ -128,11 +128,11 @@ const baseAccountSchema = v.object({
|
|||
|
||||
const accountWithMovedAccountSchema = v.object({
|
||||
...baseAccountSchema.entries,
|
||||
moved: v.fallback(v.nullable(z.lazy((): typeof baseAccountSchema => accountWithMovedAccountSchema as any)), null),
|
||||
moved: v.fallback(v.nullable(v.lazy((): typeof baseAccountSchema => accountWithMovedAccountSchema as any)), null),
|
||||
});
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Account/} */
|
||||
const untypedAccountSchema = z.preprocess(preprocessAccount, accountWithMovedAccountSchema);
|
||||
const untypedAccountSchema = v.pipe(v.any(), preprocessAccount, accountWithMovedAccountSchema);
|
||||
|
||||
type WithMoved = {
|
||||
moved: Account | null;
|
||||
|
@ -140,9 +140,9 @@ type WithMoved = {
|
|||
|
||||
type Account = v.InferOutput<typeof accountWithMovedAccountSchema> & WithMoved;
|
||||
|
||||
const accountSchema: z.ZodType<Account> = untypedAccountSchema as any;
|
||||
const accountSchema: v.BaseSchema<any, Account, v.BaseIssue<unknown>> = untypedAccountSchema as any;
|
||||
|
||||
const untypedCredentialAccountSchema = z.preprocess(preprocessAccount, v.object({
|
||||
const untypedCredentialAccountSchema = v.pipe(v.any(), preprocessAccount, v.object({
|
||||
...accountWithMovedAccountSchema.entries,
|
||||
source: v.fallback(v.nullable(v.object({
|
||||
note: v.fallback(v.string(), ''),
|
||||
|
@ -150,7 +150,7 @@ const untypedCredentialAccountSchema = z.preprocess(preprocessAccount, v.object(
|
|||
privacy: v.picklist(['public', 'unlisted', 'private', 'direct']),
|
||||
sensitive: v.fallback(v.boolean(), false),
|
||||
language: v.fallback(v.nullable(v.string()), null),
|
||||
follow_requests_count: z.number().int().nonnegative().catch(0),
|
||||
follow_requests_count: v.fallback(v.pipe(v.number(), v.integer(), v.minValue(0)), 0),
|
||||
|
||||
show_role: v.fallback(v.nullable(v.optional(v.boolean())), undefined),
|
||||
no_rich_text: v.fallback(v.nullable(v.optional(v.boolean())), undefined),
|
||||
|
@ -173,16 +173,16 @@ const untypedCredentialAccountSchema = z.preprocess(preprocessAccount, v.object(
|
|||
|
||||
type CredentialAccount = v.InferOutput<typeof untypedCredentialAccountSchema> & WithMoved;
|
||||
|
||||
const credentialAccountSchema: z.ZodType<CredentialAccount> = untypedCredentialAccountSchema as any;
|
||||
const credentialAccountSchema: v.BaseSchema<any, CredentialAccount, v.BaseIssue<unknown>> = untypedCredentialAccountSchema as any;
|
||||
|
||||
const untypedMutedAccountSchema = z.preprocess(preprocessAccount, v.object({
|
||||
const untypedMutedAccountSchema = v.pipe(v.any(), preprocessAccount, v.object({
|
||||
...accountWithMovedAccountSchema.entries,
|
||||
mute_expires_at: v.fallback(v.nullable(dateSchema), null),
|
||||
}));
|
||||
|
||||
type MutedAccount = v.InferOutput<typeof untypedMutedAccountSchema> & WithMoved;
|
||||
|
||||
const mutedAccountSchema: z.ZodType<MutedAccount> = untypedMutedAccountSchema as any;
|
||||
const mutedAccountSchema: v.BaseSchema<any, MutedAccount, v.BaseIssue<unknown>> = untypedMutedAccountSchema as any;
|
||||
|
||||
export {
|
||||
accountSchema,
|
||||
|
|
|
@ -7,59 +7,63 @@ import { dateSchema, filteredArray } from '../utils';
|
|||
import { adminIpSchema } from './ip';
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Admin_Account/} */
|
||||
const adminAccountSchema = z.preprocess((account: any) => {
|
||||
if (!account.account) {
|
||||
const adminAccountSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((account: any) => {
|
||||
if (!account.account) {
|
||||
/**
|
||||
* Convert Pleroma account schema
|
||||
* @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminusers}
|
||||
*/
|
||||
return {
|
||||
id: account.id,
|
||||
account: null,
|
||||
username: account.nickname,
|
||||
domain: account.nickname.split('@')[1] || null,
|
||||
created_at: account.created_at,
|
||||
email: account.email,
|
||||
invite_request: account.registration_reason,
|
||||
role: account.roles?.is_admin
|
||||
? v.parse(roleSchema, { name: 'Admin' })
|
||||
: account.roles?.moderator
|
||||
? v.parse(roleSchema, { name: 'Moderator ' }) :
|
||||
null,
|
||||
confirmed: account.is_confirmed,
|
||||
approved: account.is_approved,
|
||||
disabled: !account.is_active,
|
||||
return {
|
||||
id: account.id,
|
||||
account: null,
|
||||
username: account.nickname,
|
||||
domain: account.nickname.split('@')[1] || null,
|
||||
created_at: account.created_at,
|
||||
email: account.email,
|
||||
invite_request: account.registration_reason,
|
||||
role: account.roles?.is_admin
|
||||
? v.parse(roleSchema, { name: 'Admin' })
|
||||
: account.roles?.moderator
|
||||
? v.parse(roleSchema, { name: 'Moderator ' }) :
|
||||
null,
|
||||
confirmed: account.is_confirmed,
|
||||
approved: account.is_approved,
|
||||
disabled: !account.is_active,
|
||||
|
||||
actor_type: account.actor_type,
|
||||
display_name: account.display_name,
|
||||
suggested: account.is_suggested,
|
||||
};
|
||||
}
|
||||
return account;
|
||||
}, v.object({
|
||||
id: v.string(),
|
||||
username: v.string(),
|
||||
domain: v.fallback(v.nullable(v.string()), null),
|
||||
created_at: dateSchema,
|
||||
email: v.fallback(v.nullable(v.string()), null),
|
||||
ip: v.fallback(v.nullable(v.pipe(v.string(), v.ip())), null),
|
||||
ips: filteredArray(adminIpSchema),
|
||||
locale: v.fallback(v.nullable(v.string()), null),
|
||||
invite_request: v.fallback(v.nullable(v.string()), null),
|
||||
role: v.fallback(v.nullable(roleSchema), null),
|
||||
confirmed: v.fallback(v.boolean(), false),
|
||||
approved: v.fallback(v.boolean(), false),
|
||||
disabled: v.fallback(v.boolean(), false),
|
||||
silenced: v.fallback(v.boolean(), false),
|
||||
suspended: v.fallback(v.boolean(), false),
|
||||
account: v.fallback(v.nullable(accountSchema), null),
|
||||
created_by_application_id: v.fallback(v.optional(v.string()), undefined),
|
||||
invited_by_account_id: v.fallback(v.optional(v.string()), undefined),
|
||||
actor_type: account.actor_type,
|
||||
display_name: account.display_name,
|
||||
suggested: account.is_suggested,
|
||||
};
|
||||
}
|
||||
return account;
|
||||
}),
|
||||
v.object({
|
||||
id: v.string(),
|
||||
username: v.string(),
|
||||
domain: v.fallback(v.nullable(v.string()), null),
|
||||
created_at: dateSchema,
|
||||
email: v.fallback(v.nullable(v.string()), null),
|
||||
ip: v.fallback(v.nullable(v.pipe(v.string(), v.ip())), null),
|
||||
ips: filteredArray(adminIpSchema),
|
||||
locale: v.fallback(v.nullable(v.string()), null),
|
||||
invite_request: v.fallback(v.nullable(v.string()), null),
|
||||
role: v.fallback(v.nullable(roleSchema), null),
|
||||
confirmed: v.fallback(v.boolean(), false),
|
||||
approved: v.fallback(v.boolean(), false),
|
||||
disabled: v.fallback(v.boolean(), false),
|
||||
silenced: v.fallback(v.boolean(), false),
|
||||
suspended: v.fallback(v.boolean(), false),
|
||||
account: v.fallback(v.nullable(accountSchema), null),
|
||||
created_by_application_id: v.fallback(v.optional(v.string()), undefined),
|
||||
invited_by_account_id: v.fallback(v.optional(v.string()), undefined),
|
||||
|
||||
actor_type: v.fallback(v.nullable(v.string()), null),
|
||||
display_name: v.fallback(v.nullable(v.string()), null),
|
||||
suggested: v.fallback(v.nullable(v.boolean()), null),
|
||||
}));
|
||||
actor_type: v.fallback(v.nullable(v.string()), null),
|
||||
display_name: v.fallback(v.nullable(v.string()), null),
|
||||
suggested: v.fallback(v.nullable(v.boolean()), null),
|
||||
}),
|
||||
);
|
||||
|
||||
type AdminAccount = v.InferOutput<typeof adminAccountSchema>;
|
||||
|
||||
|
|
|
@ -4,13 +4,17 @@ import * as v from 'valibot';
|
|||
import { announcementSchema } from '../announcement';
|
||||
|
||||
/** @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminannouncements} */
|
||||
const adminAnnouncementSchema = z.preprocess((announcement: any) => ({
|
||||
...announcement,
|
||||
...pick(announcement.pleroma, 'raw_content'),
|
||||
}), v.object({
|
||||
...announcementSchema.entries,
|
||||
raw_content: v.fallback(v.string(), ''),
|
||||
}));
|
||||
const adminAnnouncementSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((announcement: any) => ({
|
||||
...announcement,
|
||||
...pick(announcement.pleroma, 'raw_content'),
|
||||
})),
|
||||
v.object({
|
||||
...announcementSchema.entries,
|
||||
raw_content: v.fallback(v.string(), ''),
|
||||
}),
|
||||
);
|
||||
|
||||
type AdminAnnouncement = v.InferOutput<typeof adminAnnouncementSchema>;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import { dateSchema } from '../utils';
|
|||
/** @see {@link https://docs.joinmastodon.org/entities/Admin_IpBlock/} */
|
||||
const adminIpBlockSchema = v.object({
|
||||
id: v.string(),
|
||||
ip: z.string().ip(),
|
||||
ip: v.pipe(v.string(), v.ip()),
|
||||
severity: v.picklist(['sign_up_requires_approval', 'sign_up_block', 'no_access']),
|
||||
comment: v.fallback(v.string(), ''),
|
||||
created_at: dateSchema,
|
||||
|
|
|
@ -4,7 +4,7 @@ import { dateSchema } from '../utils';
|
|||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Admin_Ip/} */
|
||||
const adminIpSchema = v.object({
|
||||
ip: z.string().ip(),
|
||||
ip: v.pipe(v.string(), v.ip()),
|
||||
used_at: dateSchema,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import * as v from 'valibot';
|
||||
|
||||
const adminRelaySchema = z.preprocess((data: any) => ({ id: data.actor, ...data }), v.object({
|
||||
actor: v.fallback(v.string(), ''),
|
||||
id: v.string(),
|
||||
followed_back: v.fallback(v.boolean(), false),
|
||||
}));
|
||||
const adminRelaySchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((data: any) => ({ id: data.actor, ...data })),
|
||||
v.object({
|
||||
actor: v.fallback(v.string(), ''),
|
||||
id: v.string(),
|
||||
followed_back: v.fallback(v.boolean(), false),
|
||||
}),
|
||||
);
|
||||
|
||||
type AdminRelay = v.InferOutput<typeof adminRelaySchema>
|
||||
|
||||
|
|
|
@ -8,38 +8,42 @@ import { dateSchema, filteredArray } from '../utils';
|
|||
import { adminAccountSchema } from './account';
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Admin_Report/} */
|
||||
const adminReportSchema = z.preprocess((report: any) => {
|
||||
if (report.actor) {
|
||||
const adminReportSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((report: any) => {
|
||||
if (report.actor) {
|
||||
/**
|
||||
* Convert Pleroma report schema
|
||||
* @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminreports}
|
||||
*/
|
||||
return {
|
||||
action_taken: report.state !== 'open',
|
||||
comment: report.content,
|
||||
updated_at: report.created_at,
|
||||
account: report.actor,
|
||||
target_account: report.account,
|
||||
...(pick(report, ['id', 'assigned_account', 'created_at', 'rules', 'statuses'])),
|
||||
};
|
||||
}
|
||||
return report;
|
||||
}, v.object({
|
||||
id: v.string(),
|
||||
action_taken: v.fallback(v.optional(v.boolean()), undefined),
|
||||
action_taken_at: v.fallback(v.nullable(dateSchema), null),
|
||||
category: v.fallback(v.optional(v.string()), undefined),
|
||||
comment: v.fallback(v.optional(v.string()), undefined),
|
||||
forwarded: v.fallback(v.optional(v.boolean()), undefined),
|
||||
created_at: v.fallback(v.optional(dateSchema), undefined),
|
||||
updated_at: v.fallback(v.optional(dateSchema), undefined),
|
||||
account: adminAccountSchema,
|
||||
target_account: adminAccountSchema,
|
||||
assigned_account: v.fallback(v.nullable(adminAccountSchema), null),
|
||||
action_taken_by_account: v.fallback(v.nullable(adminAccountSchema), null),
|
||||
statuses: filteredArray(statusWithoutAccountSchema),
|
||||
rules: filteredArray(ruleSchema),
|
||||
}));
|
||||
return {
|
||||
action_taken: report.state !== 'open',
|
||||
comment: report.content,
|
||||
updated_at: report.created_at,
|
||||
account: report.actor,
|
||||
target_account: report.account,
|
||||
...(pick(report, ['id', 'assigned_account', 'created_at', 'rules', 'statuses'])),
|
||||
};
|
||||
}
|
||||
return report;
|
||||
}),
|
||||
v.object({
|
||||
id: v.string(),
|
||||
action_taken: v.fallback(v.optional(v.boolean()), undefined),
|
||||
action_taken_at: v.fallback(v.nullable(dateSchema), null),
|
||||
category: v.fallback(v.optional(v.string()), undefined),
|
||||
comment: v.fallback(v.optional(v.string()), undefined),
|
||||
forwarded: v.fallback(v.optional(v.boolean()), undefined),
|
||||
created_at: v.fallback(v.optional(dateSchema), undefined),
|
||||
updated_at: v.fallback(v.optional(dateSchema), undefined),
|
||||
account: adminAccountSchema,
|
||||
target_account: adminAccountSchema,
|
||||
assigned_account: v.fallback(v.nullable(adminAccountSchema), null),
|
||||
action_taken_by_account: v.fallback(v.nullable(adminAccountSchema), null),
|
||||
statuses: filteredArray(statusWithoutAccountSchema),
|
||||
rules: filteredArray(ruleSchema),
|
||||
}),
|
||||
);
|
||||
|
||||
type AdminReport = v.InferOutput<typeof adminReportSchema>;
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as v from 'valibot';
|
|||
/** @see {@link https://docs.joinmastodon.org/entities/announcement/} */
|
||||
const announcementReactionSchema = v.object({
|
||||
name: v.fallback(v.string(), ''),
|
||||
count: z.number().int().nonnegative().catch(0),
|
||||
count: v.fallback(v.pipe(v.number(), v.integer(), v.minValue(0)), 0),
|
||||
me: v.fallback(v.boolean(), false),
|
||||
url: v.fallback(v.nullable(v.string()), null),
|
||||
static_url: v.fallback(v.nullable(v.string()), null),
|
||||
|
|
|
@ -16,11 +16,12 @@ const announcementSchema = v.object({
|
|||
read: v.fallback(v.boolean(), false),
|
||||
published_at: dateSchema,
|
||||
reactions: filteredArray(announcementReactionSchema),
|
||||
statuses: z.preprocess(
|
||||
(statuses: any) => Array.isArray(statuses)
|
||||
statuses: v.pipe(
|
||||
v.any(),
|
||||
v.transform((statuses: any) => Array.isArray(statuses)
|
||||
? Object.fromEntries(statuses.map((status: any) => [status.url, status.account?.acct]) || [])
|
||||
: statuses,
|
||||
v.record(v.string(), v.string(), v.string()),
|
||||
: statuses),
|
||||
v.record(v.string(), v.string()),
|
||||
),
|
||||
mentions: filteredArray(mentionSchema),
|
||||
tags: filteredArray(tagSchema),
|
||||
|
|
|
@ -24,11 +24,15 @@ const customEmojiReactionSchema = v.object({
|
|||
* Pleroma emoji reaction.
|
||||
* @see {@link https://docs.pleroma.social/backend/development/API/differences_in_mastoapi_responses/#statuses}
|
||||
*/
|
||||
const emojiReactionSchema = z.preprocess((reaction: any) => reaction ? {
|
||||
static_url: reaction.url,
|
||||
account_ids: reaction.accounts?.map((account: any) => account?.id),
|
||||
...reaction,
|
||||
} : null, v.union([baseEmojiReactionSchema, customEmojiReactionSchema]);
|
||||
const emojiReactionSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((reaction: any) => reaction ? {
|
||||
static_url: reaction.url,
|
||||
account_ids: reaction.accounts?.map((account: any) => account?.id),
|
||||
...reaction,
|
||||
} : null),
|
||||
v.union([baseEmojiReactionSchema, customEmojiReactionSchema]),
|
||||
);
|
||||
|
||||
type EmojiReaction = v.InferOutput<typeof emojiReactionSchema>;
|
||||
|
||||
|
|
|
@ -16,29 +16,33 @@ const filterStatusSchema = v.object({
|
|||
});
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Filter/} */
|
||||
const filterSchema = z.preprocess((filter: any) => {
|
||||
if (filter.phrase) {
|
||||
return {
|
||||
...filter,
|
||||
title: filter.phrase,
|
||||
keywords: [{
|
||||
id: '1',
|
||||
keyword: filter.phrase,
|
||||
whole_word: filter.whole_word,
|
||||
}],
|
||||
filter_action: filter.irreversible ? 'hide' : 'warn',
|
||||
};
|
||||
}
|
||||
return filter;
|
||||
}, v.object({
|
||||
id: v.string(),
|
||||
title: v.string(),
|
||||
context: v.array(v.picklist(['home', 'notifications', 'public', 'thread', 'account'])),
|
||||
expires_at: v.fallback(v.nullable(z.string().datetime({ offset: true })), null),
|
||||
filter_action: v.fallback(v.picklist(['warn', 'hide']), 'warn'),
|
||||
keywords: filteredArray(filterKeywordSchema),
|
||||
statuses: filteredArray(filterStatusSchema),
|
||||
}));
|
||||
const filterSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((filter: any) => {
|
||||
if (filter.phrase) {
|
||||
return {
|
||||
...filter,
|
||||
title: filter.phrase,
|
||||
keywords: [{
|
||||
id: '1',
|
||||
keyword: filter.phrase,
|
||||
whole_word: filter.whole_word,
|
||||
}],
|
||||
filter_action: filter.irreversible ? 'hide' : 'warn',
|
||||
};
|
||||
}
|
||||
return filter;
|
||||
}),
|
||||
v.object({
|
||||
id: v.string(),
|
||||
title: v.string(),
|
||||
context: v.array(v.picklist(['home', 'notifications', 'public', 'thread', 'account'])),
|
||||
expires_at: v.fallback(v.nullable(z.string().datetime({ offset: true })), null),
|
||||
filter_action: v.fallback(v.picklist(['warn', 'hide']), 'warn'),
|
||||
keywords: filteredArray(filterKeywordSchema),
|
||||
statuses: filteredArray(filterStatusSchema),
|
||||
}),
|
||||
);
|
||||
|
||||
type Filter = v.InferOutput<typeof filterSchema>;
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ type GroupRole =`${GroupRoles}`;
|
|||
const groupMemberSchema = v.object({
|
||||
id: v.string(),
|
||||
account: accountSchema,
|
||||
role: z.nativeEnum(GroupRoles),
|
||||
role: v.enum(GroupRoles),
|
||||
});
|
||||
|
||||
type GroupMember = v.InferOutput<typeof groupMemberSchema>;
|
||||
|
|
|
@ -5,7 +5,7 @@ import { GroupRoles } from './group-member';
|
|||
const groupRelationshipSchema = v.object({
|
||||
id: v.string(),
|
||||
member: v.fallback(v.boolean(), false),
|
||||
role: z.nativeEnum(GroupRoles).catch(GroupRoles.USER),
|
||||
role: v.fallback(v.enum(GroupRoles), GroupRoles.USER),
|
||||
requested: v.fallback(v.boolean(), false),
|
||||
});
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ const groupSchema = v.object({
|
|||
membership_required: v.fallback(v.boolean(), false),
|
||||
members_count: v.fallback(v.number(), 0),
|
||||
owner: v.fallback(v.nullable(v.object({ id: v.string() })), null),
|
||||
note: z.string().transform(note => note === '<p></p>' ? '' : note).catch(''),
|
||||
note: v.fallback(v.pipe(v.string(), v.transform(note => note === '<p></p>' ? '' : note)), ''),
|
||||
relationship: v.fallback(v.nullable(groupRelationshipSchema), null), // Dummy field to be overwritten later
|
||||
statuses_visibility: v.fallback(v.string(), 'public'),
|
||||
uri: v.fallback(v.string(), ''),
|
||||
|
|
|
@ -184,9 +184,9 @@ const pleromaSchema = coerceObject({
|
|||
}),
|
||||
}),
|
||||
fields_limits: coerceObject({
|
||||
max_fields: z.number().nonnegative().catch(4),
|
||||
name_length: z.number().nonnegative().catch(255),
|
||||
value_length: z.number().nonnegative().catch(2047),
|
||||
max_fields: v.fallback(v.pipe(v.number(), v.integer(), v.minValue(0)), 4),
|
||||
name_length: v.fallback(v.pipe(v.number(), v.integer(), v.minValue(0)), 255),
|
||||
value_length: v.fallback(v.pipe(v.number(), v.integer(), v.minValue(0)), 2047),
|
||||
}),
|
||||
markup: coerceObject({
|
||||
allow_headings: v.fallback(v.boolean(), false),
|
||||
|
@ -293,43 +293,40 @@ const instanceV1Schema = coerceObject({
|
|||
});
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Instance/} */
|
||||
const instanceSchema = z.preprocess((data: any) => {
|
||||
const instanceSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((data: any) => {
|
||||
// Detect GoToSocial
|
||||
if (typeof data.configuration?.accounts?.allow_custom_css === 'boolean') {
|
||||
data.version = `0.0.0 (compatible; GoToSocial ${data.version})`;
|
||||
}
|
||||
if (typeof data.configuration?.accounts?.allow_custom_css === 'boolean') {
|
||||
data.version = `0.0.0 (compatible; GoToSocial ${data.version})`;
|
||||
}
|
||||
|
||||
const apiVersions = getApiVersions(data);
|
||||
const apiVersions = getApiVersions(data);
|
||||
|
||||
if (data.domain) return { account_domain: data.domain, ...data, api_versions: apiVersions };
|
||||
if (data.domain) return { account_domain: data.domain, ...data, api_versions: apiVersions };
|
||||
|
||||
return instanceV1ToV2({ ...data, api_versions: apiVersions });
|
||||
}, coerceObject({
|
||||
account_domain: v.fallback(v.string(), ''),
|
||||
api_versions: v.fallback(v.record(v.string(), v.number()), {}),
|
||||
configuration: configurationSchema,
|
||||
contact: contactSchema,
|
||||
description: v.fallback(v.string(), ''),
|
||||
domain: v.fallback(v.string(), ''),
|
||||
feature_quote: v.fallback(v.boolean(), false),
|
||||
fedibird_capabilities: v.fallback(v.array(v.string()), []),
|
||||
languages: v.fallback(v.array(v.string()), []),
|
||||
pleroma: pleromaSchema,
|
||||
registrations: registrations,
|
||||
rules: filteredArray(ruleSchema),
|
||||
stats: statsSchema,
|
||||
thumbnail: thumbnailSchema,
|
||||
title: v.fallback(v.string(), ''),
|
||||
usage: usageSchema,
|
||||
version: v.fallback(v.string(), '0.0.0'),
|
||||
}).transform((instance) => {
|
||||
const version = fixVersion(instance.version);
|
||||
|
||||
return {
|
||||
...instance,
|
||||
version,
|
||||
};
|
||||
}));
|
||||
return instanceV1ToV2({ ...data, api_versions: apiVersions });
|
||||
}),
|
||||
coerceObject({
|
||||
account_domain: v.fallback(v.string(), ''),
|
||||
api_versions: v.fallback(v.record(v.string(), v.number()), {}),
|
||||
configuration: configurationSchema,
|
||||
contact: contactSchema,
|
||||
description: v.fallback(v.string(), ''),
|
||||
domain: v.fallback(v.string(), ''),
|
||||
feature_quote: v.fallback(v.boolean(), false),
|
||||
fedibird_capabilities: v.fallback(v.array(v.string()), []),
|
||||
languages: v.fallback(v.array(v.string()), []),
|
||||
pleroma: pleromaSchema,
|
||||
registrations: registrations,
|
||||
rules: filteredArray(ruleSchema),
|
||||
stats: statsSchema,
|
||||
thumbnail: thumbnailSchema,
|
||||
title: v.fallback(v.string(), ''),
|
||||
usage: usageSchema,
|
||||
version: v.pipe(v.fallback(v.string(), '0.0.0'), v.transform(fixVersion)),
|
||||
}),
|
||||
);
|
||||
|
||||
type Instance = v.InferOutput<typeof instanceSchema>;
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ const locationSchema = v.object({
|
|||
type: v.fallback(v.string(), ''),
|
||||
timezone: v.fallback(v.string(), ''),
|
||||
geom: v.fallback(v.nullable(v.object({
|
||||
coordinates: v.fallback(v.nullable(z.tuple([v.number(), v.number()])), null),
|
||||
coordinates: v.fallback(v.nullable(v.tuple([v.number(), v.number()])), null),
|
||||
srid: v.fallback(v.string(), ''),
|
||||
})), null),
|
||||
});
|
||||
|
|
|
@ -2,15 +2,19 @@ import * as v from 'valibot';
|
|||
|
||||
import { dateSchema } from './utils';
|
||||
|
||||
const markerSchema = z.preprocess((marker: any) => marker ? ({
|
||||
unread_count: marker.pleroma?.unread_count,
|
||||
...marker,
|
||||
}) : null, v.object({
|
||||
last_read_id: v.string(),
|
||||
version: v.pipe(v.number(), v.integer()),
|
||||
updated_at: dateSchema,
|
||||
unread_count: v.fallback(v.optional(v.pipe(v.number(), v.integer())), undefined),
|
||||
}));
|
||||
const markerSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((marker: any) => marker ? ({
|
||||
unread_count: marker.pleroma?.unread_count,
|
||||
...marker,
|
||||
}) : null),
|
||||
v.object({
|
||||
last_read_id: v.string(),
|
||||
version: v.pipe(v.number(), v.integer()),
|
||||
updated_at: dateSchema,
|
||||
unread_count: v.fallback(v.optional(v.pipe(v.number(), v.integer())), undefined),
|
||||
}),
|
||||
);
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Marker/} */
|
||||
type Marker = v.InferOutput<typeof markerSchema>;
|
||||
|
|
|
@ -3,16 +3,10 @@ import * as v from 'valibot';
|
|||
|
||||
import { mimeSchema } from './utils';
|
||||
|
||||
const blurhashSchema = z.string().superRefine((value, ctx) => {
|
||||
const r = isBlurhashValid(value);
|
||||
|
||||
if (!r.result) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: r.errorReason,
|
||||
});
|
||||
}
|
||||
});
|
||||
const blurhashSchema = v.pipe(v.string(), v.check(
|
||||
(value) => isBlurhashValid(value).result,
|
||||
'invalid blurhash', // .errorReason
|
||||
));
|
||||
|
||||
const baseAttachmentSchema = v.object({
|
||||
id: v.string(),
|
||||
|
@ -40,8 +34,8 @@ const imageAttachmentSchema = v.object({
|
|||
original: v.fallback(v.optional(imageMetaSchema), undefined),
|
||||
small: v.fallback(v.optional(imageMetaSchema), undefined),
|
||||
focus: v.fallback(v.optional(v.object({
|
||||
x: z.number().min(-1).max(1),
|
||||
y: z.number().min(-1).max(1),
|
||||
x: v.pipe(v.number(), v.minValue(-1), v.maxValue(1)),
|
||||
y: v.pipe(v.number(), v.minValue(-1), v.maxValue(1)),
|
||||
})), undefined),
|
||||
}), {}),
|
||||
});
|
||||
|
@ -54,7 +48,7 @@ const videoAttachmentSchema = v.object({
|
|||
original: v.fallback(v.optional(v.object({
|
||||
...imageMetaSchema.entries,
|
||||
frame_rate: v.fallback(v.nullable(v.pipe(v.string(), v.regex(/\d+\/\d+$/))), null),
|
||||
duration: v.fallback(v.nullable(z.number().nonnegative()), null),
|
||||
duration: v.fallback(v.nullable(v.pipe(v.number(), v.minValue(0))), null),
|
||||
})), undefined),
|
||||
small: v.fallback(v.optional(imageMetaSchema), undefined),
|
||||
// WIP: add rest
|
||||
|
@ -83,7 +77,7 @@ const audioAttachmentSchema = v.object({
|
|||
})), undefined),
|
||||
original: v.fallback(v.optional(v.object({
|
||||
duration: v.fallback(v.optional(v.number()), undefined),
|
||||
bitrate: z.number().nonnegative().optional().catch(undefined),
|
||||
bitrate: v.fallback(v.optional(v.pipe(v.number(), v.minValue(0))), undefined),
|
||||
})), undefined),
|
||||
}), {}),
|
||||
});
|
||||
|
@ -94,21 +88,25 @@ const unknownAttachmentSchema = v.object({
|
|||
});
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/MediaAttachment} */
|
||||
const mediaAttachmentSchema = z.preprocess((data: any) => {
|
||||
if (!data) return null;
|
||||
const mediaAttachmentSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((data: any) => {
|
||||
if (!data) return null;
|
||||
|
||||
return {
|
||||
mime_type: data.pleroma?.mime_type,
|
||||
preview_url: data.url,
|
||||
...data,
|
||||
};
|
||||
}, v.variant('type', [
|
||||
imageAttachmentSchema,
|
||||
videoAttachmentSchema,
|
||||
gifvAttachmentSchema,
|
||||
audioAttachmentSchema,
|
||||
unknownAttachmentSchema,
|
||||
]));
|
||||
return {
|
||||
mime_type: data.pleroma?.mime_type,
|
||||
preview_url: data.url,
|
||||
...data,
|
||||
};
|
||||
}),
|
||||
v.variant('type', [
|
||||
imageAttachmentSchema,
|
||||
videoAttachmentSchema,
|
||||
gifvAttachmentSchema,
|
||||
audioAttachmentSchema,
|
||||
unknownAttachmentSchema,
|
||||
]),
|
||||
);
|
||||
|
||||
type MediaAttachment = v.InferOutput<typeof mediaAttachmentSchema>;
|
||||
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
import * as v from 'valibot';
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Status/#Mention} */
|
||||
const mentionSchema = v.object({
|
||||
id: v.string(),
|
||||
username: v.fallback(v.string(), ''),
|
||||
url: v.fallback(v.pipe(v.string(), v.url()), ''),
|
||||
acct: v.string(),
|
||||
}).transform((mention) => {
|
||||
if (!mention.username) {
|
||||
mention.username = mention.acct.split('@')[0];
|
||||
}
|
||||
const mentionSchema = v.pipe(
|
||||
v.object({
|
||||
id: v.string(),
|
||||
username: v.fallback(v.string(), ''),
|
||||
url: v.fallback(v.pipe(v.string(), v.url()), ''),
|
||||
acct: v.string(),
|
||||
}),
|
||||
v.transform((mention) => {
|
||||
if (!mention.username) {
|
||||
mention.username = mention.acct.split('@')[0];
|
||||
}
|
||||
|
||||
return mention;
|
||||
});
|
||||
return mention;
|
||||
}),
|
||||
);
|
||||
|
||||
type Mention = v.InferOutput<typeof mentionSchema>;
|
||||
|
||||
|
|
|
@ -84,25 +84,28 @@ const eventParticipationRequestNotificationSchema = v.object({
|
|||
});
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Notification/} */
|
||||
const notificationSchema: z.ZodType<Notification> = z.preprocess((notification: any) => ({
|
||||
group_key: `ungrouped-${notification.id}`,
|
||||
...pick(notification.pleroma || {}, ['is_muted', 'is_seen']),
|
||||
...notification,
|
||||
type: notification.type === 'pleroma:report'
|
||||
? 'admin.report'
|
||||
: notification.type?.replace(/^pleroma:/, ''),
|
||||
}), v.variant('type', [
|
||||
accountNotificationSchema,
|
||||
mentionNotificationSchema,
|
||||
statusNotificationSchema,
|
||||
reportNotificationSchema,
|
||||
severedRelationshipNotificationSchema,
|
||||
moderationWarningNotificationSchema,
|
||||
moveNotificationSchema,
|
||||
emojiReactionNotificationSchema,
|
||||
chatMentionNotificationSchema,
|
||||
eventParticipationRequestNotificationSchema,
|
||||
])) as any;
|
||||
const notificationSchema: v.BaseSchema<any, Notification, v.BaseIssue<unknown>> = v.pipe(
|
||||
v.any(),
|
||||
v.transform((notification: any) => ({
|
||||
group_key: `ungrouped-${notification.id}`,
|
||||
...pick(notification.pleroma || {}, ['is_muted', 'is_seen']),
|
||||
...notification,
|
||||
type: notification.type === 'pleroma:report'
|
||||
? 'admin.report'
|
||||
: notification.type?.replace(/^pleroma:/, ''),
|
||||
})),
|
||||
v.variant('type', [
|
||||
accountNotificationSchema,
|
||||
mentionNotificationSchema,
|
||||
statusNotificationSchema,
|
||||
reportNotificationSchema,
|
||||
severedRelationshipNotificationSchema,
|
||||
moderationWarningNotificationSchema,
|
||||
moveNotificationSchema,
|
||||
emojiReactionNotificationSchema,
|
||||
chatMentionNotificationSchema,
|
||||
eventParticipationRequestNotificationSchema,
|
||||
])) as any;
|
||||
|
||||
type Notification = v.InferOutput<
|
||||
| typeof accountNotificationSchema
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import * as v from 'valibot';
|
||||
|
||||
/** @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apioauth_tokens} */
|
||||
const oauthTokenSchema = z.preprocess((token: any) => ({
|
||||
...token,
|
||||
valid_until: token?.valid_until?.padEnd(27, 'Z'),
|
||||
}), v.object({
|
||||
app_name: v.string(),
|
||||
id: v.number(),
|
||||
valid_until: z.string().datetime({ offset: true }),
|
||||
}));
|
||||
const oauthTokenSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((token: any) => ({
|
||||
...token,
|
||||
valid_until: token?.valid_until?.padEnd(27, 'Z'),
|
||||
})),
|
||||
v.object({
|
||||
app_name: v.string(),
|
||||
id: v.number(),
|
||||
valid_until: z.string().datetime({ offset: true }),
|
||||
}),
|
||||
);
|
||||
|
||||
type OauthToken = v.InferOutput<typeof oauthTokenSchema>;
|
||||
|
||||
|
|
|
@ -17,10 +17,10 @@ const pollSchema = v.object({
|
|||
expires_at: v.fallback(v.nullable(z.string().datetime()), null),
|
||||
id: v.string(),
|
||||
multiple: v.fallback(v.boolean(), false),
|
||||
options: v.array(pollOptionSchema).min(2),
|
||||
options: v.pipe(v.array(pollOptionSchema), v.minLength(2)),
|
||||
voters_count: v.fallback(v.number(), 0),
|
||||
votes_count: v.fallback(v.number(), 0),
|
||||
own_votes: v.fallback(v.nullable(v.array(v.number())).nonempty(), null),
|
||||
own_votes: v.fallback(v.nullable(v.pipe(v.array(v.number()), v.minLength(1))), null),
|
||||
voted: v.fallback(v.boolean(), false),
|
||||
|
||||
non_anonymous: v.fallback(v.boolean(), false),
|
||||
|
|
|
@ -7,10 +7,14 @@ const baseRuleSchema = v.object({
|
|||
});
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Rule/} */
|
||||
const ruleSchema = z.preprocess((data: any) => ({
|
||||
...data,
|
||||
hint: data.hint || data.subtext,
|
||||
}), baseRuleSchema);
|
||||
const ruleSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((data: any) => ({
|
||||
...data,
|
||||
hint: data.hint || data.subtext,
|
||||
})),
|
||||
baseRuleSchema,
|
||||
);
|
||||
|
||||
type Rule = v.InferOutput<typeof ruleSchema>;
|
||||
|
||||
|
|
|
@ -2,19 +2,23 @@ import * as v from 'valibot';
|
|||
|
||||
import { accountSchema } from './account';
|
||||
|
||||
const scrobbleSchema = z.preprocess((scrobble: any) => scrobble ? {
|
||||
external_link: scrobble.externalLink,
|
||||
...scrobble,
|
||||
} : null, v.object({
|
||||
id: v.pipe(v.unknown(), v.transform(String)),
|
||||
account: accountSchema,
|
||||
created_at: z.string().datetime({ offset: true }),
|
||||
title: v.string(),
|
||||
artist: v.fallback(v.string(), ''),
|
||||
album: v.fallback(v.string(), ''),
|
||||
external_link: v.fallback(v.nullable(v.string()), null),
|
||||
length: v.fallback(v.nullable(v.number()), null),
|
||||
}));
|
||||
const scrobbleSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((scrobble: any) => scrobble ? {
|
||||
external_link: scrobble.externalLink,
|
||||
...scrobble,
|
||||
} : null),
|
||||
v.object({
|
||||
id: v.pipe(v.unknown(), v.transform(String)),
|
||||
account: accountSchema,
|
||||
created_at: z.string().datetime({ offset: true }),
|
||||
title: v.string(),
|
||||
artist: v.fallback(v.string(), ''),
|
||||
album: v.fallback(v.string(), ''),
|
||||
external_link: v.fallback(v.nullable(v.string()), null),
|
||||
length: v.fallback(v.nullable(v.number()), null),
|
||||
}),
|
||||
);
|
||||
|
||||
type Scrobble = v.InferOutput<typeof scrobbleSchema>;
|
||||
|
||||
|
|
|
@ -134,19 +134,19 @@ const preprocess = (status: any) => {
|
|||
return status;
|
||||
};
|
||||
|
||||
const statusSchema: z.ZodType<Status> = z.preprocess(preprocess, v.object({
|
||||
const statusSchema: v.BaseSchema<any, Status, v.BaseIssue<unknown>> = v.pipe(v.any(), v.transform(preprocess), v.object({
|
||||
...baseStatusSchema.entries,
|
||||
reblog: v.fallback(v.nullable(z.lazy(() => statusSchema)), null),
|
||||
reblog: v.fallback(v.nullable(v.lazy(() => statusSchema)), null),
|
||||
|
||||
quote: v.fallback(v.nullable(z.lazy(() => statusSchema)), null),
|
||||
quote: v.fallback(v.nullable(v.lazy(() => statusSchema)), null),
|
||||
})) as any;
|
||||
|
||||
const statusWithoutAccountSchema = z.preprocess(preprocess, v.object({
|
||||
const statusWithoutAccountSchema = v.pipe(v.any(), v.transform(preprocess), v.object({
|
||||
...(v.omit(baseStatusSchema, ['account']).entries),
|
||||
account: v.fallback(v.nullable(accountSchema), null),
|
||||
reblog: v.fallback(v.nullable(z.lazy(() => statusSchema)), null),
|
||||
reblog: v.fallback(v.nullable(v.lazy(() => statusSchema)), null),
|
||||
|
||||
quote: v.fallback(v.nullable(z.lazy(() => statusSchema)), null),
|
||||
quote: v.fallback(v.nullable(v.lazy(() => statusSchema)), null),
|
||||
}));
|
||||
|
||||
type Status = v.InferOutput<typeof baseStatusSchema> & {
|
||||
|
|
|
@ -31,7 +31,7 @@ const baseStreamingEventSchema = v.object({
|
|||
const statusStreamingEventSchema = v.object({
|
||||
...baseStreamingEventSchema.entries,
|
||||
event: v.picklist(['update', 'status.update']),
|
||||
payload: z.preprocess((payload: any) => JSON.parse(payload), statusSchema),
|
||||
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), statusSchema),
|
||||
});
|
||||
|
||||
const stringStreamingEventSchema = v.object({
|
||||
|
@ -43,7 +43,7 @@ const stringStreamingEventSchema = v.object({
|
|||
const notificationStreamingEventSchema = v.object({
|
||||
...baseStreamingEventSchema.entries,
|
||||
event: v.literal('notification'),
|
||||
payload: z.preprocess((payload: any) => JSON.parse(payload), notificationSchema),
|
||||
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), notificationSchema),
|
||||
});
|
||||
|
||||
const emptyStreamingEventSchema = v.object({
|
||||
|
@ -54,37 +54,37 @@ const emptyStreamingEventSchema = v.object({
|
|||
const conversationStreamingEventSchema = v.object({
|
||||
...baseStreamingEventSchema.entries,
|
||||
event: v.literal('conversation'),
|
||||
payload: z.preprocess((payload: any) => JSON.parse(payload), conversationSchema),
|
||||
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), conversationSchema),
|
||||
});
|
||||
|
||||
const announcementStreamingEventSchema = v.object({
|
||||
...baseStreamingEventSchema.entries,
|
||||
event: v.literal('announcement'),
|
||||
payload: z.preprocess((payload: any) => JSON.parse(payload), announcementSchema),
|
||||
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), announcementSchema),
|
||||
});
|
||||
|
||||
const announcementReactionStreamingEventSchema = v.object({
|
||||
...baseStreamingEventSchema.entries,
|
||||
event: v.literal('announcement.reaction'),
|
||||
payload: z.preprocess((payload: any) => JSON.parse(payload), announcementReactionSchema),
|
||||
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), announcementReactionSchema),
|
||||
});
|
||||
|
||||
const chatUpdateStreamingEventSchema = v.object({
|
||||
...baseStreamingEventSchema.entries,
|
||||
event: v.literal('chat_update'),
|
||||
payload: z.preprocess((payload: any) => JSON.parse(payload), chatSchema),
|
||||
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), chatSchema),
|
||||
});
|
||||
|
||||
const followRelationshipsUpdateStreamingEventSchema = v.object({
|
||||
...baseStreamingEventSchema.entries,
|
||||
event: v.literal('follow_relationships_update'),
|
||||
payload: z.preprocess((payload: any) => JSON.parse(payload), followRelationshipUpdateSchema),
|
||||
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), followRelationshipUpdateSchema),
|
||||
});
|
||||
|
||||
const respondStreamingEventSchema = v.object({
|
||||
...baseStreamingEventSchema.entries,
|
||||
event: v.literal('respond'),
|
||||
payload: z.preprocess((payload: any) => JSON.parse(payload), v.object({
|
||||
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), v.object({
|
||||
type: v.string(),
|
||||
result: v.picklist(['success', 'ignored', 'error']),
|
||||
})),
|
||||
|
@ -93,26 +93,30 @@ const respondStreamingEventSchema = v.object({
|
|||
const markerStreamingEventSchema = v.object({
|
||||
...baseStreamingEventSchema.entries,
|
||||
event: v.literal('marker'),
|
||||
payload: z.preprocess((payload: any) => JSON.parse(payload), markersSchema),
|
||||
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), markersSchema),
|
||||
});
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/methods/streaming/#events} */
|
||||
const streamingEventSchema: z.ZodType<StreamingEvent> = z.preprocess((event: any) => ({
|
||||
...event,
|
||||
event: event.event?.replace(/^pleroma:/, ''),
|
||||
}), v.variant('event', [
|
||||
statusStreamingEventSchema,
|
||||
stringStreamingEventSchema,
|
||||
notificationStreamingEventSchema,
|
||||
emptyStreamingEventSchema,
|
||||
conversationStreamingEventSchema,
|
||||
announcementStreamingEventSchema,
|
||||
announcementReactionStreamingEventSchema,
|
||||
chatUpdateStreamingEventSchema,
|
||||
followRelationshipsUpdateStreamingEventSchema,
|
||||
respondStreamingEventSchema,
|
||||
markerStreamingEventSchema,
|
||||
])) as any;
|
||||
const streamingEventSchema: v.BaseSchema<any, StreamingEvent, v.BaseIssue<unknown>> = v.pipe(
|
||||
v.any(),
|
||||
v.transform((event: any) => ({
|
||||
...event,
|
||||
event: event.event?.replace(/^pleroma:/, ''),
|
||||
})),
|
||||
v.variant('event', [
|
||||
statusStreamingEventSchema,
|
||||
stringStreamingEventSchema,
|
||||
notificationStreamingEventSchema,
|
||||
emptyStreamingEventSchema,
|
||||
conversationStreamingEventSchema,
|
||||
announcementStreamingEventSchema,
|
||||
announcementReactionStreamingEventSchema,
|
||||
chatUpdateStreamingEventSchema,
|
||||
followRelationshipsUpdateStreamingEventSchema,
|
||||
respondStreamingEventSchema,
|
||||
markerStreamingEventSchema,
|
||||
]),
|
||||
) as any;
|
||||
|
||||
type StreamingEvent = v.InferOutput<
|
||||
| typeof statusStreamingEventSchema
|
||||
|
|
|
@ -3,37 +3,41 @@ import * as v from 'valibot';
|
|||
import { accountSchema } from './account';
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Suggestion} */
|
||||
const suggestionSchema = z.preprocess((suggestion: any) => {
|
||||
const suggestionSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((suggestion: any) => {
|
||||
/**
|
||||
* Support `/api/v1/suggestions`
|
||||
* @see {@link https://docs.joinmastodon.org/methods/suggestions/#v1}
|
||||
*/
|
||||
if (!suggestion) return null;
|
||||
if (!suggestion) return null;
|
||||
|
||||
if (suggestion?.acct) return {
|
||||
source: 'staff',
|
||||
sources: ['featured'],
|
||||
account: suggestion,
|
||||
};
|
||||
if (suggestion?.acct) return {
|
||||
source: 'staff',
|
||||
sources: ['featured'],
|
||||
account: suggestion,
|
||||
};
|
||||
|
||||
if (!suggestion.sources) {
|
||||
suggestion.sources = [];
|
||||
switch (suggestion.source) {
|
||||
case 'staff':
|
||||
suggestion.sources.push('staff');
|
||||
break;
|
||||
case 'global':
|
||||
suggestion.sources.push('most_interactions');
|
||||
break;
|
||||
if (!suggestion.sources) {
|
||||
suggestion.sources = [];
|
||||
switch (suggestion.source) {
|
||||
case 'staff':
|
||||
suggestion.sources.push('staff');
|
||||
break;
|
||||
case 'global':
|
||||
suggestion.sources.push('most_interactions');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return suggestion;
|
||||
}, v.object({
|
||||
source: v.fallback(v.nullable(v.string()), null),
|
||||
sources: v.fallback(v.array(v.string()), []),
|
||||
account: accountSchema,
|
||||
}));
|
||||
return suggestion;
|
||||
}),
|
||||
v.object({
|
||||
source: v.fallback(v.nullable(v.string()), null),
|
||||
sources: v.fallback(v.array(v.string()), []),
|
||||
account: accountSchema,
|
||||
}),
|
||||
);
|
||||
|
||||
type Suggestion = v.InferOutput<typeof suggestionSchema>;
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ const historySchema = v.object({
|
|||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/tag} */
|
||||
const tagSchema = v.object({
|
||||
name: z.string().min(1),
|
||||
name: v.pipe(v.string(), v.minLength(1)),
|
||||
url: v.fallback(v.pipe(v.string(), v.url()), ''),
|
||||
history: v.fallback(v.nullable(historySchema), null),
|
||||
following: v.fallback(v.optional(v.boolean()), undefined),
|
||||
|
|
|
@ -15,27 +15,31 @@ const translationMediaAttachment = v.object({
|
|||
});
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/Translation/} */
|
||||
const translationSchema = z.preprocess((translation: any) => {
|
||||
const translationSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((translation: any) => {
|
||||
/**
|
||||
* handle Akkoma
|
||||
* @see {@link https://akkoma.dev/AkkomaGang/akkoma/src/branch/develop/lib/pleroma/web/mastodon_api/controllers/status_controller.ex#L504}
|
||||
*/
|
||||
if (translation?.text) return {
|
||||
content: translation.text,
|
||||
detected_source_language: translation.detected_language,
|
||||
provider: '',
|
||||
};
|
||||
if (translation?.text) return {
|
||||
content: translation.text,
|
||||
detected_source_language: translation.detected_language,
|
||||
provider: '',
|
||||
};
|
||||
|
||||
return translation;
|
||||
}, v.object({
|
||||
id: v.fallback(v.nullable(v.string()), null),
|
||||
content: v.fallback(v.string(), ''),
|
||||
spoiler_text: v.fallback(v.string(), ''),
|
||||
poll: v.fallback(v.optional(translationPollSchema), undefined),
|
||||
media_attachments: filteredArray(translationMediaAttachment),
|
||||
detected_source_language: v.string(),
|
||||
provider: v.string(),
|
||||
}));
|
||||
return translation;
|
||||
}),
|
||||
v.object({
|
||||
id: v.fallback(v.nullable(v.string()), null),
|
||||
content: v.fallback(v.string(), ''),
|
||||
spoiler_text: v.fallback(v.string(), ''),
|
||||
poll: v.fallback(v.optional(translationPollSchema), undefined),
|
||||
media_attachments: filteredArray(translationMediaAttachment),
|
||||
detected_source_language: v.string(),
|
||||
provider: v.string(),
|
||||
}),
|
||||
);
|
||||
|
||||
type Translation = v.InferOutput<typeof translationSchema>;
|
||||
|
||||
|
|
|
@ -4,25 +4,29 @@ import { blurhashSchema } from './media-attachment';
|
|||
import { historySchema } from './tag';
|
||||
|
||||
/** @see {@link https://docs.joinmastodon.org/entities/PreviewCard/#trends-link} */
|
||||
const trendsLinkSchema = z.preprocess((link: any) => ({ ...link, id: link.url }), v.object({
|
||||
id: v.fallback(v.string(), ''),
|
||||
url: v.fallback(v.pipe(v.string(), v.url()), ''),
|
||||
title: v.fallback(v.string(), ''),
|
||||
description: v.fallback(v.string(), ''),
|
||||
type: v.fallback(v.picklist(['link', 'photo', 'video', 'rich']), 'link'),
|
||||
author_name: v.fallback(v.string(), ''),
|
||||
author_url: v.fallback(v.string(), ''),
|
||||
provider_name: v.fallback(v.string(), ''),
|
||||
provider_url: v.fallback(v.string(), ''),
|
||||
html: v.fallback(v.string(), ''),
|
||||
width: v.fallback(v.nullable(v.number()), null),
|
||||
height: v.fallback(v.nullable(v.number()), null),
|
||||
image: v.fallback(v.nullable(v.string()), null),
|
||||
image_description: v.fallback(v.nullable(v.string()), null),
|
||||
embed_url: v.fallback(v.string(), ''),
|
||||
blurhash: v.fallback(v.nullable(blurhashSchema), null),
|
||||
history: v.fallback(v.nullable(historySchema), null),
|
||||
}));
|
||||
const trendsLinkSchema = v.pipe(
|
||||
v.any(),
|
||||
v.transform((link: any) => ({ ...link, id: link.url })),
|
||||
v.object({
|
||||
id: v.fallback(v.string(), ''),
|
||||
url: v.fallback(v.pipe(v.string(), v.url()), ''),
|
||||
title: v.fallback(v.string(), ''),
|
||||
description: v.fallback(v.string(), ''),
|
||||
type: v.fallback(v.picklist(['link', 'photo', 'video', 'rich']), 'link'),
|
||||
author_name: v.fallback(v.string(), ''),
|
||||
author_url: v.fallback(v.string(), ''),
|
||||
provider_name: v.fallback(v.string(), ''),
|
||||
provider_url: v.fallback(v.string(), ''),
|
||||
html: v.fallback(v.string(), ''),
|
||||
width: v.fallback(v.nullable(v.number()), null),
|
||||
height: v.fallback(v.nullable(v.number()), null),
|
||||
image: v.fallback(v.nullable(v.string()), null),
|
||||
image_description: v.fallback(v.nullable(v.string()), null),
|
||||
embed_url: v.fallback(v.string(), ''),
|
||||
blurhash: v.fallback(v.nullable(blurhashSchema), null),
|
||||
history: v.fallback(v.nullable(historySchema), null),
|
||||
}),
|
||||
);
|
||||
|
||||
type TrendsLink = v.InferOutput<typeof trendsLinkSchema>;
|
||||
|
||||
|
|
|
@ -4,14 +4,16 @@ import * as v from 'valibot';
|
|||
const dateSchema = z.string().datetime({ offset: true }).catch(new Date().toUTCString());
|
||||
|
||||
/** Validates individual items in an array, dropping any that aren't valid. */
|
||||
const filteredArray = <T extends z.ZodTypeAny>(schema: T) =>
|
||||
z.any().array().catch([])
|
||||
.transform((arr) => (
|
||||
const filteredArray = <T>(schema: v.BaseSchema<any, T, v.BaseIssue<unknown>>) =>
|
||||
v.pipe(
|
||||
v.fallback(v.array(v.any()), []),
|
||||
v.transform((arr) => (
|
||||
arr.map((item) => {
|
||||
const parsed = schema.safeParse(item);
|
||||
return parsed.success ? parsed.data : undefined;
|
||||
}).filter((item): item is v.InferOutput<T> => Boolean(item))
|
||||
));
|
||||
const parsed = v.safeParse(schema, item);
|
||||
return parsed.success ? parsed.output : undefined;
|
||||
}).filter((item): item is T => Boolean(item))
|
||||
)),
|
||||
);
|
||||
|
||||
/** Validates the string as an emoji. */
|
||||
const emojiSchema = v.pipe(v.string(), v.emoji());
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { Entities } from 'pl-fe/entity-store/entities';
|
||||
import { useEntity } from 'pl-fe/entity-store/hooks';
|
||||
|
@ -19,7 +19,7 @@ const useRelationship = (accountId: string | undefined, opts: UseRelationshipOpt
|
|||
() => client.accounts.getRelationships([accountId!]),
|
||||
{
|
||||
enabled: enabled && !!accountId,
|
||||
schema: z.any().transform(arr => arr[0]),
|
||||
schema: v.pipe(v.any(), v.transform(arr => arr[0])),
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { Entities } from 'pl-fe/entity-store/entities';
|
||||
import { useCreateEntity } from 'pl-fe/entity-store/hooks';
|
||||
|
@ -13,7 +13,7 @@ const useDemoteGroupMember = (group: Pick<Group, 'id'>, groupMember: Pick<GroupM
|
|||
const { createEntity } = useCreateEntity(
|
||||
[Entities.GROUP_MEMBERSHIPS, groupMember.id],
|
||||
({ account_ids, role }: { account_ids: string[]; role: GroupRole }) => client.experimental.groups.demoteGroupUsers(group.id, account_ids, role),
|
||||
{ schema: z.any().transform((arr) => arr[0]), transform: normalizeGroupMember },
|
||||
{ schema: v.pipe(v.any(), v.transform(arr => arr[0])), transform: normalizeGroupMember },
|
||||
);
|
||||
|
||||
return createEntity;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { Entities } from 'pl-fe/entity-store/entities';
|
||||
import { useEntity } from 'pl-fe/entity-store/hooks';
|
||||
|
@ -14,7 +14,7 @@ const useGroupRelationship = (groupId: string | undefined) => {
|
|||
() => client.experimental.groups.getGroupRelationships([groupId!]),
|
||||
{
|
||||
enabled: !!groupId,
|
||||
schema: z.any().transform(arr => arr[0]),
|
||||
schema: v.pipe(v.any(), v.transform(arr => arr[0])),
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { z } from 'zod';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { Entities } from 'pl-fe/entity-store/entities';
|
||||
import { useCreateEntity } from 'pl-fe/entity-store/hooks';
|
||||
|
@ -13,7 +13,7 @@ const usePromoteGroupMember = (group: Pick<Group, 'id'>, groupMember: Pick<Group
|
|||
const { createEntity } = useCreateEntity(
|
||||
[Entities.GROUP_MEMBERSHIPS, groupMember.id],
|
||||
({ account_ids, role }: { account_ids: string[]; role: GroupRole }) => client.experimental.groups.promoteGroupUsers(group.id, account_ids, role),
|
||||
{ schema: z.any().transform((arr) => arr[0]), transform: normalizeGroupMember },
|
||||
{ schema: v.pipe(v.any(), v.transform(arr => arr[0])), transform: normalizeGroupMember },
|
||||
);
|
||||
|
||||
return createEntity;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { Entity } from '../types';
|
||||
import type z from 'zod';
|
||||
import type { BaseSchema, BaseIssue } from 'valibot';
|
||||
|
||||
type EntitySchema<TEntity extends Entity = Entity> = z.ZodType<TEntity, z.ZodTypeDef, any>;
|
||||
type EntitySchema<TEntity extends Entity = Entity> = BaseSchema<any, TEntity, BaseIssue<unknown>>;
|
||||
|
||||
/**
|
||||
* Tells us where to find/store the entity in the cache.
|
||||
|
|
|
@ -54,7 +54,7 @@ const useBatchedEntities = <TEntity extends Entity>(
|
|||
dispatch(entitiesFetchRequest(entityType, listKey));
|
||||
try {
|
||||
const response = await entityFn(filteredIds);
|
||||
const entities = filteredArray(schema).parse(response);
|
||||
const entities = v.parse(filteredArray(schema), response);
|
||||
dispatch(entitiesFetchSuccess(entities, entityType, listKey, 'end', {
|
||||
next: null,
|
||||
prev: null,
|
||||
|
|
|
@ -32,7 +32,7 @@ const useCreateEntity = <TEntity extends Entity = Entity, TTransformedEntity ext
|
|||
): Promise<void> => {
|
||||
const result = await setPromise(entityFn(data));
|
||||
const schema = opts.schema || z.custom<TEntity>();
|
||||
let entity: TEntity | TTransformedEntity = schema.parse(result);
|
||||
let entity: TEntity | TTransformedEntity = v.parse(schema, result);
|
||||
if (opts.transform) entity = opts.transform(entity);
|
||||
|
||||
// TODO: optimistic updating
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect } from 'react';
|
||||
import z from 'zod';
|
||||
import * as v from 'valibot';
|
||||
|
||||
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/useAppSelector';
|
||||
|
@ -64,7 +64,7 @@ const useEntities = <TEntity extends Entity, TTransformedEntity extends Entity =
|
|||
dispatch(entitiesFetchRequest(entityType, listKey));
|
||||
try {
|
||||
const response = await req();
|
||||
const entities = filteredArray(schema).parse(response);
|
||||
const entities = v.parse(filteredArray(schema), response);
|
||||
const transformedEntities = opts.transform && entities.map(opts.transform);
|
||||
|
||||
dispatch(entitiesFetchSuccess(transformedEntities || entities, entityType, listKey, pos, {
|
||||
|
|
|
@ -47,7 +47,7 @@ const useEntity = <TEntity extends Entity, TTransformedEntity extends Entity = T
|
|||
const fetchEntity = async () => {
|
||||
try {
|
||||
const response = await setPromise(entityFn());
|
||||
let entity: TEntity | TTransformedEntity = schema.parse(response);
|
||||
let entity: TEntity | TTransformedEntity = v.parse(schema, response);
|
||||
if (opts.transform) entity = opts.transform(entity);
|
||||
dispatch(importEntities([entity], entityType));
|
||||
} catch (e) {
|
||||
|
|
|
@ -36,7 +36,7 @@ const useEntityLookup = <TEntity extends Entity, TTransformedEntity extends Enti
|
|||
const fetchEntity = async () => {
|
||||
try {
|
||||
const response = await setPromise(entityFn());
|
||||
const entity = schema.parse(response);
|
||||
const entity = v.parse(schema, response);
|
||||
const transformedEntity = opts.transform ? opts.transform(entity) : entity;
|
||||
setFetchedEntity(transformedEntity as TTransformedEntity);
|
||||
dispatch(importEntities([transformedEntity], entityType));
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import * as v from 'valibot';
|
||||
import z from 'zod';
|
||||
|
||||
import type { CustomEmoji } from 'pl-api';
|
||||
|
||||
/** Validates individual items in an array, dropping any that aren't valid. */
|
||||
const filteredArray = <T extends z.ZodTypeAny>(schema: T) =>
|
||||
z.any().array().catch([])
|
||||
.transform((arr) => (
|
||||
const filteredArray = <T>(schema: v.BaseSchema<any, T, v.BaseIssue<unknown>>) =>
|
||||
v.pipe(
|
||||
v.fallback(v.array(v.any()), []),
|
||||
v.transform((arr) => (
|
||||
arr.map((item) => {
|
||||
const parsed = schema.safeParse(item);
|
||||
return parsed.success ? parsed.data : undefined;
|
||||
}).filter((item): item is z.infer<T> => Boolean(item))
|
||||
));
|
||||
const parsed = v.safeParse(schema, item);
|
||||
return parsed.success ? parsed.output : undefined;
|
||||
}).filter((item): item is T => Boolean(item))
|
||||
)),
|
||||
);
|
||||
|
||||
/** Map a list of CustomEmoji to their shortcodes. */
|
||||
const makeCustomEmojiMap = (customEmojis: CustomEmoji[]) =>
|
||||
|
|
Loading…
Reference in a new issue