Merge branch 'develop' into hooks-migration

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2024-10-17 00:21:01 +02:00
138 changed files with 2421 additions and 2296 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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);
}
}

View File

@ -1,25 +1,25 @@
import { z } from 'zod';
import * as v from 'valibot';
import { accountSchema } from './account';
import { dateSchema } from './utils';
import { datetimeSchema } from './utils';
/** @see {@link https://docs.joinmastodon.org/entities/Appeal/} */
const appealSchema = z.object({
text: z.string(),
state: z.enum(['approved', 'rejected', 'pending']),
const appealSchema = v.object({
text: v.string(),
state: v.picklist(['approved', 'rejected', 'pending']),
});
/** @see {@link https://docs.joinmastodon.org/entities/AccountWarning/} */
const accountWarningSchema = z.object({
id: z.string(),
action: z.enum(['none', 'disable', 'mark_statuses_as_sensitive', 'delete_statuses', 'sensitive', 'silence', 'suspend']),
text: z.string().catch(''),
status_ids: z.array(z.string()).catch([]),
const accountWarningSchema = v.object({
id: v.string(),
action: v.picklist(['none', 'disable', 'mark_statuses_as_sensitive', 'delete_statuses', 'sensitive', 'silence', 'suspend']),
text: v.fallback(v.string(), ''),
status_ids: v.fallback(v.array(v.string()), []),
target_account: accountSchema,
appeal: appealSchema.nullable().catch(null),
created_at: dateSchema,
appeal: v.fallback(v.nullable(appealSchema), null),
created_at: v.fallback(datetimeSchema, new Date().toISOString()),
});
type AccountWarning = z.infer<typeof accountWarningSchema>;
type AccountWarning = v.InferOutput<typeof accountWarningSchema>;
export { accountWarningSchema, type AccountWarning };

View File

@ -1,10 +1,10 @@
import pick from 'lodash.pick';
import z from 'zod';
import * as v from 'valibot';
import { customEmojiSchema } from './custom-emoji';
import { relationshipSchema } from './relationship';
import { roleSchema } from './role';
import { coerceObject, dateSchema, filteredArray } from './utils';
import { coerceObject, datetimeSchema, filteredArray } from './utils';
const getDomainFromURL = (account: Pick<Account, 'url'>): string => {
try {
@ -27,9 +27,9 @@ const guessFqn = (account: Pick<Account, 'acct' | 'url'>): string => {
};
const filterBadges = (tags?: string[]) =>
tags?.filter(tag => tag.startsWith('badge:')).map(tag => roleSchema.parse({ id: tag, name: tag.replace(/^badge:/, '') }));
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];
@ -82,129 +82,131 @@ const preprocessAccount = (account: any) => {
])), ...account.source }
: undefined,
};
};
const fieldSchema = z.object({
name: z.string(),
value: z.string(),
verified_at: z.string().datetime({ offset: true }).nullable().catch(null),
});
const baseAccountSchema = z.object({
id: z.string(),
username: z.string().catch(''),
acct: z.string().catch(''),
url: z.string().url(),
display_name: z.string().catch(''),
note: z.string().transform(note => note === '<p></p>' ? '' : note).catch(''),
avatar: z.string().catch(''),
avatar_static: z.string().url().catch(''),
header: z.string().url().catch(''),
header_static: z.string().url().catch(''),
locked: z.boolean().catch(false),
const fieldSchema = v.object({
name: v.string(),
value: v.string(),
verified_at: v.fallback(v.nullable(datetimeSchema), null),
});
const baseAccountSchema = v.object({
id: v.string(),
username: v.fallback(v.string(), ''),
acct: v.fallback(v.string(), ''),
url: v.pipe(v.string(), v.url()),
display_name: v.fallback(v.string(), ''),
content: v.fallback(v.pipe(v.string(), v.transform((note => note === '<p></p>' ? '' : note))), ''),
avatar: v.fallback(v.string(), ''),
avatar_static: v.fallback(v.pipe(v.string(), v.url()), ''),
header: v.fallback(v.pipe(v.string(), v.url()), ''),
header_static: v.fallback(v.pipe(v.string(), v.url()), ''),
locked: v.fallback(v.boolean(), false),
fields: filteredArray(fieldSchema),
emojis: filteredArray(customEmojiSchema),
bot: z.boolean().catch(false),
group: z.boolean().catch(false),
discoverable: z.boolean().catch(false),
noindex: z.boolean().nullable().catch(null),
moved: z.null().catch(null),
suspended: z.boolean().optional().catch(undefined),
limited: z.boolean().optional().catch(undefined),
created_at: z.string().datetime().catch(new Date().toUTCString()),
last_status_at: z.string().date().nullable().catch(null),
statuses_count: z.number().catch(0),
followers_count: z.number().catch(0),
following_count: z.number().catch(0),
bot: v.fallback(v.boolean(), false),
group: v.fallback(v.boolean(), false),
discoverable: v.fallback(v.boolean(), false),
noindex: v.fallback(v.nullable(v.boolean()), null),
suspended: v.fallback(v.optional(v.boolean()), undefined),
limited: v.fallback(v.optional(v.boolean()), undefined),
created_at: v.fallback(datetimeSchema, new Date().toUTCString()),
last_status_at: v.fallback(v.nullable(v.pipe(v.string(), v.isoDate())), null),
statuses_count: v.fallback(v.number(), 0),
followers_count: v.fallback(v.number(), 0),
following_count: v.fallback(v.number(), 0),
roles: filteredArray(roleSchema),
fqn: z.string().nullable().catch(null),
ap_id: z.string().nullable().catch(null),
background_image: z.string().nullable().catch(null),
relationship: relationshipSchema.optional().catch(undefined),
is_moderator: z.boolean().optional().catch(undefined),
is_admin: z.boolean().optional().catch(undefined),
is_suggested: z.boolean().optional().catch(undefined),
hide_favorites: z.boolean().catch(true),
hide_followers: z.boolean().optional().catch(undefined),
hide_follows: z.boolean().optional().catch(undefined),
hide_followers_count: z.boolean().optional().catch(undefined),
hide_follows_count: z.boolean().optional().catch(undefined),
accepts_chat_messages: z.boolean().nullable().catch(null),
favicon: z.string().optional().catch(undefined),
birthday: z.string().date().optional().catch(undefined),
deactivated: z.boolean().optional().catch(undefined),
fqn: v.fallback(v.nullable(v.string()), null),
ap_id: v.fallback(v.nullable(v.string()), null),
background_image: v.fallback(v.nullable(v.string()), null),
relationship: v.fallback(v.optional(relationshipSchema), undefined),
is_moderator: v.fallback(v.optional(v.boolean()), undefined),
is_admin: v.fallback(v.optional(v.boolean()), undefined),
is_suggested: v.fallback(v.optional(v.boolean()), undefined),
hide_favorites: v.fallback(v.boolean(), true),
hide_followers: v.fallback(v.optional(v.boolean()), undefined),
hide_follows: v.fallback(v.optional(v.boolean()), undefined),
hide_followers_count: v.fallback(v.optional(v.boolean()), undefined),
hide_follows_count: v.fallback(v.optional(v.boolean()), undefined),
accepts_chat_messages: v.fallback(v.nullable(v.boolean()), null),
favicon: v.fallback(v.optional(v.string()), undefined),
birthday: v.fallback(v.optional(v.pipe(v.string(), v.isoDate())), undefined),
deactivated: v.fallback(v.optional(v.boolean()), undefined),
location: z.string().optional().catch(undefined),
local: z.boolean().optional().catch(false),
location: v.fallback(v.optional(v.string()), undefined),
local: v.fallback(v.optional(v.boolean()), false),
avatar_description: z.string().catch(''),
enable_rss: z.boolean().catch(false),
header_description: z.string().catch(''),
avatar_description: v.fallback(v.string(), ''),
enable_rss: v.fallback(v.boolean(), false),
header_description: v.fallback(v.string(), ''),
verified: z.boolean().optional().catch(undefined),
domain: z.string().catch(''),
verified: v.fallback(v.optional(v.boolean()), undefined),
domain: v.fallback(v.string(), ''),
__meta: coerceObject({
pleroma: z.any().optional().catch(undefined),
source: z.any().optional().catch(undefined),
pleroma: v.fallback(v.any(), undefined),
source: v.fallback(v.any(), undefined),
}),
});
const accountWithMovedAccountSchema = baseAccountSchema.extend({
moved: z.lazy((): typeof baseAccountSchema => accountWithMovedAccountSchema as any).nullable().catch(null),
const accountWithMovedAccountSchema = v.object({
...baseAccountSchema.entries,
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;
};
type Account = z.infer<typeof accountWithMovedAccountSchema> & 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, accountWithMovedAccountSchema.extend({
source: z.object({
note: z.string().catch(''),
const untypedCredentialAccountSchema = v.pipe(v.any(), preprocessAccount, v.object({
...accountWithMovedAccountSchema.entries,
source: v.fallback(v.nullable(v.object({
note: v.fallback(v.string(), ''),
fields: filteredArray(fieldSchema),
privacy: z.enum(['public', 'unlisted', 'private', 'direct']),
sensitive: z.boolean().catch(false),
language: z.string().nullable().catch(null),
follow_requests_count: z.number().int().nonnegative().catch(0),
privacy: v.picklist(['public', 'unlisted', 'private', 'direct']),
sensitive: v.fallback(v.boolean(), false),
language: v.fallback(v.nullable(v.string()), null),
follow_requests_count: v.fallback(v.pipe(v.number(), v.integer(), v.minValue(0)), 0),
show_role: z.boolean().optional().nullable().catch(undefined),
no_rich_text: z.boolean().optional().nullable().catch(undefined),
discoverable: z.boolean().optional().catch(undefined),
actor_type: z.string().optional().catch(undefined),
show_birthday: z.boolean().optional().catch(undefined),
}).nullable().catch(null),
role: roleSchema.nullable().catch(null),
show_role: v.fallback(v.nullable(v.optional(v.boolean())), undefined),
no_rich_text: v.fallback(v.nullable(v.optional(v.boolean())), undefined),
discoverable: v.fallback(v.optional(v.boolean()), undefined),
actor_type: v.fallback(v.optional(v.string()), undefined),
show_birthday: v.fallback(v.optional(v.boolean()), undefined),
})), null),
role: v.fallback(v.nullable(roleSchema), null),
settings_store: z.record(z.any()).optional().catch(undefined),
chat_token: z.string().optional().catch(undefined),
allow_following_move: z.boolean().optional().catch(undefined),
unread_conversation_count: z.number().optional().catch(undefined),
unread_notifications_count: z.number().optional().catch(undefined),
notification_settings: z.object({
block_from_strangers: z.boolean().catch(false),
hide_notification_contents: z.boolean().catch(false),
}).optional().catch(undefined),
settings_store: v.fallback(v.optional(v.record(v.string(), v.any())), undefined),
chat_token: v.fallback(v.optional(v.string()), undefined),
allow_following_move: v.fallback(v.optional(v.boolean()), undefined),
unread_conversation_count: v.fallback(v.optional(v.number()), undefined),
unread_notifications_count: v.fallback(v.optional(v.number()), undefined),
notification_settings: v.fallback(v.optional(v.object({
block_from_strangers: v.fallback(v.boolean(), false),
hide_notification_contents: v.fallback(v.boolean(), false),
})), undefined),
}));
type CredentialAccount = z.infer<typeof untypedCredentialAccountSchema> & WithMoved;
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, accountWithMovedAccountSchema.extend({
mute_expires_at: dateSchema.nullable().catch(null),
const untypedMutedAccountSchema = v.pipe(v.any(), preprocessAccount, v.object({
...accountWithMovedAccountSchema.entries,
mute_expires_at: v.fallback(v.nullable(datetimeSchema), null),
}));
type MutedAccount = z.infer<typeof untypedMutedAccountSchema> & WithMoved;
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,

View File

@ -1,67 +1,71 @@
import { z } from 'zod';
import * as v from 'valibot';
import { accountSchema } from '../account';
import { roleSchema } from '../role';
import { dateSchema, filteredArray } from '../utils';
import { datetimeSchema, 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
? roleSchema.parse({ name: 'Admin' })
: account.roles?.moderator
? roleSchema.parse({ 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;
}, z.object({
id: z.string(),
username: z.string(),
domain: z.string().nullable().catch(null),
created_at: dateSchema,
email: z.string().nullable().catch(null),
ip: z.string().ip().nullable().catch(null),
ips: filteredArray(adminIpSchema),
locale: z.string().nullable().catch(null),
invite_request: z.string().nullable().catch(null),
role: roleSchema.nullable().catch(null),
confirmed: z.boolean().catch(false),
approved: z.boolean().catch(false),
disabled: z.boolean().catch(false),
silenced: z.boolean().catch(false),
suspended: z.boolean().catch(false),
account: accountSchema.nullable().catch(null),
created_by_application_id: z.string().optional().catch(undefined),
invited_by_account_id: z.string().optional().catch(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: v.fallback(datetimeSchema, new Date().toISOString()),
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: z.string().nullable().catch(null),
display_name: z.string().nullable().catch(null),
suggested: z.boolean().nullable().catch(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 = z.infer<typeof adminAccountSchema>;
type AdminAccount = v.InferOutput<typeof adminAccountSchema>;
export {
adminAccountSchema,

View File

@ -1,16 +1,21 @@
import pick from 'lodash.pick';
import { z } from 'zod';
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'),
}), announcementSchema.extend({
raw_content: z.string().catch(''),
}));
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 = z.infer<typeof adminAnnouncementSchema>;
type AdminAnnouncement = v.InferOutput<typeof adminAnnouncementSchema>;
export { adminAnnouncementSchema, type AdminAnnouncement };

View File

@ -1,12 +1,12 @@
import { z } from 'zod';
import * as v from 'valibot';
/** @see {@link https://docs.joinmastodon.org/entities/Admin_CanonicalEmailBlock/} */
const adminCanonicalEmailBlockSchema = z.object({
id: z.string(),
canonical_email_hash: z.string(),
const adminCanonicalEmailBlockSchema = v.object({
id: v.string(),
canonical_email_hash: v.string(),
});
type AdminCanonicalEmailBlock = z.infer<typeof adminCanonicalEmailBlockSchema>;
type AdminCanonicalEmailBlock = v.InferOutput<typeof adminCanonicalEmailBlockSchema>;
export {
adminCanonicalEmailBlockSchema,

View File

@ -1,17 +1,19 @@
import { z } from 'zod';
import * as v from 'valibot';
import { datetimeSchema } from '../utils';
/** @see {@link https://docs.joinmastodon.org/entities/Admin_Cohort/} */
const adminCohortSchema = z.object({
period: z.string().datetime({ offset: true }),
frequency: z.enum(['day', 'month']),
data: z.array(z.object({
date: z.string().datetime({ offset: true }),
rate: z.number(),
value: z.number().int(),
const adminCohortSchema = v.object({
period: datetimeSchema,
frequency: v.picklist(['day', 'month']),
data: v.array(v.object({
date: datetimeSchema,
rate: v.number(),
value: v.pipe(v.number(), v.integer()),
})),
});
type AdminCohort = z.infer<typeof adminCohortSchema>;
type AdminCohort = v.InferOutput<typeof adminCohortSchema>;
export {
adminCohortSchema,

View File

@ -1,18 +1,18 @@
import { z } from 'zod';
import * as v from 'valibot';
/** @see {@link https://docs.joinmastodon.org/entities/Admin_Dimension/} */
const adminDimensionSchema = z.object({
key: z.string(),
data: z.object({
key: z.string(),
human_key: z.string(),
value: z.string(),
unit: z.string().optional().catch(undefined),
human_value: z.string().optional().catch(undefined),
const adminDimensionSchema = v.object({
key: v.string(),
data: v.object({
key: v.string(),
human_key: v.string(),
value: v.string(),
unit: v.fallback(v.optional(v.string()), undefined),
human_value: v.fallback(v.optional(v.string()), undefined),
}),
});
type AdminDimension = z.infer<typeof adminDimensionSchema>;
type AdminDimension = v.InferOutput<typeof adminDimensionSchema>;
export {
adminDimensionSchema,

View File

@ -1,15 +1,15 @@
import { z } from 'zod';
import * as v from 'valibot';
import { dateSchema } from '../utils';
import { datetimeSchema } from '../utils';
/** @see {@link https://docs.joinmastodon.org/entities/Admin_DomainAllow/} */
const adminDomainAllowSchema = z.object({
id: z.string(),
domain: z.string(),
created_at: dateSchema,
const adminDomainAllowSchema = v.object({
id: v.string(),
domain: v.string(),
created_at: datetimeSchema,
});
type AdminDomainAllow = z.infer<typeof adminDomainAllowSchema>;
type AdminDomainAllow = v.InferOutput<typeof adminDomainAllowSchema>;
export {
adminDomainAllowSchema,

View File

@ -1,22 +1,22 @@
import { z } from 'zod';
import * as v from 'valibot';
import { dateSchema } from '../utils';
import { datetimeSchema } from '../utils';
/** @see {@link https://docs.joinmastodon.org/entities/Admin_DomainBlock/} */
const adminDomainBlockSchema = z.object({
id: z.string(),
domain: z.string(),
digest: z.string(),
created_at: dateSchema,
severity: z.enum(['silence', 'suspend', 'noop']),
reject_media: z.boolean(),
reject_reports: z.boolean(),
private_comment: z.string().nullable().catch(null),
public_comment: z.string().nullable().catch(null),
obfuscate: z.boolean(),
const adminDomainBlockSchema = v.object({
id: v.string(),
domain: v.string(),
digest: v.string(),
created_at: datetimeSchema,
severity: v.picklist(['silence', 'suspend', 'noop']),
reject_media: v.boolean(),
reject_reports: v.boolean(),
private_comment: v.fallback(v.nullable(v.string()), null),
public_comment: v.fallback(v.nullable(v.string()), null),
obfuscate: v.boolean(),
});
type AdminDomainBlock = z.infer<typeof adminDomainBlockSchema>;
type AdminDomainBlock = v.InferOutput<typeof adminDomainBlockSchema>;
export {
adminDomainBlockSchema,

View File

@ -1,13 +1,15 @@
import z from 'zod';
import * as v from 'valibot';
const adminDomainSchema = z.object({
domain: z.string().catch(''),
id: z.coerce.string(),
public: z.boolean().catch(false),
resolves: z.boolean().catch(false),
last_checked_at: z.string().datetime().catch(''),
import { datetimeSchema } from '../utils';
const adminDomainSchema = v.object({
domain: v.fallback(v.string(), ''),
id: v.pipe(v.unknown(), v.transform(String)),
public: v.fallback(v.boolean(), false),
resolves: v.fallback(v.boolean(), false),
last_checked_at: v.fallback(v.nullable(datetimeSchema), null),
});
type AdminDomain = z.infer<typeof adminDomainSchema>
type AdminDomain = v.InferOutput<typeof adminDomainSchema>
export { adminDomainSchema, type AdminDomain };

View File

@ -1,20 +1,20 @@
import { z } from 'zod';
import * as v from 'valibot';
import { dateSchema } from '../utils';
import { datetimeSchema } from '../utils';
/** @see {@link https://docs.joinmastodon.org/entities/Admin_EmailDomainBlock/} */
const adminEmailDomainBlockSchema = z.object({
id: z.string(),
domain: z.string(),
created_at: dateSchema,
history: z.array(z.object({
day: z.coerce.string(),
accounts: z.coerce.string(),
uses: z.coerce.string(),
const adminEmailDomainBlockSchema = v.object({
id: v.string(),
domain: v.string(),
created_at: datetimeSchema,
history: v.array(v.object({
day: v.pipe(v.unknown(), v.transform(String)),
accounts: v.pipe(v.unknown(), v.transform(String)),
uses: v.pipe(v.unknown(), v.transform(String)),
})),
});
type AdminEmailDomainBlock = z.infer<typeof adminEmailDomainBlockSchema>;
type AdminEmailDomainBlock = v.InferOutput<typeof adminEmailDomainBlockSchema>;
export {
adminEmailDomainBlockSchema,

View File

@ -1,18 +1,18 @@
import { z } from 'zod';
import * as v from 'valibot';
import { dateSchema } from '../utils';
import { datetimeSchema } from '../utils';
/** @see {@link https://docs.joinmastodon.org/entities/Admin_IpBlock/} */
const adminIpBlockSchema = z.object({
id: z.string(),
ip: z.string().ip(),
severity: z.enum(['sign_up_requires_approval', 'sign_up_block', 'no_access']),
comment: z.string().catch(''),
created_at: dateSchema,
expires_at: z.string().datetime({ offset: true }),
const adminIpBlockSchema = v.object({
id: v.string(),
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: datetimeSchema,
expires_at: v.fallback(v.nullable(datetimeSchema), null),
});
type AdminIpBlock = z.infer<typeof adminIpBlockSchema>;
type AdminIpBlock = v.InferOutput<typeof adminIpBlockSchema>;
export {
adminIpBlockSchema,

View File

@ -1,14 +1,14 @@
import { z } from 'zod';
import * as v from 'valibot';
import { dateSchema } from '../utils';
import { datetimeSchema } from '../utils';
/** @see {@link https://docs.joinmastodon.org/entities/Admin_Ip/} */
const adminIpSchema = z.object({
ip: z.string().ip(),
used_at: dateSchema,
const adminIpSchema = v.object({
ip: v.pipe(v.string(), v.ip()),
used_at: datetimeSchema,
});
type AdminIp = z.infer<typeof adminIpSchema>;
type AdminIp = v.InferOutput<typeof adminIpSchema>;
export {
adminIpSchema,

View File

@ -1,19 +1,21 @@
import { z } from 'zod';
import * as v from 'valibot';
import { datetimeSchema } from '../utils';
/** @see {@link https://docs.joinmastodon.org/entities/Admin_Measure/} */
const adminMeasureSchema = z.object({
key: z.string(),
unit: z.string().nullable().catch(null),
total: z.coerce.number(),
human_value: z.string().optional().catch(undefined),
previous_total: z.coerce.string().optional().catch(undefined),
data: z.array(z.object({
date: z.string().datetime({ offset: true }),
value: z.coerce.string(),
const adminMeasureSchema = v.object({
key: v.string(),
unit: v.fallback(v.nullable(v.string()), null),
total: v.pipe(v.unknown(), v.transform(Number)),
human_value: v.fallback(v.optional(v.string()), undefined),
previous_total: v.fallback(v.optional(v.pipe(v.unknown(), v.transform(String))), undefined),
data: v.array(v.object({
date: datetimeSchema,
value: v.pipe(v.unknown(), v.transform(String)),
})),
});
type AdminMeasure = z.infer<typeof adminMeasureSchema>;
type AdminMeasure = v.InferOutput<typeof adminMeasureSchema>;
export {
adminMeasureSchema,

View File

@ -1,13 +1,13 @@
import z from 'zod';
import * as v from 'valibot';
/** @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminmoderation_log} */
const adminModerationLogEntrySchema = z.object({
id: z.coerce.string(),
data: z.record(z.string(), z.any()).catch({}),
time: z.number().catch(0),
message: z.string().catch(''),
const adminModerationLogEntrySchema = v.object({
id: v.pipe(v.unknown(), v.transform(String)),
data: v.fallback(v.record(v.string(), v.any()), {}),
time: v.fallback(v.number(), 0),
message: v.fallback(v.string(), ''),
});
type AdminModerationLogEntry = z.infer<typeof adminModerationLogEntrySchema>
type AdminModerationLogEntry = v.InferOutput<typeof adminModerationLogEntrySchema>
export { adminModerationLogEntrySchema, type AdminModerationLogEntry };

View File

@ -1,14 +1,14 @@
import { z } from 'zod';
import * as v from 'valibot';
const pleromaConfigSchema = z.object({
configs: z.array(z.object({
value: z.any(),
group: z.string(),
key: z.string(),
const pleromaConfigSchema = v.object({
configs: v.array(v.object({
value: v.any(),
group: v.string(),
key: v.string(),
})),
need_reboot: z.boolean(),
need_reboot: v.boolean(),
});
type PleromaConfig = z.infer<typeof pleromaConfigSchema>
type PleromaConfig = v.InferOutput<typeof pleromaConfigSchema>
export { pleromaConfigSchema, type PleromaConfig };

View File

@ -1,11 +1,15 @@
import z from 'zod';
import * as v from 'valibot';
const adminRelaySchema = z.preprocess((data: any) => ({ id: data.actor, ...data }), z.object({
actor: z.string().catch(''),
id: z.string(),
followed_back: z.boolean().catch(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 = z.infer<typeof adminRelaySchema>
type AdminRelay = v.InferOutput<typeof adminRelaySchema>
export { adminRelaySchema, type AdminRelay };

View File

@ -1,46 +1,50 @@
import pick from 'lodash.pick';
import { z } from 'zod';
import * as v from 'valibot';
import { ruleSchema } from '../rule';
import { statusWithoutAccountSchema } from '../status';
import { dateSchema, filteredArray } from '../utils';
import { datetimeSchema, 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;
}, z.object({
id: z.string(),
action_taken: z.boolean().optional().catch(undefined),
action_taken_at: dateSchema.nullable().catch(null),
category: z.string().optional().catch(undefined),
comment: z.string().optional().catch(undefined),
forwarded: z.boolean().optional().catch(undefined),
created_at: dateSchema.optional().catch(undefined),
updated_at: dateSchema.optional().catch(undefined),
account: adminAccountSchema,
target_account: adminAccountSchema,
assigned_account: adminAccountSchema.nullable().catch(null),
action_taken_by_account: adminAccountSchema.nullable().catch(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(datetimeSchema), 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(datetimeSchema), undefined),
updated_at: v.fallback(v.optional(datetimeSchema), 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 = z.infer<typeof adminReportSchema>;
type AdminReport = v.InferOutput<typeof adminReportSchema>;
export { adminReportSchema, type AdminReport };

View File

@ -1,13 +1,13 @@
import { z } from 'zod';
import * as v from 'valibot';
/** @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminrules} */
const adminRuleSchema = z.object({
id: z.string(),
text: z.string().catch(''),
hint: z.string().catch(''),
priority: z.number().nullable().catch(null),
const adminRuleSchema = v.object({
id: v.string(),
text: v.fallback(v.string(), ''),
hint: v.fallback(v.string(), ''),
priority: v.fallback(v.nullable(v.number()), null),
});
type AdminRule = z.infer<typeof adminRuleSchema>;
type AdminRule = v.InferOutput<typeof adminRuleSchema>;
export { adminRuleSchema, type AdminRule };

View File

@ -1,16 +1,17 @@
import { z } from 'zod';
import * as v from 'valibot';
import { tagSchema } from '../tag';
/** @see {@link https://docs.joinmastodon.org/entities/Tag/#admin} */
const adminTagSchema = tagSchema.extend({
id: z.string(),
trendable: z.boolean(),
usable: z.boolean(),
requires_review: z.boolean(),
const adminTagSchema = v.object({
...tagSchema.entries,
id: v.string(),
trendable: v.boolean(),
usable: v.boolean(),
requires_review: v.boolean(),
});
type AdminTag = z.infer<typeof adminTagSchema>;
type AdminTag = v.InferOutput<typeof adminTagSchema>;
export {
adminTagSchema,

View File

@ -1,15 +1,15 @@
import { z } from 'zod';
import * as v from 'valibot';
/** @see {@link https://docs.joinmastodon.org/entities/announcement/} */
const announcementReactionSchema = z.object({
name: z.string().catch(''),
count: z.number().int().nonnegative().catch(0),
me: z.boolean().catch(false),
url: z.string().nullable().catch(null),
static_url: z.string().nullable().catch(null),
announcement_id: z.string().catch(''),
const announcementReactionSchema = v.object({
name: v.fallback(v.string(), ''),
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),
announcement_id: v.fallback(v.string(), ''),
});
type AnnouncementReaction = z.infer<typeof announcementReactionSchema>;
type AnnouncementReaction = v.InferOutput<typeof announcementReactionSchema>;
export { announcementReactionSchema, type AnnouncementReaction };

View File

@ -1,33 +1,34 @@
import { z } from 'zod';
import * as v from 'valibot';
import { announcementReactionSchema } from './announcement-reaction';
import { customEmojiSchema } from './custom-emoji';
import { mentionSchema } from './mention';
import { tagSchema } from './tag';
import { dateSchema, filteredArray } from './utils';
import { datetimeSchema, filteredArray } from './utils';
/** @see {@link https://docs.joinmastodon.org/entities/announcement/} */
const announcementSchema = z.object({
id: z.string(),
content: z.string().catch(''),
starts_at: z.string().datetime().nullable().catch(null),
ends_at: z.string().datetime().nullable().catch(null),
all_day: z.boolean().catch(false),
read: z.boolean().catch(false),
published_at: dateSchema,
const announcementSchema = v.object({
id: v.string(),
content: v.fallback(v.string(), ''),
starts_at: v.fallback(v.nullable(datetimeSchema), null),
ends_at: v.fallback(v.nullable(datetimeSchema), null),
all_day: v.fallback(v.boolean(), false),
read: v.fallback(v.boolean(), false),
published_at: v.fallback(datetimeSchema, new Date().toISOString()),
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,
z.record(z.string(), z.string()),
: statuses),
v.record(v.string(), v.string()),
),
mentions: filteredArray(mentionSchema),
tags: filteredArray(tagSchema),
emojis: filteredArray(customEmojiSchema),
updated_at: dateSchema,
updated_at: v.fallback(datetimeSchema, new Date().toISOString()),
});
type Announcement = z.infer<typeof announcementSchema>;
type Announcement = v.InferOutput<typeof announcementSchema>;
export { announcementSchema, type Announcement };

View File

@ -1,19 +1,19 @@
import { z } from 'zod';
import * as v from 'valibot';
/** @see {@link https://docs.joinmastodon.org/entities/Application/} */
const applicationSchema = z.object({
name: z.string().catch(''),
website: z.string().optional().catch(undefined),
client_id: z.string().optional().catch(undefined),
client_secret: z.string().optional().catch(undefined),
redirect_uri: z.string().optional().catch(undefined),
const applicationSchema = v.object({
name: v.fallback(v.string(), ''),
website: v.fallback(v.optional(v.string()), undefined),
client_id: v.fallback(v.optional(v.string()), undefined),
client_secret: v.fallback(v.optional(v.string()), undefined),
redirect_uri: v.fallback(v.optional(v.string()), undefined),
id: z.string().optional().catch(undefined),
id: v.fallback(v.optional(v.string()), undefined),
/** @deprecated */
vapid_key: z.string().optional().catch(undefined),
vapid_key: v.fallback(v.optional(v.string()), undefined),
});
type Application = z.infer<typeof applicationSchema>;
type Application = v.InferOutput<typeof applicationSchema>;
export { applicationSchema, type Application };

View File

@ -1,17 +1,17 @@
import { z } from 'zod';
import * as v from 'valibot';
import { dateSchema, mimeSchema } from './utils';
import { datetimeSchema, mimeSchema } from './utils';
/** @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#post-apiv1pleromabackups} */
const backupSchema = z.object({
id: z.coerce.string(),
const backupSchema = v.object({
id: v.pipe(v.unknown(), v.transform(String)),
contentType: mimeSchema,
file_size: z.number().catch(0),
inserted_at: dateSchema,
processed: z.boolean().catch(false),
url: z.string().catch(''),
file_size: v.fallback(v.number(), 0),
inserted_at: datetimeSchema,
processed: v.fallback(v.boolean(), false),
url: v.fallback(v.string(), ''),
});
type Backup = z.infer<typeof backupSchema>;
type Backup = v.InferOutput<typeof backupSchema>;
export { backupSchema, type Backup };

View File

@ -1,12 +1,12 @@
import { z } from 'zod';
import * as v from 'valibot';
const bookmarkFolderSchema = z.object({
id: z.coerce.string(),
name: z.string().catch(''),
emoji: z.string().nullable().catch(null),
emoji_url: z.string().nullable().catch(null),
const bookmarkFolderSchema = v.object({
id: v.pipe(v.unknown(), v.transform(String)),
name: v.fallback(v.string(), ''),
emoji: v.fallback(v.nullable(v.string()), null),
emoji_url: v.fallback(v.nullable(v.string()), null),
});
type BookmarkFolder = z.infer<typeof bookmarkFolderSchema>;
type BookmarkFolder = v.InferOutput<typeof bookmarkFolderSchema>;
export { bookmarkFolderSchema, type BookmarkFolder };

View File

@ -1,23 +1,23 @@
import { z } from 'zod';
import * as v from 'valibot';
import { customEmojiSchema } from './custom-emoji';
import { mediaAttachmentSchema } from './media-attachment';
import { previewCardSchema } from './preview-card';
import { dateSchema, filteredArray } from './utils';
import { datetimeSchema, filteredArray } from './utils';
/** @see {@link https://docs.pleroma.social/backend/development/API/chats/#getting-the-messages-for-a-chat} */
const chatMessageSchema = z.object({
id: z.string(),
content: z.string().catch(''),
chat_id: z.string(),
account_id: z.string(),
created_at: dateSchema,
const chatMessageSchema = v.object({
id: v.string(),
content: v.fallback(v.string(), ''),
chat_id: v.string(),
account_id: v.string(),
created_at: datetimeSchema,
emojis: filteredArray(customEmojiSchema),
attachment: mediaAttachmentSchema.nullable().catch(null),
unread: z.boolean(),
card: previewCardSchema.nullable().catch(null),
attachment: v.fallback(v.nullable(mediaAttachmentSchema), null),
unread: v.boolean(),
card: v.fallback(v.nullable(previewCardSchema), null),
});
type ChatMessage = z.infer<typeof chatMessageSchema>;
type ChatMessage = v.InferOutput<typeof chatMessageSchema>;
export { chatMessageSchema, type ChatMessage };

View File

@ -1,18 +1,18 @@
import { z } from 'zod';
import * as v from 'valibot';
import { accountSchema } from './account';
import { chatMessageSchema } from './chat-message';
import { dateSchema } from './utils';
import { datetimeSchema } from './utils';
/** @see {@link https://docs.pleroma.social/backend/development/API/chats/#getting-a-list-of-chats} */
const chatSchema = z.object({
id: z.string(),
const chatSchema = v.object({
id: v.string(),
account: accountSchema,
unread: z.number().int(),
last_message: chatMessageSchema.nullable().catch(null),
created_at: dateSchema,
unread: v.pipe(v.number(), v.integer()),
last_message: v.fallback(v.nullable(chatMessageSchema), null),
updated_at: datetimeSchema,
});
type Chat = z.infer<typeof chatSchema>;
type Chat = v.InferOutput<typeof chatSchema>;
export { chatSchema, type Chat };

View File

@ -1,13 +1,13 @@
import { z } from 'zod';
import * as v from 'valibot';
import { statusSchema } from './status';
/** @see {@link https://docs.joinmastodon.org/entities/Context/} */
const contextSchema = z.object({
ancestors: z.array(statusSchema),
descendants: z.array(statusSchema),
const contextSchema = v.object({
ancestors: v.array(statusSchema),
descendants: v.array(statusSchema),
});
type Context = z.infer<typeof contextSchema>;
type Context = v.InferOutput<typeof contextSchema>;
export { contextSchema, type Context };

View File

@ -1,17 +1,17 @@
import { z } from 'zod';
import * as v from 'valibot';
import { filteredArray } from './utils';
import { accountSchema, statusSchema } from '.';
/** @see {@link https://docs.joinmastodon.org/entities/Conversation} */
const conversationSchema = z.object({
id: z.string(),
unread: z.boolean().catch(false),
const conversationSchema = v.object({
id: v.string(),
unread: v.fallback(v.boolean(), false),
accounts: filteredArray(accountSchema),
last_status: statusSchema.nullable().catch(null),
last_status: v.fallback(v.nullable(statusSchema), null),
});
type Conversation = z.infer<typeof conversationSchema>;
type Conversation = v.InferOutput<typeof conversationSchema>;
export { conversationSchema, type Conversation };

View File

@ -1,17 +1,17 @@
import z from 'zod';
import * as v from 'valibot';
/**
* Represents a custom emoji.
* @see {@link https://docs.joinmastodon.org/entities/CustomEmoji/}
*/
const customEmojiSchema = z.object({
shortcode: z.string(),
url: z.string(),
static_url: z.string().catch(''),
visible_in_picker: z.boolean().catch(true),
category: z.string().nullable().catch(null),
const customEmojiSchema = v.object({
shortcode: v.string(),
url: v.string(),
static_url: v.fallback(v.string(), ''),
visible_in_picker: v.fallback(v.boolean(), true),
category: v.fallback(v.nullable(v.string()), null),
});
type CustomEmoji = z.infer<typeof customEmojiSchema>;
type CustomEmoji = v.InferOutput<typeof customEmojiSchema>;
export { customEmojiSchema, type CustomEmoji };

View File

@ -1,10 +1,10 @@
import { z } from 'zod';
import * as v from 'valibot';
const directoryCategorySchema = z.object({
category: z.string(),
servers_count: z.coerce.number().nullable().catch(null),
const directoryCategorySchema = v.object({
category: v.string(),
servers_count: v.fallback(v.nullable(v.pipe(v.unknown(), v.transform(Number))), null),
});
type DirectoryCategory = z.infer<typeof directoryCategorySchema>;
type DirectoryCategory = v.InferOutput<typeof directoryCategorySchema>;
export { directoryCategorySchema, type DirectoryCategory };

View File

@ -1,11 +1,11 @@
import { z } from 'zod';
import * as v from 'valibot';
const directoryLanguageSchema = z.object({
locale: z.string(),
language: z.string(),
servers_count: z.coerce.number().nullable().catch(null),
const directoryLanguageSchema = v.object({
locale: v.string(),
language: v.string(),
servers_count: v.fallback(v.nullable(v.pipe(v.unknown(), v.transform(Number))), null),
});
type DirectoryLanguage = z.infer<typeof directoryLanguageSchema>;
type DirectoryLanguage = v.InferOutput<typeof directoryLanguageSchema>;
export { directoryLanguageSchema, type DirectoryLanguage };

View File

@ -1,21 +1,21 @@
import { z } from 'zod';
import * as v from 'valibot';
const directoryServerSchema = z.object({
domain: z.string(),
version: z.string(),
description: z.string(),
languages: z.array(z.string()),
region: z.string(),
categories: z.array(z.string()),
proxied_thumbnail: z.string().url().nullable().catch(null),
blurhash: z.string().nullable().catch(null),
total_users: z.coerce.number(),
last_week_users: z.coerce.number(),
approval_required: z.boolean(),
language: z.string(),
category: z.string(),
const directoryServerSchema = v.object({
domain: v.string(),
version: v.string(),
description: v.string(),
languages: v.array(v.string()),
region: v.string(),
categories: v.array(v.string()),
proxied_thumbnail: v.fallback(v.nullable(v.pipe(v.string(), v.url())), null),
blurhash: v.fallback(v.nullable(v.string()), null),
total_users: v.pipe(v.unknown(), v.transform(Number)),
last_week_users: v.pipe(v.unknown(), v.transform(Number)),
approval_required: v.boolean(),
language: v.string(),
category: v.string(),
});
type DirectoryServer = z.infer<typeof directoryServerSchema>;
type DirectoryServer = v.InferOutput<typeof directoryServerSchema>;
export { directoryServerSchema, type DirectoryServer };

View File

@ -1,12 +1,12 @@
import { z } from 'zod';
import * as v from 'valibot';
const directoryStatisticsPeriodSchema = z.object({
period: z.string().date(),
server_count: z.coerce.number().nullable().catch(null),
user_count: z.coerce.number().nullable().catch(null),
active_user_count: z.coerce.number().nullable().catch(null),
const directoryStatisticsPeriodSchema = v.object({
period: v.pipe(v.string(), v.isoDate()),
server_count: v.fallback(v.nullable(v.pipe(v.unknown(), v.transform(Number))), null),
user_count: v.fallback(v.nullable(v.pipe(v.unknown(), v.transform(Number))), null),
active_user_count: v.fallback(v.nullable(v.pipe(v.unknown(), v.transform(Number))), null),
});
type DirectoryStatisticsPeriod = z.infer<typeof directoryStatisticsPeriodSchema>;
type DirectoryStatisticsPeriod = v.InferOutput<typeof directoryStatisticsPeriodSchema>;
export { directoryStatisticsPeriodSchema, type DirectoryStatisticsPeriod };

View File

@ -1,13 +1,13 @@
import { z } from 'zod';
import * as v from 'valibot';
/** @see {@link https://docs.joinmastodon.org/entities/DomainBlock} */
const domainBlockSchema = z.object({
domain: z.string(),
digest: z.string(),
severity: z.enum(['silence', 'suspend']),
comment: z.string().optional().catch(undefined),
const domainBlockSchema = v.object({
domain: v.string(),
digest: v.string(),
severity: v.picklist(['silence', 'suspend']),
comment: v.fallback(v.optional(v.string()), undefined),
});
type DomainBlock = z.infer<typeof domainBlockSchema>;
type DomainBlock = v.InferOutput<typeof domainBlockSchema>;
export { domainBlockSchema, type DomainBlock };

View File

@ -1,34 +1,39 @@
import { z } from 'zod';
import * as v from 'valibot';
import { accountSchema } from './account';
import { emojiSchema, filteredArray } from './utils';
const baseEmojiReactionSchema = z.object({
count: z.number().nullable().catch(null),
me: z.boolean().catch(false),
const baseEmojiReactionSchema = v.object({
count: v.fallback(v.nullable(v.number()), null),
me: v.fallback(v.boolean(), false),
name: emojiSchema,
url: z.literal(undefined).catch(undefined),
static_url: z.literal(undefined).catch(undefined),
url: v.fallback(v.undefined(), undefined),
static_url: v.fallback(v.undefined(), undefined),
accounts: filteredArray(accountSchema),
account_ids: filteredArray(z.string()).catch([]),
account_ids: v.fallback(filteredArray(v.string()), []),
});
const customEmojiReactionSchema = baseEmojiReactionSchema.extend({
name: z.string(),
url: z.string().url(),
static_url: z.string().url(),
const customEmojiReactionSchema = v.object({
...baseEmojiReactionSchema.entries,
name: v.string(),
url: v.pipe(v.string(), v.url()),
static_url: v.pipe(v.string(), v.url()),
});
/**
* 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, baseEmojiReactionSchema.or(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 = z.infer<typeof emojiReactionSchema>;
type EmojiReaction = v.InferOutput<typeof emojiReactionSchema>;
export { emojiReactionSchema, type EmojiReaction };

View File

@ -1,13 +1,13 @@
import { z } from 'zod';
import * as v from 'valibot';
import { dateSchema } from './utils';
import { datetimeSchema } from './utils';
/** @see {@link https://docs.joinmastodon.org/entities/ExtendedDescription} */
const extendedDescriptionSchema = z.object({
updated_at: dateSchema,
content: z.string(),
const extendedDescriptionSchema = v.object({
updated_at: datetimeSchema,
content: v.string(),
});
type ExtendedDescription = z.infer<typeof extendedDescriptionSchema>;
type ExtendedDescription = v.InferOutput<typeof extendedDescriptionSchema>;
export { extendedDescriptionSchema, type ExtendedDescription };

View File

@ -1,14 +1,14 @@
import z from 'zod';
import * as v from 'valibot';
import { accountSchema } from './account';
import { filteredArray } from './utils';
/** @see {@link https://docs.joinmastodon.org/entities/FamiliarFollowers/} */
const familiarFollowersSchema = z.object({
id: z.string(),
const familiarFollowersSchema = v.object({
id: v.string(),
accounts: filteredArray(accountSchema),
});
type FamiliarFollowers = z.infer<typeof familiarFollowersSchema>
type FamiliarFollowers = v.InferOutput<typeof familiarFollowersSchema>
export { familiarFollowersSchema, type FamiliarFollowers };

View File

@ -1,14 +1,14 @@
import { z } from 'zod';
import * as v from 'valibot';
/** @see {@link https://docs.joinmastodon.org/entities/FeaturedTag/} */
const featuredTagSchema = z.object({
id: z.string(),
name: z.string(),
url: z.string().optional().catch(undefined),
statuses_count: z.number(),
last_status_at: z.number(),
const featuredTagSchema = v.object({
id: v.string(),
name: v.string(),
url: v.fallback(v.optional(v.string()), undefined),
statuses_count: v.number(),
last_status_at: v.number(),
});
type FeaturedTag = z.infer<typeof featuredTagSchema>;
type FeaturedTag = v.InferOutput<typeof featuredTagSchema>;
export { featuredTagSchema, type FeaturedTag };

View File

@ -1,14 +1,14 @@
import { z } from 'zod';
import * as v from 'valibot';
import { filterSchema } from './filter';
/** @see {@link https://docs.joinmastodon.org/entities/FilterResult/} */
const filterResultSchema = z.object({
const filterResultSchema = v.object({
filter: filterSchema,
keyword_matches: z.array(z.string()).nullable().catch(null),
status_matches: z.array(z.string()).nullable().catch(null),
keyword_matches: v.fallback(v.nullable(v.string()), null),
status_matches: v.fallback(v.nullable(v.string()), null),
});
type FilterResult = z.infer<typeof filterResultSchema>;
type FilterResult = v.InferOutput<typeof filterResultSchema>;
export { filterResultSchema, type FilterResult };

View File

@ -1,45 +1,49 @@
import { z } from 'zod';
import * as v from 'valibot';
import { filteredArray } from './utils';
import { datetimeSchema, filteredArray } from './utils';
/** @see {@link https://docs.joinmastodon.org/entities/FilterKeyword/} */
const filterKeywordSchema = z.object({
id: z.string(),
keyword: z.string(),
whole_word: z.boolean(),
const filterKeywordSchema = v.object({
id: v.string(),
keyword: v.string(),
whole_word: v.boolean(),
});
/** @see {@link https://docs.joinmastodon.org/entities/FilterStatus/} */
const filterStatusSchema = z.object({
id: z.string(),
status_id: z.string(),
const filterStatusSchema = v.object({
id: v.string(),
status_id: v.string(),
});
/** @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;
}, z.object({
id: z.string(),
title: z.string(),
context: z.array(z.enum(['home', 'notifications', 'public', 'thread', 'account'])),
expires_at: z.string().datetime({ offset: true }).nullable().catch(null),
filter_action: z.enum(['warn', 'hide']).catch('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(datetimeSchema), null),
filter_action: v.fallback(v.picklist(['warn', 'hide']), 'warn'),
keywords: filteredArray(filterKeywordSchema),
statuses: filteredArray(filterStatusSchema),
}),
);
type Filter = z.infer<typeof filterSchema>;
type Filter = v.InferOutput<typeof filterSchema>;
export { filterKeywordSchema, filterStatusSchema, filterSchema, type Filter };

View File

@ -1,4 +1,4 @@
import z from 'zod';
import * as v from 'valibot';
import { accountSchema } from './account';
@ -10,12 +10,12 @@ enum GroupRoles {
type GroupRole =`${GroupRoles}`;
const groupMemberSchema = z.object({
id: z.string(),
const groupMemberSchema = v.object({
id: v.string(),
account: accountSchema,
role: z.nativeEnum(GroupRoles),
role: v.enum(GroupRoles),
});
type GroupMember = z.infer<typeof groupMemberSchema>;
type GroupMember = v.InferOutput<typeof groupMemberSchema>;
export { groupMemberSchema, type GroupMember, GroupRoles, type GroupRole };

View File

@ -1,14 +1,14 @@
import z from 'zod';
import * as v from 'valibot';
import { GroupRoles } from './group-member';
const groupRelationshipSchema = z.object({
id: z.string(),
member: z.boolean().catch(false),
role: z.nativeEnum(GroupRoles).catch(GroupRoles.USER),
requested: z.boolean().catch(false),
const groupRelationshipSchema = v.object({
id: v.string(),
member: v.fallback(v.boolean(), false),
role: v.fallback(v.enum(GroupRoles), GroupRoles.USER),
requested: v.fallback(v.boolean(), false),
});
type GroupRelationship = z.infer<typeof groupRelationshipSchema>;
type GroupRelationship = v.InferOutput<typeof groupRelationshipSchema>;
export { groupRelationshipSchema, type GroupRelationship };

View File

@ -1,33 +1,33 @@
import z from 'zod';
import * as v from 'valibot';
import { customEmojiSchema } from './custom-emoji';
import { groupRelationshipSchema } from './group-relationship';
import { filteredArray } from './utils';
import { datetimeSchema, filteredArray } from './utils';
const groupSchema = z.object({
avatar: z.string().catch(''),
avatar_static: z.string().catch(''),
created_at: z.string().datetime().catch(new Date().toUTCString()),
display_name: z.string().catch(''),
domain: z.string().catch(''),
const groupSchema = v.object({
avatar: v.fallback(v.string(), ''),
avatar_static: v.fallback(v.string(), ''),
created_at: v.fallback(datetimeSchema, new Date().toISOString()),
display_name: v.fallback(v.string(), ''),
domain: v.fallback(v.string(), ''),
emojis: filteredArray(customEmojiSchema),
header: z.string().catch(''),
header_static: z.string().catch(''),
id: z.coerce.string(),
locked: z.boolean().catch(false),
membership_required: z.boolean().catch(false),
members_count: z.number().catch(0),
owner: z.object({ id: z.string() }).nullable().catch(null),
note: z.string().transform(note => note === '<p></p>' ? '' : note).catch(''),
relationship: groupRelationshipSchema.nullable().catch(null), // Dummy field to be overwritten later
statuses_visibility: z.string().catch('public'),
uri: z.string().catch(''),
url: z.string().catch(''),
header: v.fallback(v.string(), ''),
header_static: v.fallback(v.string(), ''),
id: v.pipe(v.unknown(), v.transform(String)),
locked: v.fallback(v.boolean(), false),
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: 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(), ''),
url: v.fallback(v.string(), ''),
avatar_description: z.string().catch(''),
header_description: z.string().catch(''),
avatar_description: v.fallback(v.string(), ''),
header_description: v.fallback(v.string(), ''),
});
type Group = z.infer<typeof groupSchema>;
type Group = v.InferOutput<typeof groupSchema>;
export { groupSchema, type Group };

View File

@ -1,5 +1,5 @@
/* eslint sort-keys: "error" */
import z from 'zod';
import * as v from 'valibot';
import { accountSchema } from './account';
import { ruleSchema } from './rule';
@ -36,7 +36,7 @@ const instanceV1ToV2 = (data: any) => {
uri,
urls,
...instance
} = instanceV1Schema.parse(data);
} = v.parse(instanceV1Schema, data);
return {
...instance,
@ -108,231 +108,226 @@ const fixVersion = (version: string) => {
};
const configurationSchema = coerceObject({
accounts: z.object({
allow_custom_css: z.boolean(),
max_featured_tags: z.number().int(),
max_profile_fields: z.number().int(),
}).nullable().catch(null),
accounts: v.fallback(v.nullable(v.object({
allow_custom_css: v.boolean(),
max_featured_tags: v.pipe(v.number(), v.integer()),
max_profile_fields: v.pipe(v.number(), v.integer()),
})), null),
chats: coerceObject({
max_characters: z.number().catch(5000),
max_characters: v.fallback(v.number(), 5000),
}),
groups: coerceObject({
max_characters_description: z.number().catch(160),
max_characters_name: z.number().catch(50),
max_characters_description: v.fallback(v.number(), 160),
max_characters_name: v.fallback(v.number(), 50),
}),
media_attachments: coerceObject({
image_matrix_limit: z.number().optional().catch(undefined),
image_size_limit: z.number().optional().catch(undefined),
supported_mime_types: mimeSchema.array().optional().catch(undefined),
video_duration_limit: z.number().optional().catch(undefined),
video_frame_rate_limit: z.number().optional().catch(undefined),
video_matrix_limit: z.number().optional().catch(undefined),
video_size_limit: z.number().optional().catch(undefined),
image_matrix_limit: v.fallback(v.optional(v.number()), undefined),
image_size_limit: v.fallback(v.optional(v.number()), undefined),
supported_mime_types: v.fallback(v.optional(v.array(mimeSchema)), undefined),
video_duration_limit: v.fallback(v.optional(v.number()), undefined),
video_frame_rate_limit: v.fallback(v.optional(v.number()), undefined),
video_matrix_limit: v.fallback(v.optional(v.number()), undefined),
video_size_limit: v.fallback(v.optional(v.number()), undefined),
}),
polls: coerceObject({
max_characters_per_option: z.number().catch(25),
max_expiration: z.number().catch(2629746),
max_options: z.number().catch(4),
min_expiration: z.number().catch(300),
max_characters_per_option: v.fallback(v.number(), 25),
max_expiration: v.fallback(v.number(), 2629746),
max_options: v.fallback(v.number(), 4),
min_expiration: v.fallback(v.number(), 300),
}),
reactions: coerceObject({
max_reactions: z.number().catch(0),
max_reactions: v.fallback(v.number(), 0),
}),
statuses: coerceObject({
characters_reserved_per_url: z.number().optional().catch(undefined),
max_characters: z.number().catch(500),
max_media_attachments: z.number().catch(4),
characters_reserved_per_url: v.fallback(v.optional(v.number()), undefined),
max_characters: v.fallback(v.number(), 500),
max_media_attachments: v.fallback(v.number(), 4),
}),
translation: coerceObject({
enabled: z.boolean().catch(false),
enabled: v.fallback(v.boolean(), false),
}),
urls: coerceObject({
streaming: z.string().url().optional().catch(undefined),
streaming: v.fallback(v.optional(v.pipe(v.string(), v.url())), undefined),
}),
vapid: coerceObject({
public_key: z.string().catch(''),
public_key: v.fallback(v.string(), ''),
}),
});
const contactSchema = coerceObject({
contact_account: accountSchema.optional().catch(undefined),
email: z.string().email().catch(''),
contact_account: v.fallback(v.optional(accountSchema), undefined),
email: v.fallback(v.pipe(v.string(), v.email()), ''),
});
const pleromaSchema = coerceObject({
metadata: coerceObject({
account_activation_required: z.boolean().catch(false),
birthday_min_age: z.number().catch(0),
birthday_required: z.boolean().catch(false),
description_limit: z.number().catch(1500),
features: z.string().array().catch([]),
account_activation_required: v.fallback(v.boolean(), false),
birthday_min_age: v.fallback(v.number(), 0),
birthday_required: v.fallback(v.boolean(), false),
description_limit: v.fallback(v.number(), 1500),
features: v.fallback(v.array(v.string()), []),
federation: coerceObject({
enabled: z.boolean().catch(true), // Assume true unless explicitly false
mrf_policies: z.string().array().optional().catch(undefined),
enabled: v.fallback(v.boolean(), true), // Assume true unless explicitly false
mrf_policies: v.fallback(v.optional(v.array(v.string())), undefined),
mrf_simple: coerceObject({
accept: z.string().array().catch([]),
avatar_removal: z.string().array().catch([]),
banner_removal: z.string().array().catch([]),
federated_timeline_removal: z.string().array().catch([]),
followers_only: z.string().array().catch([]),
media_nsfw: z.string().array().catch([]),
media_removal: z.string().array().catch([]),
reject: z.string().array().catch([]),
reject_deletes: z.string().array().catch([]),
report_removal: z.string().array().catch([]),
accept: v.fallback(v.array(v.string()), []),
avatar_removal: v.fallback(v.array(v.string()), []),
banner_removal: v.fallback(v.array(v.string()), []),
federated_timeline_removal: v.fallback(v.array(v.string()), []),
followers_only: v.fallback(v.array(v.string()), []),
media_nsfw: v.fallback(v.array(v.string()), []),
media_removal: v.fallback(v.array(v.string()), []),
reject: v.fallback(v.array(v.string()), []),
reject_deletes: v.fallback(v.array(v.string()), []),
report_removal: v.fallback(v.array(v.string()), []),
}),
}),
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: z.boolean().catch(false),
allow_inline_images: z.boolean().catch(false),
allow_headings: v.fallback(v.boolean(), false),
allow_inline_images: v.fallback(v.boolean(), false),
}),
migration_cooldown_period: z.number().optional().catch(undefined),
migration_cooldown_period: v.fallback(v.optional(v.number()), undefined),
multitenancy: coerceObject({
domains: z
.array(
z.object({
domain: z.coerce.string(),
id: z.string(),
public: z.boolean().catch(false),
}),
)
.optional(),
enabled: z.boolean().catch(false),
domains: v.optional(v.array(
v.object({
domain: v.pipe(v.unknown(), v.transform(String)),
id: v.string(),
public: v.fallback(v.boolean(), false),
}),
)),
enabled: v.fallback(v.boolean(), false),
}),
post_formats: z.string().array().optional().catch(undefined),
post_formats: v.fallback(v.optional(v.array(v.string())), undefined),
restrict_unauthenticated: coerceObject({
activities: coerceObject({
local: z.boolean().catch(false),
remote: z.boolean().catch(false),
local: v.fallback(v.boolean(), false),
remote: v.fallback(v.boolean(), false),
}),
profiles: coerceObject({
local: z.boolean().catch(false),
remote: z.boolean().catch(false),
local: v.fallback(v.boolean(), false),
remote: v.fallback(v.boolean(), false),
}),
timelines: coerceObject({
bubble: z.boolean().catch(false),
federated: z.boolean().catch(false),
local: z.boolean().catch(false),
bubble: v.fallback(v.boolean(), false),
federated: v.fallback(v.boolean(), false),
local: v.fallback(v.boolean(), false),
}),
}),
translation: coerceObject({
allow_remote: z.boolean().catch(true),
allow_unauthenticated: z.boolean().catch(false),
source_languages: z.string().array().optional().catch(undefined),
target_languages: z.string().array().optional().catch(undefined),
allow_remote: v.fallback(v.boolean(), true),
allow_unauthenticated: v.fallback(v.boolean(), false),
source_languages: v.fallback(v.optional(v.array(v.string())), undefined),
target_languages: v.fallback(v.optional(v.array(v.string())), undefined),
}),
}),
oauth_consumer_strategies: z.string().array().catch([]),
oauth_consumer_strategies: v.fallback(v.array(v.string()), []),
stats: coerceObject({
mau: z.number().optional().catch(undefined),
mau: v.fallback(v.optional(v.number()), undefined),
}),
vapid_public_key: z.string().catch(''),
vapid_public_key: v.fallback(v.string(), ''),
});
const pleromaPollLimitsSchema = coerceObject({
max_expiration: z.number().optional().catch(undefined),
max_option_chars: z.number().optional().catch(undefined),
max_options: z.number().optional().catch(undefined),
min_expiration: z.number().optional().catch(undefined),
max_expiration: v.fallback(v.optional(v.number()), undefined),
max_option_chars: v.fallback(v.optional(v.number()), undefined),
max_options: v.fallback(v.optional(v.number()), undefined),
min_expiration: v.fallback(v.optional(v.number()), undefined),
});
const registrations = coerceObject({
approval_required: z.boolean().catch(false),
enabled: z.boolean().catch(false),
message: z.string().optional().catch(undefined),
approval_required: v.fallback(v.boolean(), false),
enabled: v.fallback(v.boolean(), false),
message: v.fallback(v.optional(v.string()), undefined),
});
const statsSchema = coerceObject({
domain_count: z.number().optional().catch(undefined),
status_count: z.number().optional().catch(undefined),
user_count: z.number().optional().catch(undefined),
domain_count: v.fallback(v.optional(v.number()), undefined),
status_count: v.fallback(v.optional(v.number()), undefined),
user_count: v.fallback(v.optional(v.number()), undefined),
});
const thumbnailSchema = coerceObject({
url: z.string().catch(''),
url: v.fallback(v.string(), ''),
});
const usageSchema = coerceObject({
users: coerceObject({
active_month: z.number().catch(0),
active_month: v.fallback(v.number(), 0),
}),
});
const instanceV1Schema = coerceObject({
account_domain: z.string().catch(''),
approval_required: z.boolean().catch(false),
account_domain: v.fallback(v.string(), ''),
approval_required: v.fallback(v.boolean(), false),
configuration: configurationSchema,
contact_account: accountSchema.optional().catch(undefined),
description: z.string().catch(''),
description_limit: z.number().catch(1500),
email: z.string().email().catch(''),
feature_quote: z.boolean().catch(false),
fedibird_capabilities: z.array(z.string()).catch([]),
languages: z.string().array().catch([]),
max_media_attachments: z.number().optional().catch(undefined),
max_toot_chars: z.number().optional().catch(undefined),
contact_account: v.fallback(v.optional(accountSchema), undefined),
description: v.fallback(v.string(), ''),
description_limit: v.fallback(v.number(), 1500),
email: v.fallback(v.pipe(v.string(), v.email()), ''),
feature_quote: v.fallback(v.boolean(), false),
fedibird_capabilities: v.fallback(v.array(v.string()), []),
languages: v.fallback(v.array(v.string()), []),
max_media_attachments: v.fallback(v.optional(v.number()), undefined),
max_toot_chars: v.fallback(v.optional(v.number()), undefined),
pleroma: pleromaSchema,
poll_limits: pleromaPollLimitsSchema,
registrations: z.boolean().catch(false),
registrations: v.fallback(v.boolean(), false),
rules: filteredArray(ruleSchema),
short_description: z.string().catch(''),
short_description: v.fallback(v.string(), ''),
stats: statsSchema,
thumbnail: z.string().catch(''),
title: z.string().catch(''),
upload_limit: z.number().optional().catch(undefined),
uri: z.string().catch(''),
thumbnail: v.fallback(v.string(), ''),
title: v.fallback(v.string(), ''),
upload_limit: v.fallback(v.optional(v.number()), undefined),
uri: v.fallback(v.string(), ''),
urls: coerceObject({
streaming_api: z.string().url().optional().catch(undefined),
streaming_api: v.fallback(v.optional(v.pipe(v.string(), v.url())), undefined),
}),
usage: usageSchema,
version: z.string().catch('0.0.0'),
version: v.fallback(v.string(), '0.0.0'),
});
/** @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: z.string().catch(''),
api_versions: z.record(z.number()).catch({}),
configuration: configurationSchema,
contact: contactSchema,
description: z.string().catch(''),
domain: z.string().catch(''),
feature_quote: z.boolean().catch(false),
fedibird_capabilities: z.array(z.string()).catch([]),
languages: z.string().array().catch([]),
pleroma: pleromaSchema,
registrations: registrations,
rules: filteredArray(ruleSchema),
stats: statsSchema,
thumbnail: thumbnailSchema,
title: z.string().catch(''),
usage: usageSchema,
version: z.string().catch('0.0.0'),
}).transform((instance) => {
const version = fixVersion(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)),
}),
);
return {
...instance,
version,
};
}));
type Instance = z.infer<typeof instanceSchema>;
type Instance = v.InferOutput<typeof instanceSchema>;
export { instanceSchema, type Instance };

View File

@ -1,12 +1,12 @@
import { z } from 'zod';
import * as v from 'valibot';
import { coerceObject } from './utils';
const interactionPolicyEntrySchema = z.enum(['public', 'followers', 'following', 'mutuals', 'mentioned', 'author', 'me']);
const interactionPolicyEntrySchema = v.picklist(['public', 'followers', 'following', 'mutuals', 'mentioned', 'author', 'me']);
const interactionPolicyRuleSchema = coerceObject({
always: z.array(interactionPolicyEntrySchema).default(['public']),
with_approval: z.array(interactionPolicyEntrySchema).default([]),
always: v.fallback(v.array(interactionPolicyEntrySchema), ['public']),
with_approval: v.fallback(v.array(interactionPolicyEntrySchema), []),
});
/** @see {@link https://docs.gotosocial.org/en/latest/api/swagger/} */
@ -16,7 +16,7 @@ const interactionPolicySchema = coerceObject({
can_reply: interactionPolicyRuleSchema,
});
type InteractionPolicy = z.infer<typeof interactionPolicySchema>;
type InteractionPolicy = v.InferOutput<typeof interactionPolicySchema>;
const interactionPoliciesSchema = coerceObject({
public: interactionPolicySchema,
@ -25,7 +25,7 @@ const interactionPoliciesSchema = coerceObject({
direct: interactionPolicySchema,
});
type InteractionPolicies = z.infer<typeof interactionPoliciesSchema>;
type InteractionPolicies = v.InferOutput<typeof interactionPoliciesSchema>;
export { interactionPolicySchema, interactionPoliciesSchema, type InteractionPolicy, type InteractionPolicies };

View File

@ -1,21 +1,22 @@
import { z } from 'zod';
import * as v from 'valibot';
import { accountSchema } from './account';
import { statusSchema } from './status';
import { datetimeSchema } from './utils';
/** @see {@link https://docs.gotosocial.org/en/latest/api/swagger.yaml#/definitions/interactionRequest} */
const interactionRequestSchema = z.object({
accepted_at: z.string().datetime().nullable().catch(null),
const interactionRequestSchema = v.object({
accepted_at: v.fallback(v.nullable(datetimeSchema), null),
account: accountSchema,
created_at: z.string().datetime(),
id: z.string(),
rejected_at: z.string().datetime().nullable().catch(null),
reply: statusSchema.nullable().catch(null),
status: statusSchema.nullable().catch(null),
type: z.enum(['favourite', 'reply', 'reblog']),
uri: z.string().nullable().catch(null),
created_at: datetimeSchema,
id: v.string(),
rejected_at: v.fallback(v.nullable(datetimeSchema), null),
reply: v.fallback(v.nullable(statusSchema), null),
status: v.fallback(v.nullable(statusSchema), null),
type: v.picklist(['favourite', 'reply', 'reblog']),
uri: v.fallback(v.nullable(v.string()), null),
});
type InteractionRequest = z.infer<typeof interactionRequestSchema>;
type InteractionRequest = v.InferOutput<typeof interactionRequestSchema>;
export { interactionRequestSchema, type InteractionRequest };

View File

@ -1,13 +1,13 @@
import { z } from 'zod';
import * as v from 'valibot';
/** @see {@link https://docs.joinmastodon.org/entities/List/} */
const listSchema = z.object({
id: z.coerce.string(),
title: z.string(),
replies_policy: z.string().optional().catch(undefined),
exclusive: z.boolean().optional().catch(undefined),
const listSchema = v.object({
id: v.pipe(v.unknown(), v.transform(String)),
title: v.string(),
replies_policy: v.fallback(v.optional(v.string()), undefined),
exclusive: v.fallback(v.optional(v.boolean()), undefined),
});
type List = z.infer<typeof listSchema>;
type List = v.InferOutput<typeof listSchema>;
export { listSchema, type List };

View File

@ -1,23 +1,23 @@
import { z } from 'zod';
import * as v from 'valibot';
const locationSchema = z.object({
url: z.string().url().catch(''),
description: z.string().catch(''),
country: z.string().catch(''),
locality: z.string().catch(''),
region: z.string().catch(''),
postal_code: z.string().catch(''),
street: z.string().catch(''),
origin_id: z.string().catch(''),
origin_provider: z.string().catch(''),
type: z.string().catch(''),
timezone: z.string().catch(''),
geom: z.object({
coordinates: z.tuple([z.number(), z.number()]).nullable().catch(null),
srid: z.string().catch(''),
}).nullable().catch(null),
const locationSchema = v.object({
url: v.fallback(v.pipe(v.string(), v.url()), ''),
description: v.fallback(v.string(), ''),
country: v.fallback(v.string(), ''),
locality: v.fallback(v.string(), ''),
region: v.fallback(v.string(), ''),
postal_code: v.fallback(v.string(), ''),
street: v.fallback(v.string(), ''),
origin_id: v.fallback(v.string(), ''),
origin_provider: v.fallback(v.string(), ''),
type: v.fallback(v.string(), ''),
timezone: v.fallback(v.string(), ''),
geom: v.fallback(v.nullable(v.object({
coordinates: v.fallback(v.nullable(v.tuple([v.number(), v.number()])), null),
srid: v.fallback(v.string(), ''),
})), null),
});
type Location = z.infer<typeof locationSchema>;
type Location = v.InferOutput<typeof locationSchema>;
export { locationSchema, type Location };

View File

@ -1,23 +1,27 @@
import { z } from 'zod';
import * as v from 'valibot';
import { dateSchema } from './utils';
import { datetimeSchema } from './utils';
const markerSchema = z.preprocess((marker: any) => marker ? ({
unread_count: marker.pleroma?.unread_count,
...marker,
}) : null, z.object({
last_read_id: z.string(),
version: z.number().int(),
updated_at: dateSchema,
unread_count: z.number().int().optional().catch(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: datetimeSchema,
unread_count: v.fallback(v.optional(v.pipe(v.number(), v.integer())), undefined),
}),
);
/** @see {@link https://docs.joinmastodon.org/entities/Marker/} */
type Marker = z.infer<typeof markerSchema>;
type Marker = v.InferOutput<typeof markerSchema>;
const markersSchema = z.record(markerSchema);
const markersSchema = v.record(v.string(), markerSchema);
type Markers = z.infer<typeof markersSchema>;
type Markers = v.InferOutput<typeof markersSchema>;
export {
markerSchema,

View File

@ -1,109 +1,113 @@
import { isBlurhashValid } from 'blurhash';
import { z } from 'zod';
import * as v from 'valibot';
import { mimeSchema } from './utils';
const blurhashSchema = z.string().superRefine((value, ctx) => {
const r = isBlurhashValid(value);
const blurhashSchema = v.pipe(v.string(), v.check(
(value) => isBlurhashValid(value).result,
'invalid blurhash', // .errorReason
));
if (!r.result) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: r.errorReason,
});
}
const baseAttachmentSchema = v.object({
id: v.string(),
type: v.string(),
url: v.fallback(v.pipe(v.string(), v.url()), ''),
preview_url: v.fallback(v.pipe(v.string(), v.url()), ''),
remote_url: v.fallback(v.nullable(v.pipe(v.string(), v.url())), null),
description: v.fallback(v.string(), ''),
blurhash: v.fallback(v.nullable(blurhashSchema), null),
mime_type: v.fallback(v.nullable(mimeSchema), null),
});
const baseAttachmentSchema = z.object({
id: z.string(),
type: z.string(),
url: z.string().url().catch(''),
preview_url: z.string().url().catch(''),
remote_url: z.string().url().nullable().catch(null),
description: z.string().catch(''),
blurhash: blurhashSchema.nullable().catch(null),
mime_type: mimeSchema.nullable().catch(null),
const imageMetaSchema = v.object({
width: v.number(),
height: v.number(),
size: v.fallback(v.nullable(v.pipe(v.string(), v.regex(/\d+x\d+$/))), null),
aspect: v.fallback(v.nullable(v.number()), null),
});
const imageMetaSchema = z.object({
width: z.number(),
height: z.number(),
size: z.string().regex(/\d+x\d+$/).nullable().catch(null),
aspect: z.number().nullable().catch(null),
const imageAttachmentSchema = v.object({
...baseAttachmentSchema.entries,
type: v.literal('image'),
meta: v.fallback(v.object({
original: v.fallback(v.optional(imageMetaSchema), undefined),
small: v.fallback(v.optional(imageMetaSchema), undefined),
focus: v.fallback(v.optional(v.object({
x: v.pipe(v.number(), v.minValue(-1), v.maxValue(1)),
y: v.pipe(v.number(), v.minValue(-1), v.maxValue(1)),
})), undefined),
}), {}),
});
const imageAttachmentSchema = baseAttachmentSchema.extend({
type: z.literal('image'),
meta: z.object({
original: imageMetaSchema.optional().catch(undefined),
small: imageMetaSchema.optional().catch(undefined),
focus: z.object({
x: z.number().min(-1).max(1),
y: z.number().min(-1).max(1),
}).optional().catch(undefined),
}).catch({}),
});
const videoAttachmentSchema = baseAttachmentSchema.extend({
type: z.literal('video'),
meta: z.object({
duration: z.number().optional().catch(undefined),
original: imageMetaSchema.extend({
frame_rate: z.string().regex(/\d+\/\d+$/).nullable().catch(null),
duration: z.number().nonnegative().nullable().catch(null),
}).optional().catch(undefined),
small: imageMetaSchema.optional().catch(undefined),
const videoAttachmentSchema = v.object({
...baseAttachmentSchema.entries,
type: v.literal('video'),
meta: v.fallback(v.object({
duration: v.fallback(v.optional(v.number()), undefined),
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(v.pipe(v.number(), v.minValue(0))), null),
})), undefined),
small: v.fallback(v.optional(imageMetaSchema), undefined),
// WIP: add rest
}).catch({}),
}), {}),
});
const gifvAttachmentSchema = baseAttachmentSchema.extend({
type: z.literal('gifv'),
meta: z.object({
duration: z.number().optional().catch(undefined),
original: imageMetaSchema.optional().catch(undefined),
}).catch({}),
const gifvAttachmentSchema = v.object({
...baseAttachmentSchema.entries,
type: v.literal('gifv'),
meta: v.fallback(v.object({
duration: v.fallback(v.optional(v.number()), undefined),
original: v.fallback(v.optional(imageMetaSchema), undefined),
}), {}),
});
const audioAttachmentSchema = baseAttachmentSchema.extend({
type: z.literal('audio'),
meta: z.object({
duration: z.number().optional().catch(undefined),
colors: z.object({
background: z.string().optional().catch(undefined),
foreground: z.string().optional().catch(undefined),
accent: z.string().optional().catch(undefined),
duration: z.number().optional().catch(undefined),
}).optional().catch(undefined),
original: z.object({
duration: z.number().optional().catch(undefined),
bitrate: z.number().nonnegative().optional().catch(undefined),
}).optional().catch(undefined),
}).catch({}),
const audioAttachmentSchema = v.object({
...baseAttachmentSchema.entries,
type: v.literal('audio'),
meta: v.fallback(v.object({
duration: v.fallback(v.optional(v.number()), undefined),
colors: v.fallback(v.optional(v.object({
background: v.fallback(v.optional(v.string()), undefined),
foreground: v.fallback(v.optional(v.string()), undefined),
accent: v.fallback(v.optional(v.string()), undefined),
duration: v.fallback(v.optional(v.number()), undefined),
})), undefined),
original: v.fallback(v.optional(v.object({
duration: v.fallback(v.optional(v.number()), undefined),
bitrate: v.fallback(v.optional(v.pipe(v.number(), v.minValue(0))), undefined),
})), undefined),
}), {}),
});
const unknownAttachmentSchema = baseAttachmentSchema.extend({
type: z.literal('unknown'),
const unknownAttachmentSchema = v.object({
...baseAttachmentSchema.entries,
type: v.literal('unknown'),
});
/** @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,
};
}, z.discriminatedUnion('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 = z.infer<typeof mediaAttachmentSchema>;
type MediaAttachment = v.InferOutput<typeof mediaAttachmentSchema>;
export { blurhashSchema, mediaAttachmentSchema, type MediaAttachment };

View File

@ -1,19 +1,22 @@
import { z } from 'zod';
import * as v from 'valibot';
/** @see {@link https://docs.joinmastodon.org/entities/Status/#Mention} */
const mentionSchema = z.object({
id: z.string(),
username: z.string().catch(''),
url: z.string().url().catch(''),
acct: z.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 = z.infer<typeof mentionSchema>;
type Mention = v.InferOutput<typeof mentionSchema>;
export { mentionSchema, type Mention };

View File

@ -1,17 +1,17 @@
import { z } from 'zod';
import * as v from 'valibot';
/** @see {@link https://docs.joinmastodon.org/entities/NotificationPolicy} */
const notificationPolicySchema = z.object({
filter_not_following: z.boolean(),
filter_not_followers: z.boolean(),
filter_new_accounts: z.boolean(),
filter_private_mentions: z.boolean(),
summary: z.object({
pending_requests_count: z.number().int(),
pending_notifications_count: z.number().int(),
const notificationPolicySchema = v.object({
filter_not_following: v.boolean(),
filter_not_followers: v.boolean(),
filter_new_accounts: v.boolean(),
filter_private_mentions: v.boolean(),
summary: v.object({
pending_requests_count: v.pipe(v.number(), v.integer()),
pending_notifications_count: v.pipe(v.number(), v.integer()),
}),
});
type NotificationPolicy = z.infer<typeof notificationPolicySchema>;
type NotificationPolicy = v.InferOutput<typeof notificationPolicySchema>;
export { notificationPolicySchema, type NotificationPolicy };

View File

@ -1,19 +1,19 @@
import { z } from 'zod';
import * as v from 'valibot';
import { dateSchema } from './utils';
import { datetimeSchema } from './utils';
import { accountSchema, statusSchema } from '.';
/** @see {@link https://docs.joinmastodon.org/entities/NotificationRequest} */
const notificationRequestSchema = z.object({
id: z.string(),
created_at: dateSchema,
updated_at: dateSchema,
const notificationRequestSchema = v.object({
id: v.string(),
created_at: datetimeSchema,
updated_at: datetimeSchema,
account: accountSchema,
notifications_count: z.coerce.string(),
last_status: statusSchema.optional().catch(undefined),
notifications_count: v.pipe(v.unknown(), v.transform(String)),
last_status: v.fallback(v.optional(statusSchema), undefined),
});
type NotificationRequest = z.infer<typeof notificationRequestSchema>;
type NotificationRequest = v.InferOutput<typeof notificationRequestSchema>;
export { notificationRequestSchema, type NotificationRequest };

View File

@ -1,5 +1,5 @@
import pick from 'lodash.pick';
import { z } from 'zod';
import * as v from 'valibot';
import { accountSchema } from './account';
import { accountWarningSchema } from './account-warning';
@ -7,94 +7,107 @@ import { chatMessageSchema } from './chat-message';
import { relationshipSeveranceEventSchema } from './relationship-severance-event';
import { reportSchema } from './report';
import { statusSchema } from './status';
import { dateSchema } from './utils';
import { datetimeSchema } from './utils';
const baseNotificationSchema = z.object({
const baseNotificationSchema = v.object({
account: accountSchema,
created_at: dateSchema,
id: z.string(),
group_key: z.string(),
type: z.string(),
created_at: v.fallback(datetimeSchema, new Date().toISOString()),
id: v.string(),
group_key: v.string(),
type: v.string(),
is_muted: z.boolean().optional().catch(undefined),
is_seen: z.boolean().optional().catch(undefined),
is_muted: v.fallback(v.optional(v.boolean()), undefined),
is_seen: v.fallback(v.optional(v.boolean()), undefined),
});
const accountNotificationSchema = baseNotificationSchema.extend({
type: z.enum(['follow', 'follow_request', 'admin.sign_up', 'bite']),
const accountNotificationSchema = v.object({
...baseNotificationSchema.entries,
type: v.picklist(['follow', 'follow_request', 'admin.sign_up', 'bite']),
});
const mentionNotificationSchema = baseNotificationSchema.extend({
type: z.literal('mention'),
subtype: z.enum(['reply']).nullable().catch(null),
const mentionNotificationSchema = v.object({
...baseNotificationSchema.entries,
type: v.literal('mention'),
subtype: v.fallback(v.nullable(v.picklist(['reply'])), null),
status: statusSchema,
});
const statusNotificationSchema = baseNotificationSchema.extend({
type: z.enum(['status', 'reblog', 'favourite', 'poll', 'update', 'event_reminder']),
const statusNotificationSchema = v.object({
...baseNotificationSchema.entries,
type: v.picklist(['status', 'reblog', 'favourite', 'poll', 'update', 'event_reminder']),
status: statusSchema,
});
const reportNotificationSchema = baseNotificationSchema.extend({
type: z.literal('admin.report'),
const reportNotificationSchema = v.object({
...baseNotificationSchema.entries,
type: v.literal('admin.report'),
report: reportSchema,
});
const severedRelationshipNotificationSchema = baseNotificationSchema.extend({
type: z.literal('severed_relationships'),
const severedRelationshipNotificationSchema = v.object({
...baseNotificationSchema.entries,
type: v.literal('severed_relationships'),
relationship_severance_event: relationshipSeveranceEventSchema,
});
const moderationWarningNotificationSchema = baseNotificationSchema.extend({
type: z.literal('moderation_warning'),
const moderationWarningNotificationSchema = v.object({
...baseNotificationSchema.entries,
type: v.literal('moderation_warning'),
moderation_warning: accountWarningSchema,
});
const moveNotificationSchema = baseNotificationSchema.extend({
type: z.literal('move'),
const moveNotificationSchema = v.object({
...baseNotificationSchema.entries,
type: v.literal('move'),
target: accountSchema,
});
const emojiReactionNotificationSchema = baseNotificationSchema.extend({
type: z.literal('emoji_reaction'),
emoji: z.string(),
emoji_url: z.string().nullable().catch(null),
const emojiReactionNotificationSchema = v.object({
...baseNotificationSchema.entries,
type: v.literal('emoji_reaction'),
emoji: v.string(),
emoji_url: v.fallback(v.nullable(v.string()), null),
status: statusSchema,
});
const chatMentionNotificationSchema = baseNotificationSchema.extend({
type: z.literal('chat_mention'),
const chatMentionNotificationSchema = v.object({
...baseNotificationSchema.entries,
type: v.literal('chat_mention'),
chat_message: chatMessageSchema,
});
const eventParticipationRequestNotificationSchema = baseNotificationSchema.extend({
type: z.enum(['participation_accepted', 'participation_request']),
const eventParticipationRequestNotificationSchema = v.object({
...baseNotificationSchema.entries,
type: v.picklist(['participation_accepted', 'participation_request']),
status: statusSchema,
participation_message: z.string().nullable().catch(null),
participation_message: v.fallback(v.nullable(v.string()), null),
});
/** @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:/, ''),
}), z.discriminatedUnion('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 = z.infer<
type Notification = v.InferOutput<
| typeof accountNotificationSchema
| typeof mentionNotificationSchema
| typeof statusNotificationSchema

View File

@ -1,15 +1,21 @@
import { z } from 'zod';
import * as v from 'valibot';
import { datetimeSchema } from './utils';
/** @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'),
}), z.object({
app_name: z.string(),
id: z.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: datetimeSchema,
}),
);
type OauthToken = z.infer<typeof oauthTokenSchema>;
type OauthToken = v.InferOutput<typeof oauthTokenSchema>;
export { oauthTokenSchema, type OauthToken };

View File

@ -1,32 +1,32 @@
import { z } from 'zod';
import * as v from 'valibot';
import { customEmojiSchema } from './custom-emoji';
import { filteredArray } from './utils';
import { datetimeSchema, filteredArray } from './utils';
const pollOptionSchema = z.object({
title: z.string().catch(''),
votes_count: z.number().catch(0),
const pollOptionSchema = v.object({
title: v.fallback(v.string(), ''),
votes_count: v.fallback(v.number(), 0),
title_map: z.record(z.string()).nullable().catch(null),
title_map: v.fallback(v.nullable(v.record(v.string(), v.string())), null),
});
/** @see {@link https://docs.joinmastodon.org/entities/Poll/} */
const pollSchema = z.object({
const pollSchema = v.object({
emojis: filteredArray(customEmojiSchema),
expired: z.boolean().catch(false),
expires_at: z.string().datetime().nullable().catch(null),
id: z.string(),
multiple: z.boolean().catch(false),
options: z.array(pollOptionSchema).min(2),
voters_count: z.number().catch(0),
votes_count: z.number().catch(0),
own_votes: z.array(z.number()).nonempty().nullable().catch(null),
voted: z.boolean().catch(false),
expired: v.fallback(v.boolean(), false),
expires_at: v.fallback(v.nullable(datetimeSchema), null),
id: v.string(),
multiple: v.fallback(v.boolean(), false),
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.pipe(v.array(v.number()), v.minLength(1))), null),
voted: v.fallback(v.boolean(), false),
non_anonymous: z.boolean().catch(false),
non_anonymous: v.fallback(v.boolean(), false),
});
type Poll = z.infer<typeof pollSchema>;
type Poll = v.InferOutput<typeof pollSchema>;
type PollOption = Poll['options'][number];
export { pollSchema, type Poll, type PollOption };

View File

@ -1,24 +1,24 @@
import { z } from 'zod';
import * as v from 'valibot';
/** @see {@link https://docs.joinmastodon.org/entities/PreviewCard/} */
const previewCardSchema = z.object({
author_name: z.string().catch(''),
author_url: z.string().url().catch(''),
blurhash: z.string().nullable().catch(null),
description: z.string().catch(''),
embed_url: z.string().url().catch(''),
height: z.number().catch(0),
html: z.string().catch(''),
image: z.string().nullable().catch(null),
image_description: z.string().catch(''),
provider_name: z.string().catch(''),
provider_url: z.string().url().catch(''),
title: z.string().catch(''),
type: z.enum(['link', 'photo', 'video', 'rich']).catch('link'),
url: z.string().url(),
width: z.number().catch(0),
const previewCardSchema = v.object({
author_name: v.fallback(v.string(), ''),
author_url: v.fallback(v.pipe(v.string(), v.url()), ''),
blurhash: v.fallback(v.nullable(v.string()), null),
description: v.fallback(v.string(), ''),
embed_url: v.fallback(v.pipe(v.string(), v.url()), ''),
height: v.fallback(v.number(), 0),
html: v.fallback(v.string(), ''),
image: v.fallback(v.nullable(v.string()), null),
image_description: v.fallback(v.string(), ''),
provider_name: v.fallback(v.string(), ''),
provider_url: v.fallback(v.pipe(v.string(), v.url()), ''),
title: v.fallback(v.string(), ''),
type: v.fallback(v.picklist(['link', 'photo', 'video', 'rich']), 'link'),
url: v.pipe(v.string(), v.url()),
width: v.fallback(v.number(), 0),
});
type PreviewCard = z.infer<typeof previewCardSchema>;
type PreviewCard = v.InferOutput<typeof previewCardSchema>;
export { previewCardSchema, type PreviewCard };

View File

@ -1,16 +1,16 @@
import { z } from 'zod';
import * as v from 'valibot';
import { dateSchema } from './utils';
import { datetimeSchema } from './utils';
/** @see {@link https://docs.joinmastodon.org/entities/RelationshipSeveranceEvent/} */
const relationshipSeveranceEventSchema = z.object({
id: z.string(),
type: z.enum(['domain_block', 'user_domain_block', 'account_suspension']),
purged: z.string(),
relationships_count: z.number().optional().catch(undefined),
created_at: dateSchema,
const relationshipSeveranceEventSchema = v.object({
id: v.string(),
type: v.picklist(['domain_block', 'user_domain_block', 'account_suspension']),
purged: v.string(),
relationships_count: v.fallback(v.optional(v.number()), undefined),
created_at: datetimeSchema,
});
type RelationshipSeveranceEvent = z.infer<typeof relationshipSeveranceEventSchema>;
type RelationshipSeveranceEvent = v.InferOutput<typeof relationshipSeveranceEventSchema>;
export { relationshipSeveranceEventSchema, type RelationshipSeveranceEvent };

View File

@ -1,22 +1,22 @@
import z from 'zod';
import * as v from 'valibot';
/** @see {@link https://docs.joinmastodon.org/entities/Relationship/} */
const relationshipSchema = z.object({
blocked_by: z.boolean().catch(false),
blocking: z.boolean().catch(false),
domain_blocking: z.boolean().catch(false),
endorsed: z.boolean().catch(false),
followed_by: z.boolean().catch(false),
following: z.boolean().catch(false),
id: z.string(),
muting: z.boolean().catch(false),
muting_notifications: z.boolean().catch(false),
note: z.string().catch(''),
notifying: z.boolean().catch(false),
requested: z.boolean().catch(false),
showing_reblogs: z.boolean().catch(false),
const relationshipSchema = v.object({
blocked_by: v.fallback(v.boolean(), false),
blocking: v.fallback(v.boolean(), false),
domain_blocking: v.fallback(v.boolean(), false),
endorsed: v.fallback(v.boolean(), false),
followed_by: v.fallback(v.boolean(), false),
following: v.fallback(v.boolean(), false),
id: v.string(),
muting: v.fallback(v.boolean(), false),
muting_notifications: v.fallback(v.boolean(), false),
note: v.fallback(v.string(), ''),
notifying: v.fallback(v.boolean(), false),
requested: v.fallback(v.boolean(), false),
showing_reblogs: v.fallback(v.boolean(), false),
});
type Relationship = z.infer<typeof relationshipSchema>;
type Relationship = v.InferOutput<typeof relationshipSchema>;
export { relationshipSchema, type Relationship };

View File

@ -1,22 +1,22 @@
import { z } from 'zod';
import * as v from 'valibot';
import { accountSchema } from './account';
import { dateSchema } from './utils';
import { datetimeSchema } from './utils';
/** @see {@link https://docs.joinmastodon.org/entities/Report/} */
const reportSchema = z.object({
id: z.string(),
action_taken: z.boolean().optional().catch(undefined),
action_taken_at: dateSchema.nullable().catch(null),
category: z.string().optional().catch(undefined),
comment: z.string().optional().catch(undefined),
forwarded: z.boolean().optional().catch(undefined),
created_at: dateSchema.optional().catch(undefined),
status_ids: z.array(z.string()).nullable().catch(null),
rule_ids: z.array(z.string()).nullable().catch(null),
target_account: accountSchema.nullable().catch(null),
const reportSchema = v.object({
id: v.string(),
action_taken: v.fallback(v.optional(v.boolean()), undefined),
action_taken_at: v.fallback(v.nullable(datetimeSchema), 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(datetimeSchema), undefined),
status_ids: v.fallback(v.nullable(v.string()), null),
rule_ids: v.fallback(v.nullable(v.string()), null),
target_account: v.fallback(v.nullable(accountSchema), null),
});
type Report = z.infer<typeof reportSchema>;
type Report = v.InferOutput<typeof reportSchema>;
export { reportSchema, type Report };

View File

@ -1,16 +1,16 @@
import { z } from 'zod';
import * as v from 'valibot';
const hexSchema = z.string().regex(/^#[a-f0-9]{6}$/i);
const hexSchema = v.pipe(v.string(), v.regex(/^#[a-f0-9]{6}$/i));
const roleSchema = z.object({
id: z.string().catch(''),
name: z.string().catch(''),
color: hexSchema.catch(''),
permissions: z.string().catch(''),
highlighted: z.boolean().catch(true),
const roleSchema = v.object({
id: v.fallback(v.string(), ''),
name: v.fallback(v.string(), ''),
color: v.fallback(hexSchema, ''),
permissions: v.fallback(v.string(), ''),
highlighted: v.fallback(v.boolean(), true),
});
type Role = z.infer<typeof roleSchema>;
type Role = v.InferOutput<typeof roleSchema>;
export {
roleSchema,

View File

@ -1,17 +1,21 @@
import { z } from 'zod';
import * as v from 'valibot';
const baseRuleSchema = z.object({
id: z.string(),
text: z.string().catch(''),
hint: z.string().catch(''),
const baseRuleSchema = v.object({
id: v.string(),
text: v.fallback(v.string(), ''),
hint: v.fallback(v.string(), ''),
});
/** @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 = z.infer<typeof ruleSchema>;
type Rule = v.InferOutput<typeof ruleSchema>;
export { ruleSchema, type Rule };

View File

@ -1,36 +1,36 @@
import { z } from 'zod';
import * as v from 'valibot';
import { mediaAttachmentSchema } from './media-attachment';
import { filteredArray } from './utils';
import { datetimeSchema, filteredArray } from './utils';
/** @see {@link https://docs.joinmastodon.org/entities/ScheduledStatus/} */
const scheduledStatusSchema = z.object({
id: z.string(),
scheduled_at: z.string().datetime({ offset: true }),
params: z.object({
text: z.string().nullable().catch(null),
poll: z.object({
options: z.array(z.string()),
expires_in: z.coerce.string(),
multiple: z.boolean().optional().catch(undefined),
hide_totals: z.boolean().optional().catch(undefined),
}).nullable().catch(null),
media_ids: z.array(z.string()).nullable().catch(null),
sensitive: z.coerce.boolean().nullable().catch(null),
spoiler_text: z.string().nullable().catch(null),
visibility: z.string().catch('public'),
in_reply_to_id: z.string().nullable().catch(null),
language: z.string().nullable().catch(null),
application_id: z.number().int().nullable().catch(null),
scheduled_at: z.string().datetime({ offset: true }).nullable().catch(null),
idempotency: z.string().nullable().catch(null),
with_rate_limit: z.boolean().catch(false),
const scheduledStatusSchema = v.object({
id: v.string(),
scheduled_at: datetimeSchema,
params: v.object({
text: v.fallback(v.nullable(v.string()), null),
poll: v.fallback(v.nullable(v.object({
options: v.array(v.string()),
expires_in: v.pipe(v.unknown(), v.transform(String)),
multiple: v.fallback(v.optional(v.boolean()), undefined),
hide_totals: v.fallback(v.optional(v.boolean()), undefined),
})), null),
media_ids: v.fallback(v.nullable(v.string()), null),
sensitive: v.fallback(v.nullable(v.pipe(v.unknown(), v.transform(Boolean))), null),
spoiler_text: v.fallback(v.nullable(v.string()), null),
visibility: v.fallback(v.string(), 'public'),
in_reply_to_id: v.fallback(v.nullable(v.string()), null),
language: v.fallback(v.nullable(v.string()), null),
application_id: v.fallback(v.nullable(v.pipe(v.number(), v.integer())), null),
scheduled_at: v.fallback(v.nullable(datetimeSchema), null),
idempotency: v.fallback(v.nullable(v.string()), null),
with_rate_limit: v.fallback(v.boolean(), false),
expires_in: z.number().nullable().catch(null),
expires_in: v.fallback(v.nullable(v.number()), null),
}),
media_attachments: filteredArray(mediaAttachmentSchema),
});
type ScheduledStatus = z.infer<typeof scheduledStatusSchema>;
type ScheduledStatus = v.InferOutput<typeof scheduledStatusSchema>;
export { scheduledStatusSchema, type ScheduledStatus };

View File

@ -1,21 +1,26 @@
import { z } from 'zod';
import * as v from 'valibot';
import { accountSchema } from './account';
import { datetimeSchema } from './utils';
const scrobbleSchema = z.preprocess((scrobble: any) => scrobble ? {
external_link: scrobble.externalLink,
...scrobble,
} : null, z.object({
id: z.coerce.string(),
account: accountSchema,
created_at: z.string().datetime({ offset: true }),
title: z.string(),
artist: z.string().catch(''),
album: z.string().catch(''),
external_link: z.string().nullable().catch(null),
length: z.number().nullable().catch(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: datetimeSchema,
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 = z.infer<typeof scrobbleSchema>;
type Scrobble = v.InferOutput<typeof scrobbleSchema>;
export { scrobbleSchema, type Scrobble };

View File

@ -1,17 +1,17 @@
import { z } from 'zod';
import * as v from 'valibot';
import { filteredArray } from './utils';
import { accountSchema, groupSchema, statusSchema, tagSchema } from '.';
/** @see {@link https://docs.joinmastodon.org/entities/Search} */
const searchSchema = z.object({
const searchSchema = v.object({
accounts: filteredArray(accountSchema),
statuses: filteredArray(statusSchema),
hashtags: filteredArray(tagSchema),
groups: filteredArray(groupSchema),
});
type Search = z.infer<typeof searchSchema>;
type Search = v.InferOutput<typeof searchSchema>;
export { searchSchema, type Search };

View File

@ -1,26 +1,26 @@
import { z } from 'zod';
import * as v from 'valibot';
import { accountSchema } from './account';
import { customEmojiSchema } from './custom-emoji';
import { mediaAttachmentSchema } from './media-attachment';
import { dateSchema, filteredArray } from './utils';
import { datetimeSchema, filteredArray } from './utils';
/** @see {@link https://docs.joinmastodon.org/entities/StatusEdit/} */
const statusEditSchema = z.object({
content: z.string().catch(''),
spoiler_text: z.string().catch(''),
sensitive: z.coerce.boolean(),
created_at: dateSchema,
const statusEditSchema = v.object({
content: v.fallback(v.string(), ''),
spoiler_text: v.fallback(v.string(), ''),
sensitive: v.pipe(v.unknown(), v.transform(Boolean)),
created_at: v.fallback(datetimeSchema, new Date().toISOString()),
account: accountSchema,
poll: z.object({
options: z.array(z.object({
title: z.string(),
poll: v.fallback(v.nullable(v.object({
options: v.array(v.object({
title: v.string(),
})),
}).nullable().catch(null),
})), null),
media_attachments: filteredArray(mediaAttachmentSchema),
emojis: filteredArray(customEmojiSchema),
});
type StatusEdit = z.infer<typeof statusEditSchema>;
type StatusEdit = v.InferOutput<typeof statusEditSchema>;
export { statusEditSchema, type StatusEdit };

View File

@ -1,20 +1,20 @@
import { z } from 'zod';
import * as v from 'valibot';
import { locationSchema } from './location';
/** @see {@link https://docs.joinmastodon.org/entities/StatusSource/} */
const statusSourceSchema = z.object({
id: z.string(),
text: z.string().catch(''),
spoiler_text: z.string().catch(''),
const statusSourceSchema = v.object({
id: v.string(),
text: v.fallback(v.string(), ''),
spoiler_text: v.fallback(v.string(), ''),
content_type: z.string().catch('text/plain'),
location: locationSchema.nullable().catch(null),
content_type: v.fallback(v.string(), 'text/plain'),
location: v.fallback(v.nullable(locationSchema), null),
text_map: z.record(z.string()).nullable().catch(null),
spoiler_text_map: z.record(z.string()).nullable().catch(null),
text_map: v.fallback(v.nullable(v.record(v.string(), v.string())), null),
spoiler_text_map: v.fallback(v.nullable(v.record(v.string(), v.string())), null),
});
type StatusSource = z.infer<typeof statusSourceSchema>;
type StatusSource = v.InferOutput<typeof statusSourceSchema>;
export { statusSourceSchema, type StatusSource };

View File

@ -1,5 +1,5 @@
import pick from 'lodash.pick';
import { z } from 'zod';
import * as v from 'valibot';
import { type Account, accountSchema } from './account';
import { customEmojiSchema } from './custom-emoji';
@ -13,91 +13,91 @@ import { pollSchema } from './poll';
import { previewCardSchema } from './preview-card';
import { tagSchema } from './tag';
import { translationSchema } from './translation';
import { dateSchema, filteredArray } from './utils';
import { datetimeSchema, filteredArray } from './utils';
const statusEventSchema = z.object({
name: z.string().catch(''),
start_time: z.string().datetime().nullable().catch(null),
end_time: z.string().datetime().nullable().catch(null),
join_mode: z.enum(['free', 'restricted', 'invite']).nullable().catch(null),
participants_count: z.number().catch(0),
location: z.object({
name: z.string().catch(''),
url: z.string().url().catch(''),
latitude: z.number().catch(0),
longitude: z.number().catch(0),
street: z.string().catch(''),
postal_code: z.string().catch(''),
locality: z.string().catch(''),
region: z.string().catch(''),
country: z.string().catch(''),
}).nullable().catch(null),
join_state: z.enum(['pending', 'reject', 'accept']).nullable().catch(null),
const statusEventSchema = v.object({
name: v.fallback(v.string(), ''),
start_time: v.fallback(v.nullable(datetimeSchema), null),
end_time: v.fallback(v.nullable(datetimeSchema), null),
join_mode: v.fallback(v.nullable(v.picklist(['free', 'restricted', 'invite'])), null),
participants_count: v.fallback(v.number(), 0),
location: v.fallback(v.nullable(v.object({
name: v.fallback(v.string(), ''),
url: v.fallback(v.pipe(v.string(), v.url()), ''),
latitude: v.fallback(v.number(), 0),
longitude: v.fallback(v.number(), 0),
street: v.fallback(v.string(), ''),
postal_code: v.fallback(v.string(), ''),
locality: v.fallback(v.string(), ''),
region: v.fallback(v.string(), ''),
country: v.fallback(v.string(), ''),
})), null),
join_state: v.fallback(v.nullable(v.picklist(['pending', 'reject', 'accept'])), null),
});
/** @see {@link https://docs.joinmastodon.org/entities/Status/} */
const baseStatusSchema = z.object({
id: z.string(),
uri: z.string().url().catch(''),
created_at: dateSchema,
const baseStatusSchema = v.object({
id: v.string(),
uri: v.fallback(v.pipe(v.string(), v.url()), ''),
created_at: v.fallback(datetimeSchema, new Date().toISOString()),
account: accountSchema,
content: z.string().transform(note => note === '<p></p>' ? '' : note).catch(''),
visibility: z.string().catch('public'),
sensitive: z.coerce.boolean(),
spoiler_text: z.string().catch(''),
content: v.fallback(v.pipe(v.string(), v.transform((note => note === '<p></p>' ? '' : note))), ''),
visibility: v.fallback(v.string(), 'public'),
sensitive: v.pipe(v.unknown(), v.transform(Boolean)),
spoiler_text: v.fallback(v.string(), ''),
media_attachments: filteredArray(mediaAttachmentSchema),
application: z.object({
name: z.string(),
website: z.string().url().nullable().catch(null),
}).nullable().catch(null),
application: v.fallback(v.nullable(v.object({
name: v.string(),
website: v.fallback(v.nullable(v.pipe(v.string(), v.url())), null),
})), null),
mentions: filteredArray(mentionSchema),
tags: filteredArray(tagSchema),
emojis: filteredArray(customEmojiSchema),
reblogs_count: z.number().catch(0),
favourites_count: z.number().catch(0),
replies_count: z.number().catch(0),
url: z.string().url().catch(''),
in_reply_to_id: z.string().nullable().catch(null),
in_reply_to_account_id: z.string().nullable().catch(null),
poll: pollSchema.nullable().catch(null),
card: previewCardSchema.nullable().catch(null),
language: z.string().nullable().catch(null),
text: z.string().nullable().catch(null),
edited_at: z.string().datetime().nullable().catch(null),
favourited: z.coerce.boolean(),
reblogged: z.coerce.boolean(),
muted: z.coerce.boolean(),
bookmarked: z.coerce.boolean(),
pinned: z.coerce.boolean(),
reblogs_count: v.fallback(v.number(), 0),
favourites_count: v.fallback(v.number(), 0),
replies_count: v.fallback(v.number(), 0),
url: v.fallback(v.pipe(v.string(), v.url()), ''),
in_reply_to_id: v.fallback(v.nullable(v.string()), null),
in_reply_to_account_id: v.fallback(v.nullable(v.string()), null),
poll: v.fallback(v.nullable(pollSchema), null),
card: v.fallback(v.nullable(previewCardSchema), null),
language: v.fallback(v.nullable(v.string()), null),
text: v.fallback(v.nullable(v.string()), null),
edited_at: v.fallback(v.nullable(datetimeSchema), null),
favourited: v.pipe(v.unknown(), v.transform(Boolean)),
reblogged: v.pipe(v.unknown(), v.transform(Boolean)),
muted: v.pipe(v.unknown(), v.transform(Boolean)),
bookmarked: v.pipe(v.unknown(), v.transform(Boolean)),
pinned: v.pipe(v.unknown(), v.transform(Boolean)),
filtered: filteredArray(filterResultSchema),
approval_status: z.enum(['pending', 'approval', 'rejected']).nullable().catch(null),
group: groupSchema.nullable().catch(null),
scheduled_at: z.null().catch(null),
approval_status: v.fallback(v.nullable(v.picklist(['pending', 'approval', 'rejected'])), null),
group: v.fallback(v.nullable(groupSchema), null),
scheduled_at: v.fallback(v.null(), null),
quote_id: z.string().nullable().catch(null),
local: z.boolean().optional().catch(undefined),
conversation_id: z.string().optional().catch(undefined),
direct_conversation_id: z.string().optional().catch(undefined),
in_reply_to_account_acct: z.string().optional().catch(undefined),
expires_at: z.string().datetime({ offset: true }).optional().catch(undefined),
thread_muted: z.boolean().optional().catch(undefined),
quote_id: v.fallback(v.nullable(v.string()), null),
local: v.fallback(v.optional(v.boolean()), undefined),
conversation_id: v.fallback(v.optional(v.string()), undefined),
direct_conversation_id: v.fallback(v.optional(v.string()), undefined),
in_reply_to_account_acct: v.fallback(v.optional(v.string()), undefined),
expires_at: v.fallback(v.optional(datetimeSchema), undefined),
thread_muted: v.fallback(v.optional(v.boolean()), undefined),
emoji_reactions: filteredArray(emojiReactionSchema),
parent_visible: z.boolean().optional().catch(undefined),
pinned_at: z.string().datetime({ offset: true }).nullable().catch(null),
quote_visible: z.boolean().optional().catch(undefined),
quote_url: z.string().optional().catch(undefined),
quotes_count: z.number().catch(0),
bookmark_folder: z.string().nullable().catch(null),
parent_visible: v.fallback(v.optional(v.boolean()), undefined),
pinned_at: v.fallback(v.nullable(datetimeSchema), null),
quote_visible: v.fallback(v.optional(v.boolean()), undefined),
quote_url: v.fallback(v.optional(v.string()), undefined),
quotes_count: v.fallback(v.number(), 0),
bookmark_folder: v.fallback(v.nullable(v.string()), null),
event: statusEventSchema.nullable().catch(null),
translation: translationSchema.nullable().or(z.literal(false)).catch(null),
event: v.fallback(v.nullable(statusEventSchema), null),
translation: v.fallback(v.union([v.nullable(translationSchema), v.literal(false)]), null),
content_map: z.record(z.string()).nullable().catch(null),
text_map: z.record(z.string()).nullable().catch(null),
spoiler_text_map: z.record(z.string()).nullable().catch(null),
content_map: v.fallback(v.nullable(v.record(v.string(), v.string())), null),
text_map: v.fallback(v.nullable(v.record(v.string(), v.string())), null),
spoiler_text_map: v.fallback(v.nullable(v.record(v.string(), v.string())), null),
dislikes_count: z.number().catch(0),
disliked: z.coerce.boolean().catch(false),
dislikes_count: v.fallback(v.number(), 0),
disliked: v.fallback(v.pipe(v.unknown(), v.transform(Boolean)), false),
interaction_policy: interactionPolicySchema,
});
@ -134,26 +134,28 @@ const preprocess = (status: any) => {
return status;
};
const statusSchema: z.ZodType<Status> = z.preprocess(preprocess, baseStatusSchema.extend({
reblog: z.lazy(() => statusSchema).nullable().catch(null),
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(v.lazy(() => statusSchema)), null),
quote: z.lazy(() => statusSchema).nullable().catch(null),
quote: v.fallback(v.nullable(v.lazy(() => statusSchema)), null),
})) as any;
const statusWithoutAccountSchema = z.preprocess(preprocess, baseStatusSchema.omit({ account: true }).extend({
account: accountSchema.nullable().catch(null),
reblog: z.lazy(() => statusSchema).nullable().catch(null),
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(v.lazy(() => statusSchema)), null),
quote: z.lazy(() => statusSchema).nullable().catch(null),
quote: v.fallback(v.nullable(v.lazy(() => statusSchema)), null),
}));
type StatusWithoutAccount = Omit<z.infer<typeof baseStatusSchema>, 'account'> & {
type StatusWithoutAccount = Omit<v.InferOutput<typeof baseStatusSchema>, 'account'> & {
account: Account | null;
reblog: Status | null;
quote: Status | null;
}
type Status = z.infer<typeof baseStatusSchema> & {
type Status = v.InferOutput<typeof baseStatusSchema> & {
reblog: Status | null;
quote: Status | null;
}

View File

@ -1,4 +1,4 @@
import { z } from 'zod';
import * as v from 'valibot';
import { announcementSchema } from './announcement';
import { announcementReactionSchema } from './announcement-reaction';
@ -8,102 +8,117 @@ import { markersSchema } from './marker';
import { notificationSchema } from './notification';
import { statusSchema } from './status';
const followRelationshipUpdateSchema = z.object({
state: z.enum(['follow_pending', 'follow_accept', 'follow_reject']),
follower: z.object({
id: z.string(),
follower_count: z.number().nullable().catch(null),
following_count: z.number().nullable().catch(null),
const followRelationshipUpdateSchema = v.object({
state: v.picklist(['follow_pending', 'follow_accept', 'follow_reject']),
follower: v.object({
id: v.string(),
follower_count: v.fallback(v.nullable(v.number()), null),
following_count: v.fallback(v.nullable(v.number()), null),
}),
following: z.object({
id: z.string(),
follower_count: z.number().nullable().catch(null),
following_count: z.number().nullable().catch(null),
following: v.object({
id: v.string(),
follower_count: v.fallback(v.nullable(v.number()), null),
following_count: v.fallback(v.nullable(v.number()), null),
}),
});
type FollowRelationshipUpdate = z.infer<typeof followRelationshipUpdateSchema>;
type FollowRelationshipUpdate = v.InferOutput<typeof followRelationshipUpdateSchema>;
const baseStreamingEventSchema = z.object({
stream: z.array(z.string()).catch([]),
const baseStreamingEventSchema = v.object({
stream: v.fallback(v.array(v.string()), []),
});
const statusStreamingEventSchema = baseStreamingEventSchema.extend({
event: z.enum(['update', 'status.update']),
payload: z.preprocess((payload: any) => JSON.parse(payload), statusSchema),
const statusStreamingEventSchema = v.object({
...baseStreamingEventSchema.entries,
event: v.picklist(['update', 'status.update']),
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), statusSchema),
});
const stringStreamingEventSchema = baseStreamingEventSchema.extend({
event: z.enum(['delete', 'announcement.delete']),
payload: z.string(),
const stringStreamingEventSchema = v.object({
...baseStreamingEventSchema.entries,
event: v.picklist(['delete', 'announcement.delete']),
payload: v.string(),
});
const notificationStreamingEventSchema = baseStreamingEventSchema.extend({
event: z.literal('notification'),
payload: z.preprocess((payload: any) => JSON.parse(payload), notificationSchema),
const notificationStreamingEventSchema = v.object({
...baseStreamingEventSchema.entries,
event: v.literal('notification'),
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), notificationSchema),
});
const emptyStreamingEventSchema = baseStreamingEventSchema.extend({
event: z.literal('filters_changed'),
const emptyStreamingEventSchema = v.object({
...baseStreamingEventSchema.entries,
event: v.literal('filters_changed'),
});
const conversationStreamingEventSchema = baseStreamingEventSchema.extend({
event: z.literal('conversation'),
payload: z.preprocess((payload: any) => JSON.parse(payload), conversationSchema),
const conversationStreamingEventSchema = v.object({
...baseStreamingEventSchema.entries,
event: v.literal('conversation'),
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), conversationSchema),
});
const announcementStreamingEventSchema = baseStreamingEventSchema.extend({
event: z.literal('announcement'),
payload: z.preprocess((payload: any) => JSON.parse(payload), announcementSchema),
const announcementStreamingEventSchema = v.object({
...baseStreamingEventSchema.entries,
event: v.literal('announcement'),
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), announcementSchema),
});
const announcementReactionStreamingEventSchema = baseStreamingEventSchema.extend({
event: z.literal('announcement.reaction'),
payload: z.preprocess((payload: any) => JSON.parse(payload), announcementReactionSchema),
const announcementReactionStreamingEventSchema = v.object({
...baseStreamingEventSchema.entries,
event: v.literal('announcement.reaction'),
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), announcementReactionSchema),
});
const chatUpdateStreamingEventSchema = baseStreamingEventSchema.extend({
event: z.literal('chat_update'),
payload: z.preprocess((payload: any) => JSON.parse(payload), chatSchema),
const chatUpdateStreamingEventSchema = v.object({
...baseStreamingEventSchema.entries,
event: v.literal('chat_update'),
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), chatSchema),
});
const followRelationshipsUpdateStreamingEventSchema = baseStreamingEventSchema.extend({
event: z.literal('follow_relationships_update'),
payload: z.preprocess((payload: any) => JSON.parse(payload), followRelationshipUpdateSchema),
const followRelationshipsUpdateStreamingEventSchema = v.object({
...baseStreamingEventSchema.entries,
event: v.literal('follow_relationships_update'),
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), followRelationshipUpdateSchema),
});
const respondStreamingEventSchema = baseStreamingEventSchema.extend({
event: z.literal('respond'),
payload: z.preprocess((payload: any) => JSON.parse(payload), z.object({
type: z.string(),
result: z.enum(['success', 'ignored', 'error']),
const respondStreamingEventSchema = v.object({
...baseStreamingEventSchema.entries,
event: v.literal('respond'),
payload: v.pipe(v.any(), v.transform((payload: any) => JSON.parse(payload)), v.object({
type: v.string(),
result: v.picklist(['success', 'ignored', 'error']),
})),
});
const markerStreamingEventSchema = baseStreamingEventSchema.extend({
event: z.literal('marker'),
payload: z.preprocess((payload: any) => JSON.parse(payload), markersSchema),
const markerStreamingEventSchema = v.object({
...baseStreamingEventSchema.entries,
event: v.literal('marker'),
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:/, ''),
}), z.discriminatedUnion('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 = z.infer<
type StreamingEvent = v.InferOutput<
| typeof statusStreamingEventSchema
| typeof stringStreamingEventSchema
| typeof notificationStreamingEventSchema

View File

@ -1,40 +1,44 @@
import { z } from 'zod';
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;
}, z.object({
source: z.string().nullable().catch(null),
sources: z.array(z.string()).catch([]),
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 = z.infer<typeof suggestionSchema>;
type Suggestion = v.InferOutput<typeof suggestionSchema>;
export { suggestionSchema, type Suggestion };

View File

@ -1,19 +1,19 @@
import { z } from 'zod';
import * as v from 'valibot';
const historySchema = z.object({
day: z.coerce.number(),
accounts: z.coerce.number(),
uses: z.coerce.number(),
});
const historySchema = v.array(v.object({
day: v.pipe(v.unknown(), v.transform(Number)),
accounts: v.pipe(v.unknown(), v.transform(Number)),
uses: v.pipe(v.unknown(), v.transform(Number)),
}));
/** @see {@link https://docs.joinmastodon.org/entities/tag} */
const tagSchema = z.object({
name: z.string().min(1),
url: z.string().url().catch(''),
history: z.array(historySchema).nullable().catch(null),
following: z.boolean().optional().catch(undefined),
const tagSchema = v.object({
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),
});
type Tag = z.infer<typeof tagSchema>;
type Tag = v.InferOutput<typeof tagSchema>;
export { historySchema, tagSchema, type Tag };

View File

@ -1,18 +1,18 @@
import { z } from 'zod';
import * as v from 'valibot';
/** @see {@link https://docs.joinmastodon.org/entities/Token/} */
const tokenSchema = z.object({
access_token: z.string(),
token_type: z.string(),
scope: z.string(),
created_at: z.number().optional().catch(undefined),
const tokenSchema = v.object({
access_token: v.string(),
token_type: v.string(),
scope: v.string(),
created_at: v.fallback(v.optional(v.number()), undefined),
id: z.number().optional().catch(undefined),
refresh_token: z.string().optional().catch(undefined),
expires_in: z.number().optional().catch(undefined),
me: z.string().optional().catch(undefined),
id: v.fallback(v.optional(v.number()), undefined),
refresh_token: v.fallback(v.optional(v.string()), undefined),
expires_in: v.fallback(v.optional(v.number()), undefined),
me: v.fallback(v.optional(v.string()), undefined),
});
type Token = z.infer<typeof tokenSchema>;
type Token = v.InferOutput<typeof tokenSchema>;
export { tokenSchema, type Token };

View File

@ -1,42 +1,46 @@
import { z } from 'zod';
import * as v from 'valibot';
import { filteredArray } from './utils';
const translationPollSchema = z.object({
id: z.string(),
options: z.array(z.object({
title: z.string(),
const translationPollSchema = v.object({
id: v.string(),
options: v.array(v.object({
title: v.string(),
})),
});
const translationMediaAttachment = z.object({
id: z.string(),
description: z.string().catch(''),
const translationMediaAttachment = v.object({
id: v.string(),
description: v.fallback(v.string(), ''),
});
/** @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;
}, z.object({
id: z.string().nullable().catch(null),
content: z.string().catch(''),
spoiler_text: z.string().catch(''),
poll: translationPollSchema.optional().catch(undefined),
media_attachments: filteredArray(translationMediaAttachment),
detected_source_language: z.string(),
provider: z.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 = z.infer<typeof translationSchema>;
type Translation = v.InferOutput<typeof translationSchema>;
export { translationSchema, type Translation };

View File

@ -1,29 +1,33 @@
import { z } from 'zod';
import * as v from 'valibot';
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 }), z.object({
id: z.string().catch(''),
url: z.string().url().catch(''),
title: z.string().catch(''),
description: z.string().catch(''),
type: z.enum(['link', 'photo', 'video', 'rich']).catch('link'),
author_name: z.string().catch(''),
author_url: z.string().catch(''),
provider_name: z.string().catch(''),
provider_url: z.string().catch(''),
html: z.string().catch(''),
width: z.number().nullable().catch(null),
height: z.number().nullable().catch(null),
image: z.string().nullable().catch(null),
image_description: z.string().nullable().catch(null),
embed_url: z.string().catch(''),
blurhash: blurhashSchema.nullable().catch(null),
history: z.array(historySchema).nullable().catch(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 = z.infer<typeof trendsLinkSchema>;
type TrendsLink = v.InferOutput<typeof trendsLinkSchema>;
export { trendsLinkSchema, type TrendsLink };

View File

@ -1,26 +1,38 @@
import z from 'zod';
import * as v from 'valibot';
/** Validate to Mastodon's date format, or use the current date. */
const dateSchema = z.string().datetime({ offset: true }).catch(new Date().toUTCString());
const datetimeSchema = v.pipe(
v.string(),
// Adapted from Zod
// https://github.com/colinhacks/zod/blob/main/src/types.ts#L619
// at least it's not chatgpt
v.regex(/^\d{4}-\d{2}-\d{2}T([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d+)?(([+-]\d{2}:?\d{2})|(Z)?)$/),
);
/** 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))
)),
);
/** Validates the string as an emoji. */
const emojiSchema = z.string().refine((v) => /\p{Extended_Pictographic}|[\u{1F1E6}-\u{1F1FF}]{2}/u.test(v));
const emojiSchema = v.pipe(v.string(), v.emoji());
/** MIME schema, eg `image/png`. */
const mimeSchema = z.string().regex(/^\w+\/[-+.\w]+$/);
const mimeSchema = v.pipe(v.string(), v.regex(/^\w+\/[-+.\w]+$/));
/** zod schema to force the value into an object, if it isn't already. */
const coerceObject = <T extends z.ZodRawShape>(shape: T) =>
z.object({}).passthrough().catch({}).pipe(z.object(shape));
/** valibot schema to force the value into an object, if it isn't already. */
const coerceObject = <T extends v.ObjectEntries>(shape: T) =>
v.pipe(
v.any(),
v.transform((input) => typeof input === 'object' ? input : {}),
v.object(shape),
);
export { filteredArray, emojiSchema, dateSchema, mimeSchema, coerceObject };
export { filteredArray, emojiSchema, datetimeSchema, mimeSchema, coerceObject };

View File

@ -1,13 +1,13 @@
import { z } from 'zod';
import * as v from 'valibot';
/** @see {@link https://docs.joinmastodon.org/entities/WebPushSubscription/} */
const webPushSubscriptionSchema = z.object({
id: z.coerce.string(),
endpoint: z.string(),
alerts: z.record(z.boolean()),
server_key: z.string(),
const webPushSubscriptionSchema = v.object({
id: v.pipe(v.unknown(), v.transform(String)),
endpoint: v.string(),
alerts: v.record(v.string(), v.boolean()),
server_key: v.string(),
});
type WebPushSubscription = z.infer<typeof webPushSubscriptionSchema>;
type WebPushSubscription = v.InferOutput<typeof webPushSubscriptionSchema>;
export { webPushSubscriptionSchema, type WebPushSubscription };

View File

@ -1,6 +1,6 @@
{
"name": "pl-api",
"version": "0.0.42",
"version": "0.1.1",
"type": "module",
"homepage": "https://github.com/mkljczk/pl-fe/tree/fork/packages/pl-api",
"repository": {
@ -42,7 +42,7 @@
"object-to-formdata": "^4.5.1",
"query-string": "^9.1.0",
"semver": "^7.6.3",
"zod": "^3.23.8"
"valibot": "^0.42.1"
},
"module": "./dist/main.es.js",
"types": "dist/main.d.ts",

View File

@ -3,6 +3,8 @@ import { resolve } from 'path';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
import pkg from './package.json';
export default defineConfig({
plugins: [dts({ include: ['lib'], insertTypesEntry: true })],
build: {
@ -15,5 +17,8 @@ export default defineConfig({
},
target: 'esnext',
sourcemap: true,
rollupOptions: {
external: Object.keys(pkg.dependencies),
},
},
});

View File

@ -2551,6 +2551,11 @@ uri-js@^4.2.2, uri-js@^4.4.1:
dependencies:
punycode "^2.1.0"
valibot@^0.42.1:
version "0.42.1"
resolved "https://registry.yarnpkg.com/valibot/-/valibot-0.42.1.tgz#a31183d8e9d7552f98e22ca0977172cab8815188"
integrity sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw==
vite-plugin-dts@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/vite-plugin-dts/-/vite-plugin-dts-4.2.3.tgz#e0d9616eb574700111dbd19ae98e166541433263"
@ -2630,8 +2635,3 @@ yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod@^3.23.8:
version "3.23.8"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==

View File

@ -26,7 +26,7 @@ Changes made since the project forked from Soapbox in April 2024.
- You can write posts with multiple language versions, when supported by backend.
- Language detection is done client-side for composed posts, utilizing `fasttext.wasm.js`.
- Draft posts. They are stored locally only and work with any backend.
- New visibility scopes are supported local-only and list-only for Pleroma. Local-only is a separate switch on GoToSocial.
**Features:**
- The most recent scrobble is displayed on user profile/card.
@ -34,7 +34,7 @@ Changes made since the project forked from Soapbox in April 2024.
- You can bite users, if supported by backend.
- You can browse Bubble timeline, if supported by backend.
- Mastodon displays trending articles on Search page.
- Postsa can be addressed to lists of users, on Pleroma.
- Posts can be addressed to lists of users, on Pleroma.
### Changed
@ -62,13 +62,14 @@ Changes made since the project forked from Soapbox in April 2024.
- Updated Lists UI, to match the overall style.
- RSS button is displayed in account header for local users, when unauthenticated.
- Conversations page is always displayed, even when Chats are supported.
- Made it woke.
- Emojis are zoomed on hover.
**Internal:**
- Migrated some local stores from Redux to Zustand.
**Dependencies:**
- `@tanstack/react-virtual` is used for list virtualization, instead of `react-virtuoso`.
- `@tanstack/react-virtual` is used for list virtualization, instead of `react-virtuoso`. This improves compatibility with Ladybird browser.
- Replaced `react-popper` and `react-overlays` with `@floating-ui/react`.
- `uuid` package is replaced by the `randomUUID()` method.

View File

@ -103,7 +103,7 @@
"mini-css-extract-plugin": "^2.9.1",
"multiselect-react-dropdown": "^2.0.25",
"path-browserify": "^1.0.1",
"pl-api": "^0.0.42",
"pl-api": "^0.1.1",
"postcss": "^8.4.47",
"process": "^0.11.10",
"punycode": "^2.1.1",
@ -132,7 +132,6 @@
"reselect": "^5.1.1",
"resize-observer-polyfill": "^1.5.1",
"sass": "^1.79.4",
"semver": "^7.6.3",
"stringz": "^2.1.0",
"tiny-queue": "^0.2.1",
"tslib": "^2.7.0",
@ -140,13 +139,13 @@
"type-fest": "^4.26.1",
"typescript": "^5.6.2",
"util": "^0.12.5",
"valibot": "^0.42.1",
"vite": "^5.4.8",
"vite-plugin-compile-time": "^0.2.1",
"vite-plugin-html": "^3.2.2",
"vite-plugin-require": "^1.2.14",
"vite-plugin-static-copy": "^1.0.6",
"wicg-inert": "^3.1.3",
"zod": "^3.23.8",
"zustand": "^5.0.0-rc.2"
},
"devDependencies": {

View File

@ -9,6 +9,7 @@
import { credentialAccountSchema, PlApiClient, type CreateAccountParams, type Token } from 'pl-api';
import { importEntities } from 'pl-hooks/importer';
import { defineMessages } from 'react-intl';
import * as v from 'valibot';
import { createAccount } from 'pl-fe/actions/accounts';
import { createApp } from 'pl-fe/actions/apps';
@ -155,7 +156,7 @@ const verifyCredentials = (token: string, accountUrl?: string) =>
if (error?.response?.status === 403 && error?.response?.json?.id) {
// The user is waitlisted
const account = error.response.json;
const parsedAccount = credentialAccountSchema.parse(error.response.json);
const parsedAccount = v.parse(credentialAccountSchema, error.response.json);
importEntities({ accounts: [parsedAccount] });
dispatch({ type: VERIFY_CREDENTIALS_SUCCESS, token, account: parsedAccount });
if (account.id === getState().me) dispatch(fetchMeSuccess(parsedAccount));

View File

@ -7,6 +7,7 @@
*/
import { instanceSchema, PlApiClient, type Instance } from 'pl-api';
import * as v from 'valibot';
import { createApp } from 'pl-fe/actions/apps';
import { authLoggedIn, verifyCredentials, switchAccount } from 'pl-fe/actions/auth';
@ -24,7 +25,7 @@ const fetchExternalInstance = (baseURL: string) =>
if (error.response?.status === 401) {
// Authenticated fetch is enabled.
// Continue with a limited featureset.
return instanceSchema.parse({});
return v.parse(instanceSchema, {});
} else {
throw error;
}

View File

@ -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])),
},
);

View File

@ -5,6 +5,7 @@ import {
type AdminCreateAnnouncementParams,
type AdminUpdateAnnouncementParams,
} from 'pl-api';
import * as v from 'valibot';
import { useClient } from 'pl-fe/hooks';
import { normalizeAnnouncement, AdminAnnouncement } from 'pl-fe/normalizers';
@ -36,7 +37,7 @@ const useAnnouncements = () => {
retry: false,
onSuccess: (data) =>
queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
[...prevResult, adminAnnouncementSchema.parse(data)],
[...prevResult, v.parse(adminAnnouncementSchema, data)],
),
onSettled: () => userAnnouncements.refetch(),
});
@ -50,7 +51,7 @@ const useAnnouncements = () => {
retry: false,
onSuccess: (data) =>
queryClient.setQueryData(['admin', 'announcements'], (prevResult: ReadonlyArray<AdminAnnouncement>) =>
prevResult.map((announcement) => announcement.id === data.id ? adminAnnouncementSchema.parse(data) : announcement),
prevResult.map((announcement) => announcement.id === data.id ? v.parse(adminAnnouncementSchema, data) : announcement),
),
onSettled: () => userAnnouncements.refetch(),
});

View File

@ -1,11 +1,12 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { announcementReactionSchema, type AnnouncementReaction } from 'pl-api';
import * as v from 'valibot';
import { useClient } from 'pl-fe/hooks';
import { type Announcement, normalizeAnnouncement } from 'pl-fe/normalizers';
import { queryClient } from 'pl-fe/queries/client';
const updateReaction = (reaction: AnnouncementReaction, count: number, me?: boolean, overwrite?: boolean) => announcementReactionSchema.parse({
const updateReaction = (reaction: AnnouncementReaction, count: number, me?: boolean, overwrite?: boolean) => v.parse(announcementReactionSchema, {
...reaction,
me: typeof me === 'boolean' ? me : reaction.me,
count: overwrite ? count : (reaction.count + count),
@ -18,7 +19,7 @@ const updateReactions = (reactions: AnnouncementReaction[], name: string, count:
reactions = reactions.map(reaction => reaction.name === name ? updateReaction(reaction, count, me, overwrite) : reaction);
}
return [...reactions, updateReaction(announcementReactionSchema.parse({ name }), count, me, overwrite)];
return [...reactions, updateReaction(v.parse(announcementReactionSchema, { name }), count, me, overwrite)];
};
const useAnnouncements = () => {

View File

@ -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;

View File

@ -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])),
},
);

View File

@ -1,4 +1,5 @@
import { instanceSchema } from 'pl-api';
import * as v from 'valibot';
import { __stub } from 'pl-fe/api';
import { buildGroup } from 'pl-fe/jest/factory';
@ -8,7 +9,7 @@ import { useGroups } from './useGroups';
const group = buildGroup({ id: '1', display_name: 'soapbox' });
const store = {
instance: instanceSchema.parse({
instance: v.parse(instanceSchema, {
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
}),
};

View File

@ -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;

View File

@ -1,10 +1,11 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { type InteractionPolicies, interactionPoliciesSchema } from 'pl-api';
import * as v from 'valibot';
import { useClient, useFeatures, useLoggedIn } from 'pl-fe/hooks';
import { queryClient } from 'pl-fe/queries/client';
const emptySchema = interactionPoliciesSchema.parse({});
const emptySchema = v.parse(interactionPoliciesSchema, {});
const useInteractionPolicies = () => {
const client = useClient();

View File

@ -113,7 +113,7 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
<div className='relative mt-1 rounded-md shadow-sm'>
<DatePicker
selected={selected}
wrapperClassName='react-datepicker-wrapper'
wrapperClassName='w-fit'
onChange={handleChange}
placeholderText={intl.formatMessage(messages.birthdayPlaceholder)}
minDate={new Date('1900-01-01')}

View File

@ -1,6 +1,7 @@
import clsx from 'clsx';
import { type MediaAttachment, type PreviewCard as CardEntity, mediaAttachmentSchema } from 'pl-api';
import React, { useState, useEffect } from 'react';
import * as v from 'valibot';
import Blurhash from 'pl-fe/components/blurhash';
import { HStack, Stack, Text, Icon } from 'pl-fe/components/ui';
@ -43,7 +44,7 @@ const PreviewCard: React.FC<IPreviewCard> = ({
const trimmedDescription = trim(card.description, maxDescription);
const handlePhotoClick = () => {
const attachment = mediaAttachmentSchema.parse({
const attachment = v.parse(mediaAttachmentSchema, {
id: '',
type: 'image',
url: card.embed_url,

View File

@ -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.

View File

@ -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';
@ -29,7 +29,7 @@ const useBatchedEntities = <TEntity extends Entity>(
const getState = useGetState();
const dispatch = useAppDispatch();
const { entityType, listKey, path } = parseEntitiesPath(expandedPath);
const schema = opts.schema || z.custom<TEntity>();
const schema = opts.schema || v.custom<TEntity>(() => true);
const isEnabled = opts.enabled ?? true;
const isFetching = useListState(path, 'fetching');
@ -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,

View File

@ -1,4 +1,4 @@
import { z } from 'zod';
import * as v from 'valibot';
import { useAppDispatch } from 'pl-fe/hooks/useAppDispatch';
import { useLoading } from 'pl-fe/hooks/useLoading';
@ -31,8 +31,8 @@ const useCreateEntity = <TEntity extends Entity = Entity, TTransformedEntity ext
callbacks: EntityCallbacks<TTransformedEntity, { response?: PlfeResponse }> = {},
): Promise<void> => {
const result = await setPromise(entityFn(data));
const schema = opts.schema || z.custom<TEntity>();
let entity: TEntity | TTransformedEntity = schema.parse(result);
const schema = opts.schema || v.custom<TEntity>(() => true);
let entity: TEntity | TTransformedEntity = v.parse(schema, result);
if (opts.transform) entity = opts.transform(entity);
// TODO: optimistic updating

View File

@ -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';
@ -17,7 +17,7 @@ import type { PaginatedResponse } from 'pl-api';
/** Additional options for the hook. */
interface UseEntitiesOpts<TEntity extends Entity, TTransformedEntity extends Entity> {
/** A zod schema to parse the API entities. */
/** A valibot schema to parse the API entities. */
schema?: EntitySchema<TEntity>;
/**
* Time (milliseconds) until this query becomes stale and should be refetched.
@ -43,7 +43,7 @@ const useEntities = <TEntity extends Entity, TTransformedEntity extends Entity =
const { entityType, listKey, path } = parseEntitiesPath(expandedPath);
const entities = useAppSelector(state => selectEntities<TTransformedEntity>(state, path));
const schema = opts.schema || z.custom<TEntity>();
const schema = opts.schema || v.custom<TEntity>(() => true);
const isEnabled = opts.enabled ?? true;
const isFetching = useListState(path, 'fetching');
@ -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, {

Some files were not shown because too many files have changed in this diff Show More