From 264e2fe8ac539d8b6032715f98138e0594a88e49 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Tue, 11 Feb 2025 18:22:39 +0100 Subject: [PATCH] feat(api): :label: Port Role and CustomEmoji OpenAPI schemas --- api/api/v1/accounts/:id/roles/index.ts | 3 +- api/api/v1/custom_emojis/index.ts | 3 +- api/api/v1/emojis/:id/index.ts | 5 +- api/api/v1/emojis/index.ts | 3 +- api/api/v1/roles/:id/index.ts | 5 +- api/api/v1/roles/index.ts | 7 +- bun.lock | 162 ++++++++++++------------- classes/database/emoji.ts | 20 +-- classes/database/reaction.ts | 13 +- classes/database/role.ts | 15 +-- classes/database/user.ts | 28 ++--- classes/schemas/account.ts | 14 ++- classes/schemas/context.ts | 25 ++++ classes/schemas/emoji.ts | 82 +++++++++++++ classes/schemas/status.ts | 20 +-- classes/schemas/versia.ts | 77 ++++++++++++ drizzle/schema.ts | 14 +-- 17 files changed, 319 insertions(+), 177 deletions(-) create mode 100644 classes/schemas/context.ts create mode 100644 classes/schemas/emoji.ts create mode 100644 classes/schemas/versia.ts diff --git a/api/api/v1/accounts/:id/roles/index.ts b/api/api/v1/accounts/:id/roles/index.ts index 4adcba97..7e24d2af 100644 --- a/api/api/v1/accounts/:id/roles/index.ts +++ b/api/api/v1/accounts/:id/roles/index.ts @@ -1,6 +1,7 @@ import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute, z } from "@hono/zod-openapi"; import { Role } from "@versia/kit/db"; +import { Role as RoleSchema } from "~/classes/schemas/versia.ts"; const route = createRoute({ method: "get", @@ -22,7 +23,7 @@ const route = createRoute({ description: "List of roles", content: { "application/json": { - schema: z.array(Role.schema), + schema: z.array(RoleSchema), }, }, }, diff --git a/api/api/v1/custom_emojis/index.ts b/api/api/v1/custom_emojis/index.ts index eb49c728..fd754c36 100644 --- a/api/api/v1/custom_emojis/index.ts +++ b/api/api/v1/custom_emojis/index.ts @@ -3,6 +3,7 @@ import { createRoute, z } from "@hono/zod-openapi"; import { Emoji } from "@versia/kit/db"; import { Emojis, RolePermissions } from "@versia/kit/tables"; import { and, eq, isNull, or } from "drizzle-orm"; +import { CustomEmoji } from "~/classes/schemas/emoji"; const route = createRoute({ method: "get", @@ -20,7 +21,7 @@ const route = createRoute({ description: "Emojis", content: { "application/json": { - schema: z.array(Emoji.schema), + schema: z.array(CustomEmoji), }, }, }, diff --git a/api/api/v1/emojis/:id/index.ts b/api/api/v1/emojis/:id/index.ts index 7f69fb12..f40c6a21 100644 --- a/api/api/v1/emojis/:id/index.ts +++ b/api/api/v1/emojis/:id/index.ts @@ -4,6 +4,7 @@ import { createRoute, z } from "@hono/zod-openapi"; import { Emoji } from "@versia/kit/db"; import { RolePermissions } from "@versia/kit/tables"; import { ApiError } from "~/classes/errors/api-error"; +import { CustomEmoji } from "~/classes/schemas/emoji"; import { config } from "~/packages/config-manager"; import { ErrorSchema } from "~/types/api"; @@ -69,7 +70,7 @@ const routeGet = createRoute({ description: "Emoji", content: { "application/json": { - schema: Emoji.schema, + schema: CustomEmoji, }, }, }, @@ -120,7 +121,7 @@ const routePatch = createRoute({ description: "Emoji modified", content: { "application/json": { - schema: Emoji.schema, + schema: CustomEmoji, }, }, }, diff --git a/api/api/v1/emojis/index.ts b/api/api/v1/emojis/index.ts index 75b60d80..fe1bddd7 100644 --- a/api/api/v1/emojis/index.ts +++ b/api/api/v1/emojis/index.ts @@ -5,6 +5,7 @@ import { Emoji, Media } from "@versia/kit/db"; import { Emojis, RolePermissions } from "@versia/kit/tables"; import { and, eq, isNull, or } from "drizzle-orm"; import { ApiError } from "~/classes/errors/api-error"; +import { CustomEmoji } from "~/classes/schemas/emoji"; import { config } from "~/packages/config-manager"; import { ErrorSchema } from "~/types/api"; @@ -82,7 +83,7 @@ const route = createRoute({ description: "Uploaded emoji", content: { "application/json": { - schema: Emoji.schema, + schema: CustomEmoji, }, }, }, diff --git a/api/api/v1/roles/:id/index.ts b/api/api/v1/roles/:id/index.ts index 521cc385..9196f091 100644 --- a/api/api/v1/roles/:id/index.ts +++ b/api/api/v1/roles/:id/index.ts @@ -3,6 +3,7 @@ import { createRoute, z } from "@hono/zod-openapi"; import { Role } from "@versia/kit/db"; import { RolePermissions } from "@versia/kit/tables"; import { ApiError } from "~/classes/errors/api-error"; +import { Role as RoleSchema } from "~/classes/schemas/versia.ts"; import { ErrorSchema } from "~/types/api"; const routeGet = createRoute({ @@ -24,7 +25,7 @@ const routeGet = createRoute({ description: "Role", content: { "application/json": { - schema: Role.schema, + schema: RoleSchema, }, }, }, @@ -57,7 +58,7 @@ const routePatch = createRoute({ body: { content: { "application/json": { - schema: Role.schema.partial(), + schema: RoleSchema.omit({ id: true }).partial(), }, }, }, diff --git a/api/api/v1/roles/index.ts b/api/api/v1/roles/index.ts index ede0e6db..adb6a839 100644 --- a/api/api/v1/roles/index.ts +++ b/api/api/v1/roles/index.ts @@ -2,6 +2,7 @@ import { apiRoute, auth } from "@/api"; import { createRoute, z } from "@hono/zod-openapi"; import { Role } from "@versia/kit/db"; import { ApiError } from "~/classes/errors/api-error"; +import { Role as RoleSchema } from "~/classes/schemas/versia.ts"; import { RolePermissions } from "~/drizzle/schema"; import { ErrorSchema } from "~/types/api"; @@ -19,7 +20,7 @@ const routeGet = createRoute({ description: "List of all roles", content: { "application/json": { - schema: z.array(Role.schema), + schema: z.array(RoleSchema), }, }, }, @@ -40,7 +41,7 @@ const routePost = createRoute({ body: { content: { "application/json": { - schema: Role.schema.omit({ id: true }), + schema: RoleSchema.omit({ id: true }), }, }, }, @@ -50,7 +51,7 @@ const routePost = createRoute({ description: "Role created", content: { "application/json": { - schema: Role.schema, + schema: RoleSchema, }, }, }, diff --git a/bun.lock b/bun.lock index 2d3ae81f..a387c94d 100644 --- a/bun.lock +++ b/bun.lock @@ -4,90 +4,90 @@ "": { "name": "versia-server", "dependencies": { - "@bull-board/api": "latest", - "@bull-board/hono": "latest", - "@hackmd/markdown-it-task-lists": "latest", - "@hono/prometheus": "latest", - "@hono/swagger-ui": "latest", - "@hono/zod-openapi": "latest", - "@hono/zod-validator": "latest", - "@inquirer/confirm": "latest", - "@inquirer/input": "latest", - "@json2csv/plainjs": "latest", - "@logtape/logtape": "npm:@jsr/logtape__logtape@latest", - "@oclif/core": "latest", - "@sentry/bun": "latest", - "@tufjs/canonical-json": "latest", - "@versia/client": "latest", - "@versia/federation": "latest", + "@bull-board/api": "^6.7.4", + "@bull-board/hono": "^6.7.4", + "@hackmd/markdown-it-task-lists": "^2.1.4", + "@hono/prometheus": "^1.0.1", + "@hono/swagger-ui": "^0.5.0", + "@hono/zod-openapi": "0.18.3", + "@hono/zod-validator": "^0.4.2", + "@inquirer/confirm": "^5.1.4", + "@inquirer/input": "^4.1.4", + "@json2csv/plainjs": "^7.0.6", + "@logtape/logtape": "npm:@jsr/logtape__logtape@0.9.0-dev.114+327c9473", + "@oclif/core": "^4.2.5", + "@sentry/bun": "^8.53.0", + "@tufjs/canonical-json": "^2.0.0", + "@versia/client": "^0.1.5", + "@versia/federation": "^0.1.4", "@versia/kit": "workspace:*", - "altcha-lib": "latest", - "blurhash": "latest", - "bullmq": "latest", - "c12": "latest", - "chalk": "latest", - "cli-progress": "latest", - "cli-table": "latest", - "confbox": "latest", - "drizzle-orm": "latest", - "extract-zip": "latest", - "hono": "latest", - "html-to-text": "latest", - "ioredis": "latest", - "ip-matching": "latest", - "iso-639-1": "latest", - "jose": "latest", - "linkify-html": "latest", - "linkify-string": "latest", - "linkifyjs": "latest", - "magic-regexp": "latest", - "markdown-it": "latest", - "markdown-it-anchor": "latest", - "markdown-it-container": "latest", - "markdown-it-toc-done-right": "latest", - "mime-types": "latest", - "mitata": "latest", - "oauth4webapi": "latest", - "ora": "latest", - "pg": "latest", - "prom-client": "latest", - "qs": "latest", - "sharp": "latest", - "sonic-channel": "latest", - "string-comparison": "latest", - "stringify-entities": "latest", - "strip-ansi": "latest", - "table": "latest", - "unzipit": "latest", - "uqr": "latest", - "web-push": "latest", - "xss": "latest", - "zod": "latest", - "zod-validation-error": "latest", + "altcha-lib": "^1.2.0", + "blurhash": "^2.0.5", + "bullmq": "^5.39.1", + "c12": "^2.0.1", + "chalk": "^5.4.1", + "cli-progress": "^3.12.0", + "cli-table": "^0.3.11", + "confbox": "^0.1.8", + "drizzle-orm": "^0.39.1", + "extract-zip": "^2.0.1", + "hono": "^4.6.20", + "html-to-text": "^9.0.5", + "ioredis": "^5.4.2", + "ip-matching": "^2.1.2", + "iso-639-1": "^3.1.4", + "jose": "^5.9.6", + "linkify-html": "^4.2.0", + "linkify-string": "^4.2.0", + "linkifyjs": "^4.2.0", + "magic-regexp": "^0.8.0", + "markdown-it": "^14.1.0", + "markdown-it-anchor": "^9.2.0", + "markdown-it-container": "^4.0.0", + "markdown-it-toc-done-right": "^4.2.0", + "mime-types": "^2.1.35", + "mitata": "^1.0.33", + "oauth4webapi": "^3.1.4", + "ora": "^8.1.1", + "pg": "^8.13.1", + "prom-client": "^15.1.3", + "qs": "^6.14.0", + "sharp": "^0.33.5", + "sonic-channel": "^1.3.1", + "string-comparison": "^1.3.0", + "stringify-entities": "^4.0.4", + "strip-ansi": "^7.1.0", + "table": "^6.9.0", + "unzipit": "^1.4.3", + "uqr": "^0.1.2", + "web-push": "^3.6.7", + "xss": "^1.0.15", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0", }, "devDependencies": { - "@biomejs/biome": "latest", - "@types/bun": "latest", - "@types/cli-progress": "latest", - "@types/cli-table": "latest", - "@types/html-to-text": "latest", - "@types/jsonld": "latest", - "@types/markdown-it-container": "latest", - "@types/mime-types": "latest", - "@types/pg": "latest", - "@types/qs": "latest", - "@types/web-push": "latest", - "drizzle-kit": "latest", - "markdown-it-image-figures": "latest", - "markdown-it-mathjax3": "latest", - "oclif": "latest", - "ts-prune": "latest", - "typescript": "latest", - "vitepress": "latest", - "vitepress-plugin-tabs": "latest", - "vitepress-sidebar": "latest", - "vue": "latest", - "zod-to-json-schema": "latest", + "@biomejs/biome": "^1.9.4", + "@types/bun": "^1.2.2", + "@types/cli-progress": "^3.11.6", + "@types/cli-table": "^0.3.4", + "@types/html-to-text": "^9.0.4", + "@types/jsonld": "^1.5.15", + "@types/markdown-it-container": "^2.0.10", + "@types/mime-types": "^2.1.4", + "@types/pg": "^8.11.11", + "@types/qs": "^6.9.18", + "@types/web-push": "^3.6.4", + "drizzle-kit": "^0.30.4", + "markdown-it-image-figures": "^2.1.1", + "markdown-it-mathjax3": "^4.3.2", + "oclif": "^4.17.21", + "ts-prune": "^0.10.3", + "typescript": "^5.7.3", + "vitepress": "^1.6.3", + "vitepress-plugin-tabs": "^0.5.0", + "vitepress-sidebar": "^1.30.2", + "vue": "^3.5.13", + "zod-to-json-schema": "^3.24.1", }, "peerDependencies": { "typescript": "^5.7.2", diff --git a/classes/database/emoji.ts b/classes/database/emoji.ts index 4b15a58a..41fe92d1 100644 --- a/classes/database/emoji.ts +++ b/classes/database/emoji.ts @@ -1,7 +1,6 @@ import { emojiValidatorWithColons, emojiValidatorWithIdentifiers } from "@/api"; import { proxyUrl } from "@/response"; -import { z } from "@hono/zod-openapi"; -import type { Emoji as APIEmoji } from "@versia/client/types"; +import type { z } from "@hono/zod-openapi"; import type { CustomEmojiExtension } from "@versia/federation/types"; import { type Instance, Media, db } from "@versia/kit/db"; import { Emojis, type Instances, type Medias } from "@versia/kit/tables"; @@ -15,6 +14,7 @@ import { inArray, isNull, } from "drizzle-orm"; +import type { CustomEmoji } from "../schemas/emoji.ts"; import { BaseInterface } from "./base.ts"; type EmojiType = InferSelectModel & { @@ -23,16 +23,6 @@ type EmojiType = InferSelectModel & { }; export class Emoji extends BaseInterface { - public static schema = z.object({ - id: z.string(), - shortcode: z.string(), - url: z.string(), - visible_in_picker: z.boolean(), - category: z.string().optional(), - static_url: z.string(), - global: z.boolean(), - }); - public static $type: EmojiType; public media: Media; @@ -184,18 +174,18 @@ export class Emoji extends BaseInterface { ); } - public toApi(): APIEmoji { + public toApi(): z.infer { return { id: this.id, shortcode: this.data.shortcode, static_url: proxyUrl(this.media.getUrl()).toString(), url: proxyUrl(this.media.getUrl()).toString(), visible_in_picker: this.data.visibleInPicker, - category: this.data.category ?? undefined, + category: this.data.category, global: this.data.ownerId === null, description: this.media.data.content[this.media.getPreferredMimeType()] - .description ?? undefined, + .description ?? null, }; } diff --git a/classes/database/reaction.ts b/classes/database/reaction.ts index 36992594..66226651 100644 --- a/classes/database/reaction.ts +++ b/classes/database/reaction.ts @@ -12,6 +12,7 @@ import { inArray, } from "drizzle-orm"; import { config } from "~/packages/config-manager/index.ts"; +import { CustomEmoji } from "../schemas/emoji.ts"; import { BaseInterface } from "./base.ts"; type ReactionType = InferSelectModel & { @@ -30,7 +31,7 @@ export class Reaction extends BaseInterface { public static schema: z.ZodType = z.object({ id: z.string().uuid(), author_id: z.string().uuid(), - emoji: z.lazy(() => Emoji.schema), + emoji: CustomEmoji, }); public static $type: ReactionType; @@ -169,16 +170,6 @@ export class Reaction extends BaseInterface { return this.data.id; } - public toApi(): APIReaction { - return { - id: this.data.id, - author_id: this.data.authorId, - emoji: this.hasCustomEmoji() - ? new Emoji(this.data.emoji as typeof Emoji.$type).toApi() - : this.data.emojiText || "", - }; - } - public getUri(baseUrl: URL): URL { return this.data.uri ? new URL(this.data.uri) diff --git a/classes/database/role.ts b/classes/database/role.ts index 46fbb308..f29d6cea 100644 --- a/classes/database/role.ts +++ b/classes/database/role.ts @@ -1,7 +1,6 @@ import { proxyUrl } from "@/response"; -import { z } from "@hono/zod-openapi"; -import { - type VersiaRole as APIRole, +import type { + VersiaRole as APIRole, RolePermission, } from "@versia/client/types"; import { db } from "@versia/kit/db"; @@ -21,16 +20,6 @@ import { BaseInterface } from "./base.ts"; type RoleType = InferSelectModel; export class Role extends BaseInterface { - public static schema = z.object({ - id: z.string().uuid(), - name: z.string().min(1).max(128), - permissions: z.array(z.nativeEnum(RolePermission)).default([]), - priority: z.number().int().default(0), - description: z.string().min(0).max(1024).optional(), - visible: z.boolean().default(true), - icon: z.string().url().optional(), - }); - public static $type: RoleType; public async reload(): Promise { diff --git a/classes/database/user.ts b/classes/database/user.ts index e52d7bca..36709e6a 100644 --- a/classes/database/user.ts +++ b/classes/database/user.ts @@ -1,5 +1,5 @@ import { idValidator } from "@/api"; -import { getBestContentType, urlToContentFormat } from "@/content_types"; +import { getBestContentType } from "@/content_types"; import { randomString } from "@/math"; import { proxyUrl } from "@/response"; import { sentry } from "@/sentry"; @@ -52,7 +52,7 @@ import { type Config, config } from "~/packages/config-manager"; import type { KnownEntity } from "~/types/api.ts"; import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts"; import { PushJobType, pushQueue } from "../queues/push.ts"; -import type { Account } from "../schemas/account.ts"; +import type { Account, Source } from "../schemas/account.ts"; import { BaseInterface } from "./base.ts"; import { Emoji } from "./emoji.ts"; import { Instance } from "./instance.ts"; @@ -686,12 +686,12 @@ export class User extends BaseInterface { note: getBestContentType(user.bio).content, publicKey: user.public_key.key, source: { - language: null, + language: "en", note: "", privacy: "public", sensitive: false, fields: [], - }, + } as z.infer, }; const userEmojis = @@ -888,12 +888,12 @@ export class User extends BaseInterface { privateKey: keys.private_key, updatedAt: new Date().toISOString(), source: { - language: null, + language: "en", note: "", privacy: "public", sensitive: false, fields: [], - }, + } as z.infer, }) .returning() )[0]; @@ -1107,6 +1107,7 @@ export class User extends BaseInterface { public toApi(isOwnAccount = false): z.infer { const user = this.data; + return { id: user.id, username: user.username, @@ -1177,6 +1178,8 @@ export class User extends BaseInterface { ) .map((r) => r.toApi()), group: false, + // TODO + last_status_at: null, }; } @@ -1235,17 +1238,8 @@ export class User extends BaseInterface { indexable: false, username: user.username, manually_approves_followers: this.data.isLocked, - avatar: - urlToContentFormat( - this.getAvatarUrl(config), - this.data.source.avatar?.content_type, - ) ?? undefined, - header: this.getHeaderUrl(config) - ? (urlToContentFormat( - this.getHeaderUrl(config) as URL, - this.data.source.header?.content_type, - ) ?? undefined) - : undefined, + avatar: this.avatar?.toVersia(), + header: this.header?.toVersia(), display_name: user.displayName, fields: user.fields, public_key: { diff --git a/classes/schemas/account.ts b/classes/schemas/account.ts index dbcb418b..fed37940 100644 --- a/classes/schemas/account.ts +++ b/classes/schemas/account.ts @@ -3,8 +3,8 @@ import type { Account as ApiAccount } from "@versia/client/types"; import ISO6391 from "iso-639-1"; import { config } from "~/packages/config-manager"; import { zBoolean } from "~/packages/config-manager/config.type"; -import { Emoji } from "../database/emoji.ts"; -import { Role } from "../database/role.ts"; +import { CustomEmoji } from "./emoji.ts"; +import { Role } from "./versia.ts"; export const Field = z.object({ name: z @@ -117,7 +117,7 @@ export const Account = z.object({ .string() .uuid() .openapi({ - description: "The account id.", + description: "The account ID in the database.", example: "9e84842b-4db6-4a9b-969d-46ab408278da", externalDocs: { url: "https://docs.joinmastodon.org/entities/Account/#id", @@ -260,7 +260,7 @@ export const Account = z.object({ url: "https://docs.joinmastodon.org/entities/Account/#fields", }, }), - emojis: z.array(Emoji.schema).openapi({ + emojis: z.array(CustomEmoji).openapi({ description: "Custom emoji entities to be used when rendering the profile.", externalDocs: { @@ -384,6 +384,7 @@ export const Account = z.object({ 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.", @@ -396,7 +397,10 @@ export const Account = z.object({ name: z.string(), }) .optional(), - roles: z.array(Role.schema), + /* 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", diff --git a/classes/schemas/context.ts b/classes/schemas/context.ts new file mode 100644 index 00000000..e29a4b85 --- /dev/null +++ b/classes/schemas/context.ts @@ -0,0 +1,25 @@ +import { z } from "@hono/zod-openapi"; +import { Status } from "./status.ts"; + +export const Context = z + .object({ + ancestors: z.array(Status).openapi({ + description: "Parents in the thread.", + externalDocs: { + url: "https://docs.joinmastodon.org/entities/Context/#ancestors", + }, + }), + descendants: z.array(Status).openapi({ + description: "Children in the thread.", + externalDocs: { + url: "https://docs.joinmastodon.org/entities/Context/#descendants", + }, + }), + }) + .openapi({ + description: + "Represents the tree around a given status. Used for reconstructing threads of statuses.", + externalDocs: { + url: "https://docs.joinmastodon.org/entities/Context/#context", + }, + }); diff --git a/classes/schemas/emoji.ts b/classes/schemas/emoji.ts new file mode 100644 index 00000000..0d71c13b --- /dev/null +++ b/classes/schemas/emoji.ts @@ -0,0 +1,82 @@ +import { z } from "@hono/zod-openapi"; +import { zBoolean } from "~/packages/config-manager/config.type"; +import { Id } from "./common.ts"; + +export const CustomEmoji = z + .object({ + /* Versia Server API extension */ + id: Id.openapi({ + description: "ID of the custom emoji in the database.", + example: "af9ccd29-c689-477f-aa27-d7d95fd8fb05", + }), + shortcode: z.string().openapi({ + description: "The name of the custom emoji.", + example: "blobaww", + externalDocs: { + url: "https://docs.joinmastodon.org/entities/CustomEmoji/#shortcode", + }, + }), + url: z + .string() + .url() + .openapi({ + description: "A link to the custom emoji.", + example: + "https://cdn.versia.social/emojis/images/000/011/739/original/blobaww.png", + externalDocs: { + url: "https://docs.joinmastodon.org/entities/CustomEmoji/#url", + }, + }), + static_url: z + .string() + .url() + .openapi({ + description: "A link to a static copy of the custom emoji.", + example: + "https://cdn.versia.social/emojis/images/000/011/739/static/blobaww.png", + externalDocs: { + url: "https://docs.joinmastodon.org/entities/CustomEmoji/#static_url", + }, + }), + visible_in_picker: z.boolean().openapi({ + description: + "Whether this Emoji should be visible in the picker or unlisted.", + example: true, + externalDocs: { + url: "https://docs.joinmastodon.org/entities/CustomEmoji/#visible_in_picker", + }, + }), + category: z + .string() + .nullable() + .openapi({ + description: "Used for sorting custom emoji in the picker.", + example: "Blobs", + externalDocs: { + url: "https://docs.joinmastodon.org/entities/CustomEmoji/#category", + }, + }), + /* Versia Server API extension */ + global: zBoolean.openapi({ + description: "Whether this emoji is visible to all users.", + example: false, + }), + /* Versia Server API extension */ + description: z + .string() + .nullable() + .openapi({ + description: + "Emoji description for users using screen readers.", + example: "A cute blob.", + externalDocs: { + url: "https://docs.joinmastodon.org/entities/CustomEmoji/#description", + }, + }), + }) + .openapi({ + description: "Represents a custom emoji.", + externalDocs: { + url: "https://docs.joinmastodon.org/entities/CustomEmoji", + }, + }); diff --git a/classes/schemas/status.ts b/classes/schemas/status.ts index 9f2d205a..9a7bf0a1 100644 --- a/classes/schemas/status.ts +++ b/classes/schemas/status.ts @@ -1,12 +1,14 @@ import { z } from "@hono/zod-openapi"; import type { Status as ApiNote } from "@versia/client/types"; -import { Emoji, 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 { Account } from "./account.ts"; import { PreviewCard } from "./card.ts"; import { Id } from "./common.ts"; +import { CustomEmoji } from "./emoji.ts"; import { FilterResult } from "./filters.ts"; +import { NoteReaction } from "./versia.ts"; export const Mention = z .object({ @@ -168,7 +170,7 @@ export const Poll = z.object({ url: "https://docs.joinmastodon.org/entities/Poll/#options", }, }), - emojis: z.array(Emoji.schema).openapi({ + emojis: z.array(CustomEmoji).openapi({ description: "Custom emoji to be used for rendering poll options.", externalDocs: { url: "https://docs.joinmastodon.org/entities/Poll/#emojis", @@ -284,7 +286,7 @@ export const Status = z.object({ url: "https://docs.joinmastodon.org/entities/Status/#edited_at", }, }), - emojis: z.array(Emoji.schema).openapi({ + emojis: z.array(CustomEmoji).openapi({ description: "Custom emoji to be used when rendering status content.", externalDocs: { url: "https://docs.joinmastodon.org/entities/Status/#emojis", @@ -455,17 +457,7 @@ export const Status = z.object({ 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(), - }), - ), + reactions: z.array(NoteReaction).openapi({}), quote: z .lazy((): z.ZodType => Status as z.ZodType) .nullable(), diff --git a/classes/schemas/versia.ts b/classes/schemas/versia.ts new file mode 100644 index 00000000..fd44db0c --- /dev/null +++ b/classes/schemas/versia.ts @@ -0,0 +1,77 @@ +import { z } from "@hono/zod-openapi"; +import { RolePermission } from "@versia/client/types"; +import { Id } from "./common.ts"; +import { config } from "~/packages/config-manager/index.ts"; + +/* Versia Server API extension */ +export const Role = z + .object({ + id: Id.openapi({}).openapi({ + description: "The role ID in the database.", + example: "b4a7e0f0-8f6a-479b-910b-9265c070d5bd", + }), + name: z.string().min(1).max(128).trim().openapi({ + description: "The name of the role.", + example: "Moderator", + }), + permissions: z + .array(z.nativeEnum(RolePermission)) + .transform( + // Deduplicate permissions + (permissions) => Array.from(new Set(permissions)), + ) + .default([]) + .openapi({ + description: "The permissions granted to the role.", + example: [ + RolePermission.ManageEmojis, + RolePermission.ManageAccounts, + ], + }), + priority: z.number().int().default(0).openapi({ + description: + "Role priority. Higher priority roles allow overriding lower priority roles.", + example: 100, + }), + description: z.string().min(0).max(1024).trim().optional().openapi({ + description: "Short role description.", + example: "Allows managing emojis and accounts.", + }), + visible: z.boolean().default(true).openapi({ + description: "Whether the role should be shown in the UI.", + }), + icon: z.string().url().optional().openapi({ + description: "URL to the role icon.", + example: "https://example.com/role-icon.png", + }), + }) + .openapi({ + description: + "Information about a role in the system, as well as its permissions.", + }); + +/* Versia Server API extension */ +export const NoteReaction = z + .object({ + name: z + .string() + .min(1) + .max(config.validation.max_emoji_shortcode_size) + .trim() + .openapi({ + description: "Custom Emoji shortcode or Unicode emoji.", + example: "blobfox_coffee", + }), + count: z.number().int().nonnegative().openapi({ + description: "Number of users who reacted with this emoji.", + example: 5, + }), + me: z.boolean().optional().openapi({ + description: + "Whether the current authenticated user reacted with this emoji.", + example: true, + }), + }) + .openapi({ + description: "Information about a reaction to a note.", + }); diff --git a/drizzle/schema.ts b/drizzle/schema.ts index aa3d47f7..8847905d 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -1,4 +1,4 @@ -import type { Source as ApiSource } from "@versia/client/types"; +import type { z } from "@hono/zod-openapi"; import type { ContentFormat, InstanceMetadata } from "@versia/federation/types"; import type { Challenge } from "altcha-lib/types"; import { relations, sql } from "drizzle-orm"; @@ -14,6 +14,7 @@ import { uniqueIndex, uuid, } from "drizzle-orm/pg-core"; +import type { Source } from "~/classes/schemas/account"; // biome-ignore lint/nursery/useExplicitType: Type is too complex const createdAt = () => @@ -553,16 +554,7 @@ export const Users = pgTable( inbox: string; outbox: string; }> | null>(), - source: jsonb("source").notNull().$type< - ApiSource & { - avatar?: { - content_type: string; - }; - header?: { - content_type: string; - }; - } - >(), + source: jsonb("source").notNull().$type>(), avatarId: uuid("avatarId").references(() => Medias.id, { onDelete: "set null", onUpdate: "cascade",