mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
refactor(api): 🏷️ Port more misc endpoints to use new schemas
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s
This commit is contained in:
parent
e3e285571e
commit
247a8fbce3
|
|
@ -1,4 +1,4 @@
|
||||||
import { apiRoute, auth, jsonOrForm } from "@/api";
|
import { apiRoute, auth, jsonOrForm, reusedResponses } from "@/api";
|
||||||
import { tempmailDomains } from "@/tempmail";
|
import { tempmailDomains } from "@/tempmail";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { User } from "@versia/kit/db";
|
import { User } from "@versia/kit/db";
|
||||||
|
|
@ -74,8 +74,9 @@ const route = createRoute({
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Account created",
|
description: "Token for the created account",
|
||||||
},
|
},
|
||||||
|
401: reusedResponses[401],
|
||||||
422: {
|
422: {
|
||||||
description: "Validation failed",
|
description: "Validation failed",
|
||||||
content: {
|
content: {
|
||||||
|
|
@ -346,9 +347,9 @@ export default apiRoute((app) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
await User.fromDataLocal({
|
await User.fromDataLocal({
|
||||||
username: username ?? "",
|
username: username,
|
||||||
password: password ?? "",
|
password: password,
|
||||||
email: email ?? "",
|
email: email,
|
||||||
});
|
});
|
||||||
|
|
||||||
return context.text("", 200);
|
return context.text("", 200);
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,61 @@
|
||||||
import { apiRoute, jsonOrForm } from "@/api";
|
import { apiRoute, jsonOrForm, reusedResponses } from "@/api";
|
||||||
import { randomString } from "@/math";
|
import { randomString } from "@/math";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Application } from "@versia/kit/db";
|
import { Application } from "@versia/kit/db";
|
||||||
|
import {
|
||||||
const schemas = {
|
Application as ApplicationSchema,
|
||||||
json: z.object({
|
CredentialApplication as CredentialApplicationSchema,
|
||||||
client_name: z.string().trim().min(1).max(100),
|
} from "~/classes/schemas/application";
|
||||||
redirect_uris: z
|
|
||||||
.string()
|
|
||||||
.min(0)
|
|
||||||
.max(2000)
|
|
||||||
.url()
|
|
||||||
.or(z.literal("urn:ietf:wg:oauth:2.0:oob")),
|
|
||||||
scopes: z.string().min(1).max(200),
|
|
||||||
website: z
|
|
||||||
.string()
|
|
||||||
.min(0)
|
|
||||||
.max(2000)
|
|
||||||
.url()
|
|
||||||
.optional()
|
|
||||||
// Allow empty websites because Traewelling decides to give an empty
|
|
||||||
// value instead of not providing anything at all
|
|
||||||
.or(z.literal("").transform(() => undefined)),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/apps",
|
path: "/api/v1/apps",
|
||||||
summary: "Create app",
|
summary: "Create an application",
|
||||||
description: "Create an OAuth2 app",
|
description: "Create a new application to obtain OAuth2 credentials.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/apps/#create",
|
||||||
|
},
|
||||||
|
tags: ["Apps"],
|
||||||
middleware: [jsonOrForm()],
|
middleware: [jsonOrForm()],
|
||||||
request: {
|
request: {
|
||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: schemas.json,
|
schema: z.object({
|
||||||
|
client_name: ApplicationSchema.shape.name,
|
||||||
|
redirect_uris: ApplicationSchema.shape.redirect_uris.or(
|
||||||
|
ApplicationSchema.shape.redirect_uri.transform(
|
||||||
|
(u) => u.split("\n"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
scopes: z
|
||||||
|
.string()
|
||||||
|
.default("read")
|
||||||
|
.transform((s) => s.split(" "))
|
||||||
|
.openapi({
|
||||||
|
description: "Space separated list of scopes.",
|
||||||
|
}),
|
||||||
|
// Allow empty websites because Traewelling decides to give an empty
|
||||||
|
// value instead of not providing anything at all
|
||||||
|
website: ApplicationSchema.shape.website
|
||||||
|
.optional()
|
||||||
|
.or(z.literal("").transform(() => undefined)),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "App",
|
description:
|
||||||
|
"Store the client_id and client_secret in your cache, as these will be used to obtain OAuth tokens.",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.object({
|
schema: CredentialApplicationSchema,
|
||||||
id: z.string().uuid(),
|
|
||||||
name: z.string(),
|
|
||||||
website: z.string().nullable(),
|
|
||||||
client_id: z.string(),
|
|
||||||
client_secret: z.string(),
|
|
||||||
redirect_uri: z.string(),
|
|
||||||
vapid_link: z.string().nullable(),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
422: reusedResponses[422],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -66,25 +65,14 @@ export default apiRoute((app) =>
|
||||||
context.req.valid("json");
|
context.req.valid("json");
|
||||||
|
|
||||||
const app = await Application.insert({
|
const app = await Application.insert({
|
||||||
name: client_name || "",
|
name: client_name,
|
||||||
redirectUri: decodeURI(redirect_uris) || "",
|
redirectUri: redirect_uris.join("\n"),
|
||||||
scopes: scopes || "read",
|
scopes: scopes.join(" "),
|
||||||
website: website || null,
|
website: website,
|
||||||
clientId: randomString(32, "base64url"),
|
clientId: randomString(32, "base64url"),
|
||||||
secret: randomString(64, "base64url"),
|
secret: randomString(64, "base64url"),
|
||||||
});
|
});
|
||||||
|
|
||||||
return context.json(
|
return context.json(app.toApiCredential(), 200);
|
||||||
{
|
|
||||||
id: app.id,
|
|
||||||
name: app.data.name,
|
|
||||||
website: app.data.website,
|
|
||||||
client_id: app.data.clientId,
|
|
||||||
client_secret: app.data.secret,
|
|
||||||
redirect_uri: app.data.redirectUri,
|
|
||||||
vapid_link: app.data.vapidKey,
|
|
||||||
},
|
|
||||||
200,
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
import { apiRoute, auth } from "@/api";
|
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||||
import { createRoute } from "@hono/zod-openapi";
|
import { createRoute } from "@hono/zod-openapi";
|
||||||
import { Application } from "@versia/kit/db";
|
import { Application } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { Application as ApplicationSchema } from "~/classes/schemas/application";
|
import { Application as ApplicationSchema } from "~/classes/schemas/application";
|
||||||
import { ErrorSchema } from "~/types/api";
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/apps/verify_credentials",
|
path: "/api/v1/apps/verify_credentials",
|
||||||
summary: "Verify credentials",
|
summary: "Verify your app works",
|
||||||
description: "Get your own application information",
|
description: "Confirm that the app’s OAuth2 credentials work.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/apps/#verify_credentials",
|
||||||
|
},
|
||||||
|
tags: ["Apps"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -19,21 +22,15 @@ const route = createRoute({
|
||||||
] as const,
|
] as const,
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Application",
|
description:
|
||||||
|
"If the Authorization header was provided with a valid token, you should see your app returned as an Application entity.",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: ApplicationSchema,
|
schema: ApplicationSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
401: {
|
...reusedResponses,
|
||||||
description: "Unauthorized",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: ErrorSchema,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,19 @@
|
||||||
import { apiRoute, auth } from "@/api";
|
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Timeline } from "@versia/kit/db";
|
import { Timeline } from "@versia/kit/db";
|
||||||
import { RolePermissions, Users } from "@versia/kit/tables";
|
import { RolePermissions, Users } from "@versia/kit/tables";
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { Account } from "~/classes/schemas/account";
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
|
|
||||||
const schemas = {
|
|
||||||
query: z.object({
|
|
||||||
max_id: z.string().uuid().optional(),
|
|
||||||
since_id: z.string().uuid().optional(),
|
|
||||||
min_id: z.string().uuid().optional(),
|
|
||||||
limit: z.coerce.number().int().min(1).max(80).default(40),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/blocks",
|
path: "/api/v1/blocks",
|
||||||
summary: "Get blocks",
|
summary: "View your blocks.",
|
||||||
description: "Get users you have blocked",
|
description: "View blocked users.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/blocks/#get",
|
||||||
|
},
|
||||||
|
tags: ["Blocks"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -27,17 +22,49 @@ const route = createRoute({
|
||||||
}),
|
}),
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
query: schemas.query,
|
query: z.object({
|
||||||
|
max_id: AccountSchema.shape.id.optional().openapi({
|
||||||
|
description:
|
||||||
|
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||||
|
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||||
|
}),
|
||||||
|
since_id: AccountSchema.shape.id.optional().openapi({
|
||||||
|
description:
|
||||||
|
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||||
|
example: undefined,
|
||||||
|
}),
|
||||||
|
min_id: AccountSchema.shape.id.optional().openapi({
|
||||||
|
description:
|
||||||
|
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||||
|
example: undefined,
|
||||||
|
}),
|
||||||
|
limit: z.coerce.number().int().min(1).max(80).default(40).openapi({
|
||||||
|
description: "Maximum number of results to return.",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Blocks",
|
description: "List of blocked users",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.array(Account),
|
schema: z.array(AccountSchema),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
headers: z.object({
|
||||||
|
link: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.openapi({
|
||||||
|
description: "Links to the next and previous pages",
|
||||||
|
example: `<https://versia.social/api/v1/blocks?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/blocks?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"`,
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
import { apiRoute, auth } from "@/api";
|
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Emoji } from "@versia/kit/db";
|
import { Emoji } from "@versia/kit/db";
|
||||||
import { Emojis, RolePermissions } from "@versia/kit/tables";
|
import { Emojis, RolePermissions } from "@versia/kit/tables";
|
||||||
import { and, eq, isNull, or } from "drizzle-orm";
|
import { and, eq, isNull, or } from "drizzle-orm";
|
||||||
import { CustomEmoji } from "~/classes/schemas/emoji";
|
import { CustomEmoji as CustomEmojiSchema } from "~/classes/schemas/emoji";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/custom_emojis",
|
path: "/api/v1/custom_emojis",
|
||||||
summary: "Get custom emojis",
|
summary: "View all custom emoji",
|
||||||
description: "Get custom emojis",
|
description: "Returns custom emojis that are available on the server.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/custom_emojis/#get",
|
||||||
|
},
|
||||||
|
tags: ["Emojis"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: false,
|
auth: false,
|
||||||
|
|
@ -18,13 +22,14 @@ const route = createRoute({
|
||||||
] as const,
|
] as const,
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Emojis",
|
description: "List of custom emojis",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.array(CustomEmoji),
|
schema: z.array(CustomEmojiSchema),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
422: reusedResponses[422],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,80 +1,73 @@
|
||||||
import { apiRoute, auth, emojiValidator, jsonOrForm } from "@/api";
|
import {
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
jsonOrForm,
|
||||||
|
reusedResponses,
|
||||||
|
withEmojiParam,
|
||||||
|
} from "@/api";
|
||||||
import { mimeLookup } from "@/content_types";
|
import { mimeLookup } from "@/content_types";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Emoji } from "@versia/kit/db";
|
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { CustomEmoji } from "~/classes/schemas/emoji";
|
import { CustomEmoji as CustomEmojiSchema } from "~/classes/schemas/emoji";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/packages/config-manager";
|
||||||
import { ErrorSchema } from "~/types/api";
|
import { ErrorSchema } from "~/types/api";
|
||||||
|
|
||||||
const schemas = {
|
const schema = z
|
||||||
param: z.object({
|
|
||||||
id: z.string().uuid(),
|
|
||||||
}),
|
|
||||||
json: z
|
|
||||||
.object({
|
.object({
|
||||||
shortcode: z
|
shortcode: CustomEmojiSchema.shape.shortcode,
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(1)
|
|
||||||
.max(config.validation.max_emoji_shortcode_size)
|
|
||||||
.regex(
|
|
||||||
emojiValidator,
|
|
||||||
"Shortcode must only contain letters (any case), numbers, dashes or underscores.",
|
|
||||||
),
|
|
||||||
element: z
|
element: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
|
||||||
.min(1)
|
|
||||||
.max(2000)
|
|
||||||
.url()
|
.url()
|
||||||
.transform((a) => new URL(a))
|
.transform((a) => new URL(a))
|
||||||
|
.openapi({
|
||||||
|
description: "Emoji image URL",
|
||||||
|
})
|
||||||
.or(
|
.or(
|
||||||
z
|
z
|
||||||
.instanceof(File)
|
.instanceof(File)
|
||||||
|
.openapi({
|
||||||
|
description:
|
||||||
|
"Emoji image encoded using multipart/form-data",
|
||||||
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(v) => v.size <= config.validation.max_emoji_size,
|
(v) => v.size <= config.validation.max_emoji_size,
|
||||||
`Emoji must be less than ${config.validation.max_emoji_size} bytes`,
|
`Emoji must be less than ${config.validation.max_emoji_size} bytes`,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
category: z.string().max(64).optional(),
|
category: CustomEmojiSchema.shape.category.optional(),
|
||||||
alt: z
|
alt: CustomEmojiSchema.shape.description.optional(),
|
||||||
.string()
|
global: CustomEmojiSchema.shape.global.default(false),
|
||||||
.max(config.validation.max_emoji_description_size)
|
|
||||||
.optional(),
|
|
||||||
global: z
|
|
||||||
.string()
|
|
||||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
|
||||||
.or(z.boolean())
|
|
||||||
.optional(),
|
|
||||||
})
|
})
|
||||||
.partial(),
|
.partial();
|
||||||
};
|
|
||||||
|
|
||||||
const routeGet = createRoute({
|
const routeGet = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/emojis/{id}",
|
path: "/api/v1/emojis/{id}",
|
||||||
summary: "Get emoji data",
|
summary: "Get emoji",
|
||||||
|
description: "Retrieves a custom emoji from database by ID.",
|
||||||
|
tags: ["Emojis"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
permissions: [RolePermissions.ViewEmojis],
|
permissions: [RolePermissions.ViewEmojis],
|
||||||
}),
|
}),
|
||||||
|
withEmojiParam,
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: schemas.param,
|
params: z.object({
|
||||||
|
id: CustomEmojiSchema.shape.id,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Emoji",
|
description: "Emoji",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: CustomEmoji,
|
schema: CustomEmojiSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
404: {
|
404: {
|
||||||
description: "Emoji not found",
|
description: "Emoji not found",
|
||||||
content: {
|
content: {
|
||||||
|
|
@ -83,6 +76,7 @@ const routeGet = createRoute({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -90,6 +84,8 @@ const routePatch = createRoute({
|
||||||
method: "patch",
|
method: "patch",
|
||||||
path: "/api/v1/emojis/{id}",
|
path: "/api/v1/emojis/{id}",
|
||||||
summary: "Modify emoji",
|
summary: "Modify emoji",
|
||||||
|
description: "Edit image or metadata of an emoji.",
|
||||||
|
tags: ["Emojis"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -99,19 +95,22 @@ const routePatch = createRoute({
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
jsonOrForm(),
|
jsonOrForm(),
|
||||||
|
withEmojiParam,
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: schemas.param,
|
params: z.object({
|
||||||
|
id: CustomEmojiSchema.shape.id,
|
||||||
|
}),
|
||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: schemas.json,
|
schema: schema,
|
||||||
},
|
},
|
||||||
"application/x-www-form-urlencoded": {
|
"application/x-www-form-urlencoded": {
|
||||||
schema: schemas.json,
|
schema: schema,
|
||||||
},
|
},
|
||||||
"multipart/form-data": {
|
"multipart/form-data": {
|
||||||
schema: schemas.json,
|
schema: schema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -121,13 +120,12 @@ const routePatch = createRoute({
|
||||||
description: "Emoji modified",
|
description: "Emoji modified",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: CustomEmoji,
|
schema: CustomEmojiSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
403: {
|
403: {
|
||||||
description: "Insufficient credentials",
|
description: "Insufficient permissions",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: ErrorSchema,
|
schema: ErrorSchema,
|
||||||
|
|
@ -142,14 +140,7 @@ const routePatch = createRoute({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
422: {
|
...reusedResponses,
|
||||||
description: "Invalid form data",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: ErrorSchema,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -157,6 +148,8 @@ const routeDelete = createRoute({
|
||||||
method: "delete",
|
method: "delete",
|
||||||
path: "/api/v1/emojis/{id}",
|
path: "/api/v1/emojis/{id}",
|
||||||
summary: "Delete emoji",
|
summary: "Delete emoji",
|
||||||
|
description: "Delete a custom emoji from the database.",
|
||||||
|
tags: ["Emojis"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -165,15 +158,17 @@ const routeDelete = createRoute({
|
||||||
RolePermissions.ViewEmojis,
|
RolePermissions.ViewEmojis,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
withEmojiParam,
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: schemas.param,
|
params: z.object({
|
||||||
|
id: CustomEmojiSchema.shape.id,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
204: {
|
204: {
|
||||||
description: "Emoji deleted",
|
description: "Emoji deleted",
|
||||||
},
|
},
|
||||||
|
|
||||||
404: {
|
404: {
|
||||||
description: "Emoji not found",
|
description: "Emoji not found",
|
||||||
content: {
|
content: {
|
||||||
|
|
@ -186,15 +181,9 @@ const routeDelete = createRoute({
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute((app) => {
|
export default apiRoute((app) => {
|
||||||
app.openapi(routeGet, async (context) => {
|
app.openapi(routeGet, (context) => {
|
||||||
const { id } = context.req.valid("param");
|
|
||||||
const { user } = context.get("auth");
|
const { user } = context.get("auth");
|
||||||
|
const emoji = context.get("emoji");
|
||||||
const emoji = await Emoji.fromId(id);
|
|
||||||
|
|
||||||
if (!emoji) {
|
|
||||||
throw new ApiError(404, "Emoji not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't leak non-global emojis to non-admins
|
// Don't leak non-global emojis to non-admins
|
||||||
if (
|
if (
|
||||||
|
|
@ -208,14 +197,8 @@ export default apiRoute((app) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.openapi(routePatch, async (context) => {
|
app.openapi(routePatch, async (context) => {
|
||||||
const { id } = context.req.valid("param");
|
|
||||||
const { user } = context.get("auth");
|
const { user } = context.get("auth");
|
||||||
|
const emoji = context.get("emoji");
|
||||||
const emoji = await Emoji.fromId(id);
|
|
||||||
|
|
||||||
if (!emoji) {
|
|
||||||
throw new ApiError(404, "Emoji not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user is admin
|
// Check if user is admin
|
||||||
if (
|
if (
|
||||||
|
|
@ -246,7 +229,7 @@ export default apiRoute((app) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
// Check of emoji is an image
|
// Check if emoji is an image
|
||||||
const contentType =
|
const contentType =
|
||||||
element instanceof File
|
element instanceof File
|
||||||
? element.type
|
? element.type
|
||||||
|
|
@ -283,14 +266,8 @@ export default apiRoute((app) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.openapi(routeDelete, async (context) => {
|
app.openapi(routeDelete, async (context) => {
|
||||||
const { id } = context.req.valid("param");
|
|
||||||
const { user } = context.get("auth");
|
const { user } = context.get("auth");
|
||||||
|
const emoji = context.get("emoji");
|
||||||
const emoji = await Emoji.fromId(id);
|
|
||||||
|
|
||||||
if (!emoji) {
|
|
||||||
throw new ApiError(404, "Emoji not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user is admin
|
// Check if user is admin
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,44 @@
|
||||||
import { apiRoute, auth, emojiValidator, jsonOrForm } from "@/api";
|
import { apiRoute, auth, jsonOrForm, reusedResponses } from "@/api";
|
||||||
import { mimeLookup } from "@/content_types";
|
import { mimeLookup } from "@/content_types";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Emoji, Media } from "@versia/kit/db";
|
import { Emoji, Media } from "@versia/kit/db";
|
||||||
import { Emojis, RolePermissions } from "@versia/kit/tables";
|
import { Emojis, RolePermissions } from "@versia/kit/tables";
|
||||||
import { and, eq, isNull, or } from "drizzle-orm";
|
import { and, eq, isNull, or } from "drizzle-orm";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { CustomEmoji } from "~/classes/schemas/emoji";
|
import { CustomEmoji as CustomEmojiSchema } from "~/classes/schemas/emoji";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/packages/config-manager";
|
||||||
import { ErrorSchema } from "~/types/api";
|
|
||||||
|
|
||||||
const schemas = {
|
const schema = z.object({
|
||||||
json: z.object({
|
shortcode: CustomEmojiSchema.shape.shortcode,
|
||||||
shortcode: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(1)
|
|
||||||
.max(config.validation.max_emoji_shortcode_size)
|
|
||||||
.regex(
|
|
||||||
emojiValidator,
|
|
||||||
"Shortcode must only contain letters (any case), numbers, dashes or underscores.",
|
|
||||||
),
|
|
||||||
element: z
|
element: z
|
||||||
.string()
|
.string()
|
||||||
.trim()
|
|
||||||
.min(1)
|
|
||||||
.max(2000)
|
|
||||||
.url()
|
.url()
|
||||||
.transform((a) => new URL(a))
|
.transform((a) => new URL(a))
|
||||||
|
.openapi({
|
||||||
|
description: "Emoji image URL",
|
||||||
|
})
|
||||||
.or(
|
.or(
|
||||||
z
|
z
|
||||||
.instanceof(File)
|
.instanceof(File)
|
||||||
|
.openapi({
|
||||||
|
description:
|
||||||
|
"Emoji image encoded using multipart/form-data",
|
||||||
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(v) => v.size <= config.validation.max_emoji_size,
|
(v) => v.size <= config.validation.max_emoji_size,
|
||||||
`Emoji must be less than ${config.validation.max_emoji_size} bytes`,
|
`Emoji must be less than ${config.validation.max_emoji_size} bytes`,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
category: z.string().max(64).optional(),
|
category: CustomEmojiSchema.shape.category.optional(),
|
||||||
alt: z
|
alt: CustomEmojiSchema.shape.description.optional(),
|
||||||
.string()
|
global: CustomEmojiSchema.shape.global.default(false),
|
||||||
.max(config.validation.max_emoji_description_size)
|
});
|
||||||
.optional(),
|
|
||||||
global: z
|
|
||||||
.string()
|
|
||||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
|
||||||
.or(z.boolean())
|
|
||||||
.optional(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/emojis",
|
path: "/api/v1/emojis",
|
||||||
summary: "Upload emoji",
|
summary: "Upload emoji",
|
||||||
description: "Upload an emoji",
|
description: "Upload a new emoji to the server.",
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -67,13 +53,13 @@ const route = createRoute({
|
||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: schemas.json,
|
schema: schema,
|
||||||
},
|
},
|
||||||
"multipart/form-data": {
|
"multipart/form-data": {
|
||||||
schema: schemas.json,
|
schema: schema,
|
||||||
},
|
},
|
||||||
"application/x-www-form-urlencoded": {
|
"application/x-www-form-urlencoded": {
|
||||||
schema: schemas.json,
|
schema: schema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -83,19 +69,11 @@ const route = createRoute({
|
||||||
description: "Uploaded emoji",
|
description: "Uploaded emoji",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: CustomEmoji,
|
schema: CustomEmojiSchema,
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
422: {
|
|
||||||
description: "Invalid data",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: ErrorSchema,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -145,10 +123,10 @@ export default apiRoute((app) =>
|
||||||
const media =
|
const media =
|
||||||
element instanceof File
|
element instanceof File
|
||||||
? await Media.fromFile(element, {
|
? await Media.fromFile(element, {
|
||||||
description: alt,
|
description: alt ?? undefined,
|
||||||
})
|
})
|
||||||
: await Media.fromUrl(element, {
|
: await Media.fromUrl(element, {
|
||||||
description: alt,
|
description: alt ?? undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const emoji = await Emoji.insert({
|
const emoji = await Emoji.insert({
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,19 @@
|
||||||
import { apiRoute, auth } from "@/api";
|
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Timeline } from "@versia/kit/db";
|
import { Timeline } from "@versia/kit/db";
|
||||||
import { Notes, RolePermissions } from "@versia/kit/tables";
|
import { Notes, RolePermissions } from "@versia/kit/tables";
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { Status } from "~/classes/schemas/status";
|
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||||
|
|
||||||
const schemas = {
|
|
||||||
query: z.object({
|
|
||||||
max_id: z.string().uuid().optional(),
|
|
||||||
since_id: z.string().uuid().optional(),
|
|
||||||
min_id: z.string().uuid().optional(),
|
|
||||||
limit: z.coerce.number().int().min(1).max(80).default(40),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/favourites",
|
path: "/api/v1/favourites",
|
||||||
summary: "Get favourites",
|
summary: "View favourited statuses",
|
||||||
|
description: "Statuses the user has favourited.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/favourites/#get",
|
||||||
|
},
|
||||||
|
tags: ["Favourites"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -25,17 +21,49 @@ const route = createRoute({
|
||||||
}),
|
}),
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
query: schemas.query,
|
query: z.object({
|
||||||
|
max_id: StatusSchema.shape.id.optional().openapi({
|
||||||
|
description:
|
||||||
|
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||||
|
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||||
|
}),
|
||||||
|
since_id: StatusSchema.shape.id.optional().openapi({
|
||||||
|
description:
|
||||||
|
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||||
|
example: undefined,
|
||||||
|
}),
|
||||||
|
min_id: StatusSchema.shape.id.optional().openapi({
|
||||||
|
description:
|
||||||
|
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||||
|
example: undefined,
|
||||||
|
}),
|
||||||
|
limit: z.coerce.number().int().min(1).max(80).default(40).openapi({
|
||||||
|
description: "Maximum number of results to return.",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Favourites",
|
description: "List of favourited statuses",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.array(Status),
|
schema: z.array(StatusSchema),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
headers: z.object({
|
||||||
|
link: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.openapi({
|
||||||
|
description: "Links to the next and previous pages",
|
||||||
|
example: `<https://versia.social/api/v1/favourites?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/favourites?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"`,
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
import { apiRoute, auth } from "@/api";
|
import { accountNotFound, apiRoute, auth, reusedResponses } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Relationship, User } from "@versia/kit/db";
|
import { Relationship, User } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||||
import { ErrorSchema } from "~/types/api";
|
|
||||||
|
|
||||||
const schemas = {
|
|
||||||
param: z.object({
|
|
||||||
account_id: z.string().uuid(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/follow_requests/{account_id}/authorize",
|
path: "/api/v1/follow_requests/{account_id}/authorize",
|
||||||
summary: "Authorize follow request",
|
summary: "Accept follow request",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/follow_requests/#accept",
|
||||||
|
},
|
||||||
|
tags: ["Follows"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -23,26 +21,22 @@ const route = createRoute({
|
||||||
}),
|
}),
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: schemas.param,
|
params: z.object({
|
||||||
|
account_id: AccountSchema.shape.id,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Relationship",
|
description:
|
||||||
|
"Your Relationship with this account should be updated so that you are followed_by this account.",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: RelationshipSchema,
|
schema: RelationshipSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
404: accountNotFound,
|
||||||
404: {
|
...reusedResponses,
|
||||||
description: "Account not found",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: ErrorSchema,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
import { apiRoute, auth } from "@/api";
|
import { accountNotFound, apiRoute, auth, reusedResponses } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Relationship, User } from "@versia/kit/db";
|
import { Relationship, User } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||||
import { ErrorSchema } from "~/types/api";
|
|
||||||
|
|
||||||
const schemas = {
|
|
||||||
param: z.object({
|
|
||||||
account_id: z.string().uuid(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/follow_requests/{account_id}/reject",
|
path: "/api/v1/follow_requests/{account_id}/reject",
|
||||||
summary: "Reject follow request",
|
summary: "Reject follow request",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/follow_requests/#reject",
|
||||||
|
},
|
||||||
|
tags: ["Follows"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -23,26 +21,22 @@ const route = createRoute({
|
||||||
}),
|
}),
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: schemas.param,
|
params: z.object({
|
||||||
|
account_id: AccountSchema.shape.id,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Relationship",
|
description:
|
||||||
|
"Your Relationship with this account should be unchanged.",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: RelationshipSchema,
|
schema: RelationshipSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
404: accountNotFound,
|
||||||
404: {
|
...reusedResponses,
|
||||||
description: "Account not found",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: ErrorSchema,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,19 @@
|
||||||
import { apiRoute, auth } from "@/api";
|
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Timeline } from "@versia/kit/db";
|
import { Timeline } from "@versia/kit/db";
|
||||||
import { RolePermissions, Users } from "@versia/kit/tables";
|
import { RolePermissions, Users } from "@versia/kit/tables";
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { Account } from "~/classes/schemas/account";
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
|
|
||||||
const schemas = {
|
|
||||||
query: z.object({
|
|
||||||
max_id: z.string().uuid().optional(),
|
|
||||||
since_id: z.string().uuid().optional(),
|
|
||||||
min_id: z.string().uuid().optional(),
|
|
||||||
limit: z.coerce.number().int().min(1).max(80).default(40),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/follow_requests",
|
path: "/api/v1/follow_requests",
|
||||||
summary: "Get follow requests",
|
summary: "View pending follow requests",
|
||||||
|
description: "Get a list of follow requests that the user has received.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/follow_requests/#get",
|
||||||
|
},
|
||||||
|
tags: ["Follows"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -25,17 +21,50 @@ const route = createRoute({
|
||||||
}),
|
}),
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
query: schemas.query,
|
query: z.object({
|
||||||
|
max_id: AccountSchema.shape.id.optional().openapi({
|
||||||
|
description:
|
||||||
|
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||||
|
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||||
|
}),
|
||||||
|
since_id: AccountSchema.shape.id.optional().openapi({
|
||||||
|
description:
|
||||||
|
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||||
|
example: undefined,
|
||||||
|
}),
|
||||||
|
min_id: AccountSchema.shape.id.optional().openapi({
|
||||||
|
description:
|
||||||
|
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||||
|
example: undefined,
|
||||||
|
}),
|
||||||
|
limit: z.coerce.number().int().min(1).max(80).default(40).openapi({
|
||||||
|
description: "Maximum number of results to return.",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Follow requests",
|
description:
|
||||||
|
"List of accounts that have requested to follow the user",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.array(Account),
|
schema: z.array(AccountSchema),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
headers: z.object({
|
||||||
|
link: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.openapi({
|
||||||
|
description: "Links to the next and previous pages",
|
||||||
|
example: `<https://versia.social/api/v1/follow_requests?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/follow_requests?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"`,
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,19 @@
|
||||||
import { apiRoute, auth } from "@/api";
|
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Timeline } from "@versia/kit/db";
|
import { Timeline } from "@versia/kit/db";
|
||||||
import { RolePermissions, Users } from "@versia/kit/tables";
|
import { RolePermissions, Users } from "@versia/kit/tables";
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { Account } from "~/classes/schemas/account";
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
|
|
||||||
const schemas = {
|
|
||||||
query: z.object({
|
|
||||||
max_id: z.string().uuid().optional(),
|
|
||||||
since_id: z.string().uuid().optional(),
|
|
||||||
min_id: z.string().uuid().optional(),
|
|
||||||
limit: z.coerce.number().int().min(1).max(80).default(40),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/mutes",
|
path: "/api/v1/mutes",
|
||||||
summary: "Get muted users",
|
summary: "View muted accounts",
|
||||||
|
description: "View your mutes.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/mutes/#get",
|
||||||
|
},
|
||||||
|
tags: ["Mutes"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -26,17 +22,49 @@ const route = createRoute({
|
||||||
}),
|
}),
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
query: schemas.query,
|
query: z.object({
|
||||||
|
max_id: AccountSchema.shape.id.optional().openapi({
|
||||||
|
description:
|
||||||
|
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||||
|
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||||
|
}),
|
||||||
|
since_id: AccountSchema.shape.id.optional().openapi({
|
||||||
|
description:
|
||||||
|
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||||
|
example: undefined,
|
||||||
|
}),
|
||||||
|
min_id: AccountSchema.shape.id.optional().openapi({
|
||||||
|
description:
|
||||||
|
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||||
|
example: undefined,
|
||||||
|
}),
|
||||||
|
limit: z.coerce.number().int().min(1).max(80).default(40).openapi({
|
||||||
|
description: "Maximum number of results to return.",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Muted users",
|
description: "List of muted users",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.array(Account),
|
schema: z.array(AccountSchema),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
headers: z.object({
|
||||||
|
link: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.openapi({
|
||||||
|
description: "Links to the next and previous pages",
|
||||||
|
example: `<https://versia.social/api/v1/mutes?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/mutes?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"`,
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
import { z } from "@hono/zod-openapi";
|
import { z } from "@hono/zod-openapi";
|
||||||
|
|
||||||
export const Application = z.object({
|
export const Application = z.object({
|
||||||
name: z.string().openapi({
|
name: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.max(200)
|
||||||
|
.openapi({
|
||||||
description: "The name of your application.",
|
description: "The name of your application.",
|
||||||
example: "Test Application",
|
example: "Test Application",
|
||||||
externalDocs: {
|
externalDocs: {
|
||||||
|
|
@ -18,7 +23,10 @@ export const Application = z.object({
|
||||||
url: "https://docs.joinmastodon.org/entities/Application/#website",
|
url: "https://docs.joinmastodon.org/entities/Application/#website",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
scopes: z.array(z.string()).openapi({
|
scopes: z
|
||||||
|
.array(z.string())
|
||||||
|
.default(["read"])
|
||||||
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"The scopes for your application. This is the registered scopes string split on whitespace.",
|
"The scopes for your application. This is the registered scopes string split on whitespace.",
|
||||||
example: ["read", "write", "push"],
|
example: ["read", "write", "push"],
|
||||||
|
|
@ -28,7 +36,11 @@ export const Application = z.object({
|
||||||
}),
|
}),
|
||||||
redirect_uris: z
|
redirect_uris: z
|
||||||
.array(
|
.array(
|
||||||
z.string().url().openapi({
|
z
|
||||||
|
.string()
|
||||||
|
.url()
|
||||||
|
.or(z.literal("urn:ietf:wg:oauth:2.0:oob"))
|
||||||
|
.openapi({
|
||||||
description: "URL or 'urn:ietf:wg:oauth:2.0:oob'",
|
description: "URL or 'urn:ietf:wg:oauth:2.0:oob'",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -40,6 +52,7 @@ export const Application = z.object({
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
redirect_uri: z.string().openapi({
|
redirect_uri: z.string().openapi({
|
||||||
|
deprecated: true,
|
||||||
description:
|
description:
|
||||||
"The registered redirection URI(s) for your application. May contain \\n characters when multiple redirect URIs are registered.",
|
"The registered redirection URI(s) for your application. May contain \\n characters when multiple redirect URIs are registered.",
|
||||||
externalDocs: {
|
externalDocs: {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
|
import { emojiValidator } from "@/api.ts";
|
||||||
import { z } from "@hono/zod-openapi";
|
import { z } from "@hono/zod-openapi";
|
||||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||||
|
import { config } from "~/packages/config-manager/index.ts";
|
||||||
import { Id } from "./common.ts";
|
import { Id } from "./common.ts";
|
||||||
|
|
||||||
export const CustomEmoji = z
|
export const CustomEmoji = z
|
||||||
|
|
@ -9,7 +11,16 @@ export const CustomEmoji = z
|
||||||
description: "ID of the custom emoji in the database.",
|
description: "ID of the custom emoji in the database.",
|
||||||
example: "af9ccd29-c689-477f-aa27-d7d95fd8fb05",
|
example: "af9ccd29-c689-477f-aa27-d7d95fd8fb05",
|
||||||
}),
|
}),
|
||||||
shortcode: z.string().openapi({
|
shortcode: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1)
|
||||||
|
.max(config.validation.max_emoji_shortcode_size)
|
||||||
|
.regex(
|
||||||
|
emojiValidator,
|
||||||
|
"Shortcode must only contain letters (any case), numbers, dashes or underscores.",
|
||||||
|
)
|
||||||
|
.openapi({
|
||||||
description: "The name of the custom emoji.",
|
description: "The name of the custom emoji.",
|
||||||
example: "blobaww",
|
example: "blobaww",
|
||||||
externalDocs: {
|
externalDocs: {
|
||||||
|
|
@ -48,6 +59,8 @@ export const CustomEmoji = z
|
||||||
}),
|
}),
|
||||||
category: z
|
category: z
|
||||||
.string()
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(64)
|
||||||
.nullable()
|
.nullable()
|
||||||
.openapi({
|
.openapi({
|
||||||
description: "Used for sorting custom emoji in the picker.",
|
description: "Used for sorting custom emoji in the picker.",
|
||||||
|
|
@ -64,6 +77,7 @@ export const CustomEmoji = z
|
||||||
/* Versia Server API extension */
|
/* Versia Server API extension */
|
||||||
description: z
|
description: z
|
||||||
.string()
|
.string()
|
||||||
|
.max(config.validation.max_emoji_description_size)
|
||||||
.nullable()
|
.nullable()
|
||||||
.openapi({
|
.openapi({
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -49,13 +49,14 @@ describe("POST /api/v1/apps/", () => {
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
|
|
||||||
expect(json).toEqual({
|
expect(json).toEqual({
|
||||||
id: expect.any(String),
|
|
||||||
name: "Test Application",
|
name: "Test Application",
|
||||||
website: "https://example.com",
|
website: "https://example.com",
|
||||||
client_id: expect.any(String),
|
client_id: expect.any(String),
|
||||||
client_secret: expect.any(String),
|
client_secret: expect.any(String),
|
||||||
|
client_secret_expires_at: "0",
|
||||||
redirect_uri: "https://example.com",
|
redirect_uri: "https://example.com",
|
||||||
vapid_link: null,
|
redirect_uris: ["https://example.com"],
|
||||||
|
scopes: ["read", "write"],
|
||||||
});
|
});
|
||||||
|
|
||||||
clientId = json.client_id;
|
clientId = json.client_id;
|
||||||
|
|
|
||||||
39
utils/api.ts
39
utils/api.ts
|
|
@ -2,7 +2,7 @@ import type { OpenAPIHono } from "@hono/zod-openapi";
|
||||||
import { z } from "@hono/zod-openapi";
|
import { z } from "@hono/zod-openapi";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
import { getLogger } from "@logtape/logtape";
|
import { getLogger } from "@logtape/logtape";
|
||||||
import { Application, Note, Token, User, db } from "@versia/kit/db";
|
import { Application, Emoji, Note, Token, User, db } from "@versia/kit/db";
|
||||||
import { Challenges, type RolePermissions } from "@versia/kit/tables";
|
import { Challenges, type RolePermissions } from "@versia/kit/tables";
|
||||||
import { extractParams, verifySolution } from "altcha-lib";
|
import { extractParams, verifySolution } from "altcha-lib";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
|
|
@ -404,6 +404,43 @@ export const withUserParam = every(
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to check if an emoji exists and is viewable by the user
|
||||||
|
*
|
||||||
|
* Useful in /api/v1/emojis/:id/* routes
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const withEmojiParam = every(
|
||||||
|
zValidator("param", z.object({ id: z.string().uuid() }), handleZodError),
|
||||||
|
createMiddleware<
|
||||||
|
HonoEnv & {
|
||||||
|
Variables: {
|
||||||
|
emoji: Emoji;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
string,
|
||||||
|
WithIdParam
|
||||||
|
>(async (context, next) => {
|
||||||
|
const { id } = context.req.valid("param");
|
||||||
|
|
||||||
|
const emoji = await Emoji.fromId(id);
|
||||||
|
|
||||||
|
if (!emoji) {
|
||||||
|
throw new ApiError(404, "Emoji not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.set("emoji", emoji);
|
||||||
|
|
||||||
|
await next();
|
||||||
|
}),
|
||||||
|
) as MiddlewareHandler<
|
||||||
|
HonoEnv & {
|
||||||
|
Variables: {
|
||||||
|
emoji: Emoji;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
// Helper function to parse form data
|
// Helper function to parse form data
|
||||||
async function parseFormData(context: Context): Promise<{
|
async function parseFormData(context: Context): Promise<{
|
||||||
parsed: ParsedQs;
|
parsed: ParsedQs;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue