feat(api): 🏷️ Finish porting full Mastodon API to OpenAPI

This commit is contained in:
Jesse Wierzbinski 2025-02-12 23:04:44 +01:00
parent 264e2fe8ac
commit fda1167234
No known key found for this signature in database
25 changed files with 1597 additions and 395 deletions

View file

@ -2,7 +2,7 @@ import { apiRoute, auth, withUserParam } from "@/api";
import { createRoute, z } from "@hono/zod-openapi"; import { createRoute, z } from "@hono/zod-openapi";
import { Relationship } from "@versia/kit/db"; import { Relationship } from "@versia/kit/db";
import { RolePermissions } from "@versia/kit/tables"; import { RolePermissions } from "@versia/kit/tables";
import ISO6391 from "iso-639-1"; import { iso631 } from "~/classes/schemas/common";
const schemas = { const schemas = {
param: z.object({ param: z.object({
@ -12,9 +12,7 @@ const schemas = {
.object({ .object({
reblogs: z.coerce.boolean().optional(), reblogs: z.coerce.boolean().optional(),
notify: z.coerce.boolean().optional(), notify: z.coerce.boolean().optional(),
languages: z languages: z.array(iso631).optional(),
.array(z.enum(ISO6391.getAllCodes() as [string, ...string[]]))
.optional(),
}) })
.optional() .optional()
.default({ reblogs: true, notify: false, languages: [] }), .default({ reblogs: true, notify: false, languages: [] }),

View file

@ -0,0 +1,57 @@
import { z } from "@hono/zod-openapi";
import { Account } from "./account.ts";
import { Appeal } from "./appeal.ts";
import { Id } from "./common.ts";
export const AccountWarning = z
.object({
id: Id.openapi({
description: "The ID of the account warning in the database.",
example: "0968680e-fd64-4525-b818-6e1c46fbdb28",
}),
action: z
.enum([
"none",
"disable",
"mark_statuses_as_sensitive",
"delete_statuses",
"sensitive",
"silence",
"suspend",
])
.openapi({
description:
"Action taken against the account. 'none' = No action was taken, this is a simple warning; 'disable' = The account has been disabled; 'mark_statuses_as_sensitive' = Specific posts from the target account have been marked as sensitive; 'delete_statuses' = Specific statuses from the target account have been deleted; 'sensitive' = All posts from the target account are marked as sensitive; 'silence' = The target account has been limited; 'suspend' = The target account has been suspended.",
example: "none",
}),
text: z.string().openapi({
description: "Message from the moderator to the target account.",
example: "Please adhere to our community guidelines.",
}),
status_ids: z
.array(Id)
.nullable()
.openapi({
description:
"List of status IDs that are relevant to the warning. When action is mark_statuses_as_sensitive or delete_statuses, those are the affected statuses.",
example: ["5ee59275-c308-4173-bb1f-58646204579b"],
}),
target_account: Account.openapi({
description:
"Account against which a moderation decision has been taken.",
}),
appeal: Appeal.nullable().openapi({
description: "Appeal submitted by the target account, if any.",
example: null,
}),
created_at: z.string().datetime().openapi({
description: "When the event took place.",
example: "2025-01-04T14:11:00Z",
}),
})
.openapi({
description: "Moderation warning against a particular account.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/AccountWarning",
},
});

View file

@ -1,8 +1,8 @@
import { z } from "@hono/zod-openapi"; import { z } from "@hono/zod-openapi";
import type { Account as ApiAccount } from "@versia/client/types"; import type { Account as ApiAccount } from "@versia/client/types";
import ISO6391 from "iso-639-1";
import { config } from "~/packages/config-manager"; import { config } from "~/packages/config-manager";
import { zBoolean } from "~/packages/config-manager/config.type"; import { zBoolean } from "~/packages/config-manager/config.type";
import { iso631 } from "./common.ts";
import { CustomEmoji } from "./emoji.ts"; import { CustomEmoji } from "./emoji.ts";
import { Role } from "./versia.ts"; import { Role } from "./versia.ts";
@ -63,15 +63,13 @@ export const Source = z
url: "https://docs.joinmastodon.org/entities/Account/#source-sensitive", url: "https://docs.joinmastodon.org/entities/Account/#source-sensitive",
}, },
}), }),
language: z language: iso631.openapi({
.enum(ISO6391.getAllCodes() as [string, ...string[]]) description: "The default posting language for new statuses.",
.openapi({ example: "en",
description: "The default posting language for new statuses.", externalDocs: {
example: "en", url: "https://docs.joinmastodon.org/entities/Account/#source-language",
externalDocs: { },
url: "https://docs.joinmastodon.org/entities/Account/#source-language", }),
},
}),
follow_requests_count: z follow_requests_count: z
.number() .number()
.int() .int()

21
classes/schemas/appeal.ts Normal file
View file

@ -0,0 +1,21 @@
import { z } from "@hono/zod-openapi";
export const Appeal = z
.object({
text: z.string().openapi({
description:
"Text of the appeal from the moderated account to the moderators.",
example: "I believe this action was taken in error.",
}),
state: z.enum(["approved", "rejected", "pending"]).openapi({
description:
"State of the appeal. 'approved' = The appeal has been approved by a moderator, 'rejected' = The appeal has been rejected by a moderator, 'pending' = The appeal has been submitted, but neither approved nor rejected yet.",
example: "pending",
}),
})
.openapi({
description: "Appeal against a moderation action.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Appeal",
},
});

View file

@ -0,0 +1,70 @@
import { z } from "@hono/zod-openapi";
import { Id } from "./common.ts";
export const Attachment = z
.object({
id: Id.openapi({
description: "The ID of the attachment in the database.",
example: "8c33d4c6-2292-4f4d-945d-261836e09647",
}),
type: z.enum(["unknown", "image", "gifv", "video", "audio"]).openapi({
description:
"The type of the attachment. 'unknown' = unsupported or unrecognized file type, 'image' = Static image, 'gifv' = Looping, soundless animation, 'video' = Video clip, 'audio' = Audio track.",
example: "image",
}),
url: z.string().url().openapi({
description: "The location of the original full-size attachment.",
example:
"https://files.mastodon.social/media_attachments/files/022/345/792/original/57859aede991da25.jpeg",
}),
preview_url: z.string().url().nullable().openapi({
description:
"The location of a scaled-down preview of the attachment.",
example:
"https://files.mastodon.social/media_attachments/files/022/345/792/small/57859aede991da25.jpeg",
}),
remote_url: z.string().url().nullable().openapi({
description:
"The location of the full-size original attachment on the remote website, or null if the attachment is local.",
example: null,
}),
meta: z.record(z.any()).openapi({
description:
"Metadata. May contain subtrees like 'small' and 'original', and possibly a 'focus' object for smart thumbnail cropping.",
example: {
original: {
width: 640,
height: 480,
size: "640x480",
aspect: 1.3333333333333333,
},
small: {
width: 461,
height: 346,
size: "461x346",
aspect: 1.3323699421965318,
},
focus: {
x: -0.27,
y: 0.51,
},
},
}),
description: z.string().nullable().openapi({
description:
"Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load.",
example: "test media description",
}),
blurhash: z.string().nullable().openapi({
description:
"A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.",
example: "UFBWY:8_0Jxv4mx]t8t64.%M-:IUWGWAt6M}",
}),
})
.openapi({
description:
"Represents a file or media attachment that can be added to a status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Attachment",
},
});

View file

@ -27,125 +27,133 @@ export const PreviewCardAuthor = z.object({
}), }),
}); });
export const PreviewCard = z.object({ export const PreviewCard = z
url: z .object({
.string() url: z
.url() .string()
.openapi({ .url()
description: "Location of linked resource.", .openapi({
example: "https://www.youtube.com/watch?v=OMv_EPMED8Y", description: "Location of linked resource.",
example: "https://www.youtube.com/watch?v=OMv_EPMED8Y",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#url",
},
}),
title: z
.string()
.min(1)
.openapi({
description: "Title of linked resource.",
example: "♪ Brand New Friend (Christmas Song!)",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#title",
},
}),
description: z.string().openapi({
description: "Description of preview.",
example: "",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#url", url: "https://docs.joinmastodon.org/entities/PreviewCard/#description",
}, },
}), }),
title: z type: z.enum(["link", "photo", "video"]).openapi({
.string() description: "The type of the preview card.",
.min(1) example: "video",
.openapi({
description: "Title of linked resource.",
example: "♪ Brand New Friend (Christmas Song!)",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#title", url: "https://docs.joinmastodon.org/entities/PreviewCard/#type",
}, },
}), }),
description: z.string().openapi({ authors: z.array(PreviewCardAuthor).openapi({
description: "Description of preview.",
example: "",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#description",
},
}),
type: z.enum(["link", "photo", "video"]).openapi({
description: "The type of the preview card.",
example: "video",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#type",
},
}),
authors: z.array(PreviewCardAuthor).openapi({
description:
"Fediverse account of the authors of the original resource.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#authors",
},
}),
provider_name: z.string().openapi({
description: "The provider of the original resource.",
example: "YouTube",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#provider_name",
},
}),
provider_url: z
.string()
.url()
.openapi({
description: "A link to the provider of the original resource.",
example: "https://www.youtube.com/",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#provider_url",
},
}),
html: z.string().openapi({
description: "HTML to be used for generating the preview card.",
example:
'<iframe width="480" height="270" src="https://www.youtube.com/embed/OMv_EPMED8Y?feature=oembed" frameborder="0" allowfullscreen=""></iframe>',
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#html",
},
}),
width: z
.number()
.int()
.openapi({
description: "Width of preview, in pixels.",
example: 480,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#width",
},
}),
height: z
.number()
.int()
.openapi({
description: "Height of preview, in pixels.",
example: 270,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#height",
},
}),
image: z
.string()
.url()
.nullable()
.openapi({
description: "Preview thumbnail.",
example:
"https://cdn.versia.social/preview_cards/images/014/179/145/original/9cf4b7cf5567b569.jpeg",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#image",
},
}),
embed_url: z
.string()
.url()
.openapi({
description: "Used for photo embeds, instead of custom html.",
example:
"https://live.staticflickr.com/65535/49088768431_6a4322b3bb_b.jpg",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#embed_url",
},
}),
blurhash: z
.string()
.nullable()
.openapi({
description: description:
"A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.", "Fediverse account of the authors of the original resource.",
example: "UvK0HNkV,:s9xBR%njog0fo2W=WBS5ozofV@",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#blurhash", url: "https://docs.joinmastodon.org/entities/PreviewCard/#authors",
}, },
}), }),
}); provider_name: z.string().openapi({
description: "The provider of the original resource.",
example: "YouTube",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#provider_name",
},
}),
provider_url: z
.string()
.url()
.openapi({
description: "A link to the provider of the original resource.",
example: "https://www.youtube.com/",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#provider_url",
},
}),
html: z.string().openapi({
description: "HTML to be used for generating the preview card.",
example:
'<iframe width="480" height="270" src="https://www.youtube.com/embed/OMv_EPMED8Y?feature=oembed" frameborder="0" allowfullscreen=""></iframe>',
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#html",
},
}),
width: z
.number()
.int()
.openapi({
description: "Width of preview, in pixels.",
example: 480,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#width",
},
}),
height: z
.number()
.int()
.openapi({
description: "Height of preview, in pixels.",
example: 270,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#height",
},
}),
image: z
.string()
.url()
.nullable()
.openapi({
description: "Preview thumbnail.",
example:
"https://cdn.versia.social/preview_cards/images/014/179/145/original/9cf4b7cf5567b569.jpeg",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#image",
},
}),
embed_url: z
.string()
.url()
.openapi({
description: "Used for photo embeds, instead of custom html.",
example:
"https://live.staticflickr.com/65535/49088768431_6a4322b3bb_b.jpg",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#embed_url",
},
}),
blurhash: z
.string()
.nullable()
.openapi({
description:
"A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.",
example: "UvK0HNkV,:s9xBR%njog0fo2W=WBS5ozofV@",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard/#blurhash",
},
}),
})
.openapi({
description:
"Represents a rich preview card that is generated using OpenGraph tags from a URL.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PreviewCard",
},
});

View file

@ -1,3 +1,6 @@
import { z } from "@hono/zod-openapi"; import { z } from "@hono/zod-openapi";
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[]]);

View file

@ -0,0 +1,31 @@
import { z } from "@hono/zod-openapi";
export const ExtendedDescription = z
.object({
updated_at: z
.string()
.datetime()
.openapi({
description:
"A timestamp of when the extended description was last updated.",
example: "2025-01-12T13:11:00Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/ExtendedDescription/#updated_at",
},
}),
content: z.string().openapi({
description:
"The rendered HTML content of the extended description.",
example: "<p>We love casting spells.</p>",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/ExtendedDescription/#content",
},
}),
})
.openapi({
description:
"Represents an extended description for the instance, to be shown on its about page.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/ExtendedDescription",
},
});

View file

@ -0,0 +1,26 @@
import { z } from "@hono/zod-openapi";
import { Account } from "./account.ts";
export const FamiliarFollowers = z
.object({
id: Account.shape.id.openapi({
description: "The ID of the Account in the database.",
example: "48214efb-1f3c-459a-abfa-618a5aeb2f7a",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FamiliarFollowers/#id",
},
}),
accounts: z.array(Account).openapi({
description: "Accounts you follow that also follow this account.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FamiliarFollowers/#accounts",
},
}),
})
.openapi({
description:
"Represents a subset of your follows who also follow some other user.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FamiliarFollowers",
},
});

View file

@ -1,129 +1,171 @@
import { z } from "@hono/zod-openapi"; import { z } from "@hono/zod-openapi";
import { Id } from "./common.ts"; import { Id } from "./common.ts";
export const FilterStatus = z.object({ export const FilterStatus = z
id: Id.openapi({ .object({
description: "The ID of the FilterStatus in the database.", id: Id.openapi({
example: "3b19ed7c-0c4b-45e1-8c75-e21dfc8e86c3", description: "The ID of the FilterStatus in the database.",
externalDocs: { example: "3b19ed7c-0c4b-45e1-8c75-e21dfc8e86c3",
url: "https://docs.joinmastodon.org/entities/FilterStatus/#id", externalDocs: {
}, url: "https://docs.joinmastodon.org/entities/FilterStatus/#id",
}), },
status_id: Id.openapi({ }),
description: "The ID of the Status that will be filtered.", status_id: Id.openapi({
example: "4f941ac8-295c-4c2d-9300-82c162ac8028", description: "The ID of the Status that will be filtered.",
externalDocs: { example: "4f941ac8-295c-4c2d-9300-82c162ac8028",
url: "https://docs.joinmastodon.org/entities/FilterStatus/#status_id", externalDocs: {
}, url: "https://docs.joinmastodon.org/entities/FilterStatus/#status_id",
}), },
}); }),
})
export const FilterKeyword = z.object({ .openapi({
id: Id.openapi({
description: "The ID of the FilterKeyword in the database.",
example: "ca921e60-5b96-4686-90f3-d7cc420d7391",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterKeyword/#id",
},
}),
keyword: z.string().openapi({
description: "The phrase to be matched against.",
example: "badword",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterKeyword/#keyword",
},
}),
whole_word: z.boolean().openapi({
description: description:
"Should the filter consider word boundaries? See implementation guidelines for filters.", "Represents a status ID that, if matched, should cause the filter action to be taken.",
example: false,
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterKeyword/#whole_word", url: "https://docs.joinmastodon.org/entities/FilterStatus",
}, },
}), });
});
export const Filter = z.object({ export const FilterKeyword = z
id: Id.openapi({ .object({
description: "The ID of the Filter in the database.", id: Id.openapi({
example: "6b8fa22f-b128-43c2-9a1f-3c0499ef3a51", description: "The ID of the FilterKeyword in the database.",
externalDocs: { example: "ca921e60-5b96-4686-90f3-d7cc420d7391",
url: "https://docs.joinmastodon.org/entities/Filter/#id",
},
}),
title: z.string().openapi({
description: "A title given by the user to name the filter.",
example: "Test filter",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#title",
},
}),
context: z
.array(z.enum(["home", "notifications", "public", "thread", "account"]))
.openapi({
description: "The contexts in which the filter should be applied.",
example: ["home"],
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#context", url: "https://docs.joinmastodon.org/entities/FilterKeyword/#id",
}, },
}), }),
expires_at: z keyword: z.string().openapi({
.string() description: "The phrase to be matched against.",
.nullable() example: "badword",
.openapi({
description: "When the filter should no longer be applied.",
example: "2026-09-20T17:27:39.296Z",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#expires_at", url: "https://docs.joinmastodon.org/entities/FilterKeyword/#keyword",
}, },
}), }),
filter_action: z.enum(["warn", "hide"]).openapi({ whole_word: z.boolean().openapi({
description:
"Should the filter consider word boundaries? See implementation guidelines for filters.",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterKeyword/#whole_word",
},
}),
})
.openapi({
description: description:
"The action to be taken when a status matches this filter.", "Represents a keyword that, if matched, should cause the filter action to be taken.",
example: "warn",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#filter_action", url: "https://docs.joinmastodon.org/entities/FilterKeyword",
}, },
}), });
keywords: z.array(FilterKeyword).openapi({
description: "The keywords grouped under this filter.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#keywords",
},
}),
statuses: z.array(FilterStatus).openapi({
description: "The statuses grouped under this filter.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#statuses",
},
}),
});
export const FilterResult = z.object({ export const Filter = z
filter: Filter.openapi({ .object({
description: "The filter that was matched.", id: Id.openapi({
description: "The ID of the Filter in the database.",
example: "6b8fa22f-b128-43c2-9a1f-3c0499ef3a51",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#id",
},
}),
title: z.string().openapi({
description: "A title given by the user to name the filter.",
example: "Test filter",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#title",
},
}),
context: z
.array(
z.enum([
"home",
"notifications",
"public",
"thread",
"account",
]),
)
.openapi({
description:
"The contexts in which the filter should be applied.",
example: ["home"],
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#context",
},
}),
expires_at: z
.string()
.nullable()
.openapi({
description: "When the filter should no longer be applied.",
example: "2026-09-20T17:27:39.296Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#expires_at",
},
}),
filter_action: z.enum(["warn", "hide"]).openapi({
description:
"The action to be taken when a status matches this filter.",
example: "warn",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#filter_action",
},
}),
keywords: z.array(FilterKeyword).openapi({
description: "The keywords grouped under this filter.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#keywords",
},
}),
statuses: z.array(FilterStatus).openapi({
description: "The statuses grouped under this filter.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Filter/#statuses",
},
}),
})
.openapi({
description:
"Represents a user-defined filter for determining which statuses should not be shown to the user.",
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterResult/#filter", url: "https://docs.joinmastodon.org/entities/Filter",
}, },
}), });
keyword_matches: z
.array(z.string()) export const FilterResult = z
.nullable() .object({
.openapi({ filter: Filter.openapi({
description: "The keyword within the filter that was matched.", description: "The filter that was matched.",
example: ["badword"],
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterResult/#keyword_matches", url: "https://docs.joinmastodon.org/entities/FilterResult/#filter",
}, },
}), }),
status_matches: z keyword_matches: z
.array(Id) .array(z.string())
.nullable() .nullable()
.openapi({ .openapi({
description: "The status ID within the filter that was matched.", description: "The keyword within the filter that was matched.",
example: ["3819515a-5ceb-4078-8524-c939e38dcf8f"], example: ["badword"],
externalDocs: { externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterResult/#status_matches", url: "https://docs.joinmastodon.org/entities/FilterResult/#keyword_matches",
}, },
}), }),
}); status_matches: z
.array(Id)
.nullable()
.openapi({
description:
"The status ID within the filter that was matched.",
example: ["3819515a-5ceb-4078-8524-c939e38dcf8f"],
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterResult/#status_matches",
},
}),
})
.openapi({
description:
"Represents a filter whose keywords matched a given status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterResult",
},
});

View file

@ -0,0 +1,141 @@
import { z } from "@hono/zod-openapi";
import { Instance } from "./instance.ts";
import { SSOConfig } from "./versia.ts";
export const InstanceV1 = z
.object({
uri: Instance.shape.domain,
title: Instance.shape.title,
short_description: Instance.shape.description,
description: z.string().openapi({
description: "An HTML-permitted description of the site.",
example: "<p>Join the world's smallest social network.</p>",
}),
email: Instance.shape.contact.shape.email,
version: Instance.shape.version,
/* Versia Server API extension */
versia_version: Instance.shape.versia_version,
urls: z
.object({
streaming_api:
Instance.shape.configuration.shape.urls.shape.streaming,
})
.openapi({
description: "URLs of interest for clients apps.",
}),
stats: z
.object({
user_count: z.number().openapi({
description: "Total users on this instance.",
example: 812303,
}),
status_count: z.number().openapi({
description: "Total statuses on this instance.",
example: 38151616,
}),
domain_count: z.number().openapi({
description: "Total domains discovered by this instance.",
example: 25255,
}),
})
.openapi({
description:
"Statistics about how much information the instance contains.",
}),
thumbnail: z.string().url().nullable().openapi({
description: "Banner image for the website.",
example:
"https://files.mastodon.social/site_uploads/files/000/000/001/original/vlcsnap-2018-08-27-16h43m11s127.png",
}),
languages: Instance.shape.languages,
registrations: Instance.shape.registrations.shape.enabled,
approval_required: Instance.shape.registrations.shape.approval_required,
invites_enabled: z.boolean().openapi({
description: "Whether invites are enabled.",
example: true,
}),
configuration: z
.object({
accounts: z
.object({
max_featured_tags:
Instance.shape.configuration.shape.accounts.shape
.max_featured_tags,
})
.openapi({
description: "Limits related to accounts.",
}),
statuses: z
.object({
max_characters:
Instance.shape.configuration.shape.statuses.shape
.max_characters,
max_media_attachments:
Instance.shape.configuration.shape.statuses.shape
.max_media_attachments,
characters_reserved_per_url:
Instance.shape.configuration.shape.statuses.shape
.characters_reserved_per_url,
})
.openapi({
description: "Limits related to authoring statuses.",
}),
media_attachments: z
.object({
supported_mime_types:
Instance.shape.configuration.shape.media_attachments
.shape.supported_mime_types,
image_size_limit:
Instance.shape.configuration.shape.media_attachments
.shape.image_size_limit,
image_matrix_limit:
Instance.shape.configuration.shape.media_attachments
.shape.image_matrix_limit,
video_size_limit:
Instance.shape.configuration.shape.media_attachments
.shape.video_size_limit,
video_frame_rate_limit:
Instance.shape.configuration.shape.media_attachments
.shape.video_frame_rate_limit,
video_matrix_limit:
Instance.shape.configuration.shape.media_attachments
.shape.video_matrix_limit,
})
.openapi({
description:
"Hints for which attachments will be accepted.",
}),
polls: z
.object({
max_options:
Instance.shape.configuration.shape.polls.shape
.max_options,
max_characters_per_option:
Instance.shape.configuration.shape.polls.shape
.max_characters_per_option,
min_expiration:
Instance.shape.configuration.shape.polls.shape
.min_expiration,
max_expiration:
Instance.shape.configuration.shape.polls.shape
.max_expiration,
})
.openapi({
description: "Limits related to polls.",
}),
})
.openapi({
description: "Configured values and limits for this website.",
}),
contact_account: Instance.shape.contact.shape.account,
rules: Instance.shape.rules,
/* Versia Server API extension */
sso: SSOConfig,
})
.openapi({
description:
"Represents the software instance of Versia Server running on this domain.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/V1_Instance",
},
});

382
classes/schemas/instance.ts Normal file
View file

@ -0,0 +1,382 @@
import { z } from "@hono/zod-openapi";
import pkg from "~/package.json";
import { Account } from "./account.ts";
import { iso631 } from "./common.ts";
import { Rule } from "./rule.ts";
import { SSOConfig } from "./versia.ts";
const InstanceIcon = z
.object({
src: z.string().url().openapi({
description: "The URL of this icon.",
example:
"https://files.mastodon.social/site_uploads/files/000/000/003/36/accf17b0104f18e5.png",
}),
size: z.string().openapi({
description:
"The size of this icon (in the form of 12x34, where 12 is the width and 34 is the height of the icon).",
example: "36x36",
}),
})
.openapi({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/InstanceIcon",
},
});
export const Instance = z
.object({
domain: z.string().openapi({
description: "The domain name of the instance.",
example: "versia.social",
}),
title: z.string().openapi({
description: "The title of the website.",
example: "Versia Social • Now with 100% more blobs!",
}),
version: z.string().openapi({
description:
"Mastodon version that the API is compatible with. Used for compatibility with Mastodon clients.",
example: "4.3.0+glitch",
}),
/* Versia Server API extension */
versia_version: z.string().openapi({
description: "Versia Server version.",
example: "0.8.0",
}),
source_url: z.string().url().openapi({
description:
"The URL for the source code of the software running on this instance, in keeping with AGPL license requirements.",
example: pkg.repository.url,
}),
description: z.string().openapi({
description:
"A short, plain-text description defined by the admin.",
example: "The flagship Versia Server instance. Join for free hugs!",
}),
usage: z
.object({
users: z
.object({
active_month: z.number().openapi({
description:
"The number of active users in the past 4 weeks.",
example: 1_261,
}),
})
.openapi({
description:
"Usage data related to users on this instance.",
}),
})
.openapi({ description: "Usage data for this instance." }),
thumbnail: z
.object({
url: z.string().url().openapi({
description: "The URL for the thumbnail image.",
example:
"https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png",
}),
blurhash: z.string().optional().openapi({
description:
"A hash computed by the BlurHash algorithm, for generating colorful preview thumbnails when media has not been downloaded yet.",
example: "UUKJMXv|x]t7^*t7Rjaz^jazRjaz",
}),
versions: z
.object({
"@1x": z.string().url().optional().openapi({
description:
"The URL for the thumbnail image at 1x resolution.",
}),
"@2x": z.string().url().optional().openapi({
description:
"The URL for the thumbnail image at 2x resolution.",
}),
})
.optional()
.openapi({
description:
"Links to scaled resolution images, for high DPI screens.",
}),
})
.openapi({
description: "An image used to represent this instance.",
}),
icon: z.array(InstanceIcon).openapi({
description:
"The list of available size variants for this instance configured icon.",
}),
languages: z.array(iso631).openapi({
description: "Primary languages of the website and its staff.",
example: ["en"],
}),
configuration: z
.object({
urls: z
.object({
streaming: z.string().url().openapi({
description:
"The Websockets URL for connecting to the streaming API.",
example: "wss://versia.social",
}),
})
.openapi({
description: "URLs of interest for clients apps.",
}),
vapid: z
.object({
public_key: z.string().openapi({
description:
"The instance's VAPID public key, used for push notifications, the same as WebPushSubscription#server_key.",
example:
"BCkMmVdKDnKYwzVCDC99Iuc9GvId-x7-kKtuHnLgfF98ENiZp_aj-UNthbCdI70DqN1zUVis-x0Wrot2sBagkMc=",
}),
})
.openapi({ description: "VAPID configuration." }),
accounts: z
.object({
max_featured_tags: z.number().openapi({
description:
"The maximum number of featured tags allowed for each account.",
example: 10,
}),
max_pinned_statuses: z.number().openapi({
description:
"The maximum number of pinned statuses for each account.",
example: 4,
}),
/* Versia Server API extension */
max_displayname_characters: z.number().openapi({
description:
"The maximum number of characters allowed in a display name.",
example: 30,
}),
/* Versia Server API extension */
max_username_characters: z.number().openapi({
description:
"The maximum number of characters allowed in a username.",
example: 30,
}),
/* Versia Server API extension */
max_note_characters: z.number().openapi({
description:
"The maximum number of characters allowed in an account's bio/note.",
example: 500,
}),
/* Versia Server API extension */
avatar_limit: z.number().openapi({
description:
"The maximum size of an avatar image, in bytes.",
example: 1048576,
}),
/* Versia Server API extension */
header_limit: z.number().openapi({
description:
"The maximum size of a header image, in bytes.",
example: 2097152,
}),
/* Versia Server API extension */
fields: z
.object({
max_fields: z.number().openapi({
description:
"The maximum number of fields allowed per account.",
example: 4,
}),
max_name_characters: z.number().openapi({
description:
"The maximum number of characters allowed in a field name.",
example: 30,
}),
max_value_characters: z.number().openapi({
description:
"The maximum number of characters allowed in a field value.",
example: 100,
}),
})
.openapi({
description:
"Limits related to account fields.",
}),
})
.openapi({ description: "Limits related to accounts." }),
statuses: z
.object({
max_characters: z.number().openapi({
description:
"The maximum number of allowed characters per status.",
example: 500,
}),
max_media_attachments: z.number().openapi({
description:
"The maximum number of media attachments that can be added to a status.",
example: 4,
}),
characters_reserved_per_url: z.number().openapi({
description:
"Each URL in a status will be assumed to be exactly this many characters.",
example: 23,
}),
})
.openapi({
description: "Limits related to authoring statuses.",
}),
media_attachments: z
.object({
supported_mime_types: z.array(z.string()).openapi({
description:
"Contains MIME types that can be uploaded.",
example: ["image/jpeg", "image/png", "image/gif"],
}),
description_limit: z.number().openapi({
description:
"The maximum size of a description, in characters.",
example: 1500,
}),
image_size_limit: z.number().openapi({
description:
"The maximum size of any uploaded image, in bytes.",
example: 10485760,
}),
image_matrix_limit: z.number().openapi({
description:
"The maximum number of pixels (width times height) for image uploads.",
example: 16777216,
}),
video_size_limit: z.number().openapi({
description:
"The maximum size of any uploaded video, in bytes.",
example: 41943040,
}),
video_frame_rate_limit: z.number().openapi({
description:
"The maximum frame rate for any uploaded video.",
example: 60,
}),
video_matrix_limit: z.number().openapi({
description:
"The maximum number of pixels (width times height) for video uploads.",
example: 2304000,
}),
})
.openapi({
description:
"Hints for which attachments will be accepted.",
}),
/* Versia Server API extension */
emojis: z
.object({
emoji_size_limit: z.number().openapi({
description:
"The maximum size of an emoji image, in bytes.",
example: 1048576,
}),
max_shortcode_characters: z.number().openapi({
description:
"The maximum number of characters allowed in an emoji shortcode.",
example: 30,
}),
max_description_characters: z.number().openapi({
description:
"The maximum number of characters allowed in an emoji description.",
example: 100,
}),
})
.openapi({
description: "Limits related to custom emojis.",
}),
polls: z
.object({
max_options: z.number().openapi({
description:
"Each poll is allowed to have up to this many options.",
example: 4,
}),
max_characters_per_option: z.number().openapi({
description:
"Each poll option is allowed to have this many characters.",
example: 50,
}),
min_expiration: z.number().openapi({
description:
"The shortest allowed poll duration, in seconds.",
example: 300,
}),
max_expiration: z.number().openapi({
description:
"The longest allowed poll duration, in seconds.",
example: 2629746,
}),
})
.openapi({ description: "Limits related to polls." }),
translation: z
.object({
enabled: z.boolean().openapi({
description:
"Whether the Translations API is available on this instance.",
example: true,
}),
})
.openapi({ description: "Hints related to translation." }),
})
.openapi({
description: "Configured values and limits for this website.",
}),
registrations: z
.object({
enabled: z.boolean().openapi({
description: "Whether registrations are enabled.",
example: false,
}),
approval_required: z.boolean().openapi({
description:
"Whether registrations require moderator approval.",
example: false,
}),
message: z.string().nullable().openapi({
description:
"A custom message to be shown when registrations are closed.",
}),
})
.openapi({
description: "Information about registering for this website.",
}),
api_versions: z
.object({
mastodon: z.number().openapi({
description:
"API version number that this server implements.",
example: 1,
}),
})
.openapi({
description:
"Information about which version of the API is implemented by this server.",
}),
contact: z
.object({
email: z.string().email().openapi({
description:
"An email address that can be messaged regarding inquiries or issues.",
example: "contact@versia.social",
}),
account: Account.nullable().openapi({
description:
"An account that can be contacted regarding inquiries or issues.",
}),
})
.openapi({
description:
"Hints related to contacting a representative of the website.",
}),
rules: z.array(Rule).openapi({
description: "An itemized list of rules for this website.",
}),
/* Versia Server API extension */
sso: SSOConfig,
})
.openapi({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Instance",
},
});

26
classes/schemas/marker.ts Normal file
View file

@ -0,0 +1,26 @@
import { z } from "@hono/zod-openapi";
import { Id } from "./common.ts";
export const Marker = z
.object({
last_read_id: Id.openapi({
description: "The ID of the most recently viewed entity.",
example: "ead15c9d-8eda-4b2c-9546-ecbf851f001c",
}),
version: z.number().openapi({
description:
"An incrementing counter, used for locking to prevent write conflicts.",
example: 462,
}),
updated_at: z.string().datetime().openapi({
description: "The timestamp of when the marker was set.",
example: "2025-01-12T13:11:00Z",
}),
})
.openapi({
description:
"Represents the last read position within a user's timelines.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Marker",
},
});

View file

@ -0,0 +1,70 @@
import { z } from "@hono/zod-openapi";
import { AccountWarning } from "./account-warning.ts";
import { Account } from "./account.ts";
import { Id } from "./common.ts";
import { Report } from "./report.ts";
import { Status } from "./status.ts";
export const Notification = z
.object({
id: Id.openapi({
description: "The ID of the notification in the database.",
example: "6405f495-da55-4ad7-b5d6-9a773360fc07",
}),
type: z
.enum([
"mention",
"status",
"reblog",
"follow",
"follow_request",
"favourite",
"poll",
"update",
"admin.sign_up",
"admin.report",
"severed_relationships",
"moderation_warning",
])
.openapi({
description:
"The type of event that resulted in the notification.",
example: "mention",
}),
group_key: z.string().openapi({
description:
"Group key shared by similar notifications, to be used in the grouped notifications feature.",
example: "ungrouped-34975861",
}),
created_at: z.string().datetime().openapi({
description: "The timestamp of the notification.",
example: "2025-01-12T13:11:00Z",
}),
account: Account.openapi({
description:
"The account that performed the action that generated the notification.",
}),
status: Status.optional().openapi({
description:
"Status that was the object of the notification. Attached when type of the notification is favourite, reblog, status, mention, poll, or update.",
}),
report: Report.optional().openapi({
description:
"Report that was the object of the notification. Attached when type of the notification is admin.report.",
}),
event: z.undefined().openapi({
description:
"Versia Server does not sever relationships, so this field is always empty.",
}),
moderation_warning: AccountWarning.optional().openapi({
description:
"Moderation warning that caused the notification. Attached when type of the notification is moderation_warning.",
}),
})
.openapi({
description:
"Represents a notification of an event relevant to the user.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Notification",
},
});

132
classes/schemas/poll.ts Normal file
View file

@ -0,0 +1,132 @@
import { z } from "@hono/zod-openapi";
import { Id } from "./common.ts";
import { CustomEmoji } from "./emoji.ts";
export const PollOption = z
.object({
title: z.string().openapi({
description: "The text value of the poll option.",
example: "yes",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#Option-title",
},
}),
votes_count: z
.number()
.int()
.nonnegative()
.nullable()
.openapi({
description:
"The total number of received votes for this option.",
example: 6,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#Option-votes_count",
},
}),
})
.openapi({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#Option",
},
});
export const Poll = z
.object({
id: Id.openapi({
description: "ID of the poll in the database.",
example: "d87d230f-e401-4282-80c7-2044ab989662",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#id",
},
}),
expires_at: z
.string()
.datetime()
.nullable()
.openapi({
description: "When the poll ends.",
example: "2025-01-07T14:11:00.000Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#expires_at",
},
}),
expired: z.boolean().openapi({
description: "Is the poll currently expired?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#expired",
},
}),
multiple: z.boolean().openapi({
description: "Does the poll allow multiple-choice answers?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#multiple",
},
}),
votes_count: z
.number()
.int()
.nonnegative()
.openapi({
description: "How many votes have been received.",
example: 6,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#votes_count",
},
}),
voters_count: z
.number()
.int()
.nonnegative()
.nullable()
.openapi({
description:
"How many unique accounts have voted on a multiple-choice poll.",
example: 3,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#voters_count",
},
}),
options: z.array(PollOption).openapi({
description: "Possible answers for the poll.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#options",
},
}),
emojis: z.array(CustomEmoji).openapi({
description: "Custom emoji to be used for rendering poll options.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#emojis",
},
}),
voted: z
.boolean()
.optional()
.openapi({
description:
"When called with a user token, has the authorized user voted?",
example: true,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#voted",
},
}),
own_votes: z
.array(z.number().int())
.optional()
.openapi({
description:
"When called with a user token, which options has the authorized user chosen? Contains an array of index values for options.",
example: [0],
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#own_votes",
},
}),
})
.openapi({
description: "Represents a poll attached to a status.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll",
},
});

View file

@ -0,0 +1,50 @@
import { z } from "@hono/zod-openapi";
import { Source } from "./account.ts";
export const Preferences = z
.object({
"posting:default:visibility": Source.shape.privacy.openapi({
description: "Default visibility for new posts.",
example: "public",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Preferences/#posting-default-visibility",
},
}),
"posting:default:sensitive": Source.shape.sensitive.openapi({
description: "Default sensitivity flag for new posts.",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Preferences/#posting-default-sensitive",
},
}),
"posting:default:language": Source.shape.language.nullable().openapi({
description: "Default language for new posts.",
example: null,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Preferences/#posting-default-language",
},
}),
"reading:expand:media": z
.enum(["default", "show_all", "hide_all"])
.openapi({
description:
"Whether media attachments should be automatically displayed or blurred/hidden.",
example: "default",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Preferences/#reading-expand-media",
},
}),
"reading:expand:spoilers": z.boolean().openapi({
description: "Whether CWs should be expanded by default.",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Preferences/#reading-expand-spoilers",
},
}),
})
.openapi({
description: "Represents a user's preferences.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Preferences",
},
});

View file

@ -0,0 +1,29 @@
import { z } from "@hono/zod-openapi";
export const PrivacyPolicy = z
.object({
updated_at: z
.string()
.datetime()
.openapi({
description:
"A timestamp of when the privacy policy was last updated.",
example: "2025-01-12T13:11:00Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PrivacyPolicy/#updated_at",
},
}),
content: z.string().openapi({
description: "The rendered HTML content of the privacy policy.",
example: "<p><h1>Privacy Policy</h1><p>None, good luck.</p></p>",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PrivacyPolicy/#content",
},
}),
})
.openapi({
description: "Represents the privacy policy of the instance.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/PrivacyPolicy",
},
});

View file

@ -0,0 +1,74 @@
import { z } from "@hono/zod-openapi";
import { Id, iso631 } from "./common.ts";
export const Relationship = z
.object({
id: Id.openapi({
description: "The account ID.",
example: "51f34c31-c8c6-4dc2-9df1-3704fcdde9b6",
}),
following: z.boolean().openapi({
description: "Are you following this user?",
example: true,
}),
showing_reblogs: z.boolean().openapi({
description:
"Are you receiving this users boosts in your home timeline?",
example: true,
}),
notifying: z.boolean().openapi({
description: "Have you enabled notifications for this user?",
example: false,
}),
languages: z.array(iso631).openapi({
description: "Which languages are you following from this user?",
example: ["en"],
}),
followed_by: z.boolean().openapi({
description: "Are you followed by this user?",
example: true,
}),
blocking: z.boolean().openapi({
description: "Are you blocking this user?",
example: false,
}),
blocked_by: z.boolean().openapi({
description: "Is this user blocking you?",
example: false,
}),
muting: z.boolean().openapi({
description: "Are you muting this user?",
example: false,
}),
muting_notifications: z.boolean().openapi({
description: "Are you muting notifications from this user?",
example: false,
}),
requested: z.boolean().openapi({
description: "Do you have a pending follow request for this user?",
example: false,
}),
requested_by: z.boolean().openapi({
description: "Has this user requested to follow you?",
example: false,
}),
domain_blocking: z.boolean().openapi({
description: "Are you blocking this users domain?",
example: false,
}),
endorsed: z.boolean().openapi({
description: "Are you featuring this user on your profile?",
example: false,
}),
note: z.string().openapi({
description: "This users profile bio",
example: "they also like Kerbal Space Program",
}),
})
.openapi({
description:
"Represents the relationship between accounts, such as following / blocking / muting / etc.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Relationship",
},
});

59
classes/schemas/report.ts Normal file
View file

@ -0,0 +1,59 @@
import { z } from "@hono/zod-openapi";
import { Account } from "./account.ts";
import { Id } from "./common.ts";
export const Report = z
.object({
id: Id.openapi({
description: "The ID of the report in the database.",
example: "9b0cd757-324b-4ea6-beab-f6226e138886",
}),
action_taken: z.boolean().openapi({
description: "Whether an action was taken yet.",
example: false,
}),
action_taken_at: z.string().datetime().nullable().openapi({
description: "When an action was taken against the report.",
example: null,
}),
category: z.enum(["spam", "violation", "other"]).openapi({
description:
"The generic reason for the report. 'spam' = Unwanted or repetitive content, 'violation' = A specific rule was violated, 'other' = Some other reason.",
example: "spam",
}),
comment: z.string().openapi({
description: "The reason for the report.",
example: "Spam account",
}),
forwarded: z.boolean().openapi({
description: "Whether the report was forwarded to a remote domain.",
example: false,
}),
created_at: z.string().datetime().openapi({
description: "When the report was created.",
example: "2024-12-31T23:59:59.999Z",
}),
status_ids: z
.array(Id)
.nullable()
.openapi({
description:
"IDs of statuses that have been attached to this report for additional context.",
example: ["1abf027c-af03-46ff-8d17-9ee799a17ca7"],
}),
rule_ids: z.array(z.string()).nullable().openapi({
description:
"IDs of the rules that have been cited as a violation by this report.",
example: null,
}),
target_account: Account.openapi({
description: "The account that was reported.",
}),
})
.openapi({
description:
"Reports filed against users and/or statuses, to be taken action on by moderators.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Report",
},
});

23
classes/schemas/rule.ts Normal file
View file

@ -0,0 +1,23 @@
import { z } from "@hono/zod-openapi";
export const Rule = z
.object({
id: z.string().openapi({
description: "The identifier for the rule.",
example: "1",
}),
text: z.string().openapi({
description: "The rule to be followed.",
example: "Do not spam pictures of skibidi toilet.",
}),
hint: z.string().optional().openapi({
description: "Longer-form description of the rule.",
example: "Please, we beg you.",
}),
})
.openapi({
description: "Represents a rule that server users should follow.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Rule",
},
});

23
classes/schemas/search.ts Normal file
View file

@ -0,0 +1,23 @@
import { z } from "@hono/zod-openapi";
import { Account } from "./account.ts";
import { Status } from "./status.ts";
import { Tag } from "./tag.ts";
export const Search = z
.object({
accounts: z.array(Account).openapi({
description: "Accounts which match the given query",
}),
statuses: z.array(Status).openapi({
description: "Statuses which match the given query",
}),
hashtags: z.array(Tag).openapi({
description: "Hashtags which match the given query",
}),
})
.openapi({
description: "Represents the results of a search.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Search",
},
});

View file

@ -1,13 +1,14 @@
import { z } from "@hono/zod-openapi"; import { z } from "@hono/zod-openapi";
import type { Status as ApiNote } from "@versia/client/types"; import type { Status as ApiNote } from "@versia/client/types";
import { Media } from "@versia/kit/db"; import { Media } from "@versia/kit/db";
import ISO6391 from "iso-639-1";
import { zBoolean } from "~/packages/config-manager/config.type.ts"; import { zBoolean } from "~/packages/config-manager/config.type.ts";
import { Account } from "./account.ts"; import { Account } from "./account.ts";
import { PreviewCard } from "./card.ts"; import { PreviewCard } from "./card.ts";
import { Id } from "./common.ts"; import { Id, iso631 } from "./common.ts";
import { CustomEmoji } from "./emoji.ts"; import { CustomEmoji } from "./emoji.ts";
import { FilterResult } from "./filters.ts"; import { FilterResult } from "./filters.ts";
import { Poll } from "./poll.ts";
import { Tag } from "./tag.ts";
import { NoteReaction } from "./versia.ts"; import { NoteReaction } from "./versia.ts";
export const Mention = z export const Mention = z
@ -48,155 +49,6 @@ export const Mention = z
}, },
}); });
export const Tag = z
.object({
name: z
.string()
.min(1)
.max(128)
.openapi({
description: "The value of the hashtag after the # sign.",
example: "versia",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Tag-name",
},
}),
url: z
.string()
.url()
.openapi({
description: "A link to the hashtag on the instance.",
example: "https://beta.versia.social/tags/versia",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Tag-url",
},
}),
})
.openapi({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Tag",
},
});
export const PollOption = z
.object({
title: z.string().openapi({
description: "The text value of the poll option.",
example: "yes",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#Option-title",
},
}),
votes_count: z
.number()
.int()
.nonnegative()
.nullable()
.openapi({
description:
"The total number of received votes for this option.",
example: 6,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#Option-votes_count",
},
}),
})
.openapi({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#Option",
},
});
export const Poll = z.object({
id: Id.openapi({
description: "ID of the poll in the database.",
example: "d87d230f-e401-4282-80c7-2044ab989662",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#id",
},
}),
expires_at: z
.string()
.datetime()
.nullable()
.openapi({
description: "When the poll ends.",
example: "2025-01-07T14:11:00.000Z",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#expires_at",
},
}),
expired: zBoolean.openapi({
description: "Is the poll currently expired?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#expired",
},
}),
multiple: zBoolean.openapi({
description: "Does the poll allow multiple-choice answers?",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#multiple",
},
}),
votes_count: z
.number()
.int()
.nonnegative()
.openapi({
description: "How many votes have been received.",
example: 6,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#votes_count",
},
}),
voters_count: z
.number()
.int()
.nonnegative()
.nullable()
.openapi({
description:
"How many unique accounts have voted on a multiple-choice poll.",
example: 3,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#voters_count",
},
}),
options: z.array(PollOption).openapi({
description: "Possible answers for the poll.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#options",
},
}),
emojis: z.array(CustomEmoji).openapi({
description: "Custom emoji to be used for rendering poll options.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#emojis",
},
}),
voted: zBoolean.optional().openapi({
description:
"When called with a user token, has the authorized user voted?",
example: true,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#voted",
},
}),
own_votes: z
.array(z.number().int())
.optional()
.openapi({
description:
"When called with a user token, which options has the authorized user chosen? Contains an array of index values for options.",
example: [0],
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Poll/#own_votes",
},
}),
});
export const Status = z.object({ export const Status = z.object({
id: Id.openapi({ id: Id.openapi({
description: "ID of the status in the database.", description: "ID of the status in the database.",
@ -429,16 +281,13 @@ export const Status = z.object({
url: "https://docs.joinmastodon.org/entities/Status/#application", url: "https://docs.joinmastodon.org/entities/Status/#application",
}, },
}), }),
language: z language: iso631.nullable().openapi({
.enum(ISO6391.getAllCodes() as [string, ...string[]]) description: "Primary language of this status.",
.nullable() example: "en",
.openapi({ externalDocs: {
description: "Primary language of this status.", url: "https://docs.joinmastodon.org/entities/Status/#language",
example: "en", },
externalDocs: { }),
url: "https://docs.joinmastodon.org/entities/Status/#language",
},
}),
text: z text: z
.string() .string()
.nullable() .nullable()

31
classes/schemas/tag.ts Normal file
View file

@ -0,0 +1,31 @@
import { z } from "@hono/zod-openapi";
export const Tag = z
.object({
name: z
.string()
.min(1)
.max(128)
.openapi({
description: "The value of the hashtag after the # sign.",
example: "versia",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Tag-name",
},
}),
url: z
.string()
.url()
.openapi({
description: "A link to the hashtag on the instance.",
example: "https://beta.versia.social/tags/versia",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Tag-url",
},
}),
})
.openapi({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Tag",
},
});

29
classes/schemas/token.ts Normal file
View file

@ -0,0 +1,29 @@
import { z } from "@hono/zod-openapi";
export const Token = z
.object({
access_token: z.string().openapi({
description: "An OAuth token to be used for authorization.",
example: "ZA-Yj3aBD8U8Cm7lKUp-lm9O9BmDgdhHzDeqsY8tlL0",
}),
token_type: z.string().openapi({
description: "The OAuth token type. Versia uses Bearer tokens.",
example: "Bearer",
}),
scope: z.string().openapi({
description:
"The OAuth scopes granted by this token, space-separated.",
example: "read write follow push",
}),
created_at: z.number().nonnegative().openapi({
description: "When the token was generated. UNIX timestamp.",
example: 1573979017,
}),
})
.openapi({
description:
"Represents an OAuth token used for authenticating with the API and performing actions.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Token",
},
});

View file

@ -1,7 +1,7 @@
import { z } from "@hono/zod-openapi"; import { z } from "@hono/zod-openapi";
import { RolePermission } from "@versia/client/types"; import { RolePermission } from "@versia/client/types";
import { Id } from "./common.ts";
import { config } from "~/packages/config-manager/index.ts"; import { config } from "~/packages/config-manager/index.ts";
import { Id } from "./common.ts";
/* Versia Server API extension */ /* Versia Server API extension */
export const Role = z export const Role = z
@ -75,3 +75,33 @@ export const NoteReaction = z
.openapi({ .openapi({
description: "Information about a reaction to a note.", description: "Information about a reaction to a note.",
}); });
/* Versia Server API extension */
export const SSOConfig = z.object({
forced: z.boolean().openapi({
description:
"If this is enabled, normal identifier/password login is disabled and login must be done through SSO.",
example: false,
}),
providers: z
.array(
z.object({
id: z.string().min(1).openapi({
description: "The ID of the provider.",
example: "google",
}),
name: z.string().min(1).openapi({
description: "Human-readable provider name.",
example: "Google",
}),
icon: z.string().url().optional().openapi({
description: "URL to the provider icon.",
example: "https://cdn.versia.social/google-icon.png",
}),
}),
)
.openapi({
description:
"An array of external OpenID Connect providers that users can link their accounts to.",
}),
});