mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
Merge pull request #33 from versia-pub/refactor/types
Overhaul OpenAPI schemas and validation
This commit is contained in:
commit
416e3009a0
|
|
@ -1,12 +1,11 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Application, User } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { eq, or } from "drizzle-orm";
|
||||
import type { Context } from "hono";
|
||||
import { setCookie } from "hono/cookie";
|
||||
import { SignJWT } from "jose";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/packages/config-manager";
|
||||
|
||||
|
|
@ -181,7 +180,7 @@ export default apiRoute((app) =>
|
|||
// Generate JWT
|
||||
const jwt = await new SignJWT({
|
||||
sub: user.id,
|
||||
iss: new URL(config.http.base_url).origin,
|
||||
iss: config.http.base_url.origin,
|
||||
aud: client_id,
|
||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { db } from "@versia/kit/db";
|
||||
import { Applications, Tokens } from "@versia/kit/tables";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { config } from "~/packages/config-manager";
|
||||
|
||||
const schemas = {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { User } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { Context } from "hono";
|
||||
import { z } from "zod";
|
||||
import { config } from "~/packages/config-manager";
|
||||
|
||||
const schemas = {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,26 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import {
|
||||
accountNotFound,
|
||||
apiRoute,
|
||||
auth,
|
||||
reusedResponses,
|
||||
withUserParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/block",
|
||||
summary: "Block user",
|
||||
description: "Block a user",
|
||||
summary: "Block account",
|
||||
description:
|
||||
"Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline)",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#block",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -22,17 +34,20 @@ const route = createRoute({
|
|||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated relationship",
|
||||
description:
|
||||
"Successfully blocked, or account was already blocked.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Relationship.schema,
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,31 +1,28 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import {
|
||||
accountNotFound,
|
||||
apiRoute,
|
||||
auth,
|
||||
reusedResponses,
|
||||
withUserParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import { z } from "zod";
|
||||
|
||||
const schemas = {
|
||||
param: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
json: z
|
||||
.object({
|
||||
reblogs: z.coerce.boolean().optional(),
|
||||
notify: z.coerce.boolean().optional(),
|
||||
languages: z
|
||||
.array(z.enum(ISO6391.getAllCodes() as [string, ...string[]]))
|
||||
.optional(),
|
||||
})
|
||||
.optional()
|
||||
.default({ reblogs: true, notify: false, languages: [] }),
|
||||
};
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { iso631 } from "~/classes/schemas/common";
|
||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/follow",
|
||||
summary: "Follow user",
|
||||
description: "Follow a user",
|
||||
summary: "Follow account",
|
||||
description:
|
||||
"Follow the given account. Can also be used to update whether to show reblogs or enable notifications.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#follow",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -39,20 +36,53 @@ const route = createRoute({
|
|||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated relationship",
|
||||
description:
|
||||
"Successfully followed, or account was already followed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Relationship.schema,
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
403: {
|
||||
description:
|
||||
"Trying to follow someone that you block or that blocks you",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schemas.json,
|
||||
schema: z.object({
|
||||
reblogs: z.boolean().default(true).openapi({
|
||||
description:
|
||||
"Receive this account’s reblogs in home timeline?",
|
||||
example: true,
|
||||
}),
|
||||
notify: z.boolean().default(false).openapi({
|
||||
description:
|
||||
"Receive notifications when this account posts a status?",
|
||||
example: false,
|
||||
}),
|
||||
languages: z
|
||||
.array(iso631)
|
||||
.default([])
|
||||
.openapi({
|
||||
description:
|
||||
"Array of String (ISO 639-1 language two-letter code). Filter received statuses for these languages. If not provided, you will receive this account’s posts in all languages.",
|
||||
example: ["en", "fr"],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,28 +1,27 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Timeline, User } from "@versia/kit/db";
|
||||
import {
|
||||
accountNotFound,
|
||||
apiRoute,
|
||||
auth,
|
||||
reusedResponses,
|
||||
withUserParam,
|
||||
} 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 { z } from "zod";
|
||||
|
||||
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(40).optional().default(20),
|
||||
}),
|
||||
param: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
};
|
||||
import { Account } from "~/classes/schemas/account";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/{id}/followers",
|
||||
summary: "Get account followers",
|
||||
summary: "Get account’s followers",
|
||||
description:
|
||||
"Gets an paginated list of accounts that follow the specified account",
|
||||
"Accounts which follow the given account, if network is not hidden by the account owner.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#followers",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -35,23 +34,54 @@ const route = createRoute({
|
|||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
query: schemas.query,
|
||||
params: z.object({
|
||||
id: AccountSchema.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.number().int().min(1).max(40).default(20).openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "A list of accounts that follow the specified account",
|
||||
description: "Accounts which follow the given account.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(User.schema),
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
Link: {
|
||||
description: "Links to the next and previous pages",
|
||||
schema: z.array(Account),
|
||||
},
|
||||
},
|
||||
headers: z.object({
|
||||
link: z
|
||||
.string()
|
||||
.optional()
|
||||
.openapi({
|
||||
description: "Links to the next and previous pages",
|
||||
example:
|
||||
'<https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/followers?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/followers?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
404: accountNotFound,
|
||||
422: reusedResponses[422],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,28 +1,27 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Timeline, User } from "@versia/kit/db";
|
||||
import {
|
||||
accountNotFound,
|
||||
apiRoute,
|
||||
auth,
|
||||
reusedResponses,
|
||||
withUserParam,
|
||||
} 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 { z } from "zod";
|
||||
|
||||
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(40).optional().default(20),
|
||||
}),
|
||||
param: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
};
|
||||
import { Account } from "~/classes/schemas/account";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/{id}/following",
|
||||
summary: "Get account following",
|
||||
summary: "Get account’s following",
|
||||
description:
|
||||
"Gets an paginated list of accounts that the specified account follows",
|
||||
"Accounts which the given account is following, if network is not hidden by the account owner.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#following",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -35,24 +34,54 @@ const route = createRoute({
|
|||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
query: schemas.query,
|
||||
params: z.object({
|
||||
id: AccountSchema.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.number().int().min(1).max(40).default(20).openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"A list of accounts that the specified account follows",
|
||||
description: "Accounts which the given account is following.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(User.schema),
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
Link: {
|
||||
description: "Link to the next page of results",
|
||||
schema: z.array(Account),
|
||||
},
|
||||
},
|
||||
headers: z.object({
|
||||
link: z
|
||||
.string()
|
||||
.optional()
|
||||
.openapi({
|
||||
description: "Links to the next and previous pages",
|
||||
example:
|
||||
'<https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/following?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/following?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
404: accountNotFound,
|
||||
422: reusedResponses[422],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,24 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { User } from "@versia/kit/db";
|
||||
import {
|
||||
accountNotFound,
|
||||
apiRoute,
|
||||
auth,
|
||||
reusedResponses,
|
||||
withUserParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
import { Account } from "~/classes/schemas/account";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/{id}",
|
||||
summary: "Get account data",
|
||||
description: "Gets the specified account data",
|
||||
summary: "Get account",
|
||||
description: "View information about a profile.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#get",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -18,18 +28,21 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Account data",
|
||||
description:
|
||||
"The Account record will be returned. Note that acct of local users does not include the domain name.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: User.schema,
|
||||
schema: Account,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
422: reusedResponses[422],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,26 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import {
|
||||
accountNotFound,
|
||||
apiRoute,
|
||||
auth,
|
||||
reusedResponses,
|
||||
withUserParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
|
||||
const schemas = {
|
||||
param: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
json: z.object({
|
||||
notifications: z.boolean().optional(),
|
||||
duration: z
|
||||
.number()
|
||||
.int()
|
||||
.min(60)
|
||||
.max(60 * 60 * 24 * 365 * 5)
|
||||
.optional(),
|
||||
}),
|
||||
};
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/mute",
|
||||
summary: "Mute user",
|
||||
description: "Mute a user",
|
||||
summary: "Mute account",
|
||||
description:
|
||||
"Mute the given account. Clients should filter statuses and notifications from this account, if received (e.g. due to a boost in the Home timeline).",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#mute",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -36,24 +33,44 @@ const route = createRoute({
|
|||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schemas.json,
|
||||
schema: z.object({
|
||||
notifications: z.boolean().default(true).openapi({
|
||||
description:
|
||||
"Mute notifications in addition to statuses?",
|
||||
}),
|
||||
duration: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(60 * 60 * 24 * 365 * 5)
|
||||
.default(0)
|
||||
.openapi({
|
||||
description:
|
||||
"How long the mute should last, in seconds.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated relationship",
|
||||
description:
|
||||
"Successfully muted, or account was already muted. Note that you can call this API method again with notifications=false to update the relationship so that only statuses are muted.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Relationship.schema,
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -72,7 +89,7 @@ export default apiRoute((app) =>
|
|||
// TODO: Implement duration
|
||||
await foundRelationship.update({
|
||||
muting: true,
|
||||
mutingNotifications: notifications ?? true,
|
||||
mutingNotifications: notifications,
|
||||
});
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import {
|
||||
accountNotFound,
|
||||
apiRoute,
|
||||
auth,
|
||||
reusedResponses,
|
||||
withUserParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
|
||||
const schemas = {
|
||||
param: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
json: z.object({
|
||||
comment: z.string().min(0).max(5000).trim().optional(),
|
||||
}),
|
||||
};
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/note",
|
||||
summary: "Set note",
|
||||
description: "Set a note on a user's profile, visible only to you",
|
||||
summary: "Set private note on profile",
|
||||
description: "Sets a private note on a user.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#note",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -30,24 +32,35 @@ const route = createRoute({
|
|||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schemas.json,
|
||||
schema: z.object({
|
||||
comment: RelationshipSchema.shape.note
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"The comment to be set on that user. Provide an empty string or leave out this parameter to clear the currently set note.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated relationship",
|
||||
description: "Successfully updated profile note",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Relationship.schema,
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -63,7 +76,7 @@ export default apiRoute((app) =>
|
|||
);
|
||||
|
||||
await foundRelationship.update({
|
||||
note: comment,
|
||||
note: comment ?? "",
|
||||
});
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/pin",
|
||||
summary: "Pin user",
|
||||
description: "Pin a user to your profile",
|
||||
summary: "Feature account on your profile",
|
||||
description:
|
||||
"Add the given account to the user’s featured profiles. (Featured profiles are currently shown on the user’s own public profile.)",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#pin",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -22,7 +28,7 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
|
|
@ -30,7 +36,7 @@ const route = createRoute({
|
|||
description: "Updated relationship",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Relationship.schema,
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,16 +1,23 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { User } from "@versia/kit/db";
|
||||
import {
|
||||
accountNotFound,
|
||||
apiRoute,
|
||||
auth,
|
||||
reusedResponses,
|
||||
withUserParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { Account } from "~/classes/schemas/account";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/refetch",
|
||||
summary: "Refetch user",
|
||||
description: "Refetch a user's profile from the remote server",
|
||||
summary: "Refetch account",
|
||||
description: "Refetch the given account's profile from the remote server",
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -21,15 +28,15 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated user data",
|
||||
description: "Refetched account data",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: User.schema,
|
||||
schema: Account,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -41,6 +48,8 @@ const route = createRoute({
|
|||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,25 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import {
|
||||
accountNotFound,
|
||||
apiRoute,
|
||||
auth,
|
||||
reusedResponses,
|
||||
withUserParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/remove_from_followers",
|
||||
summary: "Remove user from followers",
|
||||
description: "Remove a user from your followers",
|
||||
summary: "Remove account from followers",
|
||||
description: "Remove the given account from your followers.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#remove_from_followers",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -22,18 +33,21 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated relationship",
|
||||
description:
|
||||
"Successfully removed from followers, or account was already not following you",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Relationship.schema,
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,17 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Role } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Role as RoleSchema } from "~/classes/schemas/versia";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const schemas = {
|
||||
param: z.object({
|
||||
id: z.string().uuid(),
|
||||
role_id: z.string().uuid(),
|
||||
}),
|
||||
};
|
||||
|
||||
const routePost = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/roles/{role_id}",
|
||||
summary: "Assign role to user",
|
||||
summary: "Assign role to account",
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -25,7 +20,10 @@ const routePost = createRoute({
|
|||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
role_id: RoleSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
204: {
|
||||
|
|
@ -62,7 +60,10 @@ const routeDelete = createRoute({
|
|||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
role_id: RoleSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
204: {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Role } from "@versia/kit/db";
|
||||
import { z } from "zod";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Role as RoleSchema } from "~/classes/schemas/versia.ts";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/{id}/roles",
|
||||
summary: "List user roles",
|
||||
summary: "List account roles",
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -15,7 +17,7 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
|
|
@ -23,7 +25,7 @@ const route = createRoute({
|
|||
description: "List of roles",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(Role.schema),
|
||||
schema: z.array(RoleSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -1,47 +1,27 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Note, Timeline } from "@versia/kit/db";
|
||||
import {
|
||||
accountNotFound,
|
||||
apiRoute,
|
||||
auth,
|
||||
reusedResponses,
|
||||
withUserParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { Notes, RolePermissions } from "@versia/kit/tables";
|
||||
import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
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(40).optional().default(20),
|
||||
only_media: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.optional(),
|
||||
exclude_replies: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.optional(),
|
||||
exclude_reblogs: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.optional(),
|
||||
pinned: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.optional(),
|
||||
tagged: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.optional(),
|
||||
}),
|
||||
};
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/{id}/statuses",
|
||||
summary: "Get account statuses",
|
||||
description: "Gets an paginated list of statuses by the specified account",
|
||||
summary: "Get account’s statuses",
|
||||
description: "Statuses posted to the given account.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#statuses",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -54,23 +34,58 @@ const route = createRoute({
|
|||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
query: schemas.query,
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
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(40).default(20).openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
only_media: zBoolean.default(false).openapi({
|
||||
description: "Filter out statuses without attachments.",
|
||||
}),
|
||||
exclude_replies: zBoolean.default(false).openapi({
|
||||
description:
|
||||
"Filter out statuses in reply to a different account.",
|
||||
}),
|
||||
exclude_reblogs: zBoolean.default(false).openapi({
|
||||
description: "Filter out boosts from the response.",
|
||||
}),
|
||||
pinned: zBoolean.default(false).openapi({
|
||||
description:
|
||||
"Filter for pinned statuses only. Pinned statuses do not receive special priority in the order of the returned results.",
|
||||
}),
|
||||
tagged: z.string().optional().openapi({
|
||||
description: "Filter for statuses using a specific hashtag.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "A list of statuses by the specified account",
|
||||
description: "Statuses posted to the given account.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(Note.schema),
|
||||
},
|
||||
},
|
||||
headers: {
|
||||
Link: {
|
||||
description: "Links to the next and previous pages",
|
||||
schema: z.array(StatusSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
422: reusedResponses[422],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -90,7 +105,7 @@ export default apiRoute((app) =>
|
|||
pinned,
|
||||
} = context.req.valid("query");
|
||||
|
||||
const { objects, link } = await Timeline.getNoteTimeline(
|
||||
const { objects } = await Timeline.getNoteTimeline(
|
||||
and(
|
||||
max_id ? lt(Notes.id, max_id) : undefined,
|
||||
since_id ? gte(Notes.id, since_id) : undefined,
|
||||
|
|
@ -122,9 +137,6 @@ export default apiRoute((app) =>
|
|||
return context.json(
|
||||
await Promise.all(objects.map((note) => note.toApi(otherUser))),
|
||||
200,
|
||||
{
|
||||
link,
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,25 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import {
|
||||
accountNotFound,
|
||||
apiRoute,
|
||||
auth,
|
||||
reusedResponses,
|
||||
withUserParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/unblock",
|
||||
summary: "Unblock user",
|
||||
description: "Unblock a user",
|
||||
summary: "Unblock account",
|
||||
description: "Unblock the given account.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#unblock",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -22,18 +33,21 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated relationship",
|
||||
description:
|
||||
"Successfully unblocked, or account was already not blocked",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Relationship.schema,
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,25 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import {
|
||||
accountNotFound,
|
||||
apiRoute,
|
||||
auth,
|
||||
reusedResponses,
|
||||
withUserParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/unfollow",
|
||||
summary: "Unfollow user",
|
||||
description: "Unfollow a user",
|
||||
summary: "Unfollow account",
|
||||
description: "Unfollow the given account.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#unfollow",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -23,26 +33,21 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated relationship",
|
||||
description:
|
||||
"Successfully unfollowed, or account was already not followed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Relationship.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
500: {
|
||||
description: "Failed to unfollow user during federation",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,25 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import {
|
||||
accountNotFound,
|
||||
apiRoute,
|
||||
auth,
|
||||
reusedResponses,
|
||||
withUserParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/unmute",
|
||||
summary: "Unmute user",
|
||||
description: "Unmute a user",
|
||||
summary: "Unmute account",
|
||||
description: "Unmute the given account.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#unmute",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -22,18 +33,20 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated relationship",
|
||||
description: "Successfully unmuted, or account was already unmuted",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Relationship.schema,
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,25 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import {
|
||||
accountNotFound,
|
||||
apiRoute,
|
||||
auth,
|
||||
reusedResponses,
|
||||
withUserParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/unpin",
|
||||
summary: "Unpin user",
|
||||
description: "Unpin a user from your profile",
|
||||
summary: "Unfeature account from profile",
|
||||
description: "Remove the given account from the user’s featured profiles.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#unpin",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -22,18 +33,21 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated relationship",
|
||||
description:
|
||||
"Successfully unendorsed, or account was already not endorsed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Relationship.schema,
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,10 @@
|
|||
import { apiRoute, auth, qsQuery } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, qsQuery, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { User, db } from "@versia/kit/db";
|
||||
import { RolePermissions, type Users } from "@versia/kit/tables";
|
||||
import { type InferSelectModel, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
const schemas = {
|
||||
query: z.object({
|
||||
id: z
|
||||
.array(z.string().uuid())
|
||||
.min(1)
|
||||
.max(10)
|
||||
.or(z.string().uuid())
|
||||
.transform((v) => (Array.isArray(v) ? v : [v])),
|
||||
}),
|
||||
};
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { FamiliarFollowers as FamiliarFollowersSchema } from "~/classes/schemas/familiar-followers";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
|
|
@ -22,6 +12,10 @@ const route = createRoute({
|
|||
summary: "Get familiar followers",
|
||||
description:
|
||||
"Obtain a list of all accounts that follow a given account, filtered for accounts you follow.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#familiar_followers",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -31,22 +25,32 @@ const route = createRoute({
|
|||
qsQuery(),
|
||||
] as const,
|
||||
request: {
|
||||
query: schemas.query,
|
||||
query: z.object({
|
||||
id: z
|
||||
.array(AccountSchema.shape.id)
|
||||
.min(1)
|
||||
.max(10)
|
||||
.or(AccountSchema.shape.id.transform((v) => [v]))
|
||||
.openapi({
|
||||
description:
|
||||
"Find familiar followers for the provided account IDs.",
|
||||
example: [
|
||||
"f137ce6f-ff5e-4998-b20f-0361ba9be007",
|
||||
"8424c654-5d03-4a1b-bec8-4e87db811b5d",
|
||||
],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Familiar followers",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(
|
||||
z.object({
|
||||
id: z.string().uuid(),
|
||||
accounts: z.array(User.schema),
|
||||
}),
|
||||
),
|
||||
schema: z.array(FamiliarFollowersSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ describe("/api/v1/accounts/id", () => {
|
|||
|
||||
test("should return 404 for non-existent user", async () => {
|
||||
const response = await fakeRequest(
|
||||
`/api/v1/accounts/id?username=${users[0].data.username}-nonexistent`,
|
||||
"/api/v1/accounts/id?username=nonexistent",
|
||||
);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
|
|
|
|||
|
|
@ -1,23 +1,18 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { accountNotFound, apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { User } from "@versia/kit/db";
|
||||
import { RolePermissions, Users } from "@versia/kit/tables";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const schemas = {
|
||||
query: z.object({
|
||||
username: z.string().min(1).max(512).toLowerCase(),
|
||||
}),
|
||||
};
|
||||
import { Account } from "~/classes/schemas/account";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/id",
|
||||
summary: "Get account by username",
|
||||
description: "Get an account by username",
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -25,25 +20,23 @@ const route = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: schemas.query,
|
||||
query: z.object({
|
||||
username: AccountSchema.shape.username.transform((v) =>
|
||||
v.toLowerCase(),
|
||||
),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Account",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: User.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: Account,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
422: reusedResponses[422],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +1,54 @@
|
|||
import { apiRoute, auth, jsonOrForm } from "@/api";
|
||||
import { apiRoute, auth, jsonOrForm, reusedResponses } from "@/api";
|
||||
import { tempmailDomains } from "@/tempmail";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { User } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||
|
||||
const schemas = {
|
||||
json: z.object({
|
||||
username: z.string(),
|
||||
email: z.string().toLowerCase(),
|
||||
password: z.string().optional(),
|
||||
agreement: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.or(z.boolean()),
|
||||
locale: z.string(),
|
||||
reason: z.string(),
|
||||
const schema = z.object({
|
||||
username: z.string().openapi({
|
||||
description: "The desired username for the account",
|
||||
example: "alice",
|
||||
}),
|
||||
};
|
||||
email: z.string().toLowerCase().openapi({
|
||||
description:
|
||||
"The email address to be used for login. Transformed to lowercase.",
|
||||
example: "alice@gmail.com",
|
||||
}),
|
||||
password: z.string().openapi({
|
||||
description: "The password to be used for login",
|
||||
example: "hunter2",
|
||||
}),
|
||||
agreement: zBoolean.openapi({
|
||||
description:
|
||||
"Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE.",
|
||||
example: true,
|
||||
}),
|
||||
locale: z.string().openapi({
|
||||
description:
|
||||
"The language of the confirmation email that will be sent. ISO 639-1 code.",
|
||||
example: "en",
|
||||
}),
|
||||
reason: z.string().optional().openapi({
|
||||
description:
|
||||
"If registrations require manual approval, this text will be reviewed by moderators.",
|
||||
}),
|
||||
});
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts",
|
||||
summary: "Create account",
|
||||
description: "Register a new account",
|
||||
summary: "Register an account",
|
||||
description:
|
||||
"Creates a user and account records. Returns an account access token for the app that initiated the request. The app should save this token for later, and should wait for the user to confirm their account by clicking a link in their email inbox.\n\nA relationship between the OAuth Application and created user account is stored.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#create",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -35,26 +56,27 @@ const route = createRoute({
|
|||
challenge: true,
|
||||
}),
|
||||
jsonOrForm(),
|
||||
],
|
||||
] as const,
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schemas.json,
|
||||
schema,
|
||||
},
|
||||
"multipart/form-data": {
|
||||
schema: schemas.json,
|
||||
schema,
|
||||
},
|
||||
"application/x-www-form-urlencoded": {
|
||||
schema: schemas.json,
|
||||
schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Account created",
|
||||
description: "Token for the created account",
|
||||
},
|
||||
401: reusedResponses[401],
|
||||
422: {
|
||||
description: "Validation failed",
|
||||
content: {
|
||||
|
|
@ -114,7 +136,10 @@ const route = createRoute({
|
|||
),
|
||||
reason: z.array(
|
||||
z.object({
|
||||
error: z.enum(["ERR_BLANK"]),
|
||||
error: z.enum([
|
||||
"ERR_BLANK",
|
||||
"ERR_TOO_LONG",
|
||||
]),
|
||||
description: z.string(),
|
||||
}),
|
||||
),
|
||||
|
|
@ -289,6 +314,14 @@ export default apiRoute((app) =>
|
|||
});
|
||||
}
|
||||
|
||||
// Check if reason is too long
|
||||
if ((form.reason?.length ?? 0) > 10_000) {
|
||||
errors.details.reason.push({
|
||||
error: "ERR_TOO_LONG",
|
||||
description: `is too long (maximum is ${10_000} characters)`,
|
||||
});
|
||||
}
|
||||
|
||||
// If any errors are present, return them
|
||||
if (Object.values(errors.details).some((value) => value.length > 0)) {
|
||||
// Error is something like "Validation failed: Password can't be blank, Username must contain only letters, numbers and underscores, Agreement must be accepted"
|
||||
|
|
@ -314,9 +347,9 @@ export default apiRoute((app) =>
|
|||
}
|
||||
|
||||
await User.fromDataLocal({
|
||||
username: username ?? "",
|
||||
password: password ?? "",
|
||||
email: email ?? "",
|
||||
username,
|
||||
password,
|
||||
email,
|
||||
});
|
||||
|
||||
return context.text("", 200);
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ describe("/api/v1/accounts/lookup", () => {
|
|||
);
|
||||
});
|
||||
|
||||
test("should automatically lowercase the acct", async () => {
|
||||
test("should require exact case", async () => {
|
||||
const response = await fakeRequest(
|
||||
`/api/v1/accounts/lookup?acct=${users[0].data.username.toUpperCase()}`,
|
||||
{
|
||||
|
|
@ -44,17 +44,6 @@ describe("/api/v1/accounts/lookup", () => {
|
|||
},
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = (await response.json()) as ApiAccount[];
|
||||
expect(data).toEqual(
|
||||
expect.objectContaining({
|
||||
id: users[0].id,
|
||||
username: users[0].data.username,
|
||||
display_name: users[0].data.displayName,
|
||||
avatar: expect.any(String),
|
||||
header: expect.any(String),
|
||||
}),
|
||||
);
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
import { apiRoute, auth, parseUserAddress } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import {
|
||||
accountNotFound,
|
||||
apiRoute,
|
||||
auth,
|
||||
parseUserAddress,
|
||||
reusedResponses,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Instance, User } from "@versia/kit/db";
|
||||
import { RolePermissions, Users } from "@versia/kit/tables";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { Account } from "~/classes/schemas/account";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const schemas = {
|
||||
query: z.object({
|
||||
acct: z.string().min(1).max(512).toLowerCase(),
|
||||
}),
|
||||
};
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/lookup",
|
||||
summary: "Lookup account",
|
||||
description: "Lookup an account by acct",
|
||||
summary: "Lookup account ID from Webfinger address",
|
||||
description:
|
||||
"Quickly lookup a username to see if it is available, skipping WebFinger resolution.",
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -26,33 +27,24 @@ const route = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: schemas.query,
|
||||
query: z.object({
|
||||
acct: AccountSchema.shape.acct.openapi({
|
||||
description: "The username or Webfinger address to lookup.",
|
||||
example: "lexi@beta.versia.social",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Account",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: User.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
422: {
|
||||
description: "Invalid parameter",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: Account,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
422: reusedResponses[422],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -64,12 +56,8 @@ export default apiRoute((app) =>
|
|||
// Check if acct is matching format username@domain.com or @username@domain.com
|
||||
const { username, domain } = parseUserAddress(acct);
|
||||
|
||||
if (!username) {
|
||||
throw new Error("Invalid username");
|
||||
}
|
||||
|
||||
// User is local
|
||||
if (!domain || domain === new URL(config.http.base_url).host) {
|
||||
if (!domain || domain === config.http.base_url.host) {
|
||||
const account = await User.fromSql(
|
||||
and(eq(Users.username, username), isNull(Users.instanceId)),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,21 @@
|
|||
import { apiRoute, auth, qsQuery } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, qsQuery, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
|
||||
const schemas = {
|
||||
query: z.object({
|
||||
id: z.array(z.string().uuid()).min(1).max(10).or(z.string().uuid()),
|
||||
}),
|
||||
};
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/relationships",
|
||||
summary: "Get relationships",
|
||||
description: "Get relationships by account ID",
|
||||
summary: "Check relationships to other accounts",
|
||||
description:
|
||||
"Find out whether a given account is followed, blocked, muted, etc.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#relationships",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -24,23 +25,45 @@ const route = createRoute({
|
|||
qsQuery(),
|
||||
] as const,
|
||||
request: {
|
||||
query: schemas.query,
|
||||
query: z.object({
|
||||
id: z
|
||||
.array(AccountSchema.shape.id)
|
||||
.min(1)
|
||||
.max(10)
|
||||
.or(AccountSchema.shape.id.transform((v) => [v]))
|
||||
.openapi({
|
||||
description:
|
||||
"Check relationships for the provided account IDs.",
|
||||
example: [
|
||||
"f137ce6f-ff5e-4998-b20f-0361ba9be007",
|
||||
"8424c654-5d03-4a1b-bec8-4e87db811b5d",
|
||||
],
|
||||
}),
|
||||
with_suspended: zBoolean.default(false).openapi({
|
||||
description:
|
||||
"Whether relationships should be returned for suspended users",
|
||||
example: false,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Relationships",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(Relationship.schema),
|
||||
schema: z.array(RelationshipSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
|
||||
// TODO: Implement with_suspended
|
||||
const { id } = context.req.valid("query");
|
||||
|
||||
const ids = Array.isArray(id) ? id : [id];
|
||||
|
|
|
|||
|
|
@ -1,33 +1,22 @@
|
|||
import { apiRoute, auth, parseUserAddress, userAddressValidator } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, parseUserAddress } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { User } from "@versia/kit/db";
|
||||
import { RolePermissions, Users } from "@versia/kit/tables";
|
||||
import { eq, ilike, not, or, sql } from "drizzle-orm";
|
||||
import stringComparison from "string-comparison";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const schemas = {
|
||||
query: z.object({
|
||||
q: z.string().min(1).max(512).regex(userAddressValidator),
|
||||
limit: z.coerce.number().int().min(1).max(80).default(40),
|
||||
offset: z.coerce.number().int().optional(),
|
||||
resolve: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.optional(),
|
||||
following: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.optional(),
|
||||
}),
|
||||
};
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||
|
||||
export const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/search",
|
||||
summary: "Search accounts",
|
||||
description: "Search for accounts",
|
||||
summary: "Search for matching accounts",
|
||||
description: "Search for matching accounts by username or display name.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#search",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -36,14 +25,38 @@ export const route = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: schemas.query,
|
||||
query: z.object({
|
||||
q: AccountSchema.shape.username
|
||||
.or(AccountSchema.shape.acct)
|
||||
.openapi({
|
||||
description: "Search query for accounts.",
|
||||
example: "username",
|
||||
}),
|
||||
limit: z.coerce.number().int().min(1).max(80).default(40).openapi({
|
||||
description: "Maximum number of results.",
|
||||
example: 40,
|
||||
}),
|
||||
offset: z.coerce.number().int().default(0).openapi({
|
||||
description: "Skip the first n results.",
|
||||
example: 0,
|
||||
}),
|
||||
resolve: zBoolean.default(false).openapi({
|
||||
description:
|
||||
"Attempt WebFinger lookup. Use this when q is an exact address.",
|
||||
example: false,
|
||||
}),
|
||||
following: zBoolean.default(false).openapi({
|
||||
description: "Limit the search to users you are following.",
|
||||
example: false,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Accounts",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(User.schema),
|
||||
schema: z.array(AccountSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,141 +1,25 @@
|
|||
import { apiRoute, auth, jsonOrForm } from "@/api";
|
||||
import { apiRoute, auth, jsonOrForm, reusedResponses } from "@/api";
|
||||
import { mergeAndDeduplicate } from "@/lib";
|
||||
import { sanitizedHtmlStrip } from "@/sanitization";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Emoji, User } from "@versia/kit/db";
|
||||
import { RolePermissions, Users } from "@versia/kit/tables";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { contentToHtml } from "~/classes/functions/status";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
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.object({
|
||||
display_name: z
|
||||
.string()
|
||||
.min(3)
|
||||
.trim()
|
||||
.max(config.validation.max_displayname_size)
|
||||
.refine(
|
||||
(s) =>
|
||||
!config.filters.displayname.some((filter) =>
|
||||
s.match(filter),
|
||||
),
|
||||
"Display name contains blocked words",
|
||||
)
|
||||
.optional(),
|
||||
username: z
|
||||
.string()
|
||||
.min(3)
|
||||
.trim()
|
||||
.max(config.validation.max_username_size)
|
||||
.toLowerCase()
|
||||
.regex(
|
||||
/^[a-z0-9_-]+$/,
|
||||
"Username can only contain letters, numbers, underscores and hyphens",
|
||||
)
|
||||
.refine(
|
||||
(s) =>
|
||||
!config.filters.username.some((filter) => s.match(filter)),
|
||||
"Username contains blocked words",
|
||||
)
|
||||
.optional(),
|
||||
note: z
|
||||
.string()
|
||||
.min(0)
|
||||
.max(config.validation.max_bio_size)
|
||||
.trim()
|
||||
.refine(
|
||||
(s) => !config.filters.bio.some((filter) => s.match(filter)),
|
||||
"Bio contains blocked words",
|
||||
)
|
||||
.optional(),
|
||||
avatar: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(2000)
|
||||
.url()
|
||||
.transform((a) => new URL(a))
|
||||
.or(
|
||||
z
|
||||
.instanceof(File)
|
||||
.refine(
|
||||
(v) => v.size <= config.validation.max_avatar_size,
|
||||
`Avatar must be less than ${config.validation.max_avatar_size} bytes`,
|
||||
),
|
||||
)
|
||||
.optional(),
|
||||
header: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(2000)
|
||||
.url()
|
||||
.transform((v) => new URL(v))
|
||||
.or(
|
||||
z
|
||||
.instanceof(File)
|
||||
.refine(
|
||||
(v) => v.size <= config.validation.max_header_size,
|
||||
`Header must be less than ${config.validation.max_header_size} bytes`,
|
||||
),
|
||||
)
|
||||
.optional(),
|
||||
locked: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.optional(),
|
||||
bot: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.optional(),
|
||||
discoverable: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.optional(),
|
||||
source: z
|
||||
.object({
|
||||
privacy: z
|
||||
.enum(["public", "unlisted", "private", "direct"])
|
||||
.optional(),
|
||||
sensitive: z
|
||||
.string()
|
||||
.transform((v) =>
|
||||
["true", "1", "on"].includes(v.toLowerCase()),
|
||||
)
|
||||
.optional(),
|
||||
language: z
|
||||
.enum(ISO6391.getAllCodes() as [string, ...string[]])
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
fields_attributes: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(config.validation.max_field_name_size),
|
||||
value: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(config.validation.max_field_value_size),
|
||||
}),
|
||||
)
|
||||
.max(config.validation.max_field_count)
|
||||
.optional(),
|
||||
}),
|
||||
};
|
||||
|
||||
const route = createRoute({
|
||||
method: "patch",
|
||||
path: "/api/v1/accounts/update_credentials",
|
||||
summary: "Update credentials",
|
||||
description: "Update user credentials",
|
||||
summary: "Update account credentials",
|
||||
description: "Update the user’s display and preferences.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#update_credentials",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -148,7 +32,121 @@ const route = createRoute({
|
|||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schemas.json,
|
||||
schema: z
|
||||
.object({
|
||||
display_name:
|
||||
AccountSchema.shape.display_name.openapi({
|
||||
description:
|
||||
"The display name to use for the profile.",
|
||||
example: "Lexi",
|
||||
}),
|
||||
username: AccountSchema.shape.username.openapi({
|
||||
description:
|
||||
"The username to use for the profile.",
|
||||
example: "lexi",
|
||||
}),
|
||||
note: AccountSchema.shape.note.openapi({
|
||||
description:
|
||||
"The account bio. Markdown is supported.",
|
||||
}),
|
||||
avatar: z
|
||||
.string()
|
||||
.url()
|
||||
.transform((a) => new URL(a))
|
||||
.openapi({
|
||||
description: "Avatar image URL",
|
||||
})
|
||||
.or(
|
||||
z
|
||||
.instanceof(File)
|
||||
.refine(
|
||||
(v) =>
|
||||
v.size <=
|
||||
config.validation
|
||||
.max_avatar_size,
|
||||
`Avatar must be less than ${config.validation.max_avatar_size} bytes`,
|
||||
)
|
||||
.openapi({
|
||||
description:
|
||||
"Avatar image encoded using multipart/form-data",
|
||||
}),
|
||||
),
|
||||
header: z
|
||||
.string()
|
||||
.url()
|
||||
.transform((v) => new URL(v))
|
||||
.openapi({
|
||||
description: "Header image URL",
|
||||
})
|
||||
.or(
|
||||
z
|
||||
.instanceof(File)
|
||||
.refine(
|
||||
(v) =>
|
||||
v.size <=
|
||||
config.validation
|
||||
.max_header_size,
|
||||
`Header must be less than ${config.validation.max_header_size} bytes`,
|
||||
)
|
||||
.openapi({
|
||||
description:
|
||||
"Header image encoded using multipart/form-data",
|
||||
}),
|
||||
),
|
||||
locked: AccountSchema.shape.locked.openapi({
|
||||
description:
|
||||
"Whether manual approval of follow requests is required.",
|
||||
}),
|
||||
bot: AccountSchema.shape.bot.openapi({
|
||||
description:
|
||||
"Whether the account has a bot flag.",
|
||||
}),
|
||||
discoverable:
|
||||
AccountSchema.shape.discoverable.openapi({
|
||||
description:
|
||||
"Whether the account should be shown in the profile directory.",
|
||||
}),
|
||||
// TODO: Implement :(
|
||||
hide_collections: zBoolean.openapi({
|
||||
description:
|
||||
"Whether to hide followers and followed accounts.",
|
||||
}),
|
||||
// TODO: Implement :(
|
||||
indexable: zBoolean.openapi({
|
||||
description:
|
||||
"Whether public posts should be searchable to anyone.",
|
||||
}),
|
||||
// TODO: Implement :(
|
||||
attribution_domains: z.array(z.string()).openapi({
|
||||
description:
|
||||
"Domains of websites allowed to credit the account.",
|
||||
example: ["cnn.com", "myblog.com"],
|
||||
}),
|
||||
source: z
|
||||
.object({
|
||||
privacy:
|
||||
AccountSchema.shape.source.unwrap()
|
||||
.shape.privacy,
|
||||
sensitive:
|
||||
AccountSchema.shape.source.unwrap()
|
||||
.shape.sensitive,
|
||||
language:
|
||||
AccountSchema.shape.source.unwrap()
|
||||
.shape.language,
|
||||
})
|
||||
.partial(),
|
||||
fields_attributes: z
|
||||
.array(
|
||||
z.object({
|
||||
name: AccountSchema.shape.fields.element
|
||||
.shape.name,
|
||||
value: AccountSchema.shape.fields
|
||||
.element.shape.value,
|
||||
}),
|
||||
)
|
||||
.max(config.validation.max_field_count),
|
||||
})
|
||||
.partial(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -158,27 +156,11 @@ const route = createRoute({
|
|||
description: "Updated user",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: User.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
422: {
|
||||
description: "Validation error",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
500: {
|
||||
description: "Couldn't edit user",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: AccountSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -325,6 +307,7 @@ export default apiRoute((app) =>
|
|||
self.source.fields.push({
|
||||
name: field.name,
|
||||
value: field.value,
|
||||
verified_at: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { User } from "@versia/kit/db";
|
||||
import { Account } from "~/classes/schemas/account";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/verify_credentials",
|
||||
summary: "Verify credentials",
|
||||
description: "Get your own account information",
|
||||
summary: "Verify account credentials",
|
||||
description: "Test to make sure that the user token works.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#verify_credentials",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -15,13 +19,16 @@ const route = createRoute({
|
|||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description: "Account",
|
||||
// TODO: Implement CredentialAccount
|
||||
description:
|
||||
"Note the extra source property, which is not visible on accounts other than your own. Also note that plain-text is used within source and HTML is used for their corresponding properties such as note and fields.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: User.schema,
|
||||
schema: Account,
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,63 +1,61 @@
|
|||
import { apiRoute, jsonOrForm } from "@/api";
|
||||
import { apiRoute, jsonOrForm, reusedResponses } from "@/api";
|
||||
import { randomString } from "@/math";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Application } from "@versia/kit/db";
|
||||
import { z } from "zod";
|
||||
|
||||
const schemas = {
|
||||
json: z.object({
|
||||
client_name: z.string().trim().min(1).max(100),
|
||||
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)),
|
||||
}),
|
||||
};
|
||||
import {
|
||||
Application as ApplicationSchema,
|
||||
CredentialApplication as CredentialApplicationSchema,
|
||||
} from "~/classes/schemas/application";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/apps",
|
||||
summary: "Create app",
|
||||
description: "Create an OAuth2 app",
|
||||
summary: "Create an application",
|
||||
description: "Create a new application to obtain OAuth2 credentials.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/apps/#create",
|
||||
},
|
||||
tags: ["Apps"],
|
||||
middleware: [jsonOrForm()],
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"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: {
|
||||
200: {
|
||||
description: "App",
|
||||
description:
|
||||
"Store the client_id and client_secret in your cache, as these will be used to obtain OAuth tokens.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
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(),
|
||||
}),
|
||||
schema: CredentialApplicationSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
422: reusedResponses[422],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -67,25 +65,14 @@ export default apiRoute((app) =>
|
|||
context.req.valid("json");
|
||||
|
||||
const app = await Application.insert({
|
||||
name: client_name || "",
|
||||
redirectUri: decodeURI(redirect_uris) || "",
|
||||
scopes: scopes || "read",
|
||||
website: website || null,
|
||||
name: client_name,
|
||||
redirectUri: redirect_uris.join("\n"),
|
||||
scopes: scopes.join(" "),
|
||||
website,
|
||||
clientId: randomString(32, "base64url"),
|
||||
secret: randomString(64, "base64url"),
|
||||
});
|
||||
|
||||
return context.json(
|
||||
{
|
||||
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,
|
||||
);
|
||||
return context.json(app.toApiCredential(), 200);
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Application } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
import { Application as ApplicationSchema } from "~/classes/schemas/application";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/apps/verify_credentials",
|
||||
summary: "Verify credentials",
|
||||
description: "Get your own application information",
|
||||
summary: "Verify your app works",
|
||||
description: "Confirm that the app’s OAuth2 credentials work.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/apps/#verify_credentials",
|
||||
},
|
||||
tags: ["Apps"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -18,21 +22,15 @@ const route = createRoute({
|
|||
] as const,
|
||||
responses: {
|
||||
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: {
|
||||
"application/json": {
|
||||
schema: Application.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description: "Unauthorized",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: ApplicationSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -52,13 +50,6 @@ export default apiRoute((app) =>
|
|||
throw new ApiError(401, "Application not found");
|
||||
}
|
||||
|
||||
return context.json(
|
||||
{
|
||||
...application.toApi(),
|
||||
redirect_uris: application.data.redirectUri,
|
||||
scopes: application.data.scopes,
|
||||
},
|
||||
200,
|
||||
);
|
||||
return context.json(application.toApi(), 200);
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,24 +1,19 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Timeline, User } from "@versia/kit/db";
|
||||
import { apiRoute, auth, reusedResponses } 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 { z } from "zod";
|
||||
|
||||
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),
|
||||
}),
|
||||
};
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/blocks",
|
||||
summary: "Get blocks",
|
||||
description: "Get users you have blocked",
|
||||
summary: "View your blocks.",
|
||||
description: "View blocked users.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/blocks/#get",
|
||||
},
|
||||
tags: ["Blocks"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -27,17 +22,50 @@ const route = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
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: {
|
||||
200: {
|
||||
description: "Blocks",
|
||||
description: "List of blocked users",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(User.schema),
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,14 +1,19 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Emoji } from "@versia/kit/db";
|
||||
import { Emojis, RolePermissions } from "@versia/kit/tables";
|
||||
import { and, eq, isNull, or } from "drizzle-orm";
|
||||
import { CustomEmoji as CustomEmojiSchema } from "~/classes/schemas/emoji";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/custom_emojis",
|
||||
summary: "Get custom emojis",
|
||||
description: "Get custom emojis",
|
||||
summary: "View all custom emoji",
|
||||
description: "Returns custom emojis that are available on the server.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/custom_emojis/#get",
|
||||
},
|
||||
tags: ["Emojis"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -17,13 +22,14 @@ const route = createRoute({
|
|||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description: "Emojis",
|
||||
description: "List of custom emojis",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(Emoji.schema),
|
||||
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 { createRoute } from "@hono/zod-openapi";
|
||||
import { Emoji } from "@versia/kit/db";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { CustomEmoji as CustomEmojiSchema } from "~/classes/schemas/emoji";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const schemas = {
|
||||
param: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
json: z
|
||||
.object({
|
||||
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
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(2000)
|
||||
.url()
|
||||
.transform((a) => new URL(a))
|
||||
.or(
|
||||
z
|
||||
.instanceof(File)
|
||||
.refine(
|
||||
(v) => v.size <= config.validation.max_emoji_size,
|
||||
`Emoji must be less than ${config.validation.max_emoji_size} bytes`,
|
||||
),
|
||||
),
|
||||
category: z.string().max(64).optional(),
|
||||
alt: z
|
||||
.string()
|
||||
.max(config.validation.max_emoji_description_size)
|
||||
.optional(),
|
||||
global: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.or(z.boolean())
|
||||
.optional(),
|
||||
})
|
||||
.partial(),
|
||||
};
|
||||
const schema = z
|
||||
.object({
|
||||
shortcode: CustomEmojiSchema.shape.shortcode,
|
||||
element: z
|
||||
.string()
|
||||
.url()
|
||||
.transform((a) => new URL(a))
|
||||
.openapi({
|
||||
description: "Emoji image URL",
|
||||
})
|
||||
.or(
|
||||
z
|
||||
.instanceof(File)
|
||||
.openapi({
|
||||
description:
|
||||
"Emoji image encoded using multipart/form-data",
|
||||
})
|
||||
.refine(
|
||||
(v) => v.size <= config.validation.max_emoji_size,
|
||||
`Emoji must be less than ${config.validation.max_emoji_size} bytes`,
|
||||
),
|
||||
),
|
||||
category: CustomEmojiSchema.shape.category.optional(),
|
||||
alt: CustomEmojiSchema.shape.description.optional(),
|
||||
global: CustomEmojiSchema.shape.global.default(false),
|
||||
})
|
||||
.partial();
|
||||
|
||||
const routeGet = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/emojis/{id}",
|
||||
summary: "Get emoji data",
|
||||
summary: "Get emoji",
|
||||
description: "Retrieves a custom emoji from database by ID.",
|
||||
tags: ["Emojis"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermissions.ViewEmojis],
|
||||
}),
|
||||
withEmojiParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: CustomEmojiSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Emoji",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Emoji.schema,
|
||||
schema: CustomEmojiSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
404: {
|
||||
description: "Emoji not found",
|
||||
content: {
|
||||
|
|
@ -83,6 +76,7 @@ const routeGet = createRoute({
|
|||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -90,6 +84,8 @@ const routePatch = createRoute({
|
|||
method: "patch",
|
||||
path: "/api/v1/emojis/{id}",
|
||||
summary: "Modify emoji",
|
||||
description: "Edit image or metadata of an emoji.",
|
||||
tags: ["Emojis"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -99,19 +95,22 @@ const routePatch = createRoute({
|
|||
],
|
||||
}),
|
||||
jsonOrForm(),
|
||||
withEmojiParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: CustomEmojiSchema.shape.id,
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schemas.json,
|
||||
schema,
|
||||
},
|
||||
"application/x-www-form-urlencoded": {
|
||||
schema: schemas.json,
|
||||
schema,
|
||||
},
|
||||
"multipart/form-data": {
|
||||
schema: schemas.json,
|
||||
schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -121,13 +120,12 @@ const routePatch = createRoute({
|
|||
description: "Emoji modified",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Emoji.schema,
|
||||
schema: CustomEmojiSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
403: {
|
||||
description: "Insufficient credentials",
|
||||
description: "Insufficient permissions",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
|
|
@ -142,14 +140,7 @@ const routePatch = createRoute({
|
|||
},
|
||||
},
|
||||
},
|
||||
422: {
|
||||
description: "Invalid form data",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -157,6 +148,8 @@ const routeDelete = createRoute({
|
|||
method: "delete",
|
||||
path: "/api/v1/emojis/{id}",
|
||||
summary: "Delete emoji",
|
||||
description: "Delete a custom emoji from the database.",
|
||||
tags: ["Emojis"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -165,15 +158,17 @@ const routeDelete = createRoute({
|
|||
RolePermissions.ViewEmojis,
|
||||
],
|
||||
}),
|
||||
withEmojiParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: CustomEmojiSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
204: {
|
||||
description: "Emoji deleted",
|
||||
},
|
||||
|
||||
404: {
|
||||
description: "Emoji not found",
|
||||
content: {
|
||||
|
|
@ -186,15 +181,9 @@ const routeDelete = createRoute({
|
|||
});
|
||||
|
||||
export default apiRoute((app) => {
|
||||
app.openapi(routeGet, async (context) => {
|
||||
const { id } = context.req.valid("param");
|
||||
app.openapi(routeGet, (context) => {
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const emoji = await Emoji.fromId(id);
|
||||
|
||||
if (!emoji) {
|
||||
throw new ApiError(404, "Emoji not found");
|
||||
}
|
||||
const emoji = context.get("emoji");
|
||||
|
||||
// Don't leak non-global emojis to non-admins
|
||||
if (
|
||||
|
|
@ -208,14 +197,8 @@ export default apiRoute((app) => {
|
|||
});
|
||||
|
||||
app.openapi(routePatch, async (context) => {
|
||||
const { id } = context.req.valid("param");
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const emoji = await Emoji.fromId(id);
|
||||
|
||||
if (!emoji) {
|
||||
throw new ApiError(404, "Emoji not found");
|
||||
}
|
||||
const emoji = context.get("emoji");
|
||||
|
||||
// Check if user is admin
|
||||
if (
|
||||
|
|
@ -246,7 +229,7 @@ export default apiRoute((app) => {
|
|||
}
|
||||
|
||||
if (element) {
|
||||
// Check of emoji is an image
|
||||
// Check if emoji is an image
|
||||
const contentType =
|
||||
element instanceof File
|
||||
? element.type
|
||||
|
|
@ -283,14 +266,8 @@ export default apiRoute((app) => {
|
|||
});
|
||||
|
||||
app.openapi(routeDelete, async (context) => {
|
||||
const { id } = context.req.valid("param");
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const emoji = await Emoji.fromId(id);
|
||||
|
||||
if (!emoji) {
|
||||
throw new ApiError(404, "Emoji not found");
|
||||
}
|
||||
const emoji = context.get("emoji");
|
||||
|
||||
// Check if user is admin
|
||||
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 { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Emoji, Media } from "@versia/kit/db";
|
||||
import { Emojis, RolePermissions } from "@versia/kit/tables";
|
||||
import { and, eq, isNull, or } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { CustomEmoji as CustomEmojiSchema } from "~/classes/schemas/emoji";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const schemas = {
|
||||
json: z.object({
|
||||
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
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(2000)
|
||||
.url()
|
||||
.transform((a) => new URL(a))
|
||||
.or(
|
||||
z
|
||||
.instanceof(File)
|
||||
.refine(
|
||||
(v) => v.size <= config.validation.max_emoji_size,
|
||||
`Emoji must be less than ${config.validation.max_emoji_size} bytes`,
|
||||
),
|
||||
),
|
||||
category: z.string().max(64).optional(),
|
||||
alt: z
|
||||
.string()
|
||||
.max(config.validation.max_emoji_description_size)
|
||||
.optional(),
|
||||
global: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.or(z.boolean())
|
||||
.optional(),
|
||||
}),
|
||||
};
|
||||
const schema = z.object({
|
||||
shortcode: CustomEmojiSchema.shape.shortcode,
|
||||
element: z
|
||||
.string()
|
||||
.url()
|
||||
.transform((a) => new URL(a))
|
||||
.openapi({
|
||||
description: "Emoji image URL",
|
||||
})
|
||||
.or(
|
||||
z
|
||||
.instanceof(File)
|
||||
.openapi({
|
||||
description:
|
||||
"Emoji image encoded using multipart/form-data",
|
||||
})
|
||||
.refine(
|
||||
(v) => v.size <= config.validation.max_emoji_size,
|
||||
`Emoji must be less than ${config.validation.max_emoji_size} bytes`,
|
||||
),
|
||||
),
|
||||
category: CustomEmojiSchema.shape.category.optional(),
|
||||
alt: CustomEmojiSchema.shape.description.optional(),
|
||||
global: CustomEmojiSchema.shape.global.default(false),
|
||||
});
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/emojis",
|
||||
summary: "Upload emoji",
|
||||
description: "Upload an emoji",
|
||||
description: "Upload a new emoji to the server.",
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -67,13 +53,13 @@ const route = createRoute({
|
|||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schemas.json,
|
||||
schema,
|
||||
},
|
||||
"multipart/form-data": {
|
||||
schema: schemas.json,
|
||||
schema,
|
||||
},
|
||||
"application/x-www-form-urlencoded": {
|
||||
schema: schemas.json,
|
||||
schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -83,19 +69,11 @@ const route = createRoute({
|
|||
description: "Uploaded emoji",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Emoji.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
422: {
|
||||
description: "Invalid data",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: CustomEmojiSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -145,10 +123,10 @@ export default apiRoute((app) =>
|
|||
const media =
|
||||
element instanceof File
|
||||
? await Media.fromFile(element, {
|
||||
description: alt,
|
||||
description: alt ?? undefined,
|
||||
})
|
||||
: await Media.fromUrl(element, {
|
||||
description: alt,
|
||||
description: alt ?? undefined,
|
||||
});
|
||||
|
||||
const emoji = await Emoji.insert({
|
||||
|
|
|
|||
|
|
@ -1,23 +1,19 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Note, Timeline } from "@versia/kit/db";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { Notes, RolePermissions } from "@versia/kit/tables";
|
||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
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),
|
||||
}),
|
||||
};
|
||||
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
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: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -25,17 +21,50 @@ const route = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
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: {
|
||||
200: {
|
||||
description: "Favourites",
|
||||
description: "List of favourited statuses",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(Note.schema),
|
||||
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 { createRoute } from "@hono/zod-openapi";
|
||||
import { accountNotFound, apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Relationship, User } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const schemas = {
|
||||
param: z.object({
|
||||
account_id: z.string().uuid(),
|
||||
}),
|
||||
};
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
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: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -23,26 +21,22 @@ const route = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
account_id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Relationship",
|
||||
description:
|
||||
"Your Relationship with this account should be updated so that you are followed_by this account.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Relationship.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
404: {
|
||||
description: "Account not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { accountNotFound, apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Relationship, User } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const schemas = {
|
||||
param: z.object({
|
||||
account_id: z.string().uuid(),
|
||||
}),
|
||||
};
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/follow_requests/{account_id}/reject",
|
||||
summary: "Reject follow request",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/follow_requests/#reject",
|
||||
},
|
||||
tags: ["Follows"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -23,26 +21,22 @@ const route = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
account_id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Relationship",
|
||||
description:
|
||||
"Your Relationship with this account should be unchanged.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Relationship.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
404: {
|
||||
description: "Account not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: accountNotFound,
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,19 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Timeline, User } from "@versia/kit/db";
|
||||
import { apiRoute, auth, reusedResponses } 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 { z } from "zod";
|
||||
|
||||
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),
|
||||
}),
|
||||
};
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
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: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -25,17 +21,51 @@ const route = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
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: {
|
||||
200: {
|
||||
description: "Follow requests",
|
||||
description:
|
||||
"List of accounts that have requested to follow the user",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(User.schema),
|
||||
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,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,11 @@ export default apiRoute((app) =>
|
|||
|
||||
const userCount = await User.getCount();
|
||||
|
||||
const contactAccount = await User.fromSql(
|
||||
and(isNull(Users.instanceId), eq(Users.isAdmin, true)),
|
||||
);
|
||||
// Get first admin, or first user if no admin exists
|
||||
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 +65,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 +99,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 +119,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 +139,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,36 +1,26 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import type { Marker as ApiMarker } from "@versia/client/types";
|
||||
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";
|
||||
import { type SQL, and, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { Marker as MarkerSchema } from "~/classes/schemas/marker";
|
||||
import { Notification as NotificationSchema } from "~/classes/schemas/notification";
|
||||
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||
|
||||
const schemas = {
|
||||
markers: z.object({
|
||||
home: z
|
||||
.object({
|
||||
last_read_id: z.string().uuid(),
|
||||
version: z.number(),
|
||||
updated_at: z.string(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
notifications: z
|
||||
.object({
|
||||
last_read_id: z.string().uuid(),
|
||||
version: z.number(),
|
||||
updated_at: z.string(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
}),
|
||||
};
|
||||
const MarkerResponseSchema = z.object({
|
||||
notifications: MarkerSchema.optional(),
|
||||
home: MarkerSchema.optional(),
|
||||
});
|
||||
|
||||
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,
|
||||
|
|
@ -42,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: {
|
||||
|
|
@ -51,17 +45,23 @@ const routeGet = createRoute({
|
|||
description: "Markers",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schemas.markers,
|
||||
schema: MarkerResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
...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,
|
||||
|
|
@ -69,35 +69,43 @@ const routePost = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: z.object({
|
||||
"home[last_read_id]": z.string().uuid().optional(),
|
||||
"notifications[last_read_id]": z.string().uuid().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: {
|
||||
description: "Markers",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schemas.markers,
|
||||
schema: MarkerResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
...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);
|
||||
}
|
||||
|
||||
const markers: ApiMarker = {
|
||||
const markers: z.infer<typeof MarkerResponseSchema> = {
|
||||
home: undefined,
|
||||
notifications: undefined,
|
||||
};
|
||||
|
|
@ -161,7 +169,7 @@ export default apiRoute((app) => {
|
|||
} = context.req.valid("query");
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const markers: ApiMarker = {
|
||||
const markers: z.infer<typeof MarkerResponseSchema> = {
|
||||
home: undefined,
|
||||
notifications: undefined,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,30 +1,21 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
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 { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/packages/config-manager/index.ts";
|
||||
import { Attachment as AttachmentSchema } from "~/classes/schemas/attachment";
|
||||
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: Media.schema,
|
||||
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,25 +88,28 @@ 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: Media.schema,
|
||||
schema: AttachmentSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
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 { createRoute } from "@hono/zod-openapi";
|
||||
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 { z } from "zod";
|
||||
import { config } from "~/packages/config-manager/index.ts";
|
||||
import { Attachment as AttachmentSchema } from "~/classes/schemas/attachment";
|
||||
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: Media.schema,
|
||||
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,23 +1,19 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Timeline, User } from "@versia/kit/db";
|
||||
import { apiRoute, auth, reusedResponses } 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 { z } from "zod";
|
||||
|
||||
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),
|
||||
}),
|
||||
};
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
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: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -26,17 +22,50 @@ const route = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
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: {
|
||||
200: {
|
||||
description: "Muted users",
|
||||
description: "List of muted users",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(User.schema),
|
||||
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,14 +1,19 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
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 { z } from "zod";
|
||||
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,
|
||||
|
|
@ -18,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,15 +1,20 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
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 { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { Notification as NotificationSchema } from "~/classes/schemas/notification.ts";
|
||||
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: Notification.schema,
|
||||
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,7 +1,6 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
|
||||
const schemas = {
|
||||
query: z.object({
|
||||
|
|
@ -27,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,79 +1,20 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Notification, Timeline } from "@versia/kit/db";
|
||||
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 { z } from "zod";
|
||||
|
||||
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(15),
|
||||
exclude_types: z
|
||||
.enum([
|
||||
"mention",
|
||||
"status",
|
||||
"follow",
|
||||
"follow_request",
|
||||
"reblog",
|
||||
"poll",
|
||||
"favourite",
|
||||
"update",
|
||||
"admin.sign_up",
|
||||
"admin.report",
|
||||
"chat",
|
||||
"pleroma:chat_mention",
|
||||
"pleroma:emoji_reaction",
|
||||
"pleroma:event_reminder",
|
||||
"pleroma:participation_request",
|
||||
"pleroma:participation_accepted",
|
||||
"move",
|
||||
"group_reblog",
|
||||
"group_favourite",
|
||||
"user_approved",
|
||||
])
|
||||
.array()
|
||||
.optional(),
|
||||
types: z
|
||||
.enum([
|
||||
"mention",
|
||||
"status",
|
||||
"follow",
|
||||
"follow_request",
|
||||
"reblog",
|
||||
"poll",
|
||||
"favourite",
|
||||
"update",
|
||||
"admin.sign_up",
|
||||
"admin.report",
|
||||
"chat",
|
||||
"pleroma:chat_mention",
|
||||
"pleroma:emoji_reaction",
|
||||
"pleroma:event_reminder",
|
||||
"pleroma:participation_request",
|
||||
"pleroma:participation_accepted",
|
||||
"move",
|
||||
"group_reblog",
|
||||
"group_favourite",
|
||||
"user_approved",
|
||||
])
|
||||
.array()
|
||||
.optional(),
|
||||
account_id: z.string().uuid().optional(),
|
||||
})
|
||||
.refine((val) => {
|
||||
// Can't use both exclude_types and types
|
||||
return !(val.exclude_types && val.types);
|
||||
}),
|
||||
};
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Notification as NotificationSchema } from "~/classes/schemas/notification.ts";
|
||||
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,
|
||||
|
|
@ -84,17 +25,69 @@ 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: {
|
||||
description: "Notifications",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(Notification.schema),
|
||||
schema: z.array(NotificationSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { User } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
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": {
|
||||
schema: User.schema,
|
||||
// TODO: Return a CredentialAccount
|
||||
schema: Account,
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { User } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
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: User.schema,
|
||||
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,7 +1,8 @@
|
|||
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";
|
||||
import { WebPushSubscription as WebPushSubscriptionSchema } from "~/classes/schemas/pushsubscription";
|
||||
import { RolePermissions } from "~/drizzle/schema";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
|
|
@ -15,6 +16,7 @@ export default apiRoute((app) =>
|
|||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/push/#get",
|
||||
},
|
||||
tags: ["Push Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -27,10 +29,11 @@ export default apiRoute((app) =>
|
|||
description: "WebPushSubscription",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: PushSubscription.schema,
|
||||
schema: WebPushSubscriptionSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
}),
|
||||
async (context) => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
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";
|
||||
import { WebPushSubscriptionInput } from "~/classes/schemas/pushsubscription";
|
||||
import { WebPushSubscription as WebPushSubscriptionSchema } from "~/classes/schemas/pushsubscription";
|
||||
import { RolePermissions } from "~/drizzle/schema";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
|
|
@ -16,6 +17,7 @@ export default apiRoute((app) =>
|
|||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/push/#create",
|
||||
},
|
||||
tags: ["Push Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -39,10 +41,11 @@ export default apiRoute((app) =>
|
|||
"A new PushSubscription has been generated, which will send the requested alerts to your endpoint.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: PushSubscription.schema,
|
||||
schema: WebPushSubscriptionSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
}),
|
||||
async (context) => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
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";
|
||||
import { WebPushSubscriptionInput } from "~/classes/schemas/pushsubscription";
|
||||
import {
|
||||
WebPushSubscriptionInput,
|
||||
WebPushSubscription as WebPushSubscriptionSchema,
|
||||
} from "~/classes/schemas/pushsubscription";
|
||||
import { RolePermissions } from "~/drizzle/schema";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
|
|
@ -16,6 +19,7 @@ export default apiRoute((app) =>
|
|||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/push/#update",
|
||||
},
|
||||
tags: ["Push Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -41,10 +45,11 @@ export default apiRoute((app) =>
|
|||
description: "The WebPushSubscription has been updated.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: PushSubscription.schema,
|
||||
schema: WebPushSubscriptionSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
}),
|
||||
async (context) => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Role } from "@versia/kit/db";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { Role as RoleSchema } from "~/classes/schemas/versia.ts";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const routeGet = createRoute({
|
||||
|
|
@ -25,7 +25,7 @@ const routeGet = createRoute({
|
|||
description: "Role",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Role.schema,
|
||||
schema: RoleSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -58,7 +58,7 @@ const routePatch = createRoute({
|
|||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Role.schema.partial(),
|
||||
schema: RoleSchema.omit({ id: true }).partial(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { apiRoute, auth } from "@/api";
|
|||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Role } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { Role as RoleSchema } from "~/classes/schemas/versia.ts";
|
||||
import { RolePermissions } from "~/drizzle/schema";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
|
|
@ -19,7 +20,7 @@ const routeGet = createRoute({
|
|||
description: "List of all roles",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(Role.schema),
|
||||
schema: z.array(RoleSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -40,7 +41,7 @@ const routePost = createRoute({
|
|||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Role.schema.omit({ id: true }),
|
||||
schema: RoleSchema.omit({ id: true }),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -50,7 +51,7 @@ const routePost = createRoute({
|
|||
description: "Role created",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Role.schema,
|
||||
schema: RoleSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,13 +1,24 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Note } from "@versia/kit/db";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
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,
|
||||
|
|
@ -15,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(Note.schema),
|
||||
descendants: z.array(Note.schema),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Record not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ErrorSchema,
|
||||
schema: ContextSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: noteNotFound,
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,23 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Note } from "@versia/kit/db";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
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,
|
||||
|
|
@ -20,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: Note.schema,
|
||||
schema: StatusSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: noteNotFound,
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Timeline, User } from "@versia/kit/db";
|
||||
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 { z } from "zod";
|
||||
|
||||
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,54 @@ 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(User.schema),
|
||||
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 { createRoute } from "@hono/zod-openapi";
|
||||
import { Media, Note } from "@versia/kit/db";
|
||||
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 { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
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
|
||||
.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"),
|
||||
media_ids: z
|
||||
.array(z.string().uuid())
|
||||
.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(),
|
||||
"poll[options]": z
|
||||
.array(z.string().max(config.validation.max_poll_option_size))
|
||||
.max(config.validation.max_poll_options)
|
||||
.optional(),
|
||||
"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(),
|
||||
})
|
||||
.refine(
|
||||
(obj) => !(obj.media_ids.length > 0 && obj["poll[options]"]),
|
||||
"Cannot attach poll to media",
|
||||
),
|
||||
};
|
||||
const schema = z
|
||||
.object({
|
||||
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(AttachmentSchema.shape.id)
|
||||
.max(config.validation.max_media_attachments)
|
||||
.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(PollOption.shape.title)
|
||||
.max(config.validation.max_poll_options)
|
||||
.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()
|
||||
.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: Note.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
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: Note.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
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,
|
||||
},
|
||||
"application/x-www-form-urlencoded": {
|
||||
schema: schemas.json,
|
||||
schema,
|
||||
},
|
||||
"multipart/form-data": {
|
||||
schema: schemas.json,
|
||||
schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated status",
|
||||
description: "Status has been successfully edited.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Note.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
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 { createRoute } from "@hono/zod-openapi";
|
||||
import { Note, db } from "@versia/kit/db";
|
||||
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 { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
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: Note.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
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 { createRoute } from "@hono/zod-openapi";
|
||||
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 { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
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: Note.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
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,17 @@ export default apiRoute((app) =>
|
|||
applicationId: null,
|
||||
});
|
||||
|
||||
// Refetch the note *again* to get the proper value of .reblogged
|
||||
const finalNewReblog = await Note.fromId(newReblog.id, user?.id);
|
||||
|
||||
if (!finalNewReblog) {
|
||||
throw new ApiError(500, "Failed to reblog");
|
||||
throw new Error("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 finalNewReblog.toApi(user), 200);
|
||||
}),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Timeline, User } from "@versia/kit/db";
|
||||
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 { z } from "zod";
|
||||
|
||||
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,54 @@ 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(User.schema),
|
||||
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,13 +1,27 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import type { StatusSource as ApiStatusSource } from "@versia/client/types";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
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,
|
||||
|
|
@ -20,7 +34,7 @@ const route = createRoute({
|
|||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: z.string().uuid(),
|
||||
id: StatusSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
|
|
@ -28,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],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -49,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,13 +1,23 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Note } from "@versia/kit/db";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
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,
|
||||
|
|
@ -20,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: Note.schema,
|
||||
schema: StatusSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: noteNotFound,
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,24 @@
|
|||
import { apiRoute, auth, withNoteParam } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Note } from "@versia/kit/db";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
noteNotFound,
|
||||
reusedResponses,
|
||||
withNoteParam,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { RolePermissions } from "@versia/kit/tables";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
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,
|
||||
|
|
@ -22,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: Note.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
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 { createRoute } from "@hono/zod-openapi";
|
||||
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 { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
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: Note.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
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",
|
||||
);
|
||||
|
|
@ -336,13 +336,13 @@ describe("/api/v1/statuses", () => {
|
|||
},
|
||||
body: new URLSearchParams({
|
||||
status: `Hello, @${users[1].data.username}@${
|
||||
new URL(config.http.base_url).host
|
||||
config.http.base_url.host
|
||||
}!`,
|
||||
local_only: "true",
|
||||
}),
|
||||
});
|
||||
|
||||
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 { createRoute } from "@hono/zod-openapi";
|
||||
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 { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
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
|
||||
.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"),
|
||||
media_ids: z
|
||||
.array(z.string().uuid())
|
||||
.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(),
|
||||
"poll[options]": z
|
||||
.array(z.string().max(config.validation.max_poll_option_size))
|
||||
.max(config.validation.max_poll_options)
|
||||
.optional(),
|
||||
"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"),
|
||||
scheduled_at: z.coerce
|
||||
.date()
|
||||
.min(new Date(), "Scheduled time must be in the future")
|
||||
.optional()
|
||||
.nullable(),
|
||||
local_only: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.or(z.boolean())
|
||||
.optional()
|
||||
.default(false),
|
||||
})
|
||||
.refine(
|
||||
(obj) => obj.status || obj.media_ids.length > 0,
|
||||
"Status is required unless media is attached",
|
||||
)
|
||||
.refine(
|
||||
(obj) => !(obj.media_ids.length > 0 && obj["poll[options]"]),
|
||||
"Cannot attach poll to media",
|
||||
),
|
||||
};
|
||||
const schema = z
|
||||
.object({
|
||||
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(AttachmentSchema.shape.id)
|
||||
.max(config.validation.max_media_attachments)
|
||||
.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(PollOption.shape.title)
|
||||
.max(config.validation.max_poll_options)
|
||||
.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()
|
||||
.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(Date.now() + 5 * 60 * 1000),
|
||||
"must be at least 5 minutes in the future.",
|
||||
)
|
||||
.optional()
|
||||
.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 || 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,
|
||||
},
|
||||
"application/x-www-form-urlencoded": {
|
||||
schema: schemas.json,
|
||||
schema,
|
||||
},
|
||||
"multipart/form-data": {
|
||||
schema: schemas.json,
|
||||
schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
description: "The new status",
|
||||
200: {
|
||||
description: "Status will be posted with chosen parameters.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Note.schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
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,23 +1,19 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Note, Timeline } from "@versia/kit/db";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { Notes, RolePermissions } from "@versia/kit/tables";
|
||||
import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
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(20),
|
||||
}),
|
||||
};
|
||||
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/timelines/home",
|
||||
summary: "Get home timeline",
|
||||
summary: "View home timeline",
|
||||
description: "View statuses from followed users and hashtags.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/timelines/#home",
|
||||
},
|
||||
tags: ["Timelines"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -30,17 +26,50 @@ const route = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
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(40).default(20).openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Home timeline",
|
||||
description: "Statuses in your home timeline will be returned",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(Note.schema),
|
||||
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/timelines/home?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/timelines/home?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
422: reusedResponses[422],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,35 +1,20 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Note, Timeline } from "@versia/kit/db";
|
||||
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { Notes, RolePermissions } from "@versia/kit/tables";
|
||||
import { and, eq, gt, gte, inArray, lt, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
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(20),
|
||||
local: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.optional(),
|
||||
remote: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.optional(),
|
||||
only_media: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.optional(),
|
||||
}),
|
||||
};
|
||||
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/timelines/public",
|
||||
summary: "Get public timeline",
|
||||
summary: "View public timeline",
|
||||
description: "View public statuses.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/timelines/#public",
|
||||
},
|
||||
tags: ["Timelines"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -41,17 +26,70 @@ const route = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
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,
|
||||
}),
|
||||
local: zBoolean.default(false).openapi({
|
||||
description: "Show only local statuses?",
|
||||
}),
|
||||
remote: zBoolean.default(false).openapi({
|
||||
description: "Show only remote statuses?",
|
||||
}),
|
||||
only_media: zBoolean.default(false).openapi({
|
||||
description: "Show only statuses with media attached?",
|
||||
}),
|
||||
limit: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(40)
|
||||
.default(20)
|
||||
.openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
})
|
||||
.refine(
|
||||
(o) => !(o.local && o.remote),
|
||||
"'local' and 'remote' cannot be both true",
|
||||
),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Public timeline",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(Note.schema),
|
||||
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/timelines/public?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/timelines/public?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
422: reusedResponses[422],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,80 +1,24 @@
|
|||
import { apiRoute, auth, jsonOrForm } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, jsonOrForm, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { db } from "@versia/kit/db";
|
||||
import { FilterKeywords, Filters, RolePermissions } from "@versia/kit/tables";
|
||||
import { type SQL, and, eq, inArray } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import {
|
||||
FilterKeyword as FilterKeywordSchema,
|
||||
Filter as FilterSchema,
|
||||
} from "~/classes/schemas/filters";
|
||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const schemas = {
|
||||
param: z.object({
|
||||
id: z.string().uuid(),
|
||||
}),
|
||||
json: z.object({
|
||||
title: z.string().trim().min(1).max(100).optional(),
|
||||
context: z
|
||||
.array(
|
||||
z.enum([
|
||||
"home",
|
||||
"notifications",
|
||||
"public",
|
||||
"thread",
|
||||
"account",
|
||||
]),
|
||||
)
|
||||
.optional(),
|
||||
filter_action: z.enum(["warn", "hide"]).optional().default("warn"),
|
||||
expires_in: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(60)
|
||||
.max(60 * 60 * 24 * 365 * 5)
|
||||
.optional(),
|
||||
keywords_attributes: z
|
||||
.array(
|
||||
z.object({
|
||||
keyword: z.string().trim().min(1).max(100).optional(),
|
||||
id: z.string().uuid().optional(),
|
||||
whole_word: z
|
||||
.string()
|
||||
.transform((v) =>
|
||||
["true", "1", "on"].includes(v.toLowerCase()),
|
||||
)
|
||||
.optional(),
|
||||
// biome-ignore lint/style/useNamingConvention: _destroy is a Mastodon API imposed variable name
|
||||
_destroy: z
|
||||
.string()
|
||||
.transform((v) =>
|
||||
["true", "1", "on"].includes(v.toLowerCase()),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
};
|
||||
|
||||
const filterSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
context: z.array(z.string()),
|
||||
expires_at: z.string().nullable(),
|
||||
filter_action: z.enum(["warn", "hide"]),
|
||||
keywords: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
keyword: z.string(),
|
||||
whole_word: z.boolean(),
|
||||
}),
|
||||
),
|
||||
statuses: z.array(z.string()),
|
||||
});
|
||||
|
||||
const routeGet = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v2/filters/{id}",
|
||||
summary: "Get filter",
|
||||
summary: "View a specific filter",
|
||||
externalDocs: {
|
||||
url: "Obtain a single filter group owned by the current user.",
|
||||
},
|
||||
tags: ["Filters"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -82,18 +26,19 @@ const routeGet = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: FilterSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Filter",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: filterSchema,
|
||||
schema: FilterSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
404: {
|
||||
description: "Filter not found",
|
||||
content: {
|
||||
|
|
@ -102,13 +47,19 @@ const routeGet = createRoute({
|
|||
},
|
||||
},
|
||||
},
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
const routePut = createRoute({
|
||||
method: "put",
|
||||
path: "/api/v2/filters/{id}",
|
||||
summary: "Update filter",
|
||||
summary: "Update a filter",
|
||||
description: "Update a filter group with the given parameters.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/filters/#update",
|
||||
},
|
||||
tags: ["Filters"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -117,11 +68,45 @@ const routePut = createRoute({
|
|||
jsonOrForm(),
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: FilterSchema.shape.id,
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schemas.json,
|
||||
schema: z
|
||||
.object({
|
||||
context: FilterSchema.shape.context,
|
||||
title: FilterSchema.shape.title,
|
||||
filter_action: FilterSchema.shape.filter_action,
|
||||
expires_in: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(60)
|
||||
.max(60 * 60 * 24 * 365 * 5)
|
||||
.openapi({
|
||||
description:
|
||||
"How many seconds from now should the filter expire?",
|
||||
}),
|
||||
keywords_attributes: z.array(
|
||||
FilterKeywordSchema.pick({
|
||||
keyword: true,
|
||||
whole_word: true,
|
||||
id: true,
|
||||
})
|
||||
.extend({
|
||||
// biome-ignore lint/style/useNamingConvention: _destroy is a Mastodon API imposed variable name
|
||||
_destroy: zBoolean
|
||||
.default(false)
|
||||
.openapi({
|
||||
description:
|
||||
"If true, will remove the keyword with the given ID.",
|
||||
}),
|
||||
})
|
||||
.partial(),
|
||||
),
|
||||
})
|
||||
.partial(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -131,11 +116,10 @@ const routePut = createRoute({
|
|||
description: "Filter updated",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: filterSchema,
|
||||
schema: FilterSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
404: {
|
||||
description: "Filter not found",
|
||||
content: {
|
||||
|
|
@ -144,13 +128,19 @@ const routePut = createRoute({
|
|||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
const routeDelete = createRoute({
|
||||
method: "delete",
|
||||
path: "/api/v2/filters/{id}",
|
||||
summary: "Delete filter",
|
||||
summary: "Delete a filter",
|
||||
description: "Delete a filter group with the given id.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/filters/#delete",
|
||||
},
|
||||
tags: ["Filters"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -158,13 +148,14 @@ const routeDelete = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
request: {
|
||||
params: schemas.param,
|
||||
params: z.object({
|
||||
id: FilterSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
204: {
|
||||
description: "Filter deleted",
|
||||
200: {
|
||||
description: "Filter successfully deleted",
|
||||
},
|
||||
|
||||
404: {
|
||||
description: "Filter not found",
|
||||
content: {
|
||||
|
|
@ -173,6 +164,7 @@ const routeDelete = createRoute({
|
|||
},
|
||||
},
|
||||
},
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,67 +1,22 @@
|
|||
import { apiRoute, auth, jsonOrForm } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, jsonOrForm, reusedResponses } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { db } from "@versia/kit/db";
|
||||
import { FilterKeywords, Filters, RolePermissions } from "@versia/kit/tables";
|
||||
import type { SQL } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
|
||||
const schemas = {
|
||||
json: z.object({
|
||||
title: z.string().trim().min(1).max(100),
|
||||
context: z
|
||||
.array(
|
||||
z.enum([
|
||||
"home",
|
||||
"notifications",
|
||||
"public",
|
||||
"thread",
|
||||
"account",
|
||||
]),
|
||||
)
|
||||
.min(1),
|
||||
filter_action: z.enum(["warn", "hide"]).optional().default("warn"),
|
||||
expires_in: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(60)
|
||||
.max(60 * 60 * 24 * 365 * 5)
|
||||
.optional(),
|
||||
keywords_attributes: z
|
||||
.array(
|
||||
z.object({
|
||||
keyword: z.string().trim().min(1).max(100),
|
||||
whole_word: z
|
||||
.string()
|
||||
.transform((v) =>
|
||||
["true", "1", "on"].includes(v.toLowerCase()),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
};
|
||||
|
||||
const filterSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
context: z.array(z.string()),
|
||||
expires_at: z.string().nullable(),
|
||||
filter_action: z.enum(["warn", "hide"]),
|
||||
keywords: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
keyword: z.string(),
|
||||
whole_word: z.boolean(),
|
||||
}),
|
||||
),
|
||||
statuses: z.array(z.string()),
|
||||
});
|
||||
import {
|
||||
FilterKeyword as FilterKeywordSchema,
|
||||
Filter as FilterSchema,
|
||||
} from "~/classes/schemas/filters";
|
||||
|
||||
const routeGet = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v2/filters",
|
||||
summary: "Get filters",
|
||||
summary: "View all filters",
|
||||
description: "Obtain a list of all filter groups for the current user.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/filters/#get",
|
||||
},
|
||||
tags: ["Filters"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -74,17 +29,23 @@ const routeGet = createRoute({
|
|||
description: "Filters",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(filterSchema),
|
||||
schema: z.array(FilterSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: reusedResponses[401],
|
||||
},
|
||||
});
|
||||
|
||||
const routePost = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v2/filters",
|
||||
summary: "Create filter",
|
||||
summary: "Create a filter",
|
||||
description: "Create a filter group with the given parameters.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/filters/#create",
|
||||
},
|
||||
tags: ["Filters"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -96,20 +57,43 @@ const routePost = createRoute({
|
|||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: schemas.json,
|
||||
schema: z.object({
|
||||
context: FilterSchema.shape.context,
|
||||
title: FilterSchema.shape.title,
|
||||
filter_action: FilterSchema.shape.filter_action,
|
||||
expires_in: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(60)
|
||||
.max(60 * 60 * 24 * 365 * 5)
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"How many seconds from now should the filter expire?",
|
||||
}),
|
||||
keywords_attributes: z
|
||||
.array(
|
||||
FilterKeywordSchema.pick({
|
||||
keyword: true,
|
||||
whole_word: true,
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Filter created",
|
||||
description: "Created filter",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: filterSchema,
|
||||
schema: FilterSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -159,8 +143,8 @@ export default apiRoute((app) => {
|
|||
await db
|
||||
.insert(Filters)
|
||||
.values({
|
||||
title: title ?? "",
|
||||
context: ctx ?? [],
|
||||
title,
|
||||
context: ctx,
|
||||
filterAction: filter_action,
|
||||
expireAt: new Date(
|
||||
Date.now() + (expires_in ?? 0),
|
||||
|
|
@ -203,18 +187,6 @@ export default apiRoute((app) => {
|
|||
whole_word: keyword.wholeWord,
|
||||
})),
|
||||
statuses: [],
|
||||
} as {
|
||||
id: string;
|
||||
title: string;
|
||||
context: string[];
|
||||
expires_at: string;
|
||||
filter_action: "warn" | "hide";
|
||||
keywords: {
|
||||
id: string;
|
||||
keyword: string;
|
||||
whole_word: boolean;
|
||||
}[];
|
||||
statuses: [];
|
||||
},
|
||||
200,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,108 +1,28 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { proxyUrl } from "@/response";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { User } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import manifest from "~/package.json";
|
||||
import { Instance as InstanceSchema } from "~/classes/schemas/instance";
|
||||
import pkg from "~/package.json";
|
||||
import { config } from "~/packages/config-manager";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v2/instance",
|
||||
summary: "Get instance metadata",
|
||||
summary: "View server information",
|
||||
description: "Obtain general information about the server.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/instance/#v2",
|
||||
},
|
||||
tags: ["Instance"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Instance metadata",
|
||||
description: "Server information",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
domain: z.string(),
|
||||
title: z.string(),
|
||||
version: z.string(),
|
||||
versia_version: z.string(),
|
||||
source_url: z.string(),
|
||||
description: z.string(),
|
||||
usage: z.object({
|
||||
users: z.object({
|
||||
active_month: z.number(),
|
||||
}),
|
||||
}),
|
||||
thumbnail: z.object({
|
||||
url: z.string().nullable(),
|
||||
}),
|
||||
banner: z.object({
|
||||
url: z.string().nullable(),
|
||||
}),
|
||||
languages: z.array(z.string()),
|
||||
configuration: z.object({
|
||||
urls: z.object({
|
||||
streaming: z.string().nullable(),
|
||||
status: z.string().nullable(),
|
||||
}),
|
||||
accounts: z.object({
|
||||
max_featured_tags: z.number(),
|
||||
max_displayname_characters: z.number(),
|
||||
avatar_size_limit: z.number(),
|
||||
header_size_limit: z.number(),
|
||||
max_fields_name_characters: z.number(),
|
||||
max_fields_value_characters: z.number(),
|
||||
max_fields: z.number(),
|
||||
max_username_characters: z.number(),
|
||||
max_note_characters: z.number(),
|
||||
}),
|
||||
statuses: z.object({
|
||||
max_characters: z.number(),
|
||||
max_media_attachments: z.number(),
|
||||
characters_reserved_per_url: z.number(),
|
||||
}),
|
||||
media_attachments: z.object({
|
||||
supported_mime_types: z.array(z.string()),
|
||||
image_size_limit: z.number(),
|
||||
image_matrix_limit: z.number(),
|
||||
video_size_limit: z.number(),
|
||||
video_frame_rate_limit: z.number(),
|
||||
video_matrix_limit: z.number(),
|
||||
max_description_characters: z.number(),
|
||||
}),
|
||||
polls: z.object({
|
||||
max_characters_per_option: z.number(),
|
||||
max_expiration: z.number(),
|
||||
max_options: z.number(),
|
||||
min_expiration: z.number(),
|
||||
}),
|
||||
translation: z.object({
|
||||
enabled: z.boolean(),
|
||||
}),
|
||||
}),
|
||||
registrations: z.object({
|
||||
enabled: z.boolean(),
|
||||
approval_required: z.boolean(),
|
||||
message: z.string().nullable(),
|
||||
url: z.string().nullable(),
|
||||
}),
|
||||
contact: z.object({
|
||||
email: z.string().nullable(),
|
||||
account: User.schema.nullable(),
|
||||
}),
|
||||
rules: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
text: z.string(),
|
||||
hint: z.string(),
|
||||
}),
|
||||
),
|
||||
sso: z.object({
|
||||
forced: z.boolean(),
|
||||
providers: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
icon: z.string(),
|
||||
id: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
}),
|
||||
schema: InstanceSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -111,12 +31,11 @@ const route = createRoute({
|
|||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
// Get software version from package.json
|
||||
const version = manifest.version;
|
||||
|
||||
const contactAccount = await User.fromSql(
|
||||
and(isNull(Users.instanceId), eq(Users.isAdmin, true)),
|
||||
);
|
||||
// Get first admin, or first user if no admin exists
|
||||
const contactAccount =
|
||||
(await User.fromSql(
|
||||
and(isNull(Users.instanceId), eq(Users.isAdmin, true)),
|
||||
)) ?? (await User.fromSql(isNull(Users.instanceId)));
|
||||
|
||||
const monthlyActiveUsers = await User.getActiveInPeriod(
|
||||
30 * 24 * 60 * 60 * 1000,
|
||||
|
|
@ -135,47 +54,58 @@ export default apiRoute((app) =>
|
|||
|
||||
// TODO: fill in more values
|
||||
return context.json({
|
||||
domain: new URL(config.http.base_url).hostname,
|
||||
domain: config.http.base_url.hostname,
|
||||
title: config.instance.name,
|
||||
version: "4.3.0-alpha.3+glitch",
|
||||
versia_version: version,
|
||||
source_url: "https://github.com/versia-pub/server",
|
||||
versia_version: pkg.version,
|
||||
source_url: pkg.repository.url,
|
||||
description: config.instance.description,
|
||||
usage: {
|
||||
users: {
|
||||
active_month: monthlyActiveUsers,
|
||||
},
|
||||
},
|
||||
api_versions: {
|
||||
mastodon: 1,
|
||||
},
|
||||
thumbnail: {
|
||||
url: config.instance.logo
|
||||
? proxyUrl(config.instance.logo)
|
||||
: null,
|
||||
? proxyUrl(config.instance.logo).toString()
|
||||
: pkg.icon,
|
||||
},
|
||||
banner: {
|
||||
url: config.instance.banner
|
||||
? proxyUrl(config.instance.banner)
|
||||
? proxyUrl(config.instance.banner).toString()
|
||||
: null,
|
||||
},
|
||||
icon: [],
|
||||
languages: ["en"],
|
||||
configuration: {
|
||||
urls: {
|
||||
streaming: null,
|
||||
status: null,
|
||||
// TODO: Implement Streaming API
|
||||
streaming: "",
|
||||
},
|
||||
vapid: {
|
||||
// TODO: Fill in vapid values
|
||||
public_key: "",
|
||||
},
|
||||
accounts: {
|
||||
max_featured_tags: 100,
|
||||
max_displayname_characters:
|
||||
config.validation.max_displayname_size,
|
||||
avatar_size_limit: config.validation.max_avatar_size,
|
||||
header_size_limit: config.validation.max_header_size,
|
||||
max_fields_name_characters:
|
||||
config.validation.max_field_name_size,
|
||||
max_fields_value_characters:
|
||||
config.validation.max_field_value_size,
|
||||
max_fields: config.validation.max_field_count,
|
||||
avatar_limit: config.validation.max_avatar_size,
|
||||
header_limit: config.validation.max_header_size,
|
||||
max_username_characters:
|
||||
config.validation.max_username_size,
|
||||
max_note_characters: config.validation.max_bio_size,
|
||||
max_pinned_statuses: 100,
|
||||
fields: {
|
||||
max_fields: config.validation.max_field_count,
|
||||
max_name_characters:
|
||||
config.validation.max_field_name_size,
|
||||
max_value_characters:
|
||||
config.validation.max_field_value_size,
|
||||
},
|
||||
},
|
||||
statuses: {
|
||||
max_characters: config.validation.max_note_size,
|
||||
|
|
@ -190,14 +120,14 @@ export default apiRoute((app) =>
|
|||
video_size_limit: config.validation.max_media_size,
|
||||
video_frame_rate_limit: config.validation.max_media_size,
|
||||
video_matrix_limit: config.validation.max_media_size,
|
||||
max_description_characters:
|
||||
description_limit:
|
||||
config.validation.max_media_description_size,
|
||||
},
|
||||
emojis: {
|
||||
emoji_size_limit: config.validation.max_emoji_size,
|
||||
max_emoji_shortcode_characters:
|
||||
max_shortcode_characters:
|
||||
config.validation.max_emoji_shortcode_size,
|
||||
max_emoji_description_characters:
|
||||
max_description_characters:
|
||||
config.validation.max_emoji_description_size,
|
||||
},
|
||||
polls: {
|
||||
|
|
@ -215,11 +145,11 @@ export default apiRoute((app) =>
|
|||
enabled: config.signups.registration,
|
||||
approval_required: false,
|
||||
message: null,
|
||||
url: null,
|
||||
},
|
||||
contact: {
|
||||
email: contactAccount?.data.email || null,
|
||||
account: contactAccount?.toApi() || null,
|
||||
// TODO: Add contact email
|
||||
email: "",
|
||||
account: (contactAccount as User)?.toApi(),
|
||||
},
|
||||
rules: config.signups.rules.map((rule, index) => ({
|
||||
id: String(index),
|
||||
|
|
|
|||
|
|
@ -1,27 +1,20 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
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 { z } from "zod";
|
||||
import { config } from "~/packages/config-manager/index.ts";
|
||||
import { Attachment as AttachmentSchema } from "~/classes/schemas/attachment";
|
||||
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/v2/media",
|
||||
summary: "Upload media",
|
||||
summary: "Upload media as an attachment (async)",
|
||||
description:
|
||||
"Creates a media attachment to be used with a new status. The full sized media will be processed asynchronously in the background for large uploads.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/media/#v2",
|
||||
},
|
||||
tags: ["Media"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -33,17 +26,49 @@ 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: "Uploaded media",
|
||||
description:
|
||||
"MediaAttachment was created successfully, and the full-size file was processed synchronously.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Media.schema,
|
||||
schema: AttachmentSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
202: {
|
||||
description:
|
||||
"MediaAttachment was created successfully, but the full-size file is still processing. Note that the MediaAttachment’s url will still be null, as the media is still being processed in the background. However, the preview_url should be available. Use GET /api/v1/media/:id to check the status of the media attachment.",
|
||||
content: {
|
||||
"application/json": {
|
||||
// FIXME: Can't .extend the type to have a null url because it crashes zod-to-openapi
|
||||
schema: AttachmentSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -63,6 +88,7 @@ const route = createRoute({
|
|||
},
|
||||
},
|
||||
},
|
||||
...reusedResponses,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -72,7 +98,7 @@ export default apiRoute((app) =>
|
|||
|
||||
const attachment = await Media.fromFile(file, {
|
||||
thumbnail,
|
||||
description,
|
||||
description: description ?? undefined,
|
||||
});
|
||||
|
||||
return context.json(attachment.toApi(), 200);
|
||||
|
|
|
|||
|
|
@ -1,32 +1,31 @@
|
|||
import { apiRoute, auth, parseUserAddress, userAddressValidator } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
parseUserAddress,
|
||||
reusedResponses,
|
||||
userAddressValidator,
|
||||
} from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Note, User, db } from "@versia/kit/db";
|
||||
import { Instances, Notes, RolePermissions, Users } from "@versia/kit/tables";
|
||||
import { and, eq, inArray, isNull, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||
import { Id } from "~/classes/schemas/common";
|
||||
import { Search as SearchSchema } from "~/classes/schemas/search";
|
||||
import { searchManager } from "~/classes/search/search-manager";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
const schemas = {
|
||||
query: z.object({
|
||||
q: z.string().trim(),
|
||||
type: z.string().optional(),
|
||||
resolve: z.coerce.boolean().optional(),
|
||||
following: z.coerce.boolean().optional(),
|
||||
account_id: z.string().optional(),
|
||||
max_id: z.string().optional(),
|
||||
min_id: z.string().optional(),
|
||||
limit: z.coerce.number().int().min(1).max(40).optional(),
|
||||
offset: z.coerce.number().int().optional(),
|
||||
}),
|
||||
};
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v2/search",
|
||||
summary: "Instance database search",
|
||||
summary: "Perform a search",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/search/#v2",
|
||||
},
|
||||
tags: ["Search"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
|
|
@ -39,18 +38,64 @@ const route = createRoute({
|
|||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: schemas.query,
|
||||
query: z.object({
|
||||
q: z.string().trim().openapi({
|
||||
description: "The search query.",
|
||||
example: "versia",
|
||||
}),
|
||||
type: z
|
||||
.enum(["accounts", "hashtags", "statuses"])
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"Specify whether to search for only accounts, hashtags, statuses",
|
||||
example: "accounts",
|
||||
}),
|
||||
resolve: zBoolean.default(false).openapi({
|
||||
description:
|
||||
"Only relevant if type includes accounts. If true and (a) the search query is for a remote account (e.g., someaccount@someother.server) and (b) the local server does not know about the account, WebFinger is used to try and resolve the account at someother.server. This provides the best recall at higher latency. If false only accounts the server knows about are returned.",
|
||||
}),
|
||||
following: zBoolean.default(false).openapi({
|
||||
description:
|
||||
"Only include accounts that the user is following?",
|
||||
}),
|
||||
account_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
" If provided, will only return statuses authored by this account.",
|
||||
}),
|
||||
exclude_unreviewed: zBoolean.default(false).openapi({
|
||||
description:
|
||||
"Filter out unreviewed tags? Use true when trying to find trending tags.",
|
||||
}),
|
||||
max_id: 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: 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: 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(40).default(20).openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
offset: z.coerce.number().int().min(0).default(0).openapi({
|
||||
description: "Skip the first n results.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Search results",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
accounts: z.array(User.schema),
|
||||
statuses: z.array(Note.schema),
|
||||
hashtags: z.array(z.string()),
|
||||
}),
|
||||
schema: SearchSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -71,6 +116,7 @@ const route = createRoute({
|
|||
},
|
||||
},
|
||||
},
|
||||
422: reusedResponses[422],
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -163,16 +209,16 @@ export default apiRoute((app) =>
|
|||
|
||||
accountResults = await searchManager.searchAccounts(
|
||||
q,
|
||||
Number(limit) || 10,
|
||||
Number(offset) || 0,
|
||||
limit,
|
||||
offset,
|
||||
);
|
||||
}
|
||||
|
||||
if (!type || type === "statuses") {
|
||||
statusResults = await searchManager.searchStatuses(
|
||||
q,
|
||||
Number(limit) || 10,
|
||||
Number(offset) || 0,
|
||||
limit,
|
||||
offset,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import type { Entity } from "@versia/federation/types";
|
||||
import { z } from "zod";
|
||||
import { InboxJobType, inboxQueue } from "~/classes/queues/inbox";
|
||||
|
||||
const schemas = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { z } from "zod";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import type { ContentfulStatusCode, StatusCode } from "hono/utils/http-status";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { getLogger } from "@logtape/logtape";
|
||||
import chalk from "chalk";
|
||||
import { z } from "zod";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
LikeExtension as LikeSchema,
|
||||
Note as NoteSchema,
|
||||
|
|
@ -7,7 +7,6 @@ import {
|
|||
import { Like, Note, User } from "@versia/kit/db";
|
||||
import { Likes, Notes } from "@versia/kit/tables";
|
||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import { ErrorSchema, type KnownEntity } from "~/types/api";
|
||||
|
|
@ -98,7 +97,7 @@ export default apiRoute((app) =>
|
|||
// This fixes reverse proxy errors
|
||||
const reqUrl = new URL(context.req.url);
|
||||
if (
|
||||
new URL(config.http.base_url).protocol === "https:" &&
|
||||
config.http.base_url.protocol === "https:" &&
|
||||
reqUrl.protocol === "http:"
|
||||
) {
|
||||
reqUrl.protocol = "https:";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import type { Entity } from "@versia/federation/types";
|
||||
import { z } from "zod";
|
||||
import { InboxJobType, inboxQueue } from "~/classes/queues/inbox";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { User as UserSchema } from "@versia/federation/schemas";
|
||||
import { User } from "@versia/kit/db";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Collection as CollectionSchema,
|
||||
Note as NoteSchema,
|
||||
|
|
@ -7,7 +7,6 @@ import {
|
|||
import { Note, User, db } from "@versia/kit/db";
|
||||
import { Notes } from "@versia/kit/tables";
|
||||
import { and, eq, inArray } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ const route = createRoute({
|
|||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, (context) => {
|
||||
const baseUrl = new URL(config.http.base_url);
|
||||
const baseUrl = config.http.base_url;
|
||||
return context.json(
|
||||
{
|
||||
issuer: baseUrl.origin.toString(),
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export default apiRoute((app) =>
|
|||
],
|
||||
versions: ["0.4.0"],
|
||||
},
|
||||
host: new URL(config.http.base_url).host,
|
||||
host: config.http.base_url.host,
|
||||
name: config.instance.name,
|
||||
description: config.instance.description,
|
||||
public_key: {
|
||||
|
|
|
|||
|
|
@ -4,14 +4,13 @@ import {
|
|||
parseUserAddress,
|
||||
webfingerMention,
|
||||
} from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { getLogger } from "@logtape/logtape";
|
||||
import type { ResponseError } from "@versia/federation";
|
||||
import { WebFinger } from "@versia/federation/schemas";
|
||||
import { User } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/packages/config-manager";
|
||||
import { ErrorSchema } from "~/types/api";
|
||||
|
|
@ -64,7 +63,7 @@ export default apiRoute((app) =>
|
|||
|
||||
const requestedUser = resource.split("acct:")[1];
|
||||
|
||||
const host = new URL(config.http.base_url).host;
|
||||
const host = config.http.base_url.host;
|
||||
|
||||
const { username, domain } = parseUserAddress(requestedUser);
|
||||
|
||||
|
|
@ -97,7 +96,7 @@ export default apiRoute((app) =>
|
|||
try {
|
||||
activityPubUrl = await manager.webFinger(
|
||||
user.data.username,
|
||||
new URL(config.http.base_url).host,
|
||||
config.http.base_url.host,
|
||||
"application/activity+json",
|
||||
config.federation.bridge.url?.toString(),
|
||||
);
|
||||
|
|
@ -133,9 +132,10 @@ export default apiRoute((app) =>
|
|||
},
|
||||
{
|
||||
rel: "avatar",
|
||||
// Default avatars are SVGs
|
||||
type:
|
||||
user.data.source.avatar?.content_type ||
|
||||
"application/octet-stream",
|
||||
user.avatar?.getPreferredMimeType() ??
|
||||
"image/svg+xml",
|
||||
href: user.getAvatarUrl(config),
|
||||
},
|
||||
].filter(Boolean) as {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Application as APIApplication } from "@versia/client/types";
|
||||
import type { z } from "@hono/zod-openapi";
|
||||
import { Token, db } from "@versia/kit/db";
|
||||
import { Applications } from "@versia/kit/tables";
|
||||
import {
|
||||
|
|
@ -9,20 +9,15 @@ import {
|
|||
eq,
|
||||
inArray,
|
||||
} from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import type {
|
||||
Application as ApplicationSchema,
|
||||
CredentialApplication,
|
||||
} from "../schemas/application.ts";
|
||||
import { BaseInterface } from "./base.ts";
|
||||
|
||||
type ApplicationType = InferSelectModel<typeof Applications>;
|
||||
|
||||
export class Application extends BaseInterface<typeof Applications> {
|
||||
public static schema: z.ZodType<APIApplication> = z.object({
|
||||
name: z.string(),
|
||||
website: z.string().url().optional().nullable(),
|
||||
vapid_key: z.string().optional().nullable(),
|
||||
redirect_uris: z.string().optional(),
|
||||
scopes: z.string().optional(),
|
||||
});
|
||||
|
||||
public static $type: ApplicationType;
|
||||
|
||||
public async reload(): Promise<void> {
|
||||
|
|
@ -144,11 +139,26 @@ export class Application extends BaseInterface<typeof Applications> {
|
|||
return this.data.id;
|
||||
}
|
||||
|
||||
public toApi(): APIApplication {
|
||||
public toApi(): z.infer<typeof ApplicationSchema> {
|
||||
return {
|
||||
name: this.data.name,
|
||||
website: this.data.website,
|
||||
vapid_key: this.data.vapidKey,
|
||||
scopes: this.data.scopes.split(" "),
|
||||
redirect_uri: this.data.redirectUri,
|
||||
redirect_uris: this.data.redirectUri.split("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
public toApiCredential(): z.infer<typeof CredentialApplication> {
|
||||
return {
|
||||
name: this.data.name,
|
||||
website: this.data.website,
|
||||
client_id: this.data.clientId,
|
||||
client_secret: this.data.secret,
|
||||
client_secret_expires_at: "0",
|
||||
scopes: this.data.scopes.split(" "),
|
||||
redirect_uri: this.data.redirectUri,
|
||||
redirect_uris: this.data.redirectUri.split("\n"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { emojiValidatorWithColons, emojiValidatorWithIdentifiers } from "@/api";
|
||||
import { proxyUrl } from "@/response";
|
||||
import type { Emoji as APIEmoji } from "@versia/client/types";
|
||||
import type { z } from "@hono/zod-openapi";
|
||||
import type { CustomEmojiExtension } from "@versia/federation/types";
|
||||
import { type Instance, Media, db } from "@versia/kit/db";
|
||||
import { Emojis, type Instances, type Medias } from "@versia/kit/tables";
|
||||
|
|
@ -14,7 +14,7 @@ import {
|
|||
inArray,
|
||||
isNull,
|
||||
} from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import type { CustomEmoji } from "../schemas/emoji.ts";
|
||||
import { BaseInterface } from "./base.ts";
|
||||
|
||||
type EmojiType = InferSelectModel<typeof Emojis> & {
|
||||
|
|
@ -23,16 +23,6 @@ type EmojiType = InferSelectModel<typeof Emojis> & {
|
|||
};
|
||||
|
||||
export class Emoji extends BaseInterface<typeof Emojis, EmojiType> {
|
||||
public static schema = z.object({
|
||||
id: z.string(),
|
||||
shortcode: z.string(),
|
||||
url: z.string(),
|
||||
visible_in_picker: z.boolean(),
|
||||
category: z.string().optional(),
|
||||
static_url: z.string(),
|
||||
global: z.boolean(),
|
||||
});
|
||||
|
||||
public static $type: EmojiType;
|
||||
public media: Media;
|
||||
|
||||
|
|
@ -184,18 +174,18 @@ export class Emoji extends BaseInterface<typeof Emojis, EmojiType> {
|
|||
);
|
||||
}
|
||||
|
||||
public toApi(): APIEmoji {
|
||||
public toApi(): z.infer<typeof CustomEmoji> {
|
||||
return {
|
||||
id: this.id,
|
||||
shortcode: this.data.shortcode,
|
||||
static_url: proxyUrl(this.media.getUrl()).toString(),
|
||||
url: proxyUrl(this.media.getUrl()).toString(),
|
||||
visible_in_picker: this.data.visibleInPicker,
|
||||
category: this.data.category ?? undefined,
|
||||
category: this.data.category,
|
||||
global: this.data.ownerId === null,
|
||||
description:
|
||||
this.media.data.content[this.media.getPreferredMimeType()]
|
||||
.description ?? undefined,
|
||||
.description ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { z } from "@hono/zod-openapi";
|
||||
import { RolePermission } from "@versia/client/types";
|
||||
import type { Delete, LikeExtension } from "@versia/federation/types";
|
||||
import { db } from "@versia/kit/db";
|
||||
|
|
@ -16,7 +17,6 @@ import {
|
|||
eq,
|
||||
inArray,
|
||||
} from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { config } from "~/packages/config-manager/index.ts";
|
||||
import { BaseInterface } from "./base.ts";
|
||||
import { Note } from "./note.ts";
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue