mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor(api): 🏷️ Port almost all remaining v1 endpoints to OpenAPI
This commit is contained in:
parent
247a8fbce3
commit
1856176de5
|
|
@ -21,7 +21,7 @@ beforeAll(async () => {
|
|||
},
|
||||
);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
// /api/v1/accounts/:id/statuses
|
||||
|
|
@ -78,7 +78,7 @@ describe("/api/v1/accounts/:id/statuses", () => {
|
|||
}),
|
||||
});
|
||||
|
||||
expect(replyResponse.status).toBe(201);
|
||||
expect(replyResponse.status).toBe(200);
|
||||
|
||||
const response = await fakeRequest(
|
||||
`/api/v1/accounts/${users[1].id}/statuses?exclude_replies=true`,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ const route = createRoute({
|
|||
path: "/api/v1/challenges",
|
||||
summary: "Generate a challenge",
|
||||
description: "Generate a challenge to solve",
|
||||
tags: ["Challenges"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { renderMarkdownInPath } from "@/markdown";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { ExtendedDescription as ExtendedDescriptionSchema } from "~/classes/schemas/extended-description";
|
||||
import { config } from "~/packages/config-manager";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/instance/extended_description",
|
||||
summary: "Get extended description",
|
||||
summary: "View extended description",
|
||||
description: "Obtain an extended description of this server",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/instance/#extended_description",
|
||||
},
|
||||
tags: ["Instance"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Extended description",
|
||||
description: "Server extended description",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
updated_at: z.string(),
|
||||
content: z.string(),
|
||||
}),
|
||||
schema: ExtendedDescriptionSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,16 +1,25 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { renderMarkdownInPath } from "@/markdown";
|
||||
import { proxyUrl } from "@/response";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { createRoute, type z } from "@hono/zod-openapi";
|
||||
import { Instance, Note, User } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { InstanceV1 as InstanceV1Schema } from "~/classes/schemas/instance-v1";
|
||||
import manifest from "~/package.json";
|
||||
import { config } from "~/packages/config-manager";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/instance",
|
||||
summary: "Get instance information",
|
||||
summary: "View server information (v1)",
|
||||
description:
|
||||
"Obtain general information about the server. See api/v2/instance instead.",
|
||||
deprecated: true,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/instance/#v1",
|
||||
},
|
||||
tags: ["Instance"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -20,9 +29,8 @@ const route = createRoute({
|
|||
200: {
|
||||
description: "Instance information",
|
||||
content: {
|
||||
// TODO: Add schemas for this response
|
||||
"application/json": {
|
||||
schema: z.any(),
|
||||
schema: InstanceV1Schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -38,9 +46,10 @@ export default apiRoute((app) =>
|
|||
|
||||
const userCount = await User.getCount();
|
||||
|
||||
const contactAccount = await User.fromSql(
|
||||
const contactAccount =
|
||||
(await User.fromSql(
|
||||
and(isNull(Users.instanceId), eq(Users.isAdmin, true)),
|
||||
);
|
||||
)) ?? (await User.fromSql(isNull(Users.instanceId)));
|
||||
|
||||
const knownDomainsCount = await Instance.getCount();
|
||||
|
||||
|
|
@ -55,6 +64,11 @@ export default apiRoute((app) =>
|
|||
}
|
||||
| undefined;
|
||||
|
||||
const { content } = await renderMarkdownInPath(
|
||||
config.instance.extended_description_path ?? "",
|
||||
"This is a [Versia](https://versia.pub) server with the default extended description.",
|
||||
);
|
||||
|
||||
// TODO: fill in more values
|
||||
return context.json({
|
||||
approval_required: false,
|
||||
|
|
@ -84,10 +98,13 @@ export default apiRoute((app) =>
|
|||
max_featured_tags: 100,
|
||||
},
|
||||
},
|
||||
description: config.instance.description,
|
||||
short_description: config.instance.description,
|
||||
description: content,
|
||||
// TODO: Add contact email
|
||||
email: "",
|
||||
invites_enabled: false,
|
||||
registrations: config.signups.registration,
|
||||
// TODO: Implement
|
||||
languages: ["en"],
|
||||
rules: config.signups.rules.map((r, index) => ({
|
||||
id: String(index),
|
||||
|
|
@ -101,12 +118,10 @@ export default apiRoute((app) =>
|
|||
thumbnail: config.instance.logo
|
||||
? proxyUrl(config.instance.logo).toString()
|
||||
: null,
|
||||
banner: config.instance.banner
|
||||
? proxyUrl(config.instance.banner).toString()
|
||||
: null,
|
||||
title: config.instance.name,
|
||||
uri: config.http.base_url,
|
||||
uri: config.http.base_url.host,
|
||||
urls: {
|
||||
// TODO: Implement Streaming API
|
||||
streaming_api: "",
|
||||
},
|
||||
version: "4.3.0-alpha.3+glitch",
|
||||
|
|
@ -123,18 +138,7 @@ export default apiRoute((app) =>
|
|||
id: p.id,
|
||||
})) ?? [],
|
||||
},
|
||||
contact_account: contactAccount?.toApi() || undefined,
|
||||
} satisfies Record<string, unknown> & {
|
||||
banner: string | null;
|
||||
versia_version: string;
|
||||
sso: {
|
||||
forced: boolean;
|
||||
providers: {
|
||||
id: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
}[];
|
||||
};
|
||||
});
|
||||
contact_account: (contactAccount as User).toApi(),
|
||||
} satisfies z.infer<typeof InstanceV1Schema>);
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { renderMarkdownInPath } from "@/markdown";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { PrivacyPolicy as PrivacyPolicySchema } from "~/classes/schemas/privacy-policy";
|
||||
import { config } from "~/packages/config-manager";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/instance/privacy_policy",
|
||||
summary: "Get instance privacy policy",
|
||||
summary: "View privacy policy",
|
||||
description: "Obtain the contents of this server’s privacy policy.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/instance/#privacy_policy",
|
||||
},
|
||||
tags: ["Instance"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -14,13 +20,10 @@ const route = createRoute({
|
|||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Instance privacy policy",
|
||||
description: "Server privacy policy",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
updated_at: z.string(),
|
||||
content: z.string(),
|
||||
}),
|
||||
schema: PrivacyPolicySchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Rule as RuleSchema } from "~/classes/schemas/rule";
|
||||
import { config } from "~/packages/config-manager";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/instance/rules",
|
||||
summary: "Get instance rules",
|
||||
summary: "List of rules",
|
||||
description: "Rules that the users of this service should follow.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/instance/#rules",
|
||||
},
|
||||
tags: ["Instance"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -16,13 +22,7 @@ const route = createRoute({
|
|||
description: "Instance rules",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
text: z.string(),
|
||||
hint: z.string(),
|
||||
}),
|
||||
),
|
||||
schema: z.array(RuleSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { describe, expect, test } from "bun:test";
|
||||
import { fakeRequest } from "~/tests/utils";
|
||||
|
||||
// /api/v1/instance/tos
|
||||
describe("/api/v1/instance/tos", () => {
|
||||
// /api/v1/instance/terms_of_service
|
||||
describe("/api/v1/instance/terms_of_service", () => {
|
||||
test("should return terms of service", async () => {
|
||||
const response = await fakeRequest("/api/v1/instance/tos");
|
||||
const response = await fakeRequest("/api/v1/instance/terms_of_service");
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
|
|
@ -1,12 +1,19 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { renderMarkdownInPath } from "@/markdown";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { TermsOfService as TermsOfServiceSchema } from "~/classes/schemas/tos";
|
||||
import { config } from "~/packages/config-manager";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/instance/tos",
|
||||
summary: "Get instance terms of service",
|
||||
path: "/api/v1/instance/terms_of_service",
|
||||
summary: "View terms of service",
|
||||
description:
|
||||
"Obtain the contents of this server’s terms of service, if configured.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/instance/#terms_of_service",
|
||||
},
|
||||
tags: ["Instance"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -14,13 +21,10 @@ const route = createRoute({
|
|||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Instance terms of service",
|
||||
description: "Server terms of service",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
updated_at: z.string(),
|
||||
content: z.string(),
|
||||
}),
|
||||
schema: TermsOfServiceSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { db } from "@versia/kit/db";
|
||||
import { Markers, RolePermissions } from "@versia/kit/tables";
|
||||
|
|
@ -15,7 +15,12 @@ const MarkerResponseSchema = z.object({
|
|||
const routeGet = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/markers",
|
||||
summary: "Get markers",
|
||||
summary: "Get saved timeline positions",
|
||||
description: "Get current positions in timelines.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/markers/#get",
|
||||
},
|
||||
tags: ["Timelines"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -27,8 +32,12 @@ const routeGet = createRoute({
|
|||
"timeline[]": z
|
||||
.array(z.enum(["home", "notifications"]))
|
||||
.max(2)
|
||||
.or(z.enum(["home", "notifications"]))
|
||||
.optional(),
|
||||
.or(z.enum(["home", "notifications"]).transform((t) => [t]))
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"Specify the timeline(s) for which markers should be fetched. Possible values: home, notifications. If not provided, an empty object will be returned.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
|
|
@ -40,13 +49,19 @@ const routeGet = createRoute({
|
|||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
const routePost = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/markers",
|
||||
summary: "Update markers",
|
||||
summary: "Save your position in a timeline",
|
||||
description: "Save current position in timeline.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/markers/#create",
|
||||
},
|
||||
tags: ["Timelines"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -54,11 +69,19 @@ const routePost = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: z.object({
|
||||
"home[last_read_id]": StatusSchema.shape.id.optional(),
|
||||
"notifications[last_read_id]":
|
||||
NotificationSchema.shape.id.optional(),
|
||||
query: z
|
||||
.object({
|
||||
"home[last_read_id]": StatusSchema.shape.id.openapi({
|
||||
description:
|
||||
"ID of the last status read in the home timeline.",
|
||||
example: "c62aa212-8198-4ce5-a388-2cc8344a84ef",
|
||||
}),
|
||||
"notifications[last_read_id]":
|
||||
NotificationSchema.shape.id.openapi({
|
||||
description: "ID of the last notification read.",
|
||||
}),
|
||||
})
|
||||
.partial(),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
|
|
@ -69,16 +92,15 @@ const routePost = createRoute({
|
|||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) => {
|
||||
app.openapi(routeGet, async (context) => {
|
||||
const { "timeline[]": timelines } = context.req.valid("query");
|
||||
const { "timeline[]": timeline } = context.req.valid("query");
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const timeline = Array.isArray(timelines) ? timelines : [];
|
||||
|
||||
if (!timeline) {
|
||||
return context.json({}, 200);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,21 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Media } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { Attachment as AttachmentSchema } from "~/classes/schemas/attachment";
|
||||
import { config } from "~/packages/config-manager/index.ts";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const schemas = {
|
||||
param: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
form: z.object({
|
||||
thumbnail: z.instanceof(File).optional(),
|
||||
description: z
|
||||
.string()
|
||||
.max(config.validation.max_media_description_size)
|
||||
.optional(),
|
||||
focus: z.string().optional(),
|
||||
}),
|
||||
};
|
||||
|
||||
const routePut = createRoute({
|
||||
method: "put",
|
||||
path: "/api/v1/media/{id}",
|
||||
summary: "Update media",
|
||||
summary: "Update media attachment",
|
||||
description:
|
||||
"Update a MediaAttachment’s parameters, before it is attached to a status and posted.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/media/#update",
|
||||
},
|
||||
tags: ["Media"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -33,40 +24,63 @@ const routePut = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: AttachmentSchema.shape.id,
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"multipart/form-data": {
|
||||
schema: schemas.form,
|
||||
schema: z
|
||||
.object({
|
||||
thumbnail: z.instanceof(File).openapi({
|
||||
description:
|
||||
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
|
||||
}),
|
||||
description: AttachmentSchema.shape.description,
|
||||
focus: z.string().openapi({
|
||||
description:
|
||||
"Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0. Used for media cropping on clients.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#focal-points",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.partial(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Media updated",
|
||||
description: "Updated attachment",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: AttachmentSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
404: {
|
||||
description: "Media not found",
|
||||
description: "Attachment not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
const routeGet = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/media/{id}",
|
||||
summary: "Get media",
|
||||
summary: "Get media attachment",
|
||||
description:
|
||||
"Get a media attachment, before it is attached to a status and posted, but after it is accepted for processing. Use this method to check that the full-sized media has finished processing.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/media/#get",
|
||||
},
|
||||
tags: ["Media"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -74,11 +88,13 @@ const routeGet = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: AttachmentSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Media",
|
||||
description: "Attachment",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: AttachmentSchema,
|
||||
|
|
@ -86,13 +102,14 @@ const routeGet = createRoute({
|
|||
},
|
||||
},
|
||||
404: {
|
||||
description: "Media not found",
|
||||
description: "Attachment not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,21 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Media } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { Attachment as AttachmentSchema } from "~/classes/schemas/attachment";
|
||||
import { config } from "~/packages/config-manager/index.ts";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const schemas = {
|
||||
form: z.object({
|
||||
file: z.instanceof(File),
|
||||
thumbnail: z.instanceof(File).optional(),
|
||||
description: z
|
||||
.string()
|
||||
.max(config.validation.max_media_description_size)
|
||||
.optional(),
|
||||
focus: z.string().optional(),
|
||||
}),
|
||||
};
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/media",
|
||||
summary: "Upload media",
|
||||
summary: "Upload media as an attachment (v1)",
|
||||
description:
|
||||
"Creates an attachment to be used with a new status. This method will return after the full sized media is done processing.",
|
||||
deprecated: true,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/media/#v1",
|
||||
},
|
||||
tags: ["Media"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -33,21 +27,42 @@ const route = createRoute({
|
|||
body: {
|
||||
content: {
|
||||
"multipart/form-data": {
|
||||
schema: schemas.form,
|
||||
schema: z.object({
|
||||
file: z.instanceof(File).openapi({
|
||||
description:
|
||||
"The file to be attached, encoded using multipart form data. The file must have a MIME type.",
|
||||
}),
|
||||
thumbnail: z.instanceof(File).optional().openapi({
|
||||
description:
|
||||
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
|
||||
}),
|
||||
description:
|
||||
AttachmentSchema.shape.description.optional(),
|
||||
focus: z
|
||||
.string()
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0. Used for media cropping on clients.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#focal-points",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Attachment",
|
||||
description:
|
||||
"Attachment created successfully. Note that the MediaAttachment will be created even if the file is not understood correctly due to failed processing.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: AttachmentSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
413: {
|
||||
description: "File too large",
|
||||
content: {
|
||||
|
|
@ -64,6 +79,7 @@ const route = createRoute({
|
|||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -73,7 +89,7 @@ export default apiRoute((app) =>
|
|||
|
||||
const attachment = await Media.fromFile(file, {
|
||||
thumbnail,
|
||||
description,
|
||||
description: description ?? undefined,
|
||||
});
|
||||
|
||||
return context.json(attachment.toApi(), 200);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Notification } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { Notification as NotificationSchema } from "~/classes/schemas/notification";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/notifications/{id}/dismiss",
|
||||
summary: "Dismiss notification",
|
||||
summary: "Dismiss a single notification",
|
||||
description: "Dismiss a single notification from the server.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/notifications/#dismiss",
|
||||
},
|
||||
tags: ["Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -17,13 +23,14 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: NotificationSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Notification dismissed",
|
||||
description: "Notification with given ID successfully dismissed",
|
||||
},
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Notification } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
|
|
@ -9,7 +9,12 @@ import { ErrorSchema } from "~/types/api";
|
|||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/notifications/{id}",
|
||||
summary: "Get notification",
|
||||
summary: "Get a single notification",
|
||||
description: "View information about a notification with a given ID.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/notifications/#get",
|
||||
},
|
||||
tags: ["Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -19,19 +24,18 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: NotificationSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Notification",
|
||||
description: "A single Notification",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: NotificationSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
404: {
|
||||
description: "Notification not found",
|
||||
content: {
|
||||
|
|
@ -40,6 +44,7 @@ const route = createRoute({
|
|||
},
|
||||
},
|
||||
},
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/notifications/clear",
|
||||
summary: "Clear notifications",
|
||||
summary: "Dismiss all notifications",
|
||||
description: "Clear all notifications from the server.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/notifications/#clear",
|
||||
},
|
||||
tags: ["Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -15,8 +20,9 @@ const route = createRoute({
|
|||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description: "Notifications cleared",
|
||||
description: "Notifications successfully cleared.",
|
||||
},
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
|
||||
|
|
@ -26,6 +26,7 @@ const route = createRoute({
|
|||
200: {
|
||||
description: "Notifications dismissed",
|
||||
},
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ beforeAll(async () => {
|
|||
},
|
||||
);
|
||||
|
||||
expect(res3.status).toBe(201);
|
||||
expect(res3.status).toBe(200);
|
||||
|
||||
const res4 = await fakeRequest("/api/v1/statuses", {
|
||||
method: "POST",
|
||||
|
|
@ -64,7 +64,7 @@ beforeAll(async () => {
|
|||
}),
|
||||
});
|
||||
|
||||
expect(res4.status).toBe(201);
|
||||
expect(res4.status).toBe(200);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
|
|
|||
|
|
@ -1,32 +1,20 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { Notifications, RolePermissions } from "@versia/kit/tables";
|
||||
import { and, eq, gt, gte, inArray, lt, not, sql } from "drizzle-orm";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Notification as NotificationSchema } from "~/classes/schemas/notification.ts";
|
||||
|
||||
const schemas = {
|
||||
query: z
|
||||
.object({
|
||||
max_id: NotificationSchema.shape.id.optional(),
|
||||
since_id: NotificationSchema.shape.id.optional(),
|
||||
min_id: NotificationSchema.shape.id.optional(),
|
||||
limit: z.coerce.number().int().min(1).max(80).default(15),
|
||||
exclude_types: z.array(NotificationSchema.shape.type).optional(),
|
||||
types: z.array(NotificationSchema.shape.type).optional(),
|
||||
account_id: AccountSchema.shape.id.optional(),
|
||||
})
|
||||
.refine((val) => {
|
||||
// Can't use both exclude_types and types
|
||||
return !(val.exclude_types && val.types);
|
||||
}, "Can't use both exclude_types and types"),
|
||||
};
|
||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/notifications",
|
||||
summary: "Get notifications",
|
||||
summary: "Get all notifications",
|
||||
description: "Notifications concerning the user.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/notifications/#get",
|
||||
},
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -37,7 +25,58 @@ const route = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: schemas.query,
|
||||
query: z
|
||||
.object({
|
||||
max_id: NotificationSchema.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: NotificationSchema.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: NotificationSchema.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.",
|
||||
}),
|
||||
types: z
|
||||
.array(NotificationSchema.shape.type)
|
||||
.optional()
|
||||
.openapi({
|
||||
description: "Types to include in the result.",
|
||||
}),
|
||||
exclude_types: z
|
||||
.array(NotificationSchema.shape.type)
|
||||
.optional()
|
||||
.openapi({
|
||||
description: "Types to exclude from the results.",
|
||||
}),
|
||||
account_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"Return only notifications received from the specified account.",
|
||||
}),
|
||||
// TODO: Implement
|
||||
include_filtered: zBoolean.default(false).openapi({
|
||||
description:
|
||||
"Whether to include notifications filtered by the user’s NotificationPolicy.",
|
||||
}),
|
||||
})
|
||||
.refine((val) => {
|
||||
// Can't use both exclude_types and types
|
||||
return !(val.exclude_types && val.types);
|
||||
}, "Can't use both exclude_types and types"),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
|
|
@ -48,6 +87,7 @@ const route = createRoute({
|
|||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { Account } from "~/classes/schemas/account";
|
||||
|
|
@ -6,7 +6,12 @@ import { Account } from "~/classes/schemas/account";
|
|||
const route = createRoute({
|
||||
method: "delete",
|
||||
path: "/api/v1/profile/avatar",
|
||||
summary: "Delete avatar",
|
||||
summary: "Delete profile avatar",
|
||||
description: "Deletes the avatar associated with the user’s profile.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/profile/#delete-profile-avatar",
|
||||
},
|
||||
tags: ["Profile"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -16,13 +21,16 @@ const route = createRoute({
|
|||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description: "User",
|
||||
description:
|
||||
"The avatar was successfully deleted from the user’s profile. If there were no avatar associated with the profile, the response will still indicate a successful deletion.",
|
||||
content: {
|
||||
"application/json": {
|
||||
// TODO: Return a CredentialAccount
|
||||
schema: Account,
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { Account } from "~/classes/schemas/account";
|
||||
|
|
@ -6,7 +6,12 @@ import { Account } from "~/classes/schemas/account";
|
|||
const route = createRoute({
|
||||
method: "delete",
|
||||
path: "/api/v1/profile/header",
|
||||
summary: "Delete header",
|
||||
summary: "Delete profile header",
|
||||
description: "Deletes the header image associated with the user’s profile.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/profile/#delete-profile-header",
|
||||
},
|
||||
tags: ["Profiles"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -16,13 +21,15 @@ const route = createRoute({
|
|||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description: "User",
|
||||
description:
|
||||
"The header was successfully deleted from the user’s profile. If there were no header associated with the profile, the response will still indicate a successful deletion.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Account,
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { PushSubscription } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
|
@ -14,6 +14,7 @@ export default apiRoute((app) =>
|
|||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/push/#delete",
|
||||
},
|
||||
tags: ["Push Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -31,6 +32,7 @@ export default apiRoute((app) =>
|
|||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
}),
|
||||
async (context) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { PushSubscription } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
|
@ -16,6 +16,7 @@ export default apiRoute((app) =>
|
|||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/push/#get",
|
||||
},
|
||||
tags: ["Push Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -32,6 +33,7 @@ export default apiRoute((app) =>
|
|||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
}),
|
||||
async (context) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { apiRoute, reusedResponses } from "@/api";
|
||||
import { auth, jsonOrForm } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { PushSubscription } from "@versia/kit/db";
|
||||
|
|
@ -17,6 +17,7 @@ export default apiRoute((app) =>
|
|||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/push/#create",
|
||||
},
|
||||
tags: ["Push Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -44,6 +45,7 @@ export default apiRoute((app) =>
|
|||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
}),
|
||||
async (context) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { apiRoute, auth, jsonOrForm } from "@/api";
|
||||
import { apiRoute, auth, jsonOrForm, reusedResponses } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { PushSubscription } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
|
@ -19,6 +19,7 @@ export default apiRoute((app) =>
|
|||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/push/#update",
|
||||
},
|
||||
tags: ["Push Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -48,6 +49,7 @@ export default apiRoute((app) =>
|
|||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
}),
|
||||
async (context) => {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,24 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { Status } from "~/classes/schemas/status";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
import { Context as ContextSchema } from "~/classes/schemas/context";
|
||||
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/statuses/{id}/context",
|
||||
summary: "Get parent and child statuses in context",
|
||||
description: "View statuses above and below this status in the thread.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/statuses/#context",
|
||||
},
|
||||
tags: ["Statuses"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -14,32 +26,22 @@ const route = createRoute({
|
|||
}),
|
||||
withNoteParam,
|
||||
] as const,
|
||||
summary: "Get status context",
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: StatusSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Status context",
|
||||
description: "Status parent and children",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
ancestors: z.array(Status),
|
||||
descendants: z.array(Status),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Record not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: ContextSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: noteNotFound,
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,23 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { Status } from "~/classes/schemas/status";
|
||||
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/statuses/{id}/favourite",
|
||||
summary: "Favourite a status",
|
||||
description: "Add a status to your favourites list.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/statuses/#favourite",
|
||||
},
|
||||
tags: ["Statuses"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -19,18 +30,20 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: StatusSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Favourited status",
|
||||
description: "Status favourited or was already favourited",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Status,
|
||||
schema: StatusSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: noteNotFound,
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { RolePermissions, Users } from "@versia/kit/tables";
|
||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||
import { Account } 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),
|
||||
}),
|
||||
param: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
};
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/statuses/{id}/favourited_by",
|
||||
summary: "Get users who favourited a status",
|
||||
summary: "See who favourited a status",
|
||||
description: "View who favourited a given status.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/statuses/#favourited_by",
|
||||
},
|
||||
tags: ["Statuses"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -32,18 +32,53 @@ const route = createRoute({
|
|||
withNoteParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
query: schemas.query,
|
||||
params: z.object({
|
||||
id: StatusSchema.shape.id,
|
||||
}),
|
||||
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: {
|
||||
200: {
|
||||
description: "Users who favourited a status",
|
||||
description: "A list of accounts who favourited the status",
|
||||
content: {
|
||||
"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/statuses/f048addc-49ca-4443-bdd8-a1b641ae8adc/favourited_by?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/statuses/f048addc-49ca-4443-bdd8-a1b641ae8adc/favourited_by?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"`,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
404: noteNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,75 +1,93 @@
|
|||
import { apiRoute, auth, jsonOrForm, withNoteParam } from "@/api";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
jsonOrForm,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Media } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { Status } from "~/classes/schemas/status";
|
||||
import { Attachment as AttachmentSchema } from "~/classes/schemas/attachment";
|
||||
import { PollOption } from "~/classes/schemas/poll";
|
||||
import {
|
||||
Status as StatusSchema,
|
||||
StatusSource as StatusSourceSchema,
|
||||
} from "~/classes/schemas/status";
|
||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||
import { config } from "~/packages/config-manager/index.ts";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const schemas = {
|
||||
param: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
json: z
|
||||
const schema = z
|
||||
.object({
|
||||
status: z
|
||||
.string()
|
||||
.max(config.validation.max_note_size)
|
||||
.refine(
|
||||
(s) =>
|
||||
!config.filters.note_content.some((filter) =>
|
||||
s.match(filter),
|
||||
),
|
||||
"Status contains blocked words",
|
||||
)
|
||||
.optional(),
|
||||
content_type: z.string().optional().default("text/plain"),
|
||||
status: StatusSourceSchema.shape.text.optional().openapi({
|
||||
description:
|
||||
"The text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.",
|
||||
}),
|
||||
/* Versia Server API Extension */
|
||||
content_type: z
|
||||
.enum(["text/plain", "text/html", "text/markdown"])
|
||||
.default("text/plain")
|
||||
.openapi({
|
||||
description: "Content-Type of the status text.",
|
||||
example: "text/markdown",
|
||||
}),
|
||||
media_ids: z
|
||||
.array(z.string().uuid())
|
||||
.array(AttachmentSchema.shape.id)
|
||||
.max(config.validation.max_media_attachments)
|
||||
.default([]),
|
||||
spoiler_text: z.string().max(255).optional(),
|
||||
sensitive: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.or(z.boolean())
|
||||
.optional(),
|
||||
language: z
|
||||
.enum(ISO6391.getAllCodes() as [string, ...string[]])
|
||||
.optional(),
|
||||
.default([])
|
||||
.openapi({
|
||||
description:
|
||||
"Include Attachment IDs to be attached as media. If provided, status becomes optional, and poll cannot be used.",
|
||||
}),
|
||||
spoiler_text: StatusSourceSchema.shape.spoiler_text.optional().openapi({
|
||||
description:
|
||||
"Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field.",
|
||||
}),
|
||||
sensitive: zBoolean.default(false).openapi({
|
||||
description: "Mark status and attached media as sensitive?",
|
||||
}),
|
||||
language: StatusSchema.shape.language.optional(),
|
||||
"poll[options]": z
|
||||
.array(z.string().max(config.validation.max_poll_option_size))
|
||||
.array(PollOption.shape.title)
|
||||
.max(config.validation.max_poll_options)
|
||||
.optional(),
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"Possible answers to the poll. If provided, media_ids cannot be used, and poll[expires_in] must be provided.",
|
||||
}),
|
||||
"poll[expires_in]": z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(config.validation.min_poll_duration)
|
||||
.max(config.validation.max_poll_duration)
|
||||
.optional(),
|
||||
"poll[multiple]": z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.or(z.boolean())
|
||||
.optional(),
|
||||
"poll[hide_totals]": z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.or(z.boolean())
|
||||
.optional(),
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"Duration that the poll should be open, in seconds. If provided, media_ids cannot be used, and poll[options] must be provided.",
|
||||
}),
|
||||
"poll[multiple]": zBoolean.optional().openapi({
|
||||
description: "Allow multiple choices?",
|
||||
}),
|
||||
"poll[hide_totals]": zBoolean.optional().openapi({
|
||||
description: "Hide vote counts until the poll ends?",
|
||||
}),
|
||||
})
|
||||
.refine(
|
||||
(obj) => !(obj.media_ids.length > 0 && obj["poll[options]"]),
|
||||
"Cannot attach poll to media",
|
||||
),
|
||||
};
|
||||
);
|
||||
|
||||
const routeGet = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/statuses/{id}",
|
||||
summary: "Get status",
|
||||
summary: "View a single status",
|
||||
description: "Obtain information about a status.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/statuses/#get",
|
||||
},
|
||||
tags: ["Statuses"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -78,25 +96,20 @@ const routeGet = createRoute({
|
|||
withNoteParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: StatusSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Status",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Status,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Record not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: StatusSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: noteNotFound,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -104,6 +117,11 @@ const routeDelete = createRoute({
|
|||
method: "delete",
|
||||
path: "/api/v1/statuses/{id}",
|
||||
summary: "Delete a status",
|
||||
description: "Delete one of your own statuses.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/statuses/#delete",
|
||||
},
|
||||
tags: ["Statuses"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -115,40 +133,35 @@ const routeDelete = createRoute({
|
|||
withNoteParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: StatusSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Deleted status",
|
||||
description:
|
||||
"Note the special properties text and poll or media_attachments which may be used to repost the status, e.g. in case of delete-and-redraft functionality.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Status,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description: "Unauthorized",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Record not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: StatusSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: noteNotFound,
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
const routePut = createRoute({
|
||||
method: "put",
|
||||
path: "/api/v1/statuses/{id}",
|
||||
summary: "Update a status",
|
||||
summary: "Edit a status",
|
||||
description:
|
||||
"Edit a given status to change its text, sensitivity, media attachments, or poll. Note that editing a poll’s options will reset the votes.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/statuses/#edit",
|
||||
},
|
||||
tags: ["Statuses"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -161,46 +174,34 @@ const routePut = createRoute({
|
|||
withNoteParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: StatusSchema.shape.id,
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schemas.json,
|
||||
schema: schema,
|
||||
},
|
||||
"application/x-www-form-urlencoded": {
|
||||
schema: schemas.json,
|
||||
schema: schema,
|
||||
},
|
||||
"multipart/form-data": {
|
||||
schema: schemas.json,
|
||||
schema: schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated status",
|
||||
description: "Status has been successfully edited.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Status,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description: "Unauthorized",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
422: {
|
||||
description: "Invalid media IDs",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: StatusSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: noteNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,27 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { db } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import type { SQL } from "drizzle-orm";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { Status } from "~/classes/schemas/status";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/statuses/{id}/pin",
|
||||
summary: "Pin a status",
|
||||
summary: "Pin status to profile",
|
||||
description:
|
||||
"Feature one of your own public statuses at the top of your profile.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/statuses/#pin",
|
||||
},
|
||||
tags: ["Statuses"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -23,34 +34,21 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: StatusSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Pinned status",
|
||||
description:
|
||||
"Status pinned. Note the status is not a reblog and its authoring account is your own.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Status,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description: "Unauthorized",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
422: {
|
||||
description: "Already pinned",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: StatusSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: noteNotFound,
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,26 @@
|
|||
import { apiRoute, auth, jsonOrForm, withNoteParam } from "@/api";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
jsonOrForm,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Note } from "@versia/kit/db";
|
||||
import { Notes, RolePermissions } from "@versia/kit/tables";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { Status } from "~/classes/schemas/status";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const schemas = {
|
||||
param: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
json: z.object({
|
||||
visibility: z.enum(["public", "unlisted", "private"]).default("public"),
|
||||
}),
|
||||
};
|
||||
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/statuses/{id}/reblog",
|
||||
summary: "Reblog a status",
|
||||
summary: "Boost a status",
|
||||
description: "Reshare a status on your own profile.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/statuses/#boost",
|
||||
},
|
||||
tags: ["Statuses"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -32,46 +33,44 @@ const route = createRoute({
|
|||
withNoteParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: StatusSchema.shape.id,
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schemas.json,
|
||||
schema: z.object({
|
||||
visibility:
|
||||
StatusSchema.shape.visibility.default("public"),
|
||||
}),
|
||||
},
|
||||
"application/x-www-form-urlencoded": {
|
||||
schema: schemas.json,
|
||||
schema: z.object({
|
||||
visibility:
|
||||
StatusSchema.shape.visibility.default("public"),
|
||||
}),
|
||||
},
|
||||
"multipart/form-data": {
|
||||
schema: schemas.json,
|
||||
schema: z.object({
|
||||
visibility:
|
||||
StatusSchema.shape.visibility.default("public"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
description: "Reblogged status",
|
||||
200: {
|
||||
description:
|
||||
"Status has been reblogged. Note that the top-level ID has changed. The ID of the boosted status is now inside the reblog property. The top-level ID is the ID of the reblog itself. Also note that reblogs cannot be pinned.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Status,
|
||||
},
|
||||
},
|
||||
},
|
||||
422: {
|
||||
description: "Already reblogged",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
500: {
|
||||
description: "Failed to reblog",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: StatusSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: noteNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -86,7 +85,7 @@ export default apiRoute((app) =>
|
|||
);
|
||||
|
||||
if (existingReblog) {
|
||||
throw new ApiError(422, "Already reblogged");
|
||||
return context.json(await existingReblog.toApi(user), 200);
|
||||
}
|
||||
|
||||
const newReblog = await Note.insert({
|
||||
|
|
@ -98,16 +97,10 @@ export default apiRoute((app) =>
|
|||
applicationId: null,
|
||||
});
|
||||
|
||||
const finalNewReblog = await Note.fromId(newReblog.id, user?.id);
|
||||
|
||||
if (!finalNewReblog) {
|
||||
throw new ApiError(500, "Failed to reblog");
|
||||
}
|
||||
|
||||
if (note.author.isLocal() && user.isLocal()) {
|
||||
await note.author.notify("reblog", user, newReblog);
|
||||
}
|
||||
|
||||
return context.json(await finalNewReblog.toApi(user), 201);
|
||||
return context.json(await newReblog.toApi(user), 200);
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { RolePermissions, Users } from "@versia/kit/tables";
|
||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||
import { Account } from "~/classes/schemas/account";
|
||||
|
||||
const schemas = {
|
||||
param: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
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),
|
||||
}),
|
||||
};
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/statuses/{id}/reblogged_by",
|
||||
summary: "Get users who reblogged a status",
|
||||
summary: "See who boosted a status",
|
||||
description: "View who boosted a given status.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/statuses/#reblogged_by",
|
||||
},
|
||||
tags: ["Statuses"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -32,18 +32,53 @@ const route = createRoute({
|
|||
withNoteParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
query: schemas.query,
|
||||
params: z.object({
|
||||
id: StatusSchema.shape.id,
|
||||
}),
|
||||
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: {
|
||||
200: {
|
||||
description: "Users who reblogged a status",
|
||||
description: "A list of accounts that boosted the status",
|
||||
content: {
|
||||
"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/statuses/f048addc-49ca-4443-bdd8-a1b641ae8adc/reblogged_by?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/statuses/f048addc-49ca-4443-bdd8-a1b641ae8adc/reblogged_by?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"`,
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
404: noteNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,27 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import type { StatusSource as ApiStatusSource } from "@versia/client/types";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import {
|
||||
Status as StatusSchema,
|
||||
StatusSource as StatusSourceSchema,
|
||||
} from "~/classes/schemas/status";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/statuses/{id}/source",
|
||||
summary: "Get status source",
|
||||
summary: "View status source",
|
||||
description:
|
||||
"Obtain the source properties for a status so that it can be edited.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/statuses/#source",
|
||||
},
|
||||
tags: ["Statuses"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -19,7 +34,7 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: StatusSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
|
|
@ -27,14 +42,12 @@ const route = createRoute({
|
|||
description: "Status source",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
id: z.string().uuid(),
|
||||
spoiler_text: z.string(),
|
||||
text: z.string(),
|
||||
}),
|
||||
schema: StatusSourceSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: noteNotFound,
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -48,7 +61,7 @@ export default apiRoute((app) =>
|
|||
// TODO: Give real source for spoilerText
|
||||
spoiler_text: note.data.spoilerText,
|
||||
text: note.data.contentSource,
|
||||
} satisfies ApiStatusSource,
|
||||
},
|
||||
200,
|
||||
);
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,23 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { Status } from "~/classes/schemas/status";
|
||||
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/statuses/{id}/unfavourite",
|
||||
summary: "Unfavourite a status",
|
||||
summary: "Undo favourite of a status",
|
||||
description: "Remove a status from your favourites list.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/statuses/#unfavourite",
|
||||
},
|
||||
tags: ["Statuses"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -19,18 +30,20 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: StatusSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Unfavourited status",
|
||||
description: "Status unfavourited or was already not favourited",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Status,
|
||||
schema: StatusSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: noteNotFound,
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { Status } from "~/classes/schemas/status";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/statuses/{id}/unpin",
|
||||
summary: "Unpin a status",
|
||||
summary: "Unpin status from profile",
|
||||
description: "Unfeature a status from the top of your profile.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/statuses/#unpin",
|
||||
},
|
||||
tags: ["Statuses"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -21,26 +31,20 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: StatusSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Unpinned status",
|
||||
description: "Status unpinned, or was already not pinned",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Status,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description: "Unauthorized",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: StatusSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: noteNotFound,
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,26 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Note } from "@versia/kit/db";
|
||||
import { Notes, RolePermissions } from "@versia/kit/tables";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { Status } from "~/classes/schemas/status";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/statuses/{id}/unreblog",
|
||||
summary: "Unreblog a status",
|
||||
summary: "Undo boost of a status",
|
||||
description: "Undo a reshare of a status.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/statuses/#unreblog",
|
||||
},
|
||||
tags: ["Statuses"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -23,26 +33,20 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: StatusSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Unreblogged status",
|
||||
description: "Status unboosted or was already not boosted",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Status,
|
||||
},
|
||||
},
|
||||
},
|
||||
422: {
|
||||
description: "Not already reblogged",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: StatusSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: noteNotFound,
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -59,7 +63,7 @@ export default apiRoute((app) =>
|
|||
);
|
||||
|
||||
if (!existingReblog) {
|
||||
throw new ApiError(422, "Note already reblogged");
|
||||
return context.json(await note.toApi(user), 200);
|
||||
}
|
||||
|
||||
await existingReblog.delete();
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ describe("/api/v1/statuses", () => {
|
|||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain(
|
||||
"application/json",
|
||||
);
|
||||
|
|
@ -186,7 +186,7 @@ describe("/api/v1/statuses", () => {
|
|||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain(
|
||||
"application/json",
|
||||
);
|
||||
|
|
@ -223,7 +223,7 @@ describe("/api/v1/statuses", () => {
|
|||
}),
|
||||
});
|
||||
|
||||
expect(response2.status).toBe(201);
|
||||
expect(response2.status).toBe(200);
|
||||
expect(response2.headers.get("content-type")).toContain(
|
||||
"application/json",
|
||||
);
|
||||
|
|
@ -260,7 +260,7 @@ describe("/api/v1/statuses", () => {
|
|||
}),
|
||||
});
|
||||
|
||||
expect(response2.status).toBe(201);
|
||||
expect(response2.status).toBe(200);
|
||||
expect(response2.headers.get("content-type")).toContain(
|
||||
"application/json",
|
||||
);
|
||||
|
|
@ -283,7 +283,7 @@ describe("/api/v1/statuses", () => {
|
|||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain(
|
||||
"application/json",
|
||||
);
|
||||
|
|
@ -310,7 +310,7 @@ describe("/api/v1/statuses", () => {
|
|||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain(
|
||||
"application/json",
|
||||
);
|
||||
|
|
@ -342,7 +342,7 @@ describe("/api/v1/statuses", () => {
|
|||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain(
|
||||
"application/json",
|
||||
);
|
||||
|
|
@ -371,7 +371,7 @@ describe("/api/v1/statuses", () => {
|
|||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain(
|
||||
"application/json",
|
||||
);
|
||||
|
|
@ -397,7 +397,7 @@ describe("/api/v1/statuses", () => {
|
|||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain(
|
||||
"application/json",
|
||||
);
|
||||
|
|
@ -421,7 +421,7 @@ describe("/api/v1/statuses", () => {
|
|||
}),
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain(
|
||||
"application/json",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,94 +1,115 @@
|
|||
import { apiRoute, auth, jsonOrForm } from "@/api";
|
||||
import { apiRoute, auth, jsonOrForm, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Media, Note } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { Status } from "~/classes/schemas/status";
|
||||
import { Attachment as AttachmentSchema } from "~/classes/schemas/attachment";
|
||||
import { PollOption } from "~/classes/schemas/poll";
|
||||
import {
|
||||
Status as StatusSchema,
|
||||
StatusSource as StatusSourceSchema,
|
||||
} from "~/classes/schemas/status";
|
||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||
import { config } from "~/packages/config-manager/index.ts";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const schemas = {
|
||||
json: z
|
||||
const schema = z
|
||||
.object({
|
||||
status: z
|
||||
.string()
|
||||
.max(config.validation.max_note_size)
|
||||
.trim()
|
||||
.refine(
|
||||
(s) =>
|
||||
!config.filters.note_content.some((filter) =>
|
||||
s.match(filter),
|
||||
),
|
||||
"Status contains blocked words",
|
||||
)
|
||||
.optional(),
|
||||
// TODO: Add regex to validate
|
||||
content_type: z.string().optional().default("text/plain"),
|
||||
status: StatusSourceSchema.shape.text.optional().openapi({
|
||||
description:
|
||||
"The text content of the status. If media_ids is provided, this becomes optional. Attaching a poll is optional while status is provided.",
|
||||
}),
|
||||
/* Versia Server API Extension */
|
||||
content_type: z
|
||||
.enum(["text/plain", "text/html", "text/markdown"])
|
||||
.default("text/plain")
|
||||
.openapi({
|
||||
description: "Content-Type of the status text.",
|
||||
example: "text/markdown",
|
||||
}),
|
||||
media_ids: z
|
||||
.array(z.string().uuid())
|
||||
.array(AttachmentSchema.shape.id)
|
||||
.max(config.validation.max_media_attachments)
|
||||
.default([]),
|
||||
spoiler_text: z.string().max(255).trim().optional(),
|
||||
sensitive: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.or(z.boolean())
|
||||
.optional(),
|
||||
language: z
|
||||
.enum(ISO6391.getAllCodes() as [string, ...string[]])
|
||||
.optional(),
|
||||
.default([])
|
||||
.openapi({
|
||||
description:
|
||||
"Include Attachment IDs to be attached as media. If provided, status becomes optional, and poll cannot be used.",
|
||||
}),
|
||||
spoiler_text: StatusSourceSchema.shape.spoiler_text.optional().openapi({
|
||||
description:
|
||||
"Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field.",
|
||||
}),
|
||||
sensitive: zBoolean.default(false).openapi({
|
||||
description: "Mark status and attached media as sensitive?",
|
||||
}),
|
||||
language: StatusSchema.shape.language.optional(),
|
||||
"poll[options]": z
|
||||
.array(z.string().max(config.validation.max_poll_option_size))
|
||||
.array(PollOption.shape.title)
|
||||
.max(config.validation.max_poll_options)
|
||||
.optional(),
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"Possible answers to the poll. If provided, media_ids cannot be used, and poll[expires_in] must be provided.",
|
||||
}),
|
||||
"poll[expires_in]": z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(config.validation.min_poll_duration)
|
||||
.max(config.validation.max_poll_duration)
|
||||
.optional(),
|
||||
"poll[multiple]": z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.or(z.boolean())
|
||||
.optional(),
|
||||
"poll[hide_totals]": z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.or(z.boolean())
|
||||
.optional(),
|
||||
in_reply_to_id: z.string().uuid().optional().nullable(),
|
||||
quote_id: z.string().uuid().optional().nullable(),
|
||||
visibility: z
|
||||
.enum(["public", "unlisted", "private", "direct"])
|
||||
.optional()
|
||||
.default("public"),
|
||||
.openapi({
|
||||
description:
|
||||
"Duration that the poll should be open, in seconds. If provided, media_ids cannot be used, and poll[options] must be provided.",
|
||||
}),
|
||||
"poll[multiple]": zBoolean.optional().openapi({
|
||||
description: "Allow multiple choices?",
|
||||
}),
|
||||
"poll[hide_totals]": zBoolean.optional().openapi({
|
||||
description: "Hide vote counts until the poll ends?",
|
||||
}),
|
||||
in_reply_to_id: StatusSchema.shape.id.optional().nullable().openapi({
|
||||
description:
|
||||
"ID of the status being replied to, if status is a reply.",
|
||||
}),
|
||||
/* Versia Server API Extension */
|
||||
quote_id: StatusSchema.shape.id.optional().nullable().openapi({
|
||||
description: "ID of the status being quoted, if status is a quote.",
|
||||
}),
|
||||
visibility: StatusSchema.shape.visibility.default("public"),
|
||||
scheduled_at: z.coerce
|
||||
.date()
|
||||
.min(new Date(), "Scheduled time must be in the future")
|
||||
.min(
|
||||
new Date(Date.now() + 5 * 60 * 1000),
|
||||
"must be at least 5 minutes in the future.",
|
||||
)
|
||||
.optional()
|
||||
.nullable(),
|
||||
local_only: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.or(z.boolean())
|
||||
.optional()
|
||||
.default(false),
|
||||
.nullable()
|
||||
.openapi({
|
||||
description:
|
||||
"Datetime at which to schedule a status. Providing this parameter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future.",
|
||||
}),
|
||||
/* Versia Server API Extension */
|
||||
local_only: zBoolean.default(false).openapi({
|
||||
description: "If true, this status will not be federated.",
|
||||
}),
|
||||
})
|
||||
.refine(
|
||||
(obj) => obj.status || obj.media_ids.length > 0,
|
||||
"Status is required unless media is attached",
|
||||
(obj) => obj.status || obj.media_ids.length > 0 || obj["poll[options]"],
|
||||
"Status is required unless media or poll is attached",
|
||||
)
|
||||
.refine(
|
||||
(obj) => !(obj.media_ids.length > 0 && obj["poll[options]"]),
|
||||
"Cannot attach poll to media",
|
||||
),
|
||||
};
|
||||
);
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/statuses",
|
||||
summary: "Post a new status",
|
||||
description: "Publish a status with the given parameters.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/statuses/#create",
|
||||
},
|
||||
tags: ["Statuses"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -96,40 +117,31 @@ const route = createRoute({
|
|||
}),
|
||||
jsonOrForm(),
|
||||
] as const,
|
||||
summary: "Post a new status",
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schemas.json,
|
||||
schema: schema,
|
||||
},
|
||||
"application/x-www-form-urlencoded": {
|
||||
schema: schemas.json,
|
||||
schema: schema,
|
||||
},
|
||||
"multipart/form-data": {
|
||||
schema: schemas.json,
|
||||
schema: schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
description: "The new status",
|
||||
200: {
|
||||
description: "Status will be posted with chosen parameters.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Status,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
422: {
|
||||
description: "Invalid data",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: StatusSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -193,6 +205,6 @@ export default apiRoute((app) =>
|
|||
await newNote.federateToUsers();
|
||||
}
|
||||
|
||||
return context.json(await newNote.toApi(user), 201);
|
||||
return context.json(await newNote.toApi(user), 200);
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { config } from "~/packages/config-manager/index.ts";
|
||||
import { Id } from "./common.ts";
|
||||
|
||||
export const Attachment = z
|
||||
|
|
@ -50,7 +51,12 @@ export const Attachment = z
|
|||
},
|
||||
},
|
||||
}),
|
||||
description: z.string().nullable().openapi({
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(config.validation.max_media_description_size)
|
||||
.nullable()
|
||||
.openapi({
|
||||
description:
|
||||
"Alternate text that describes what is in the media attachment, to be used for the visually impaired or when media attachments do not load.",
|
||||
example: "test media description",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { config } from "~/packages/config-manager/index.ts";
|
||||
import { Id } from "./common.ts";
|
||||
import { CustomEmoji } from "./emoji.ts";
|
||||
|
||||
export const PollOption = z
|
||||
.object({
|
||||
title: z.string().openapi({
|
||||
title: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(config.validation.max_poll_option_size)
|
||||
.openapi({
|
||||
description: "The text value of the poll option.",
|
||||
example: "yes",
|
||||
externalDocs: {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import type { Status as ApiNote } from "@versia/client/types";
|
||||
import { zBoolean } from "~/packages/config-manager/config.type.ts";
|
||||
import { config } from "~/packages/config-manager/index.ts";
|
||||
import { Account } from "./account.ts";
|
||||
import { Attachment } from "./attachment.ts";
|
||||
import { PreviewCard } from "./card.ts";
|
||||
|
|
@ -49,6 +50,39 @@ export const Mention = z
|
|||
},
|
||||
});
|
||||
|
||||
export const StatusSource = z
|
||||
.object({
|
||||
id: Id.openapi({
|
||||
description: "ID of the status in the database.",
|
||||
example: "c7db92a4-e472-4e94-a115-7411ee934ba1",
|
||||
}),
|
||||
text: z
|
||||
.string()
|
||||
.max(config.validation.max_note_size)
|
||||
.trim()
|
||||
.refine(
|
||||
(s) =>
|
||||
!config.filters.note_content.some((filter) =>
|
||||
s.match(filter),
|
||||
),
|
||||
"Status contains blocked words",
|
||||
)
|
||||
.openapi({
|
||||
description: "The plain text used to compose the status.",
|
||||
example: "this is a status that will be edited",
|
||||
}),
|
||||
spoiler_text: z.string().trim().min(1).max(1024).openapi({
|
||||
description:
|
||||
"The plain text used to compose the status’s subject or content warning.",
|
||||
example: "",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/entities/StatusSource",
|
||||
},
|
||||
});
|
||||
|
||||
export const Status = z.object({
|
||||
id: Id.openapi({
|
||||
description: "ID of the status in the database.",
|
||||
|
|
|
|||
16
classes/schemas/tos.ts
Normal file
16
classes/schemas/tos.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
|
||||
export const TermsOfService = z
|
||||
.object({
|
||||
updated_at: z.string().datetime().openapi({
|
||||
description: "A timestamp of when the ToS was last updated.",
|
||||
example: "2025-01-12T13:11:00Z",
|
||||
}),
|
||||
content: z.string().openapi({
|
||||
description: "The rendered HTML content of the ToS.",
|
||||
example: "<p><h1>ToS</h1><p>None, have fun.</p></p>",
|
||||
}),
|
||||
})
|
||||
.openapi({
|
||||
description: "Represents the ToS of the instance.",
|
||||
});
|
||||
|
|
@ -189,7 +189,7 @@ describe("API Tests", () => {
|
|||
},
|
||||
);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toContain(
|
||||
"application/json",
|
||||
);
|
||||
|
|
|
|||
|
|
@ -58,6 +58,14 @@ export const accountNotFound = {
|
|||
},
|
||||
},
|
||||
};
|
||||
export const noteNotFound = {
|
||||
description: "Status does not exist",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const apiRoute = (fn: (app: OpenAPIHono<HonoEnv>) => void): typeof fn =>
|
||||
fn;
|
||||
|
|
|
|||
Loading…
Reference in a new issue