server/classes/schemas/account.ts
2025-02-05 21:49:39 +01:00

404 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { z } from "@hono/zod-openapi";
import type { Account as ApiAccount } from "@versia/client/types";
import ISO6391 from "iso-639-1";
import { config } from "~/packages/config-manager";
import { zBoolean } from "~/packages/config-manager/config.type";
import { Emoji } from "../database/emoji.ts";
import { Role } from "../database/role.ts";
export const Field = z.object({
name: z
.string()
.trim()
.min(1)
.max(config.validation.max_field_name_size)
.openapi({
description: "The key of a given fields key-value pair.",
example: "Freak level",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#name",
},
}),
value: z
.string()
.trim()
.min(1)
.max(config.validation.max_field_value_size)
.openapi({
description: "The value associated with the name key.",
example: "<p>High</p>",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#value",
},
}),
verified_at: z
.string()
.datetime()
.nullable()
.openapi({
description:
"Timestamp of when the server verified a URL value for a rel=“me” link.",
example: null,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#verified_at",
},
}),
});
export const Source = z
.object({
privacy: z.enum(["public", "unlisted", "private", "direct"]).openapi({
description:
"The default post privacy to be used for new statuses.",
example: "unlisted",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#source-privacy",
},
}),
sensitive: zBoolean.openapi({
description:
"Whether new statuses should be marked sensitive by default.",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#source-sensitive",
},
}),
language: z
.enum(ISO6391.getAllCodes() as [string, ...string[]])
.openapi({
description: "The default posting language for new statuses.",
example: "en",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#source-language",
},
}),
follow_requests_count: z
.number()
.int()
.optional()
.openapi({
description: "The number of pending follow requests.",
example: 3,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#follow_requests_count",
},
}),
note: z
.string()
.trim()
.min(0)
.max(config.validation.max_bio_size)
.refine(
(s) => !config.filters.bio.some((filter) => s.match(filter)),
"Bio contains blocked words",
)
.openapi({
description: "Profile bio, in plain-text instead of in HTML.",
example: "ermmm what the meow meow",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#source-note",
},
}),
fields: z.array(Field).max(config.validation.max_field_count).openapi({
description: "Metadata about the account.",
}),
})
.openapi({
description:
"An extra attribute that contains source values to be used with API methods that verify credentials and update credentials.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#source",
},
});
export const Account = z.object({
id: z
.string()
.uuid()
.openapi({
description: "The account id.",
example: "9e84842b-4db6-4a9b-969d-46ab408278da",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#id",
},
}),
username: z
.string()
.min(3)
.trim()
.max(config.validation.max_username_size)
.toLowerCase()
.regex(
/^[a-z0-9_-]+$/,
"Username can only contain letters, numbers, underscores and hyphens",
)
.refine(
(s) => !config.filters.username.some((filter) => s.match(filter)),
"Username contains blocked words",
)
.openapi({
description: "The username of the account, not including domain.",
example: "lexi",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#username",
},
}),
acct: z.string().openapi({
description:
"The Webfinger account URI. Equal to username for local users, or username@domain for remote users.",
example: "lexi@beta.versia.social",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#acct",
},
}),
url: z
.string()
.url()
.openapi({
description: "The location of the users profile page.",
example: "https://beta.versia.social/@lexi",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#url",
},
}),
display_name: z
.string()
.min(3)
.trim()
.max(config.validation.max_displayname_size)
.refine(
(s) =>
!config.filters.displayname.some((filter) => s.match(filter)),
"Display name contains blocked words",
)
.openapi({
description: "The profiles display name.",
example: "Lexi :flower:",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#display_name",
},
}),
note: z
.string()
.min(0)
.max(config.validation.max_bio_size)
.trim()
.refine(
(s) => !config.filters.bio.some((filter) => s.match(filter)),
"Bio contains blocked words",
)
.openapi({
description: "The profiles bio or description.",
example: "<p>ermmm what the meow meow</p>",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#note",
},
}),
avatar: z
.string()
.url()
.openapi({
description:
"An image icon that is shown next to statuses and in the profile.",
example:
"https://cdn.versia.social/avatars/cff9aea0-0000-43fe-8b5e-e7c7ea69a488/lexi.webp",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#avatar",
},
}),
avatar_static: z
.string()
.url()
.openapi({
description:
"A static version of the avatar. Equal to avatar if its value is a static image; different if avatar is an animated GIF.",
example:
"https://cdn.versia.social/avatars/cff9aea0-0000-43fe-8b5e-e7c7ea69a488/lexi.webp",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#avatar_static",
},
}),
header: z
.string()
.url()
.openapi({
description:
"An image banner that is shown above the profile and in profile cards.",
example:
"https://cdn.versia.social/headers/a049f8e3-878c-4faa-ae4c-a6bcceddbd9d/femboy_2.webp",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#header",
},
}),
header_static: z
.string()
.url()
.openapi({
description:
"A static version of the header. Equal to header if its value is a static image; different if header is an animated GIF.",
example:
"https://cdn.versia.social/headers/a049f8e3-878c-4faa-ae4c-a6bcceddbd9d/femboy_2.webp",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#header_static",
},
}),
locked: zBoolean.openapi({
description: "Whether the account manually approves follow requests.",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#locked",
},
}),
fields: z
.array(Field)
.max(config.validation.max_field_count)
.openapi({
description:
"Additional metadata attached to a profile as name-value pairs.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#fields",
},
}),
emojis: z.array(Emoji.schema).openapi({
description:
"Custom emoji entities to be used when rendering the profile.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#emojis",
},
}),
bot: zBoolean.openapi({
description:
"Indicates that the account may perform automated actions, may not be monitored, or identifies as a robot.",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#bot",
},
}),
group: z.literal(false).openapi({
description: "Indicates that the account represents a Group actor.",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#group",
},
}),
discoverable: zBoolean.nullable().openapi({
description:
"Whether the account has opted into discovery features such as the profile directory.",
example: true,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#discoverable",
},
}),
noindex: zBoolean
.nullable()
.optional()
.openapi({
description:
"Whether the local user has opted out of being indexed by search engines.",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#noindex",
},
}),
// FIXME: Use a proper type
moved: z
.lazy((): z.ZodType<ApiAccount> => Account as z.ZodType<ApiAccount>)
.nullable()
.optional()
.openapi({
description:
"Indicates that the profile is currently inactive and that its user has moved to a new account.",
example: null,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#moved",
},
}),
suspended: zBoolean.optional().openapi({
description:
"An extra attribute returned only when an account is suspended.",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#suspended",
},
}),
limited: zBoolean.optional().openapi({
description:
"An extra attribute returned only when an account is silenced. If true, indicates that the account should be hidden behind a warning screen.",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#limited",
},
}),
created_at: z
.string()
.datetime()
.openapi({
description: "When the account was created.",
example: "2024-10-15T22:00:00.000Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#created_at",
},
}),
// TODO
last_status_at: z
.literal(null)
.openapi({
description: "When the most recent status was posted.",
example: null,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#last_status_at",
},
})
.nullable(),
statuses_count: z
.number()
.min(0)
.int()
.openapi({
description: "How many statuses are attached to this account.",
example: 42,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#statuses_count",
},
}),
followers_count: z
.number()
.min(0)
.int()
.openapi({
description: "The reported followers of this profile.",
example: 6,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#followers_count",
},
}),
following_count: z
.number()
.min(0)
.int()
.openapi({
description: "The reported follows of this profile.",
example: 23,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#following_count",
},
}),
uri: z.string().url().openapi({
description:
"The location of the user's Versia profile page, as opposed to the local representation.",
example:
"https://beta.versia.social/users/9e84842b-4db6-4a9b-969d-46ab408278da",
}),
source: Source.optional(),
role: z
.object({
name: z.string(),
})
.optional(),
roles: z.array(Role.schema),
mute_expires_at: z.string().datetime().nullable().openapi({
description: "When a timed mute will expire, if applicable.",
example: "2025-03-01T14:00:00.000Z",
}),
});