feat(api): 🏷️ Port Status OpenAPI schemas from Mastodon API docs

This commit is contained in:
Jesse Wierzbinski 2025-02-05 22:49:07 +01:00
parent 2aeada4904
commit 7c622730dc
No known key found for this signature in database
26 changed files with 920 additions and 148 deletions

View file

@ -75,6 +75,7 @@ export const Source = z
follow_requests_count: z
.number()
.int()
.nonnegative()
.optional()
.openapi({
description: "The number of pending follow requests.",
@ -352,8 +353,8 @@ export const Account = z.object({
.nullable(),
statuses_count: z
.number()
.min(0)
.int()
.nonnegative()
.openapi({
description: "How many statuses are attached to this account.",
example: 42,
@ -363,8 +364,8 @@ export const Account = z.object({
}),
followers_count: z
.number()
.min(0)
.int()
.nonnegative()
.openapi({
description: "The reported followers of this profile.",
example: 6,
@ -374,8 +375,8 @@ export const Account = z.object({
}),
following_count: z
.number()
.min(0)
.int()
.nonnegative()
.openapi({
description: "The reported follows of this profile.",
example: 23,

View file

@ -0,0 +1,71 @@
import { z } from "@hono/zod-openapi";
export const Application = z.object({
name: z.string().openapi({
description: "The name of your application.",
example: "Test Application",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Application/#name",
},
}),
website: z
.string()
.nullable()
.openapi({
description: "The website associated with your application.",
example: "https://app.example",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Application/#website",
},
}),
scopes: z.array(z.string()).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().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({
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({
client_id: z.string().openapi({
description: "Client ID key, to be used for obtaining OAuth tokens",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_id",
},
}),
client_secret: z.string().openapi({
description: "Client secret key, to be used for obtaining OAuth tokens",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_secret",
},
}),
client_secret_expires_at: z.string().openapi({
description:
"When the client secret key will expire at, presently this always returns 0 indicating that OAuth Clients do not expire",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/CredentialApplication/#client_secret_expires_at",
},
}),
});

151
classes/schemas/card.ts Normal file
View file

@ -0,0 +1,151 @@
import { z } from "@hono/zod-openapi";
import { Account } from "./account.ts";
export const PreviewCardAuthor = z.object({
name: z.string().openapi({
description: "The original resource authors name.",
example: "The Doubleclicks",
externalDocs: {
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: {
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",
},
}),
});
export const PreviewCard = z.object({
url: z
.string()
.url()
.openapi({
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: {
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:
"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",
},
}),
});

View file

@ -0,0 +1,3 @@
import { z } from "@hono/zod-openapi";
export const Id = z.string().uuid();

129
classes/schemas/filters.ts Normal file
View file

@ -0,0 +1,129 @@
import { z } from "@hono/zod-openapi";
import { Id } from "./common.ts";
export const FilterStatus = z.object({
id: Id.openapi({
description: "The ID of the FilterStatus in the database.",
example: "3b19ed7c-0c4b-45e1-8c75-e21dfc8e86c3",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterStatus/#id",
},
}),
status_id: Id.openapi({
description: "The ID of the Status that will be filtered.",
example: "4f941ac8-295c-4c2d-9300-82c162ac8028",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterStatus/#status_id",
},
}),
});
export const FilterKeyword = z.object({
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:
"Should the filter consider word boundaries? See implementation guidelines for filters.",
example: false,
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterKeyword/#whole_word",
},
}),
});
export const Filter = z.object({
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",
},
}),
});
export const FilterResult = z.object({
filter: Filter.openapi({
description: "The filter that was matched.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/FilterResult/#filter",
},
}),
keyword_matches: z
.array(z.string())
.nullable()
.openapi({
description: "The keyword within the filter that was matched.",
example: ["badword"],
externalDocs: {
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",
},
}),
});

490
classes/schemas/status.ts Normal file
View file

@ -0,0 +1,490 @@
import { z } from "@hono/zod-openapi";
import type { Status as ApiNote } from "@versia/client/types";
import { Emoji, Media } from "@versia/kit/db";
import ISO6391 from "iso-639-1";
import { zBoolean } from "~/packages/config-manager/config.type.ts";
import { Account } from "./account.ts";
import { PreviewCard } from "./card.ts";
import { Id } from "./common.ts";
import { FilterResult } from "./filters.ts";
export const Mention = z
.object({
id: Account.shape.id.openapi({
description: "The account ID of the mentioned user.",
example: "b9dcb548-bd4d-42af-8b48-3693e6d298e6",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention-id",
},
}),
username: Account.shape.username.openapi({
description: "The username of the mentioned user.",
example: "lexi",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention-username",
},
}),
url: Account.shape.url.openapi({
description: "The location of the mentioned users profile.",
example: "https://beta.versia.social/@lexi",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention-url",
},
}),
acct: Account.shape.acct.openapi({
description:
"The webfinger acct: URI of the mentioned user. Equivalent to username for local users, or username@domain for remote users.",
example: "lexi@beta.versia.social",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention-acct",
},
}),
})
.openapi({
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#Mention",
},
});
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(Emoji.schema).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({
id: Id.openapi({
description: "ID of the status in the database.",
example: "2de861d3-a3dd-42ee-ba38-2c7d3f4af588",
externalDocs: {
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: {
url: "https://docs.joinmastodon.org/entities/Status/#uri",
},
}),
url: z
.string()
.url()
.nullable()
.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",
},
}),
reblog: z
.lazy((): z.ZodType<ApiNote> => Status as z.ZodType<ApiNote>)
.nullable()
.openapi({
description: "The status being reblogged.",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#reblog",
},
}),
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(Emoji.schema).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(Media.schema).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: {
url: "https://docs.joinmastodon.org/entities/Status/#application-name",
},
}),
website: z
.string()
.url()
.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: z
.enum(ISO6391.getAllCodes() as [string, ...string[]])
.nullable()
.openapi({
description: "Primary language of this status.",
example: "en",
externalDocs: {
url: "https://docs.joinmastodon.org/entities/Status/#language",
},
}),
text: z
.string()
.nullable()
.openapi({
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: {
url: "https://docs.joinmastodon.org/entities/Status/#text",
},
}),
pinned: zBoolean.optional().openapi({
description:
"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",
},
}),
emoji_reactions: z.array(
z.object({
count: z.number().int().nonnegative(),
me: zBoolean,
name: z.string(),
url: z.string().url().optional(),
static_url: z.string().url().optional(),
accounts: z.array(Account).optional(),
account_ids: z.array(z.string().uuid()).optional(),
}),
),
quote: z
.lazy((): z.ZodType<ApiNote> => Status as z.ZodType<ApiNote>)
.nullable(),
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:
"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",
},
}),
});