refactor(api): ♻️ Refactor more routes into OpenAPI-compatible formats

This commit is contained in:
Jesse Wierzbinski 2024-08-27 18:55:02 +02:00
parent 02cb8bcd4f
commit bcbc9e6bf1
No known key found for this signature in database
17 changed files with 896 additions and 400 deletions

View file

@ -39,7 +39,7 @@ const route = createRoute({
middleware: [auth(meta.auth, meta.permissions)], middleware: [auth(meta.auth, meta.permissions)],
responses: { responses: {
200: { 200: {
description: "User blocked", description: "Updated relationship",
content: { content: {
"application/json": { "application/json": {
schema: Relationship.schema, schema: Relationship.schema,

View file

@ -50,7 +50,7 @@ const route = createRoute({
middleware: [auth(meta.auth, meta.permissions)], middleware: [auth(meta.auth, meta.permissions)],
responses: { responses: {
200: { 200: {
description: "User followed", description: "Updated relationship",
content: { content: {
"application/json": { "application/json": {
schema: Relationship.schema, schema: Relationship.schema,

View file

@ -1,8 +1,9 @@
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 { z } from "zod"; import { z } from "zod";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
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"],
@ -26,23 +27,46 @@ export const schemas = {
}), }),
}; };
export default apiRoute((app) => const route = createRoute({
app.on( method: "get",
meta.allowedMethods, path: "/api/v1/accounts/{id}",
meta.route, summary: "Get account data",
zValidator("param", schemas.param, handleZodError), description: "Gets the specified account data",
auth(meta.auth, meta.permissions), middleware: [auth(meta.auth, meta.permissions)],
async (context) => { request: {
const { id } = context.req.valid("param"); params: schemas.param,
const { user } = context.get("auth"); },
responses: {
const foundUser = await User.fromId(id); 200: {
description: "Account data",
if (!foundUser) { content: {
return context.json({ error: "User not found" }, 404); "application/json": {
} schema: User.schema,
},
return context.json(foundUser.toApi(user?.id === foundUser.id)); },
}, },
), 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 { user } = context.get("auth");
const foundUser = await User.fromId(id);
if (!foundUser) {
return context.json({ error: "User not found" }, 404);
}
return context.json(foundUser.toApi(user?.id === foundUser.id), 200);
}),
); );

View file

@ -1,9 +1,10 @@
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 { 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"],
@ -39,41 +40,78 @@ export const schemas = {
}), }),
}; };
export default apiRoute((app) => const route = createRoute({
app.on( method: "post",
meta.allowedMethods, path: "/api/v1/accounts/{id}/mute",
meta.route, summary: "Mute user",
zValidator("param", schemas.param, handleZodError), description: "Mute a user",
zValidator("json", schemas.json, handleZodError), middleware: [auth(meta.auth, meta.permissions)],
auth(meta.auth, meta.permissions), request: {
async (context) => { params: schemas.param,
const { id } = context.req.valid("param"); body: {
const { user } = context.get("auth"); content: {
// TODO: Add duration support "application/json": {
const { notifications } = context.req.valid("json"); schema: schemas.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);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
// TODO: Implement duration
await foundRelationship.update({
muting: true,
mutingNotifications: notifications ?? true,
});
return context.json(foundRelationship.toApi());
}, },
), },
responses: {
200: {
description: "Updated relationship",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
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 { user } = context.get("auth");
// TODO: Add duration support
const { notifications } = 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);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
// TODO: Implement duration
await foundRelationship.update({
muting: true,
mutingNotifications: notifications ?? true,
});
return context.json(foundRelationship.toApi(), 200);
}),
); );

View file

@ -1,9 +1,10 @@
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 { 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"],
@ -33,38 +34,75 @@ export const schemas = {
}), }),
}; };
export default apiRoute((app) => const route = createRoute({
app.on( method: "post",
meta.allowedMethods, path: "/api/v1/accounts/{id}/note",
meta.route, summary: "Set note",
zValidator("param", schemas.param, handleZodError), description: "Set a note on a user's profile, visible only to you",
zValidator("json", schemas.json, handleZodError), middleware: [auth(meta.auth, meta.permissions)],
auth(meta.auth, meta.permissions), request: {
async (context) => { params: schemas.param,
const { id } = context.req.valid("param"); body: {
const { user } = context.get("auth"); content: {
const { comment } = context.req.valid("json"); "application/json": {
schema: schemas.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);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
await foundRelationship.update({
note: comment,
});
return context.json(foundRelationship.toApi());
}, },
), },
responses: {
200: {
description: "Updated relationship",
content: {
"application/json": {
schema: Relationship.schema,
},
},
},
401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
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 { user } = context.get("auth");
const { comment } = 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);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
await foundRelationship.update({
note: comment,
});
return context.json(foundRelationship.toApi(), 200);
}),
); );

View file

@ -1,9 +1,10 @@
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 { 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"],
@ -30,36 +31,67 @@ export const schemas = {
}), }),
}; };
export default apiRoute((app) => const route = createRoute({
app.on( method: "post",
meta.allowedMethods, path: "/api/v1/accounts/{id}/pin",
meta.route, summary: "Pin user",
zValidator("param", schemas.param, handleZodError), description: "Pin a user to your profile",
auth(meta.auth, meta.permissions), middleware: [auth(meta.auth, meta.permissions)],
async (context) => { request: {
const { id } = context.req.valid("param"); params: schemas.param,
const { user } = context.get("auth"); },
responses: {
if (!user) { 200: {
return context.json({ error: "Unauthorized" }, 401); description: "Updated relationship",
} content: {
"application/json": {
const otherUser = await User.fromId(id); schema: Relationship.schema,
},
if (!otherUser) { },
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
await foundRelationship.update({
endorsed: true,
});
return context.json(foundRelationship.toApi());
}, },
), 401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
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 { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
await foundRelationship.update({
endorsed: true,
});
return context.json(foundRelationship.toApi(), 200);
}),
); );

View file

@ -1,8 +1,9 @@
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 { z } from "zod"; import { z } from "zod";
import { RolePermissions } from "~/drizzle/schema"; import { RolePermissions } from "~/drizzle/schema";
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"],
@ -26,29 +27,72 @@ export const schemas = {
}), }),
}; };
export default apiRoute((app) => const route = createRoute({
app.on( method: "post",
meta.allowedMethods, path: "/api/v1/accounts/{id}/refetch",
meta.route, summary: "Refetch user",
zValidator("param", schemas.param, handleZodError), description: "Refetch a user's profile from the remote server",
auth(meta.auth, meta.permissions), middleware: [auth(meta.auth, meta.permissions)],
async (context) => { request: {
const { id } = context.req.valid("param"); params: schemas.param,
const { user } = context.get("auth"); },
responses: {
if (!user) { 200: {
return context.json({ error: "Unauthorized" }, 401); description: "Updated user data",
} content: {
"application/json": {
const otherUser = await User.fromId(id); schema: User.schema,
},
if (!otherUser) { },
return context.json({ error: "User not found" }, 404);
}
const newUser = await otherUser.updateFromRemote();
return context.json(newUser.toApi(false));
}, },
), 401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
400: {
description: "User is local",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
if (otherUser.isLocal()) {
return context.json({ error: "Cannot refetch a local user" }, 400);
}
const newUser = await otherUser.updateFromRemote();
return context.json(newUser.toApi(false), 200);
}),
); );

View file

@ -1,9 +1,10 @@
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 { 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"],
@ -30,43 +31,74 @@ export const schemas = {
}), }),
}; };
export default apiRoute((app) => const route = createRoute({
app.on( method: "post",
meta.allowedMethods, path: "/api/v1/accounts/{id}/remove_from_followers",
meta.route, summary: "Remove user from followers",
zValidator("param", schemas.param, handleZodError), description: "Remove a user from your followers",
auth(meta.auth, meta.permissions), middleware: [auth(meta.auth, meta.permissions)],
async (context) => { request: {
const { id } = context.req.valid("param"); params: schemas.param,
const { user: self } = context.get("auth"); },
responses: {
if (!self) { 200: {
return context.json({ error: "Unauthorized" }, 401); description: "Updated relationship",
} content: {
"application/json": {
const otherUser = await User.fromId(id); schema: Relationship.schema,
},
if (!otherUser) { },
return context.json({ error: "User not found" }, 404);
}
const oppositeRelationship = await Relationship.fromOwnerAndSubject(
otherUser,
self,
);
if (oppositeRelationship.data.following) {
await oppositeRelationship.update({
following: false,
});
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
self,
otherUser,
);
return context.json(foundRelationship.toApi());
}, },
), 401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
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 { user: self } = context.get("auth");
if (!self) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const oppositeRelationship = await Relationship.fromOwnerAndSubject(
otherUser,
self,
);
if (oppositeRelationship.data.following) {
await oppositeRelationship.update({
following: false,
});
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
self,
otherUser,
);
return context.json(foundRelationship.toApi(), 200);
}),
); );

View file

@ -1,16 +1,12 @@
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, eq, gt, gte, isNull, lt, sql } from "drizzle-orm"; import { and, eq, gt, gte, isNull, lt, sql } from "drizzle-orm";
import { z } from "zod"; import { z } from "zod";
import { Notes, RolePermissions } from "~/drizzle/schema"; import { Notes, RolePermissions } from "~/drizzle/schema";
import { Note } from "~/packages/database-interface/note";
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"],
@ -60,61 +56,89 @@ export const schemas = {
}), }),
}; };
export default apiRoute((app) => const route = createRoute({
app.on( method: "get",
meta.allowedMethods, path: "/api/v1/accounts/{id}/statuses",
meta.route, summary: "Get account statuses",
zValidator("param", schemas.param, handleZodError), description: "Gets an paginated list of statuses by the specified account",
zValidator("query", schemas.query, handleZodError), middleware: [auth(meta.auth, meta.permissions)],
auth(meta.auth, meta.permissions), request: {
async (context) => { params: schemas.param,
const { id } = context.req.valid("param"); query: schemas.query,
const { user } = context.get("auth"); },
responses: {
const otherUser = await User.fromId(id); 200: {
description: "A list of statuses by the specified account",
if (!otherUser) { content: {
return context.json({ error: "User not found" }, 404); "application/json": {
} schema: z.array(Note.schema),
const {
max_id,
min_id,
since_id,
limit,
exclude_reblogs,
only_media,
exclude_replies,
pinned,
} = context.req.valid("query");
const { objects, link } = await Timeline.getNoteTimeline(
and(
max_id ? lt(Notes.id, max_id) : undefined,
since_id ? gte(Notes.id, since_id) : undefined,
min_id ? gt(Notes.id, min_id) : undefined,
eq(Notes.authorId, id),
only_media
? sql`EXISTS (SELECT 1 FROM "Attachments" WHERE "Attachments"."noteId" = ${Notes.id})`
: undefined,
pinned
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = ${Notes.id} AND "UserToPinnedNotes"."userId" = ${otherUser.id})`
: undefined,
exclude_reblogs ? isNull(Notes.reblogId) : undefined,
exclude_replies ? isNull(Notes.replyId) : undefined,
),
limit,
context.req.url,
user?.id,
);
return context.json(
await Promise.all(objects.map((note) => note.toApi(otherUser))),
200,
{
link,
}, },
); },
headers: {
Link: {
description: "Links to the next and previous pages",
},
},
}, },
), 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 { user } = context.get("auth");
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const {
max_id,
min_id,
since_id,
limit,
exclude_reblogs,
only_media,
exclude_replies,
pinned,
} = context.req.valid("query");
const { objects, link } = await Timeline.getNoteTimeline(
and(
max_id ? lt(Notes.id, max_id) : undefined,
since_id ? gte(Notes.id, since_id) : undefined,
min_id ? gt(Notes.id, min_id) : undefined,
eq(Notes.authorId, id),
only_media
? sql`EXISTS (SELECT 1 FROM "Attachments" WHERE "Attachments"."noteId" = ${Notes.id})`
: undefined,
pinned
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = ${Notes.id} AND "UserToPinnedNotes"."userId" = ${otherUser.id})`
: undefined,
exclude_reblogs ? isNull(Notes.reblogId) : undefined,
exclude_replies ? isNull(Notes.replyId) : undefined,
),
limit,
context.req.url,
user?.id,
);
return context.json(
await Promise.all(objects.map((note) => note.toApi(otherUser))),
200,
{
link,
},
);
}),
); );

View file

@ -1,9 +1,10 @@
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 { 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"],
@ -30,38 +31,69 @@ export const schemas = {
}), }),
}; };
export default apiRoute((app) => const route = createRoute({
app.on( method: "post",
meta.allowedMethods, path: "/api/v1/accounts/{id}/unblock",
meta.route, summary: "Unblock user",
zValidator("param", schemas.param, handleZodError), description: "Unblock a user",
auth(meta.auth, meta.permissions), middleware: [auth(meta.auth, meta.permissions)],
async (context) => { request: {
const { id } = context.req.valid("param"); params: schemas.param,
const { user } = context.get("auth"); },
responses: {
if (!user) { 200: {
return context.json({ error: "Unauthorized" }, 401); description: "Updated relationship",
} content: {
"application/json": {
const otherUser = await User.fromId(id); schema: Relationship.schema,
},
if (!otherUser) { },
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
if (foundRelationship.data.blocking) {
await foundRelationship.update({
blocking: false,
});
}
return context.json(foundRelationship.toApi());
}, },
), 401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
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 { user } = context.get("auth");
if (!user) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
user,
otherUser,
);
if (foundRelationship.data.blocking) {
await foundRelationship.update({
blocking: false,
});
}
return context.json(foundRelationship.toApi(), 200);
}),
); );

View file

@ -1,9 +1,10 @@
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 { 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"],
@ -30,36 +31,75 @@ export const schemas = {
}), }),
}; };
export default apiRoute((app) => const route = createRoute({
app.on( method: "post",
meta.allowedMethods, path: "/api/v1/accounts/{id}/unfollow",
meta.route, summary: "Unfollow user",
zValidator("param", schemas.param, handleZodError), description: "Unfollow a user",
auth(meta.auth, meta.permissions), middleware: [auth(meta.auth, meta.permissions)],
async (context) => { request: {
const { id } = context.req.valid("param"); params: schemas.param,
const { user: self } = context.get("auth"); },
responses: {
if (!self) { 200: {
return context.json({ error: "Unauthorized" }, 401); description: "Updated relationship",
} content: {
"application/json": {
const otherUser = await User.fromId(id); schema: Relationship.schema,
},
if (!otherUser) { },
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
self,
otherUser,
);
if (!(await self.unfollow(otherUser, foundRelationship))) {
return context.json({ error: "Failed to unfollow user" }, 500);
}
return context.json(foundRelationship.toApi());
}, },
), 401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
404: {
description: "User not found",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
500: {
description: "Failed to unfollow user during federation",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
},
});
export default apiRoute((app) =>
app.openapi(route, async (context) => {
const { id } = context.req.valid("param");
const { user: self } = context.get("auth");
if (!self) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
self,
otherUser,
);
if (!(await self.unfollow(otherUser, foundRelationship))) {
return context.json({ error: "Failed to unfollow user" }, 500);
}
return context.json(foundRelationship.toApi(), 200);
}),
); );

View file

@ -1,9 +1,10 @@
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 { 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"],
@ -30,39 +31,70 @@ export const schemas = {
}), }),
}; };
export default apiRoute((app) => const route = createRoute({
app.on( method: "post",
meta.allowedMethods, path: "/api/v1/accounts/{id}/unmute",
meta.route, summary: "Unmute user",
zValidator("param", schemas.param, handleZodError), description: "Unmute a user",
auth(meta.auth, meta.permissions), middleware: [auth(meta.auth, meta.permissions)],
async (context) => { request: {
const { id } = context.req.valid("param"); params: schemas.param,
const { user: self } = context.get("auth"); },
responses: {
if (!self) { 200: {
return context.json({ error: "Unauthorized" }, 401); description: "Updated relationship",
} content: {
"application/json": {
const user = await User.fromId(id); schema: Relationship.schema,
},
if (!user) { },
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
self,
user,
);
if (foundRelationship.data.muting) {
await foundRelationship.update({
muting: false,
mutingNotifications: false,
});
}
return context.json(foundRelationship.toApi());
}, },
), 401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
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 { user: self } = context.get("auth");
if (!self) {
return context.json({ error: "Unauthorized" }, 401);
}
const user = await User.fromId(id);
if (!user) {
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
self,
user,
);
if (foundRelationship.data.muting) {
await foundRelationship.update({
muting: false,
mutingNotifications: false,
});
}
return context.json(foundRelationship.toApi(), 200);
}),
); );

View file

@ -1,9 +1,10 @@
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 { 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"],
@ -30,38 +31,69 @@ export const schemas = {
}), }),
}; };
export default apiRoute((app) => const route = createRoute({
app.on( method: "post",
meta.allowedMethods, path: "/api/v1/accounts/{id}/unpin",
meta.route, summary: "Unpin user",
zValidator("param", schemas.param, handleZodError), description: "Unpin a user from your profile",
auth(meta.auth, meta.permissions), middleware: [auth(meta.auth, meta.permissions)],
async (context) => { request: {
const { id } = context.req.valid("param"); params: schemas.param,
const { user: self } = context.get("auth"); },
responses: {
if (!self) { 200: {
return context.json({ error: "Unauthorized" }, 401); description: "Updated relationship",
} content: {
"application/json": {
const otherUser = await User.fromId(id); schema: Relationship.schema,
},
if (!otherUser) { },
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
self,
otherUser,
);
if (foundRelationship.data.endorsed) {
await foundRelationship.update({
endorsed: false,
});
}
return context.json(foundRelationship.toApi());
}, },
), 401: {
description: "Unauthorized",
content: {
"application/json": {
schema: ErrorSchema,
},
},
},
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 { user: self } = context.get("auth");
if (!self) {
return context.json({ error: "Unauthorized" }, 401);
}
const otherUser = await User.fromId(id);
if (!otherUser) {
return context.json({ error: "User not found" }, 404);
}
const foundRelationship = await Relationship.fromOwnerAndSubject(
self,
otherUser,
);
if (foundRelationship.data.endorsed) {
await foundRelationship.update({
endorsed: false,
});
}
return context.json(foundRelationship.toApi(), 200);
}),
); );

4
app.ts
View file

@ -2,6 +2,7 @@ import { sentry } from "@/sentry";
import { cors } from "@hono/hono/cors"; import { cors } from "@hono/hono/cors";
import { prettyJSON } from "@hono/hono/pretty-json"; import { prettyJSON } from "@hono/hono/pretty-json";
import { secureHeaders } from "@hono/hono/secure-headers"; import { secureHeaders } from "@hono/hono/secure-headers";
import { swaggerUI } from "@hono/swagger-ui";
import { OpenAPIHono } from "@hono/zod-openapi"; import { OpenAPIHono } from "@hono/zod-openapi";
/* import { prometheus } from "@hono/prometheus"; /* import { prometheus } from "@hono/prometheus";
*/ import { getLogger } from "@logtape/logtape"; */ import { getLogger } from "@logtape/logtape";
@ -13,14 +14,15 @@ import { boundaryCheck } from "./middlewares/boundary-check";
import { ipBans } from "./middlewares/ip-bans"; import { ipBans } from "./middlewares/ip-bans";
import { logger } from "./middlewares/logger"; import { logger } from "./middlewares/logger";
import { routes } from "./routes"; import { routes } from "./routes";
import { swaggerUI } from "@hono/swagger-ui";
import type { ApiRouteExports, HonoEnv } from "./types/api"; import type { ApiRouteExports, HonoEnv } from "./types/api";
import { handleZodError } from "@/api";
export const appFactory = async () => { export const appFactory = async () => {
const serverLogger = getLogger("server"); const serverLogger = getLogger("server");
const app = new OpenAPIHono<HonoEnv>({ const app = new OpenAPIHono<HonoEnv>({
strict: false, strict: false,
defaultHook: handleZodError,
}); });
/* const { printMetrics, registerMetrics } = prometheus({ /* const { printMetrics, registerMetrics } = prometheus({

View file

@ -12,6 +12,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 { Attachments } from "~/drizzle/schema"; import { Attachments } from "~/drizzle/schema";
import { MediaBackendType } from "~/packages/config-manager/config.type"; import { MediaBackendType } from "~/packages/config-manager/config.type";
@ -21,6 +22,34 @@ import { BaseInterface } from "./base";
export type AttachmentType = InferSelectModel<typeof Attachments>; export type AttachmentType = InferSelectModel<typeof Attachments>;
export class Attachment extends BaseInterface<typeof Attachments> { export class Attachment extends BaseInterface<typeof Attachments> {
static schema: z.ZodType<ApiAttachment> = z.object({
id: z.string().uuid(),
type: z.enum(["unknown", "image", "gifv", "video", "audio"]),
url: z.string().url(),
remote_url: z.string().url().nullable(),
preview_url: z.string().url().nullable(),
text_url: z.string().url().nullable(),
meta: z
.object({
width: z.number().optional(),
height: z.number().optional(),
fps: z.number().optional(),
size: z.string().optional(),
duration: z.number().optional(),
length: z.string().optional(),
aspect: z.number().optional(),
original: z.object({
width: z.number().optional(),
height: z.number().optional(),
size: z.string().optional(),
aspect: z.number().optional(),
}),
})
.nullable(),
description: z.string().nullable(),
blurhash: z.string().nullable(),
});
async reload(): Promise<void> { async reload(): Promise<void> {
const reloaded = await Attachment.fromId(this.data.id); const reloaded = await Attachment.fromId(this.data.id);

View file

@ -27,6 +27,7 @@ import {
} from "drizzle-orm"; } from "drizzle-orm";
import { htmlToText } from "html-to-text"; import { htmlToText } from "html-to-text";
import { createRegExp, exactly, global } from "magic-regexp"; import { createRegExp, exactly, global } from "magic-regexp";
import { z } from "zod";
import { import {
type Application, type Application,
applicationToApi, applicationToApi,
@ -56,6 +57,96 @@ import { User } from "./user";
* Gives helpers to fetch notes from database in a nice format * Gives helpers to fetch notes from database in a nice format
*/ */
export class Note extends BaseInterface<typeof Notes, StatusWithRelations> { export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
static schema: z.ZodType<ApiStatus> = z.object({
id: z.string().uuid(),
uri: z.string().url(),
url: z.string().url(),
account: z.lazy(() => User.schema),
in_reply_to_id: z.string().uuid().nullable(),
in_reply_to_account_id: z.string().uuid().nullable(),
reblog: z.lazy(() => Note.schema).nullable(),
content: z.string(),
plain_content: z.string().nullable(),
created_at: z.string(),
edited_at: z.string().nullable(),
emojis: z.array(Emoji.schema),
replies_count: z.number().int().nonnegative(),
reblogs_count: z.number().int().nonnegative(),
favourites_count: z.number().int().nonnegative(),
reblogged: z.boolean().nullable(),
favourited: z.boolean().nullable(),
muted: z.boolean().nullable(),
sensitive: z.boolean(),
spoiler_text: z.string(),
visibility: z.enum(["public", "unlisted", "private", "direct"]),
media_attachments: z.array(Attachment.schema),
mentions: z.array(
z.object({
id: z.string().uuid(),
username: z.string(),
acct: z.string(),
url: z.string().url(),
}),
),
tags: z.array(z.object({ name: z.string(), url: z.string().url() })),
card: z
.object({
url: z.string().url(),
title: z.string(),
description: z.string(),
type: z.enum(["link", "photo", "video", "rich"]),
image: z.string().url().nullable(),
author_name: z.string().nullable(),
author_url: z.string().url().nullable(),
provider_name: z.string().nullable(),
provider_url: z.string().url().nullable(),
html: z.string().nullable(),
width: z.number().int().nonnegative().nullable(),
height: z.number().int().nonnegative().nullable(),
embed_url: z.string().url().nullable(),
blurhash: z.string().nullable(),
})
.nullable(),
poll: z
.object({
id: z.string().uuid(),
expires_at: z.string(),
expired: z.boolean(),
multiple: z.boolean(),
votes_count: z.number().int().nonnegative(),
voted: z.boolean(),
options: z.array(
z.object({
title: z.string(),
votes_count: z.number().int().nonnegative().nullable(),
}),
),
})
.nullable(),
application: z
.object({
name: z.string(),
website: z.string().url().nullable().optional(),
vapid_key: z.string().nullable().optional(),
})
.nullable(),
language: z.string().nullable(),
pinned: z.boolean().nullable(),
emoji_reactions: z.array(
z.object({
count: z.number().int().nonnegative(),
me: z.boolean(),
name: z.string(),
url: z.string().url().optional(),
static_url: z.string().url().optional(),
accounts: z.array(z.lazy(() => User.schema)).optional(),
account_ids: z.array(z.string().uuid()).optional(),
}),
),
quote: z.lazy(() => Note.schema).nullable(),
bookmarked: z.boolean(),
});
save(): Promise<StatusWithRelations> { save(): Promise<StatusWithRelations> {
return this.update(this.data); return this.update(this.data);
} }

View file

@ -65,7 +65,7 @@ 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({ static schema: z.ZodType<ApiAccount> = z.object({
id: z.string(), id: z.string(),
username: z.string(), username: z.string(),
acct: z.string(), acct: z.string(),
@ -92,12 +92,12 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
z.object({ z.object({
name: z.string(), name: z.string(),
value: z.string(), value: z.string(),
verified: z.boolean().nullable().optional(), verified: z.boolean().optional(),
verified_at: z.string().nullable().optional(), verified_at: z.string().nullable().optional(),
}), }),
), ),
// FIXME: Use a proper type // FIXME: Use a proper type
moved: z.any().nullable(), moved: z.lazy(() => User.schema).nullable(),
bot: z.boolean().nullable(), bot: z.boolean().nullable(),
source: z source: z
.object({ .object({
@ -105,6 +105,12 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
sensitive: z.boolean().nullable(), sensitive: z.boolean().nullable(),
language: z.string().nullable(), language: z.string().nullable(),
note: z.string(), note: z.string(),
fields: z.array(
z.object({
name: z.string(),
value: z.string(),
}),
),
}) })
.optional(), .optional(),
role: z role: z