mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor(api): ♻️ Convery more routes to use OpenAPI
This commit is contained in:
parent
1ab1c68d36
commit
b0b750c05d
|
|
@ -1,10 +1,11 @@
|
||||||
import { apiRoute, applyConfig, auth, handleZodError } from "@/api";
|
import { apiRoute, applyConfig, auth } from "@/api";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { createRoute } from "@hono/zod-openapi";
|
||||||
import ISO6391 from "iso-639-1";
|
import ISO6391 from "iso-639-1";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { RolePermissions } from "~/drizzle/schema";
|
import { RolePermissions } from "~/drizzle/schema";
|
||||||
import { Relationship } from "~/packages/database-interface/relationship";
|
import { Relationship } from "~/packages/database-interface/relationship";
|
||||||
import { User } from "~/packages/database-interface/user";
|
import { User } from "~/packages/database-interface/user";
|
||||||
|
import { ErrorSchema } from "~/types/api";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["POST"],
|
allowedMethods: ["POST"],
|
||||||
|
|
@ -41,42 +42,79 @@ export const schemas = {
|
||||||
.default({ reblogs: true, notify: false, languages: [] }),
|
.default({ reblogs: true, notify: false, languages: [] }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
const route = createRoute({
|
||||||
app.on(
|
method: "post",
|
||||||
meta.allowedMethods,
|
path: "/api/v1/accounts/{id}/follow",
|
||||||
meta.route,
|
summary: "Follow user",
|
||||||
zValidator("param", schemas.param, handleZodError),
|
description: "Follow a user",
|
||||||
zValidator("json", schemas.json, handleZodError),
|
middleware: [auth(meta.auth, meta.permissions)],
|
||||||
auth(meta.auth, meta.permissions),
|
responses: {
|
||||||
async (context) => {
|
200: {
|
||||||
const { id } = context.req.valid("param");
|
description: "User followed",
|
||||||
const { user } = context.get("auth");
|
content: {
|
||||||
const { reblogs, notify, languages } = context.req.valid("json");
|
"application/json": {
|
||||||
|
schema: Relationship.schema,
|
||||||
if (!user) {
|
},
|
||||||
return context.json({ error: "Unauthorized" }, 401);
|
},
|
||||||
}
|
|
||||||
|
|
||||||
const otherUser = await User.fromId(id);
|
|
||||||
|
|
||||||
if (!otherUser) {
|
|
||||||
return context.json({ error: "User not found" }, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
let relationship = await Relationship.fromOwnerAndSubject(
|
|
||||||
user,
|
|
||||||
otherUser,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!relationship.data.following) {
|
|
||||||
relationship = await user.followRequest(otherUser, {
|
|
||||||
reblogs,
|
|
||||||
notify,
|
|
||||||
languages,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return context.json(relationship.toApi());
|
|
||||||
},
|
},
|
||||||
),
|
401: {
|
||||||
|
description: "Unauthorized",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: ErrorSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
404: {
|
||||||
|
description: "User not found",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: ErrorSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
params: schemas.param,
|
||||||
|
body: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: schemas.json,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default apiRoute((app) =>
|
||||||
|
app.openapi(route, async (context) => {
|
||||||
|
const { id } = context.req.valid("param");
|
||||||
|
const { user } = context.get("auth");
|
||||||
|
const { reblogs, notify, languages } = context.req.valid("json");
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return context.json({ error: "Unauthorized" }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
const otherUser = await User.fromId(id);
|
||||||
|
|
||||||
|
if (!otherUser) {
|
||||||
|
return context.json({ error: "User not found" }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
let relationship = await Relationship.fromOwnerAndSubject(
|
||||||
|
user,
|
||||||
|
otherUser,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!relationship.data.following) {
|
||||||
|
relationship = await user.followRequest(otherUser, {
|
||||||
|
reblogs,
|
||||||
|
notify,
|
||||||
|
languages,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.json(relationship.toApi(), 200);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
import {
|
import { apiRoute, applyConfig, auth, idValidator } from "@/api";
|
||||||
apiRoute,
|
import { createRoute } from "@hono/zod-openapi";
|
||||||
applyConfig,
|
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
idValidator,
|
|
||||||
} from "@/api";
|
|
||||||
import { zValidator } from "@hono/zod-validator";
|
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { RolePermissions, Users } from "~/drizzle/schema";
|
import { RolePermissions, Users } from "~/drizzle/schema";
|
||||||
import { Timeline } from "~/packages/database-interface/timeline";
|
import { Timeline } from "~/packages/database-interface/timeline";
|
||||||
import { User } from "~/packages/database-interface/user";
|
import { User } from "~/packages/database-interface/user";
|
||||||
|
import { ErrorSchema } from "~/types/api";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -43,44 +38,72 @@ export const schemas = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
const route = createRoute({
|
||||||
app.on(
|
method: "get",
|
||||||
meta.allowedMethods,
|
path: "/api/v1/accounts/{id}/followers",
|
||||||
meta.route,
|
summary: "Get account followers",
|
||||||
zValidator("query", schemas.query, handleZodError),
|
description:
|
||||||
zValidator("param", schemas.param, handleZodError),
|
"Gets an paginated list of accounts that follow the specified account",
|
||||||
auth(meta.auth, meta.permissions),
|
middleware: [auth(meta.auth, meta.permissions)],
|
||||||
async (context) => {
|
request: {
|
||||||
const { id } = context.req.valid("param");
|
params: schemas.param,
|
||||||
const { max_id, since_id, min_id, limit } =
|
query: schemas.query,
|
||||||
context.req.valid("query");
|
},
|
||||||
|
responses: {
|
||||||
const otherUser = await User.fromId(id);
|
200: {
|
||||||
|
description: "A list of accounts that follow the specified account",
|
||||||
// TODO: Add follower/following privacy settings
|
content: {
|
||||||
|
"application/json": {
|
||||||
if (!otherUser) {
|
schema: z.array(User.schema),
|
||||||
return context.json({ error: "User not found" }, 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { objects, link } = await Timeline.getUserTimeline(
|
|
||||||
and(
|
|
||||||
max_id ? lt(Users.id, max_id) : undefined,
|
|
||||||
since_id ? gte(Users.id, since_id) : undefined,
|
|
||||||
min_id ? gt(Users.id, min_id) : undefined,
|
|
||||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${otherUser.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."following" = true)`,
|
|
||||||
),
|
|
||||||
limit,
|
|
||||||
context.req.url,
|
|
||||||
);
|
|
||||||
|
|
||||||
return context.json(
|
|
||||||
await Promise.all(objects.map((object) => object.toApi())),
|
|
||||||
200,
|
|
||||||
{
|
|
||||||
Link: link,
|
|
||||||
},
|
},
|
||||||
);
|
},
|
||||||
|
headers: {
|
||||||
|
Link: {
|
||||||
|
description: "Links to the next and previous pages",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
),
|
404: {
|
||||||
|
description: "The specified account was not found",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: ErrorSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default apiRoute((app) =>
|
||||||
|
app.openapi(route, async (context) => {
|
||||||
|
const { id } = context.req.valid("param");
|
||||||
|
const { max_id, since_id, min_id, limit } = context.req.valid("query");
|
||||||
|
|
||||||
|
const otherUser = await User.fromId(id);
|
||||||
|
|
||||||
|
// TODO: Add follower/following privacy settings
|
||||||
|
|
||||||
|
if (!otherUser) {
|
||||||
|
return context.json({ error: "User not found" }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { objects, link } = await Timeline.getUserTimeline(
|
||||||
|
and(
|
||||||
|
max_id ? lt(Users.id, max_id) : undefined,
|
||||||
|
since_id ? gte(Users.id, since_id) : undefined,
|
||||||
|
min_id ? gt(Users.id, min_id) : undefined,
|
||||||
|
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${otherUser.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."following" = true)`,
|
||||||
|
),
|
||||||
|
limit,
|
||||||
|
context.req.url,
|
||||||
|
);
|
||||||
|
|
||||||
|
return context.json(
|
||||||
|
await Promise.all(objects.map((object) => object.toApi())),
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
Link: link,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,11 @@
|
||||||
import {
|
import { apiRoute, applyConfig, auth, idValidator } from "@/api";
|
||||||
apiRoute,
|
import { createRoute } from "@hono/zod-openapi";
|
||||||
applyConfig,
|
|
||||||
auth,
|
|
||||||
handleZodError,
|
|
||||||
idValidator,
|
|
||||||
} from "@/api";
|
|
||||||
import { zValidator } from "@hono/zod-validator";
|
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { RolePermissions, Users } from "~/drizzle/schema";
|
import { RolePermissions, Users } from "~/drizzle/schema";
|
||||||
import { Timeline } from "~/packages/database-interface/timeline";
|
import { Timeline } from "~/packages/database-interface/timeline";
|
||||||
import { User } from "~/packages/database-interface/user";
|
import { User } from "~/packages/database-interface/user";
|
||||||
|
import { ErrorSchema } from "~/types/api";
|
||||||
|
|
||||||
export const meta = applyConfig({
|
export const meta = applyConfig({
|
||||||
allowedMethods: ["GET"],
|
allowedMethods: ["GET"],
|
||||||
|
|
@ -43,43 +38,73 @@ export const schemas = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
const route = createRoute({
|
||||||
app.on(
|
method: "get",
|
||||||
meta.allowedMethods,
|
path: "/api/v1/accounts/{id}/following",
|
||||||
meta.route,
|
summary: "Get account following",
|
||||||
zValidator("query", schemas.query, handleZodError),
|
description:
|
||||||
zValidator("param", schemas.param, handleZodError),
|
"Gets an paginated list of accounts that the specified account follows",
|
||||||
auth(meta.auth, meta.permissions),
|
middleware: [auth(meta.auth, meta.permissions)],
|
||||||
async (context) => {
|
request: {
|
||||||
const { id } = context.req.valid("param");
|
params: schemas.param,
|
||||||
const { max_id, since_id, min_id } = context.req.valid("query");
|
query: schemas.query,
|
||||||
|
},
|
||||||
const otherUser = await User.fromId(id);
|
responses: {
|
||||||
|
200: {
|
||||||
if (!otherUser) {
|
description:
|
||||||
return context.json({ error: "User not found" }, 404);
|
"A list of accounts that the specified account follows",
|
||||||
}
|
content: {
|
||||||
|
"application/json": {
|
||||||
// TODO: Add follower/following privacy settings
|
schema: z.array(User.schema),
|
||||||
|
|
||||||
const { objects, link } = await Timeline.getUserTimeline(
|
|
||||||
and(
|
|
||||||
max_id ? lt(Users.id, max_id) : undefined,
|
|
||||||
since_id ? gte(Users.id, since_id) : undefined,
|
|
||||||
min_id ? gt(Users.id, min_id) : undefined,
|
|
||||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${otherUser.id} AND "Relationships"."following" = true)`,
|
|
||||||
),
|
|
||||||
context.req.valid("query").limit,
|
|
||||||
context.req.url,
|
|
||||||
);
|
|
||||||
|
|
||||||
return context.json(
|
|
||||||
await Promise.all(objects.map((object) => object.toApi())),
|
|
||||||
200,
|
|
||||||
{
|
|
||||||
Link: link,
|
|
||||||
},
|
},
|
||||||
);
|
},
|
||||||
|
headers: {
|
||||||
|
Link: {
|
||||||
|
description: "Link to the next page of results",
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
),
|
404: {
|
||||||
|
description: "User not found",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: ErrorSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default apiRoute((app) =>
|
||||||
|
app.openapi(route, async (context) => {
|
||||||
|
const { id } = context.req.valid("param");
|
||||||
|
const { max_id, since_id, min_id } = context.req.valid("query");
|
||||||
|
|
||||||
|
const otherUser = await User.fromId(id);
|
||||||
|
|
||||||
|
if (!otherUser) {
|
||||||
|
return context.json({ error: "User not found" }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add follower/following privacy settings
|
||||||
|
|
||||||
|
const { objects, link } = await Timeline.getUserTimeline(
|
||||||
|
and(
|
||||||
|
max_id ? lt(Users.id, max_id) : undefined,
|
||||||
|
since_id ? gte(Users.id, since_id) : undefined,
|
||||||
|
min_id ? gt(Users.id, min_id) : undefined,
|
||||||
|
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${otherUser.id} AND "Relationships"."following" = true)`,
|
||||||
|
),
|
||||||
|
context.req.valid("query").limit,
|
||||||
|
context.req.url,
|
||||||
|
);
|
||||||
|
|
||||||
|
return context.json(
|
||||||
|
await Promise.all(objects.map((object) => object.toApi())),
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
Link: link,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
eq,
|
eq,
|
||||||
inArray,
|
inArray,
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
|
import { z } from "zod";
|
||||||
import { db } from "~/drizzle/db";
|
import { db } from "~/drizzle/db";
|
||||||
import { Emojis, Instances } from "~/drizzle/schema";
|
import { Emojis, Instances } from "~/drizzle/schema";
|
||||||
import { BaseInterface } from "./base";
|
import { BaseInterface } from "./base";
|
||||||
|
|
@ -21,6 +22,14 @@ export type EmojiWithInstance = InferSelectModel<typeof Emojis> & {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class Emoji extends BaseInterface<typeof Emojis, EmojiWithInstance> {
|
export class Emoji extends BaseInterface<typeof Emojis, EmojiWithInstance> {
|
||||||
|
static schema = z.object({
|
||||||
|
shortcode: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
visible_in_picker: z.boolean(),
|
||||||
|
category: z.string().optional(),
|
||||||
|
static_url: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
async reload(): Promise<void> {
|
async reload(): Promise<void> {
|
||||||
const reloaded = await Emoji.fromId(this.data.id);
|
const reloaded = await Emoji.fromId(this.data.id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { proxyUrl } from "@/response";
|
import { proxyUrl } from "@/response";
|
||||||
import type { RolePermission } from "@versia/client/types";
|
import { RolePermission } from "@versia/client/types";
|
||||||
import {
|
import {
|
||||||
type InferInsertModel,
|
type InferInsertModel,
|
||||||
type InferSelectModel,
|
type InferSelectModel,
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
eq,
|
eq,
|
||||||
inArray,
|
inArray,
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
|
import { z } from "zod";
|
||||||
import { db } from "~/drizzle/db";
|
import { db } from "~/drizzle/db";
|
||||||
import { RoleToUsers, Roles } from "~/drizzle/schema";
|
import { RoleToUsers, Roles } from "~/drizzle/schema";
|
||||||
import { config } from "~/packages/config-manager/index";
|
import { config } from "~/packages/config-manager/index";
|
||||||
|
|
@ -17,6 +18,16 @@ import { BaseInterface } from "./base";
|
||||||
export type RoleType = InferSelectModel<typeof Roles>;
|
export type RoleType = InferSelectModel<typeof Roles>;
|
||||||
|
|
||||||
export class Role extends BaseInterface<typeof Roles> {
|
export class Role extends BaseInterface<typeof Roles> {
|
||||||
|
static schema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
permissions: z.array(z.nativeEnum(RolePermission)),
|
||||||
|
priority: z.number(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
visible: z.boolean(),
|
||||||
|
icon: z.string().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
async reload(): Promise<void> {
|
async reload(): Promise<void> {
|
||||||
const reloaded = await Role.fromId(this.data.id);
|
const reloaded = await Role.fromId(this.data.id);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import {
|
||||||
sql,
|
sql,
|
||||||
} from "drizzle-orm";
|
} from "drizzle-orm";
|
||||||
import { htmlToText } from "html-to-text";
|
import { htmlToText } from "html-to-text";
|
||||||
|
import { z } from "zod";
|
||||||
import {
|
import {
|
||||||
type UserWithRelations,
|
type UserWithRelations,
|
||||||
findManyUsers,
|
findManyUsers,
|
||||||
|
|
@ -64,6 +65,57 @@ import { Role } from "./role";
|
||||||
* Gives helpers to fetch users from database in a nice format
|
* Gives helpers to fetch users from database in a nice format
|
||||||
*/
|
*/
|
||||||
export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
||||||
|
static schema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
username: z.string(),
|
||||||
|
acct: z.string(),
|
||||||
|
display_name: z.string(),
|
||||||
|
locked: z.boolean(),
|
||||||
|
discoverable: z.boolean().optional(),
|
||||||
|
group: z.boolean().nullable(),
|
||||||
|
noindex: z.boolean().nullable(),
|
||||||
|
suspended: z.boolean().nullable(),
|
||||||
|
limited: z.boolean().nullable(),
|
||||||
|
created_at: z.string(),
|
||||||
|
followers_count: z.number(),
|
||||||
|
following_count: z.number(),
|
||||||
|
statuses_count: z.number(),
|
||||||
|
note: z.string(),
|
||||||
|
uri: z.string(),
|
||||||
|
url: z.string(),
|
||||||
|
avatar: z.string(),
|
||||||
|
avatar_static: z.string(),
|
||||||
|
header: z.string(),
|
||||||
|
header_static: z.string(),
|
||||||
|
emojis: z.array(Emoji.schema),
|
||||||
|
fields: z.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
verified: z.boolean().nullable().optional(),
|
||||||
|
verified_at: z.string().nullable().optional(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
// FIXME: Use a proper type
|
||||||
|
moved: z.any().nullable(),
|
||||||
|
bot: z.boolean().nullable(),
|
||||||
|
source: z
|
||||||
|
.object({
|
||||||
|
privacy: z.string().nullable(),
|
||||||
|
sensitive: z.boolean().nullable(),
|
||||||
|
language: z.string().nullable(),
|
||||||
|
note: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
role: z
|
||||||
|
.object({
|
||||||
|
name: z.string(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
roles: z.array(Role.schema),
|
||||||
|
mute_expires_at: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
async reload(): Promise<void> {
|
async reload(): Promise<void> {
|
||||||
const reloaded = await User.fromId(this.data.id);
|
const reloaded = await User.fromId(this.data.id);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue