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

@ -1,5 +1,4 @@
import { z } from "@hono/zod-openapi";
import type { Application as APIApplication } from "@versia/client/types";
import type { z } from "@hono/zod-openapi";
import { Token, db } from "@versia/kit/db";
import { Applications } from "@versia/kit/tables";
import {
@ -10,19 +9,15 @@ import {
eq,
inArray,
} from "drizzle-orm";
import type {
Application as ApplicationSchema,
CredentialApplication,
} from "../schemas/application.ts";
import { BaseInterface } from "./base.ts";
type ApplicationType = InferSelectModel<typeof Applications>;
export class Application extends BaseInterface<typeof Applications> {
public static schema: z.ZodType<APIApplication> = z.object({
name: z.string(),
website: z.string().url().optional().nullable(),
vapid_key: z.string().optional().nullable(),
redirect_uris: z.string().optional(),
scopes: z.string().optional(),
});
public static $type: ApplicationType;
public async reload(): Promise<void> {
@ -144,11 +139,26 @@ export class Application extends BaseInterface<typeof Applications> {
return this.data.id;
}
public toApi(): APIApplication {
public toApi(): z.infer<typeof ApplicationSchema> {
return {
name: this.data.name,
website: this.data.website,
vapid_key: this.data.vapidKey,
scopes: this.data.scopes.split(" "),
redirect_uri: this.data.redirectUri,
redirect_uris: this.data.redirectUri.split("\n"),
};
}
public toApiCredential(): z.infer<typeof CredentialApplication> {
return {
name: this.data.name,
website: this.data.website,
client_id: this.data.clientId,
client_secret: this.data.secret,
client_secret_expires_at: "0",
scopes: this.data.scopes.split(" "),
redirect_uri: this.data.redirectUri,
redirect_uris: this.data.redirectUri.split("\n"),
};
}
}

View file

@ -3,7 +3,7 @@ import { localObjectUri } from "@/constants";
import { mergeAndDeduplicate } from "@/lib.ts";
import { sanitizedHtmlStrip } from "@/sanitization";
import { sentry } from "@/sentry";
import { z } from "@hono/zod-openapi";
import type { z } from "@hono/zod-openapi";
import { getLogger } from "@logtape/logtape";
import type {
Attachment as ApiAttachment,
@ -43,7 +43,7 @@ import {
} from "~/classes/functions/status";
import { config } from "~/packages/config-manager";
import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts";
import { Account } from "../schemas/account.ts";
import type { Status } from "../schemas/status.ts";
import { Application } from "./application.ts";
import { BaseInterface } from "./base.ts";
import { Emoji } from "./emoji.ts";
@ -81,96 +81,6 @@ export type NoteTypeWithoutRecursiveRelations = Omit<
* Gives helpers to fetch notes from database in a nice format
*/
export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
public static schema: z.ZodType<ApiStatus> = z.object({
id: z.string().uuid(),
uri: z.string().url(),
url: z.string().url(),
account: Account,
in_reply_to_id: z.string().uuid().nullable(),
in_reply_to_account_id: z.string().uuid().nullable(),
reblog: z.lazy(() => Note.schema).nullable(),
content: z.string(),
plain_content: z.string().nullable(),
created_at: z.string(),
edited_at: z.string().nullable(),
emojis: z.array(Emoji.schema),
replies_count: z.number().int().nonnegative(),
reblogs_count: z.number().int().nonnegative(),
favourites_count: z.number().int().nonnegative(),
reblogged: z.boolean().nullable(),
favourited: z.boolean().nullable(),
muted: z.boolean().nullable(),
sensitive: z.boolean(),
spoiler_text: z.string(),
visibility: z.enum(["public", "unlisted", "private", "direct"]),
media_attachments: z.array(Media.schema),
mentions: z.array(
z.object({
id: z.string().uuid(),
username: z.string(),
acct: z.string(),
url: z.string().url(),
}),
),
tags: z.array(z.object({ name: z.string(), url: z.string().url() })),
card: z
.object({
url: z.string().url(),
title: z.string(),
description: z.string(),
type: z.enum(["link", "photo", "video", "rich"]),
image: z.string().url().nullable(),
author_name: z.string().nullable(),
author_url: z.string().url().nullable(),
provider_name: z.string().nullable(),
provider_url: z.string().url().nullable(),
html: z.string().nullable(),
width: z.number().int().nonnegative().nullable(),
height: z.number().int().nonnegative().nullable(),
embed_url: z.string().url().nullable(),
blurhash: z.string().nullable(),
})
.nullable(),
poll: z
.object({
id: z.string().uuid(),
expires_at: z.string(),
expired: z.boolean(),
multiple: z.boolean(),
votes_count: z.number().int().nonnegative(),
voted: z.boolean(),
options: z.array(
z.object({
title: z.string(),
votes_count: z.number().int().nonnegative().nullable(),
}),
),
})
.nullable(),
application: z
.object({
name: z.string(),
website: z.string().url().nullable().optional(),
vapid_key: z.string().nullable().optional(),
})
.nullable(),
language: z.string().nullable(),
pinned: z.boolean().nullable(),
emoji_reactions: z.array(
z.object({
count: z.number().int().nonnegative(),
me: z.boolean(),
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(() => Note.schema).nullable(),
bookmarked: z.boolean(),
});
public static $type: NoteTypeWithRelations;
public save(): Promise<NoteTypeWithRelations> {
@ -861,7 +771,9 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
* @param userFetching - The user fetching the note (used to check if the note is favourite and such)
* @returns The note in the Mastodon API format
*/
public async toApi(userFetching?: User | null): Promise<ApiStatus> {
public async toApi(
userFetching?: User | null,
): Promise<z.infer<typeof Status>> {
const data = this.data;
// Convert mentions of local users from @username@host to @username
@ -893,7 +805,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
created_at: new Date(data.createdAt).toISOString(),
application: data.application
? new Application(data.application).toApi()
: null,
: undefined,
card: null,
content: replacedContent,
emojis: data.emojis.map((emoji) => new Emoji(emoji).toApi()),

View file

@ -16,6 +16,7 @@ import {
userRelations,
} from "../functions/user.ts";
import { Account } from "../schemas/account.ts";
import { Status } from "../schemas/status.ts";
import { BaseInterface } from "./base.ts";
export type NotificationType = InferSelectModel<typeof Notifications> & {
@ -31,7 +32,7 @@ export class Notification extends BaseInterface<
account: Account.nullable(),
created_at: z.string(),
id: z.string().uuid(),
status: z.lazy(() => Note.schema).optional(),
status: Status.optional(),
// TODO: Add reactions
type: z.enum([
"mention",

View file

@ -3,6 +3,7 @@ import { getBestContentType, urlToContentFormat } from "@/content_types";
import { randomString } from "@/math";
import { proxyUrl } from "@/response";
import { sentry } from "@/sentry";
import type { z } from "@hono/zod-openapi";
import { getLogger } from "@logtape/logtape";
import type { Mention as ApiMention } from "@versia/client/types";
import {
@ -45,7 +46,6 @@ import {
sql,
} from "drizzle-orm";
import { htmlToText } from "html-to-text";
import type { z } from "zod";
import { findManyUsers } from "~/classes/functions/user";
import { searchManager } from "~/classes/search/search-manager";
import { type Config, config } from "~/packages/config-manager";