feat(api): Add OpenAPI visualizer
Some checks failed
CodeQL Scan / Analyze (javascript-typescript) (push) Failing after 53s
Build Docker Images / lint (push) Failing after 10s
Build Docker Images / check (push) Failing after 10s
Build Docker Images / tests (push) Failing after 6s
Build Docker Images / build (server, Dockerfile, ${{ github.repository_owner }}/server) (push) Has been skipped
Build Docker Images / build (worker, Worker.Dockerfile, ${{ github.repository_owner }}/worker) (push) Has been skipped
Deploy Docs to GitHub Pages / build (push) Failing after 5s
Mirror to Codeberg / Mirror (push) Failing after 0s
Deploy Docs to GitHub Pages / Deploy (push) Has been skipped
Nix Build / check (push) Failing after 5s

This commit is contained in:
Jesse Wierzbinski 2025-03-24 15:25:40 +01:00
parent 65e2e19ff1
commit c674a1309c
No known key found for this signature in database
32 changed files with 772 additions and 732 deletions

12
app.ts
View file

@ -3,10 +3,10 @@ import { handleZodError } from "@/api";
import { applyToHono } from "@/bull-board.ts"; import { applyToHono } from "@/bull-board.ts";
import { configureLoggers } from "@/loggers"; import { configureLoggers } from "@/loggers";
import { sentry } from "@/sentry"; import { sentry } from "@/sentry";
import { swaggerUI } from "@hono/swagger-ui";
import { OpenAPIHono } from "@hono/zod-openapi"; import { OpenAPIHono } from "@hono/zod-openapi";
/* import { prometheus } from "@hono/prometheus"; */ /* import { prometheus } from "@hono/prometheus"; */
import { getLogger } from "@logtape/logtape"; import { getLogger } from "@logtape/logtape";
import { apiReference } from "@scalar/hono-api-reference";
import { inspect } from "bun"; import { inspect } from "bun";
import chalk from "chalk"; import chalk from "chalk";
import { cors } from "hono/cors"; import { cors } from "hono/cors";
@ -148,7 +148,15 @@ export const appFactory = async (): Promise<OpenAPIHono<HonoEnv>> => {
contact: pkg.author, contact: pkg.author,
}, },
}); });
app.get("/docs", swaggerUI({ url: "/openapi.json" })); app.get(
"/docs",
apiReference({
theme: "deepSpace",
hideClientButton: true,
pageTitle: "Versia Server API",
url: "/openapi.json",
}),
);
applyToHono(app); applyToHono(app);
app.options("*", (context) => { app.options("*", (context) => {

View file

@ -13,12 +13,12 @@
"@clerc/plugin-version": "^0.44.0", "@clerc/plugin-version": "^0.44.0",
"@hackmd/markdown-it-task-lists": "^2.1.4", "@hackmd/markdown-it-task-lists": "^2.1.4",
"@hono/prometheus": "^1.0.1", "@hono/prometheus": "^1.0.1",
"@hono/swagger-ui": "^0.5.1",
"@hono/zod-openapi": "0.19.2", "@hono/zod-openapi": "0.19.2",
"@hono/zod-validator": "^0.4.3", "@hono/zod-validator": "^0.4.3",
"@inquirer/confirm": "^5.1.8", "@inquirer/confirm": "^5.1.8",
"@logtape/file": "^0.9.0", "@logtape/file": "^0.9.0",
"@logtape/logtape": "^0.9.0", "@logtape/logtape": "^0.9.0",
"@scalar/hono-api-reference": "^0.7.2",
"@sentry/bun": "^9.8.0", "@sentry/bun": "^9.8.0",
"@versia/client": "workspace:*", "@versia/client": "workspace:*",
"@versia/federation": "^0.2.1", "@versia/federation": "^0.2.1",
@ -271,8 +271,6 @@
"@hono/prometheus": ["@hono/prometheus@1.0.1", "", { "peerDependencies": { "hono": ">=3.*", "prom-client": "^15.0.0" } }, "sha512-PjMbjAppCgbvRP2aLxqJc1XJLxfmg4dLsS5R5ITt7qCf9Ab/xSRul/LHNVvYK2/ECi3BOPprSnlSizksBJmXBQ=="], "@hono/prometheus": ["@hono/prometheus@1.0.1", "", { "peerDependencies": { "hono": ">=3.*", "prom-client": "^15.0.0" } }, "sha512-PjMbjAppCgbvRP2aLxqJc1XJLxfmg4dLsS5R5ITt7qCf9Ab/xSRul/LHNVvYK2/ECi3BOPprSnlSizksBJmXBQ=="],
"@hono/swagger-ui": ["@hono/swagger-ui@0.5.1", "", { "peerDependencies": { "hono": "*" } }, "sha512-XpUCfszLJ9b1rtFdzqOSHfdg9pfBiC2J5piEjuSanYpDDTIwpMz0ciiv5N3WWUaQpz9fEgH8lttQqL41vIFuDA=="],
"@hono/zod-openapi": ["@hono/zod-openapi@0.19.2", "", { "dependencies": { "@asteasolutions/zod-to-openapi": "^7.1.0", "@hono/zod-validator": "^0.4.1" }, "peerDependencies": { "hono": ">=4.3.6", "zod": "3.*" } }, "sha512-lkFa6wdQVgY7d7/m++Ixr3hvKCF5Y+zjTIPM37fex5ylCfX53A/W28gZRDuFZx3aR+noKob7lHfwdk9dURLzxw=="], "@hono/zod-openapi": ["@hono/zod-openapi@0.19.2", "", { "dependencies": { "@asteasolutions/zod-to-openapi": "^7.1.0", "@hono/zod-validator": "^0.4.1" }, "peerDependencies": { "hono": ">=4.3.6", "zod": "3.*" } }, "sha512-lkFa6wdQVgY7d7/m++Ixr3hvKCF5Y+zjTIPM37fex5ylCfX53A/W28gZRDuFZx3aR+noKob7lHfwdk9dURLzxw=="],
"@hono/zod-validator": ["@hono/zod-validator@0.4.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ=="], "@hono/zod-validator": ["@hono/zod-validator@0.4.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ=="],
@ -465,6 +463,14 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.36.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw=="], "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.36.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw=="],
"@scalar/core": ["@scalar/core@0.2.2", "", { "dependencies": { "@scalar/types": "0.1.2" } }, "sha512-jT6vfz37yQnqVjj8kXYEmV2cZvODW1A0PXjxZ9DzKqjm9tIssNwP4vvcdD1FSuiMcj+rgxAxOjIYMI+ybI/9RQ=="],
"@scalar/hono-api-reference": ["@scalar/hono-api-reference@0.7.2", "", { "dependencies": { "@scalar/core": "0.2.2" }, "peerDependencies": { "hono": "^4.0.0" } }, "sha512-CnxRjGfAWPGkV0D5TEwogvn7JSx/f9+ag6vQ6g25GigSDyj/UkxYbZqwe/QOV/+2EWruY3ypOvPuNMf7nEQhdQ=="],
"@scalar/openapi-types": ["@scalar/openapi-types@0.1.9", "", {}, "sha512-HQQudOSQBU7ewzfnBW9LhDmBE2XOJgSfwrh5PlUB7zJup/kaRkBGNgV2wMjNz9Af/uztiU/xNrO179FysmUT+g=="],
"@scalar/types": ["@scalar/types@0.1.2", "", { "dependencies": { "@scalar/openapi-types": "0.1.9", "@unhead/schema": "^1.11.11", "zod": "^3.23.8" } }, "sha512-5kCLQRwAYWt1ds110EaUb9yonc3KoQYNyo4YUCigJLOnoNugbqkEX0zRudGevItiuk+xg4uOYd30r3C+6xAasA=="],
"@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="],
"@sentry/bun": ["@sentry/bun@9.8.0", "", { "dependencies": { "@sentry/core": "9.8.0", "@sentry/node": "9.8.0", "@sentry/opentelemetry": "9.8.0" } }, "sha512-3JSaxyEomfZYLp1mwvwugj308QeZ4fYRzjAOQcWZqHFKbr6pMFKTvZKfEKIoAEXsNfka33+VpPdltlMgGe9auw=="], "@sentry/bun": ["@sentry/bun@9.8.0", "", { "dependencies": { "@sentry/core": "9.8.0", "@sentry/node": "9.8.0", "@sentry/opentelemetry": "9.8.0" } }, "sha512-3JSaxyEomfZYLp1mwvwugj308QeZ4fYRzjAOQcWZqHFKbr6pMFKTvZKfEKIoAEXsNfka33+VpPdltlMgGe9auw=="],
@ -543,6 +549,8 @@
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"@unhead/schema": ["@unhead/schema@1.11.20", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA=="],
"@versia/client": ["@versia/client@workspace:packages/client"], "@versia/client": ["@versia/client@workspace:packages/client"],
"@versia/federation": ["@versia/federation@0.2.1", "", { "dependencies": { "magic-regexp": "^0.8.0", "mime-types": "^2.1.35", "zod": "^3.24.1", "zod-validation-error": "^3.4.0" } }, "sha512-FTo3VGNJBGmCi0ZEQMzqFZBbcfbX81kmg0UgY4cKamr1dJWgEf72IAZnEDgrBffFjYtreLGdEjFkkcq3JfS8oQ=="], "@versia/federation": ["@versia/federation@0.2.1", "", { "dependencies": { "magic-regexp": "^0.8.0", "mime-types": "^2.1.35", "zod": "^3.24.1", "zod-validation-error": "^3.4.0" } }, "sha512-FTo3VGNJBGmCi0ZEQMzqFZBbcfbX81kmg0UgY4cKamr1dJWgEf72IAZnEDgrBffFjYtreLGdEjFkkcq3JfS8oQ=="],
@ -1339,6 +1347,8 @@
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="], "yoctocolors-cjs": ["yoctocolors-cjs@2.1.2", "", {}, "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA=="],
"zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="],
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="], "zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],

View file

@ -85,12 +85,12 @@
"@clerc/plugin-version": "^0.44.0", "@clerc/plugin-version": "^0.44.0",
"@hackmd/markdown-it-task-lists": "^2.1.4", "@hackmd/markdown-it-task-lists": "^2.1.4",
"@hono/prometheus": "^1.0.1", "@hono/prometheus": "^1.0.1",
"@hono/swagger-ui": "^0.5.1",
"@hono/zod-openapi": "0.19.2", "@hono/zod-openapi": "0.19.2",
"@hono/zod-validator": "^0.4.3", "@hono/zod-validator": "^0.4.3",
"@inquirer/confirm": "^5.1.8", "@inquirer/confirm": "^5.1.8",
"@logtape/file": "^0.9.0", "@logtape/file": "^0.9.0",
"@logtape/logtape": "^0.9.0", "@logtape/logtape": "^0.9.0",
"@scalar/hono-api-reference": "^0.7.2",
"@sentry/bun": "^9.8.0", "@sentry/bun": "^9.8.0",
"@versia/client": "workspace:*", "@versia/client": "workspace:*",
"@versia/federation": "^0.2.1", "@versia/federation": "^0.2.1",

View file

@ -49,7 +49,7 @@ export const AccountWarning = z
example: "2025-01-04T14:11:00Z", example: "2025-01-04T14:11:00Z",
}), }),
}) })
.openapi({ .openapi("AccountWarning", {
description: "Moderation warning against a particular account.", description: "Moderation warning against a particular account.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/AccountWarning", url: "https://docs.joinmastodon.org/entities/AccountWarning",

View file

@ -5,44 +5,46 @@ import { iso631, zBoolean } from "./common.ts";
import { CustomEmoji } from "./emoji.ts"; import { CustomEmoji } from "./emoji.ts";
import { Role } from "./versia.ts"; import { Role } from "./versia.ts";
export const Field = z.object({ export const Field = z
name: z .object({
.string() name: z
.trim() .string()
.min(1) .trim()
.max(config.validation.accounts.max_field_name_characters) .min(1)
.openapi({ .max(config.validation.accounts.max_field_name_characters)
description: "The key of a given fields key-value pair.", .openapi({
example: "Freak level", description: "The key of a given fields key-value pair.",
externalDocs: { example: "Freak level",
url: "https://docs.joinmastodon.org/entities/Account/#name", externalDocs: {
}, url: "https://docs.joinmastodon.org/entities/Account/#name",
}), },
value: z }),
.string() value: z
.trim() .string()
.min(1) .trim()
.max(config.validation.accounts.max_field_value_characters) .min(1)
.openapi({ .max(config.validation.accounts.max_field_value_characters)
description: "The value associated with the name key.", .openapi({
example: "<p>High</p>", description: "The value associated with the name key.",
externalDocs: { example: "<p>High</p>",
url: "https://docs.joinmastodon.org/entities/Account/#value", externalDocs: {
}, url: "https://docs.joinmastodon.org/entities/Account/#value",
}), },
verified_at: z }),
.string() verified_at: z
.datetime() .string()
.nullable() .datetime()
.openapi({ .nullable()
description: .openapi({
"Timestamp of when the server verified a URL value for a rel=“me” link.", description:
example: null, "Timestamp of when the server verified a URL value for a rel=“me” link.",
externalDocs: { example: null,
url: "https://docs.joinmastodon.org/entities/Account/#verified_at", externalDocs: {
}, url: "https://docs.joinmastodon.org/entities/Account/#verified_at",
}), },
}); }),
})
.openapi("AccountField");
export const Source = z export const Source = z
.object({ .object({
@ -107,7 +109,7 @@ export const Source = z
description: "Metadata about the account.", description: "Metadata about the account.",
}), }),
}) })
.openapi({ .openapi("AccountSource", {
description: description:
"An extra attribute that contains source values to be used with API methods that verify credentials and update credentials.", "An extra attribute that contains source values to be used with API methods that verify credentials and update credentials.",
externalDocs: { externalDocs: {
@ -116,304 +118,310 @@ export const Source = z
}); });
// Because Account has some recursive references, we need to define it like this // Because Account has some recursive references, we need to define it like this
const BaseAccount = z.object({ const BaseAccount = z
id: z .object({
.string() id: z
.uuid() .string()
.openapi({ .uuid()
description: "The account ID in the database.", .openapi({
example: "9e84842b-4db6-4a9b-969d-46ab408278da", description: "The account ID in the database.",
externalDocs: { example: "9e84842b-4db6-4a9b-969d-46ab408278da",
url: "https://docs.joinmastodon.org/entities/Account/#id", externalDocs: {
}, url: "https://docs.joinmastodon.org/entities/Account/#id",
}), },
username: z }),
.string() username: z
.min(3) .string()
.trim() .min(3)
.max(config.validation.accounts.max_username_characters) .trim()
.regex( .max(config.validation.accounts.max_username_characters)
/^[a-z0-9_-]+$/, .regex(
"Username can only contain letters, numbers, underscores and hyphens", /^[a-z0-9_-]+$/,
) "Username can only contain letters, numbers, underscores and hyphens",
.refine( )
(s) => .refine(
!config.validation.filters.username.some((filter) => (s) =>
filter.test(s), !config.validation.filters.username.some((filter) =>
), filter.test(s),
"Username contains blocked words", ),
) "Username contains blocked words",
.refine( )
(s) => .refine(
!config.validation.accounts.disallowed_usernames.some((u) => (s) =>
u.test(s), !config.validation.accounts.disallowed_usernames.some((u) =>
), u.test(s),
"Username is disallowed", ),
) "Username is disallowed",
.openapi({ )
description: "The username of the account, not including domain.", .openapi({
example: "lexi", description:
externalDocs: { "The username of the account, not including domain.",
url: "https://docs.joinmastodon.org/entities/Account/#username", example: "lexi",
}, externalDocs: {
}), url: "https://docs.joinmastodon.org/entities/Account/#username",
acct: z },
.string() }),
.min(1) acct: z
.trim() .string()
.regex(userAddressValidator, "Invalid user address") .min(1)
.openapi({ .trim()
.regex(userAddressValidator, "Invalid user address")
.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.accounts.max_displayname_characters)
.refine(
(s) =>
!config.validation.filters.displayname.some((filter) =>
filter.test(s),
),
"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.accounts.max_bio_characters)
.trim()
.refine(
(s) =>
!config.validation.filters.bio.some((filter) =>
filter.test(s),
),
"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: description:
"The Webfinger account URI. Equal to username for local users, or username@domain for remote users.", "Whether the account manually approves follow requests.",
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.accounts.max_displayname_characters)
.refine(
(s) =>
!config.validation.filters.displayname.some((filter) =>
filter.test(s),
),
"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.accounts.max_bio_characters)
.trim()
.refine(
(s) =>
!config.validation.filters.bio.some((filter) => filter.test(s)),
"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.accounts.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(CustomEmoji).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, example: false,
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#noindex", url: "https://docs.joinmastodon.org/entities/Account/#locked",
}, },
}), }),
suspended: zBoolean.optional().openapi({ fields: z
description: .array(Field)
"An extra attribute returned only when an account is suspended.", .max(config.validation.accounts.max_field_count)
example: false, .openapi({
externalDocs: { description:
url: "https://docs.joinmastodon.org/entities/Account/#suspended", "Additional metadata attached to a profile as name-value pairs.",
}, externalDocs: {
}), url: "https://docs.joinmastodon.org/entities/Account/#fields",
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.", emojis: z.array(CustomEmoji).openapi({
example: false, description:
externalDocs: { "Custom emoji entities to be used when rendering the profile.",
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: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#created_at", url: "https://docs.joinmastodon.org/entities/Account/#emojis",
}, },
}), }),
// TODO bot: zBoolean.openapi({
last_status_at: z description:
.literal(null) "Indicates that the account may perform automated actions, may not be monitored, or identifies as a robot.",
.openapi({ example: false,
description: "When the most recent status was posted.",
example: null,
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#last_status_at", url: "https://docs.joinmastodon.org/entities/Account/#bot",
},
})
.nullable(),
statuses_count: z
.number()
.int()
.nonnegative()
.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 group: z.literal(false).openapi({
.number() description: "Indicates that the account represents a Group actor.",
.int() example: false,
.nonnegative()
.openapi({
description: "The reported followers of this profile.",
example: 6,
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#followers_count", url: "https://docs.joinmastodon.org/entities/Account/#group",
}, },
}), }),
following_count: z discoverable: zBoolean.nullable().openapi({
.number() description:
.int() "Whether the account has opted into discovery features such as the profile directory.",
.nonnegative() example: true,
.openapi({
description: "The reported follows of this profile.",
example: 23,
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#following_count", url: "https://docs.joinmastodon.org/entities/Account/#discoverable",
}, },
}), }),
/* Versia Server API extension */ noindex: zBoolean
uri: z.string().url().openapi({ .nullable()
description: .optional()
"The location of the user's Versia profile page, as opposed to the local representation.", .openapi({
example: description:
"https://beta.versia.social/users/9e84842b-4db6-4a9b-969d-46ab408278da", "Whether the local user has opted out of being indexed by search engines.",
}), example: false,
source: Source.optional(), externalDocs: {
role: z url: "https://docs.joinmastodon.org/entities/Account/#noindex",
.object({ },
name: z.string(), }),
}) suspended: zBoolean.optional().openapi({
.optional(), description:
/* Versia Server API extension */ "An extra attribute returned only when an account is suspended.",
roles: z.array(Role).openapi({ example: false,
description: "Roles assigned to the account.", externalDocs: {
}), url: "https://docs.joinmastodon.org/entities/Account/#suspended",
mute_expires_at: z.string().datetime().nullable().openapi({ },
description: "When a timed mute will expire, if applicable.", }),
example: "2025-03-01T14:00:00.000Z", 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()
.int()
.nonnegative()
.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()
.int()
.nonnegative()
.openapi({
description: "The reported followers of this profile.",
example: 6,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#followers_count",
},
}),
following_count: z
.number()
.int()
.nonnegative()
.openapi({
description: "The reported follows of this profile.",
example: 23,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Account/#following_count",
},
}),
/* Versia Server API extension */
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(),
/* Versia Server API extension */
roles: z.array(Role).openapi({
description: "Roles assigned to the account.",
}),
mute_expires_at: z.string().datetime().nullable().openapi({
description: "When a timed mute will expire, if applicable.",
example: "2025-03-01T14:00:00.000Z",
}),
})
.openapi("BaseAccount");
export const Account = BaseAccount.extend({ export const Account = BaseAccount.extend({
moved: BaseAccount.nullable() moved: BaseAccount.nullable()
@ -426,4 +434,4 @@ export const Account = BaseAccount.extend({
url: "https://docs.joinmastodon.org/entities/Account/#moved", url: "https://docs.joinmastodon.org/entities/Account/#moved",
}, },
}), }),
}); }).openapi("Account");

View file

@ -13,7 +13,7 @@ export const Appeal = z
example: "pending", example: "pending",
}), }),
}) })
.openapi({ .openapi("Appeal", {
description: "Appeal against a moderation action.", description: "Appeal against a moderation action.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Appeal", url: "https://docs.joinmastodon.org/entities/Appeal",

View file

@ -1,65 +1,67 @@
import { z } from "@hono/zod-openapi"; import { z } from "@hono/zod-openapi";
export const Application = z.object({ export const Application = z
name: z .object({
.string() name: z
.trim() .string()
.min(1) .trim()
.max(200) .min(1)
.openapi({ .max(200)
description: "The name of your application.", .openapi({
example: "Test Application", description: "The name of your application.",
externalDocs: { example: "Test Application",
url: "https://docs.joinmastodon.org/entities/Application/#name", externalDocs: {
}, url: "https://docs.joinmastodon.org/entities/Application/#name",
}), },
website: z }),
.string() website: z
.nullable() .string()
.openapi({ .nullable()
description: "The website associated with your application.", .openapi({
example: "https://app.example", description: "The website associated with your application.",
externalDocs: { example: "https://app.example",
url: "https://docs.joinmastodon.org/entities/Application/#website", externalDocs: {
}, url: "https://docs.joinmastodon.org/entities/Application/#website",
}), },
scopes: z }),
.array(z.string()) scopes: z
.default(["read"]) .array(z.string())
.openapi({ .default(["read"])
.openapi({
description:
"The scopes for your application. This is the registered scopes string split on whitespace.",
example: ["read", "write", "push"],
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Application/#scopes",
},
}),
redirect_uris: z
.array(
z
.string()
.url()
.or(z.literal("urn:ietf:wg:oauth:2.0:oob"))
.openapi({
description: "URL or 'urn:ietf:wg:oauth:2.0:oob'",
}),
)
.openapi({
description:
"The registered redirection URI(s) for your application.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Application/#redirect_uris",
},
}),
redirect_uri: z.string().openapi({
deprecated: true,
description: description:
"The scopes for your application. This is the registered scopes string split on whitespace.", "The registered redirection URI(s) for your application. May contain \\n characters when multiple redirect URIs are registered.",
example: ["read", "write", "push"],
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Application/#scopes", url: "https://docs.joinmastodon.org/entities/Application/#redirect_uri",
}, },
}), }),
redirect_uris: z })
.array( .openapi("Application");
z
.string()
.url()
.or(z.literal("urn:ietf:wg:oauth:2.0:oob"))
.openapi({
description: "URL or 'urn:ietf:wg:oauth:2.0:oob'",
}),
)
.openapi({
description:
"The registered redirection URI(s) for your application.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Application/#redirect_uris",
},
}),
redirect_uri: z.string().openapi({
deprecated: true,
description:
"The registered redirection URI(s) for your application. May contain \\n characters when multiple redirect URIs are registered.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Application/#redirect_uri",
},
}),
});
export const CredentialApplication = Application.extend({ export const CredentialApplication = Application.extend({
client_id: z.string().openapi({ client_id: z.string().openapi({
@ -81,4 +83,4 @@ export const CredentialApplication = Application.extend({
url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_secret_expires_at", url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_secret_expires_at",
}, },
}), }),
}); }).openapi("CredentialApplication");

View file

@ -67,7 +67,7 @@ export const Attachment = z
example: "UFBWY:8_0Jxv4mx]t8t64.%M-:IUWGWAt6M}", example: "UFBWY:8_0Jxv4mx]t8t64.%M-:IUWGWAt6M}",
}), }),
}) })
.openapi({ .openapi("Attachment", {
description: description:
"Represents a file or media attachment that can be added to a status.", "Represents a file or media attachment that can be added to a status.",
externalDocs: { externalDocs: {

View file

@ -1,31 +1,33 @@
import { z } from "@hono/zod-openapi"; import { z } from "@hono/zod-openapi";
import { Account } from "./account.ts"; import { Account } from "./account.ts";
export const PreviewCardAuthor = z.object({ export const PreviewCardAuthor = z
name: z.string().openapi({ .object({
description: "The original resource authors name.", name: z.string().openapi({
example: "The Doubleclicks", description: "The original resource authors name.",
externalDocs: { example: "The Doubleclicks",
url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#name",
},
}),
url: z
.string()
.url()
.openapi({
description: "A link to the author of the original resource.",
example: "https://www.youtube.com/user/thedoubleclicks",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#url", url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#name",
}, },
}), }),
account: Account.nullable().openapi({ url: z
description: "The fediverse account of the author.", .string()
externalDocs: { .url()
url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#account", .openapi({
}, description: "A link to the author of the original resource.",
}), example: "https://www.youtube.com/user/thedoubleclicks",
}); externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#url",
},
}),
account: Account.nullable().openapi({
description: "The fediverse account of the author.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCardAuthor/#account",
},
}),
})
.openapi("PreviewCardAuthor");
export const PreviewCard = z export const PreviewCard = z
.object({ .object({
@ -150,7 +152,7 @@ export const PreviewCard = z
}, },
}), }),
}) })
.openapi({ .openapi("PreviewCard", {
description: description:
"Represents a rich preview card that is generated using OpenGraph tags from a URL.", "Represents a rich preview card that is generated using OpenGraph tags from a URL.",
externalDocs: { externalDocs: {

View file

@ -3,7 +3,9 @@ import ISO6391 from "iso-639-1";
export const Id = z.string().uuid(); export const Id = z.string().uuid();
export const iso631 = z.enum(ISO6391.getAllCodes() as [string, ...string[]]); export const iso631 = z
.enum(ISO6391.getAllCodes() as [string, ...string[]])
.openapi("ISO631");
export const zBoolean = z export const zBoolean = z
.string() .string()

View file

@ -16,7 +16,7 @@ export const Context = z
}, },
}), }),
}) })
.openapi({ .openapi("Context", {
description: description:
"Represents the tree around a given status. Used for reconstructing threads of statuses.", "Represents the tree around a given status. Used for reconstructing threads of statuses.",
externalDocs: { externalDocs: {

View file

@ -87,7 +87,7 @@ export const CustomEmoji = z
}, },
}), }),
}) })
.openapi({ .openapi("CustomEmoji", {
description: "Represents a custom emoji.", description: "Represents a custom emoji.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/CustomEmoji", url: "https://docs.joinmastodon.org/entities/CustomEmoji",

View file

@ -22,7 +22,7 @@ export const ExtendedDescription = z
}, },
}), }),
}) })
.openapi({ .openapi("ExtendedDescription", {
description: description:
"Represents an extended description for the instance, to be shown on its about page.", "Represents an extended description for the instance, to be shown on its about page.",
externalDocs: { externalDocs: {

View file

@ -17,7 +17,7 @@ export const FamiliarFollowers = z
}, },
}), }),
}) })
.openapi({ .openapi("FamiliarFollowers", {
description: description:
"Represents a subset of your follows who also follow some other user.", "Represents a subset of your follows who also follow some other user.",
externalDocs: { externalDocs: {

View file

@ -18,7 +18,7 @@ export const FilterStatus = z
}, },
}), }),
}) })
.openapi({ .openapi("FilterStatus", {
description: description:
"Represents a status ID that, if matched, should cause the filter action to be taken.", "Represents a status ID that, if matched, should cause the filter action to be taken.",
externalDocs: { externalDocs: {
@ -51,7 +51,7 @@ export const FilterKeyword = z
}, },
}), }),
}) })
.openapi({ .openapi("FilterKeyword", {
description: description:
"Represents a keyword that, if matched, should cause the filter action to be taken.", "Represents a keyword that, if matched, should cause the filter action to be taken.",
externalDocs: { externalDocs: {
@ -133,7 +133,7 @@ export const Filter = z
}, },
}), }),
}) })
.openapi({ .openapi("Filter", {
description: description:
"Represents a user-defined filter for determining which statuses should not be shown to the user.", "Represents a user-defined filter for determining which statuses should not be shown to the user.",
externalDocs: { externalDocs: {
@ -171,7 +171,7 @@ export const FilterResult = z
}, },
}), }),
}) })
.openapi({ .openapi("FilterResult", {
description: description:
"Represents a filter whose keywords matched a given status.", "Represents a filter whose keywords matched a given status.",
externalDocs: { externalDocs: {

View file

@ -132,7 +132,7 @@ export const InstanceV1 = z
/* Versia Server API extension */ /* Versia Server API extension */
sso: SSOConfig, sso: SSOConfig,
}) })
.openapi({ .openapi("InstanceV1", {
description: description:
"Represents the software instance of Versia Server running on this domain.", "Represents the software instance of Versia Server running on this domain.",
externalDocs: { externalDocs: {

View file

@ -18,7 +18,7 @@ const InstanceIcon = z
example: "36x36", example: "36x36",
}), }),
}) })
.openapi({ .openapi("InstanceIcon", {
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/InstanceIcon", url: "https://docs.joinmastodon.org/entities/InstanceIcon",
}, },
@ -375,7 +375,7 @@ export const Instance = z
/* Versia Server API extension */ /* Versia Server API extension */
sso: SSOConfig, sso: SSOConfig,
}) })
.openapi({ .openapi("Instance", {
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Instance", url: "https://docs.joinmastodon.org/entities/Instance",
}, },

View file

@ -17,7 +17,7 @@ export const Marker = z
example: "2025-01-12T13:11:00Z", example: "2025-01-12T13:11:00Z",
}), }),
}) })
.openapi({ .openapi("Marker", {
description: description:
"Represents the last read position within a user's timelines.", "Represents the last read position within a user's timelines.",
externalDocs: { externalDocs: {

View file

@ -62,7 +62,7 @@ export const Notification = z
"Moderation warning that caused the notification. Attached when type of the notification is moderation_warning.", "Moderation warning that caused the notification. Attached when type of the notification is moderation_warning.",
}), }),
}) })
.openapi({ .openapi("Notification", {
description: description:
"Represents a notification of an event relevant to the user.", "Represents a notification of an event relevant to the user.",
externalDocs: { externalDocs: {

View file

@ -31,7 +31,7 @@ export const PollOption = z
}, },
}), }),
}) })
.openapi({ .openapi("PollOption", {
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#Option", url: "https://docs.joinmastodon.org/entities/Poll/#Option",
}, },
@ -130,7 +130,7 @@ export const Poll = z
}, },
}), }),
}) })
.openapi({ .openapi("Poll", {
description: "Represents a poll attached to a status.", description: "Represents a poll attached to a status.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll", url: "https://docs.joinmastodon.org/entities/Poll",

View file

@ -42,7 +42,7 @@ export const Preferences = z
}, },
}), }),
}) })
.openapi({ .openapi("Preferences", {
description: "Represents a user's preferences.", description: "Represents a user's preferences.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Preferences", url: "https://docs.joinmastodon.org/entities/Preferences",

View file

@ -21,7 +21,7 @@ export const PrivacyPolicy = z
}, },
}), }),
}) })
.openapi({ .openapi("PrivacyPolicy", {
description: "Represents the privacy policy of the instance.", description: "Represents the privacy policy of the instance.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/PrivacyPolicy", url: "https://docs.joinmastodon.org/entities/PrivacyPolicy",

View file

@ -80,7 +80,7 @@ export const WebPushSubscription = z
description: "The streaming servers VAPID key.", description: "The streaming servers VAPID key.",
}), }),
}) })
.openapi({}); .openapi("WebPushSubscription");
export const WebPushSubscriptionInput = z export const WebPushSubscriptionInput = z
.object({ .object({

View file

@ -65,7 +65,7 @@ export const Relationship = z
example: "they also like Kerbal Space Program", example: "they also like Kerbal Space Program",
}), }),
}) })
.openapi({ .openapi("Relationship", {
description: description:
"Represents the relationship between accounts, such as following / blocking / muting / etc.", "Represents the relationship between accounts, such as following / blocking / muting / etc.",
externalDocs: { externalDocs: {

View file

@ -50,7 +50,7 @@ export const Report = z
description: "The account that was reported.", description: "The account that was reported.",
}), }),
}) })
.openapi({ .openapi("Report", {
description: description:
"Reports filed against users and/or statuses, to be taken action on by moderators.", "Reports filed against users and/or statuses, to be taken action on by moderators.",
externalDocs: { externalDocs: {

View file

@ -15,7 +15,7 @@ export const Rule = z
example: "Please, we beg you.", example: "Please, we beg you.",
}), }),
}) })
.openapi({ .openapi("Rule", {
description: "Represents a rule that server users should follow.", description: "Represents a rule that server users should follow.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Rule", url: "https://docs.joinmastodon.org/entities/Rule",

View file

@ -15,7 +15,7 @@ export const Search = z
description: "Hashtags which match the given query", description: "Hashtags which match the given query",
}), }),
}) })
.openapi({ .openapi("Search", {
description: "Represents the results of a search.", description: "Represents the results of a search.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Search", url: "https://docs.joinmastodon.org/entities/Search",

View file

@ -42,7 +42,7 @@ export const Mention = z
}, },
}), }),
}) })
.openapi({ .openapi("Mention", {
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention", url: "https://docs.joinmastodon.org/entities/Status/#Mention",
}, },
@ -75,282 +75,288 @@ export const StatusSource = z
example: "", example: "",
}), }),
}) })
.openapi({ .openapi("StatusSource", {
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/StatusSource", url: "https://docs.joinmastodon.org/entities/StatusSource",
}, },
}); });
// Because Status has some recursive references, we need to define it like this // Because Status has some recursive references, we need to define it like this
const BaseStatus = z.object({ const BaseStatus = z
id: Id.openapi({ .object({
description: "ID of the status in the database.", id: Id.openapi({
example: "2de861d3-a3dd-42ee-ba38-2c7d3f4af588", description: "ID of the status in the database.",
externalDocs: { example: "2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
url: "https://docs.joinmastodon.org/entities/Status/#id",
},
}),
uri: z
.string()
.url()
.openapi({
description: "URI of the status used for federation.",
example:
"https://beta.versia.social/@lexi/2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#uri", url: "https://docs.joinmastodon.org/entities/Status/#id",
}, },
}), }),
url: z uri: z
.string() .string()
.url() .url()
.nullable() .openapi({
.openapi({ description: "URI of the status used for federation.",
description: "A link to the statuss HTML representation.", example:
example: "https://beta.versia.social/@lexi/2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
"https://beta.versia.social/@lexi/2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#url",
},
}),
account: Account.openapi({
description: "The account that authored this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#account",
},
}),
in_reply_to_id: Id.nullable().openapi({
description: "ID of the status being replied to.",
example: "c41c9fe9-919a-4d35-a921-d3e79a5c95f8",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#in_reply_to_id",
},
}),
in_reply_to_account_id: Account.shape.id.nullable().openapi({
description:
"ID of the account that authored the status being replied to.",
example: "7b9b3ec6-1013-4cc6-8902-94ad00cf2ccc",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#in_reply_to_account_id",
},
}),
content: z.string().openapi({
description: "HTML-encoded status content.",
example: "<p>hello world</p>",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#content",
},
}),
created_at: z
.string()
.datetime()
.openapi({
description: "The date when this status was created.",
example: "2025-01-07T14:11:00.000Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#created_at",
},
}),
edited_at: z
.string()
.datetime()
.nullable()
.openapi({
description: "Timestamp of when the status was last edited.",
example: "2025-01-07T14:11:00.000Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#edited_at",
},
}),
emojis: z.array(CustomEmoji).openapi({
description: "Custom emoji to be used when rendering status content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#emojis",
},
}),
replies_count: z
.number()
.int()
.nonnegative()
.openapi({
description: "How many replies this status has received.",
example: 1,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#replies_count",
},
}),
reblogs_count: z
.number()
.int()
.nonnegative()
.openapi({
description: "How many boosts this status has received.",
example: 6,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#reblogs_count",
},
}),
favourites_count: z
.number()
.int()
.nonnegative()
.openapi({
description: "How many favourites this status has received.",
example: 11,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#favourites_count",
},
}),
reblogged: zBoolean.optional().openapi({
description:
"If the current token has an authorized user: Have you boosted this status?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#reblogged",
},
}),
favourited: zBoolean.optional().openapi({
description:
"If the current token has an authorized user: Have you favourited this status?",
example: true,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#favourited",
},
}),
muted: zBoolean.optional().openapi({
description:
"If the current token has an authorized user: Have you muted notifications for this statuss conversation?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#muted",
},
}),
sensitive: zBoolean.openapi({
description: "Is this status marked as sensitive content?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#sensitive",
},
}),
spoiler_text: z.string().openapi({
description:
"Subject or summary line, below which status content is collapsed until expanded.",
example: "lewd text",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#spoiler_text",
},
}),
visibility: z.enum(["public", "unlisted", "private", "direct"]).openapi({
description: "Visibility of this status.",
example: "public",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#visibility",
},
}),
media_attachments: z.array(Attachment).openapi({
description: "Media that is attached to this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#media_attachments",
},
}),
mentions: z.array(Mention).openapi({
description: "Mentions of users within the status content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#mentions",
},
}),
tags: z.array(Tag).openapi({
description: "Hashtags used within the status content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#tags",
},
}),
card: PreviewCard.nullable().openapi({
description: "Preview card for links included within status content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#card",
},
}),
poll: Poll.nullable().openapi({
description: "The poll attached to the status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#poll",
},
}),
application: z
.object({
name: z.string().openapi({
description:
"The name of the application that posted this status.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#application-name", url: "https://docs.joinmastodon.org/entities/Status/#uri",
}, },
}), }),
website: z url: z
.string() .string()
.url() .url()
.nullable() .nullable()
.openapi({ .openapi({
description: "A link to the statuss HTML representation.",
example:
"https://beta.versia.social/@lexi/2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#url",
},
}),
account: Account.openapi({
description: "The account that authored this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#account",
},
}),
in_reply_to_id: Id.nullable().openapi({
description: "ID of the status being replied to.",
example: "c41c9fe9-919a-4d35-a921-d3e79a5c95f8",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#in_reply_to_id",
},
}),
in_reply_to_account_id: Account.shape.id.nullable().openapi({
description:
"ID of the account that authored the status being replied to.",
example: "7b9b3ec6-1013-4cc6-8902-94ad00cf2ccc",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#in_reply_to_account_id",
},
}),
content: z.string().openapi({
description: "HTML-encoded status content.",
example: "<p>hello world</p>",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#content",
},
}),
created_at: z
.string()
.datetime()
.openapi({
description: "The date when this status was created.",
example: "2025-01-07T14:11:00.000Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#created_at",
},
}),
edited_at: z
.string()
.datetime()
.nullable()
.openapi({
description: "Timestamp of when the status was last edited.",
example: "2025-01-07T14:11:00.000Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#edited_at",
},
}),
emojis: z.array(CustomEmoji).openapi({
description:
"Custom emoji to be used when rendering status content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#emojis",
},
}),
replies_count: z
.number()
.int()
.nonnegative()
.openapi({
description: "How many replies this status has received.",
example: 1,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#replies_count",
},
}),
reblogs_count: z
.number()
.int()
.nonnegative()
.openapi({
description: "How many boosts this status has received.",
example: 6,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#reblogs_count",
},
}),
favourites_count: z
.number()
.int()
.nonnegative()
.openapi({
description: "How many favourites this status has received.",
example: 11,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#favourites_count",
},
}),
reblogged: zBoolean.optional().openapi({
description:
"If the current token has an authorized user: Have you boosted this status?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#reblogged",
},
}),
favourited: zBoolean.optional().openapi({
description:
"If the current token has an authorized user: Have you favourited this status?",
example: true,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#favourited",
},
}),
muted: zBoolean.optional().openapi({
description:
"If the current token has an authorized user: Have you muted notifications for this statuss conversation?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#muted",
},
}),
sensitive: zBoolean.openapi({
description: "Is this status marked as sensitive content?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#sensitive",
},
}),
spoiler_text: z.string().openapi({
description:
"Subject or summary line, below which status content is collapsed until expanded.",
example: "lewd text",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#spoiler_text",
},
}),
visibility: z
.enum(["public", "unlisted", "private", "direct"])
.openapi({
description: "Visibility of this status.",
example: "public",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#visibility",
},
}),
media_attachments: z.array(Attachment).openapi({
description: "Media that is attached to this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#media_attachments",
},
}),
mentions: z.array(Mention).openapi({
description: "Mentions of users within the status content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#mentions",
},
}),
tags: z.array(Tag).openapi({
description: "Hashtags used within the status content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#tags",
},
}),
card: PreviewCard.nullable().openapi({
description:
"Preview card for links included within status content.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#card",
},
}),
poll: Poll.nullable().openapi({
description: "The poll attached to the status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#poll",
},
}),
application: z
.object({
name: z.string().openapi({
description: description:
"The website associated with the application that posted this status.", "The name of the application that posted this status.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#application-website", url: "https://docs.joinmastodon.org/entities/Status/#application-name",
}, },
}), }),
}) website: z
.optional() .string()
.openapi({ .url()
description: "The application used to post this status.", .nullable()
.openapi({
description:
"The website associated with the application that posted this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#application-website",
},
}),
})
.optional()
.openapi({
description: "The application used to post this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#application",
},
}),
language: iso631.nullable().openapi({
description: "Primary language of this status.",
example: "en",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#application", url: "https://docs.joinmastodon.org/entities/Status/#language",
}, },
}), }),
language: iso631.nullable().openapi({ text: z
description: "Primary language of this status.", .string()
example: "en", .nullable()
externalDocs: { .openapi({
url: "https://docs.joinmastodon.org/entities/Status/#language", description:
}, "Plain-text source of a status. Returned instead of content when status is deleted, so the user may redraft from the source text without the client having to reverse-engineer the original text from the HTML content.",
}), externalDocs: {
text: z url: "https://docs.joinmastodon.org/entities/Status/#text",
.string() },
.nullable() }),
.openapi({ pinned: zBoolean.optional().openapi({
description: description:
"Plain-text source of a status. Returned instead of content when status is deleted, so the user may redraft from the source text without the client having to reverse-engineer the original text from the HTML content.", "If the current token has an authorized user: Have you pinned this status? Only appears if the status is pinnable.",
example: true,
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#text", url: "https://docs.joinmastodon.org/entities/Status/#pinned",
}, },
}), }),
pinned: zBoolean.optional().openapi({ reactions: z.array(NoteReaction).openapi({}),
description: bookmarked: zBoolean.optional().openapi({
"If the current token has an authorized user: Have you pinned this status? Only appears if the status is pinnable.",
example: true,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#pinned",
},
}),
reactions: z.array(NoteReaction).openapi({}),
bookmarked: zBoolean.optional().openapi({
description:
"If the current token has an authorized user: Have you bookmarked this status?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#bookmarked",
},
}),
filtered: z
.array(FilterResult)
.optional()
.openapi({
description: description:
"If the current token has an authorized user: The filter and keywords that matched this status.", "If the current token has an authorized user: Have you bookmarked this status?",
example: false,
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#filtered", url: "https://docs.joinmastodon.org/entities/Status/#bookmarked",
}, },
}), }),
}); filtered: z
.array(FilterResult)
.optional()
.openapi({
description:
"If the current token has an authorized user: The filter and keywords that matched this status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#filtered",
},
}),
})
.openapi("BaseStatus");
export const Status = BaseStatus.extend({ export const Status = BaseStatus.extend({
reblog: BaseStatus.nullable().openapi({ reblog: BaseStatus.nullable().openapi({
@ -360,50 +366,52 @@ export const Status = BaseStatus.extend({
}, },
}), }),
quote: BaseStatus.nullable(), quote: BaseStatus.nullable(),
}); }).openapi("Status");
export const ScheduledStatus = z.object({ export const ScheduledStatus = z
id: Id.openapi({ .object({
description: "ID of the scheduled status in the database.", id: Id.openapi({
example: "2de861d3-a3dd-42ee-ba38-2c7d3f4af588", description: "ID of the scheduled status in the database.",
}), example: "2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
scheduled_at: z.string().datetime().openapi({
description: "When the status will be scheduled.",
example: "2025-01-07T14:11:00.000Z",
}),
media_attachments: Status.shape.media_attachments,
params: z.object({
text: z.string().openapi({
description: "Text to be used as status content.",
example: "Hello, world!",
}), }),
poll: Status.shape.poll, scheduled_at: z.string().datetime().openapi({
media_ids: z description: "When the status will be scheduled.",
.array(Id) example: "2025-01-07T14:11:00.000Z",
.nullable() }),
.openapi({ media_attachments: Status.shape.media_attachments,
description: params: z.object({
"IDs of the MediaAttachments that will be attached to the status.", text: z.string().openapi({
example: ["1234567890", "1234567891"], description: "Text to be used as status content.",
example: "Hello, world!",
}),
poll: Status.shape.poll,
media_ids: z
.array(Id)
.nullable()
.openapi({
description:
"IDs of the MediaAttachments that will be attached to the status.",
example: ["1234567890", "1234567891"],
}),
sensitive: Status.shape.sensitive,
spoiler_text: Status.shape.spoiler_text,
visibility: Status.shape.visibility,
in_reply_to_id: Status.shape.in_reply_to_id,
/** Versia Server API Extension */
quote_id: z.string().openapi({
description: "ID of the status being quoted.",
example: "c5d62a13-f340-4e7d-8942-7fd14be688dc",
}),
language: Status.shape.language,
scheduled_at: z.null().openapi({
description:
"When the status will be scheduled. This will be null because the status is only scheduled once.",
example: null,
}),
idempotency: z.string().nullable().openapi({
description: "Idempotency key to prevent duplicate statuses.",
example: "1234567890",
}), }),
sensitive: Status.shape.sensitive,
spoiler_text: Status.shape.spoiler_text,
visibility: Status.shape.visibility,
in_reply_to_id: Status.shape.in_reply_to_id,
/** Versia Server API Extension */
quote_id: z.string().openapi({
description: "ID of the status being quoted.",
example: "c5d62a13-f340-4e7d-8942-7fd14be688dc",
}), }),
language: Status.shape.language, })
scheduled_at: z.null().openapi({ .openapi("ScheduledStatus");
description:
"When the status will be scheduled. This will be null because the status is only scheduled once.",
example: null,
}),
idempotency: z.string().nullable().openapi({
description: "Idempotency key to prevent duplicate statuses.",
example: "1234567890",
}),
}),
});

View file

@ -24,7 +24,7 @@ export const Tag = z
}, },
}), }),
}) })
.openapi({ .openapi("Tag", {
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Tag", url: "https://docs.joinmastodon.org/entities/Status/#Tag",
}, },

View file

@ -20,7 +20,7 @@ export const Token = z
example: 1573979017, example: 1573979017,
}), }),
}) })
.openapi({ .openapi("Token", {
description: description:
"Represents an OAuth token used for authenticating with the API and performing actions.", "Represents an OAuth token used for authenticating with the API and performing actions.",
externalDocs: { externalDocs: {

View file

@ -11,6 +11,6 @@ export const TermsOfService = z
example: "<p><h1>ToS</h1><p>None, have fun.</p></p>", example: "<p><h1>ToS</h1><p>None, have fun.</p></p>",
}), }),
}) })
.openapi({ .openapi("TermsOfService", {
description: "Represents the ToS of the instance.", description: "Represents the ToS of the instance.",
}); });

View file

@ -45,7 +45,7 @@ export const Role = z
example: "https://example.com/role-icon.png", example: "https://example.com/role-icon.png",
}), }),
}) })
.openapi({ .openapi("Role", {
description: description:
"Information about a role in the system, as well as its permissions.", "Information about a role in the system, as well as its permissions.",
}); });
@ -72,7 +72,7 @@ export const NoteReaction = z
example: true, example: true,
}), }),
}) })
.openapi({ .openapi("NoteReaction", {
description: "Information about a reaction to a note.", description: "Information about a reaction to a note.",
}); });
@ -134,6 +134,6 @@ export const Challenge = z
example: "1234567890", example: "1234567890",
}), }),
}) })
.openapi({ .openapi("Challenge", {
description: "A cryptographic challenge to solve. Used for Captchas.", description: "A cryptographic challenge to solve. Used for Captchas.",
}); });