From 82da70bcac4434cd89246ebb52ded8035bdde1bc Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Mon, 30 Dec 2024 21:30:10 +0100 Subject: [PATCH] refactor(api): :recycle: Group note/account fetching code in some routes --- api/api/v1/accounts/:id/block.ts | 23 +---- api/api/v1/accounts/:id/follow.ts | 23 +---- api/api/v1/accounts/:id/followers.ts | 22 +---- api/api/v1/accounts/:id/following.ts | 21 +---- api/api/v1/accounts/:id/index.ts | 25 ++---- api/api/v1/accounts/:id/mute.ts | 23 +---- api/api/v1/accounts/:id/note.ts | 23 +---- api/api/v1/accounts/:id/pin.ts | 23 +---- api/api/v1/accounts/:id/refetch.ts | 19 +--- .../v1/accounts/:id/remove_from_followers.ts | 23 +---- .../v1/accounts/:id/roles/:role_id/index.ts | 29 +++--- api/api/v1/accounts/:id/roles/index.ts | 23 +---- api/api/v1/accounts/:id/statuses.ts | 25 ++---- api/api/v1/accounts/:id/unblock.ts | 23 +---- api/api/v1/accounts/:id/unfollow.ts | 23 +---- api/api/v1/accounts/:id/unmute.ts | 24 +---- api/api/v1/accounts/:id/unpin.ts | 24 +---- api/api/v1/statuses/:id/context.ts | 17 ++-- api/api/v1/statuses/:id/favourite.test.ts | 1 - api/api/v1/statuses/:id/favourite.ts | 23 +---- api/api/v1/statuses/:id/favourited_by.ts | 26 +----- api/api/v1/statuses/:id/index.ts | 37 ++------ api/api/v1/statuses/:id/pin.ts | 37 +++----- api/api/v1/statuses/:id/reblog.ts | 20 +---- api/api/v1/statuses/:id/reblogged_by.ts | 25 +----- api/api/v1/statuses/:id/source.ts | 26 +----- api/api/v1/statuses/:id/unfavourite.ts | 22 +---- api/api/v1/statuses/:id/unpin.ts | 27 ++---- api/api/v1/statuses/:id/unreblog.ts | 20 +---- bun.lockb | Bin 398304 -> 398336 bytes package.json | 1 + utils/api.ts | 85 +++++++++++++++++- 32 files changed, 210 insertions(+), 553 deletions(-) diff --git a/api/api/v1/accounts/:id/block.ts b/api/api/v1/accounts/:id/block.ts index a53d316a..595e25a5 100644 --- a/api/api/v1/accounts/:id/block.ts +++ b/api/api/v1/accounts/:id/block.ts @@ -1,10 +1,8 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Relationship, User } from "@versia/kit/db"; +import { Relationship } 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 route = createRoute({ method: "post", @@ -20,6 +18,7 @@ const route = createRoute({ RolePermissions.ViewAccounts, ], }), + withUserParam, ] as const, responses: { 200: { @@ -30,14 +29,6 @@ const route = createRoute({ }, }, }, - 404: { - description: "User not found", - content: { - "application/json": { - schema: ErrorSchema, - }, - }, - }, }, request: { params: z.object({ @@ -48,14 +39,8 @@ const route = createRoute({ 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) { - throw new ApiError(404, "User not found"); - } + const otherUser = context.get("user"); const foundRelationship = await Relationship.fromOwnerAndSubject( user, diff --git a/api/api/v1/accounts/:id/follow.ts b/api/api/v1/accounts/:id/follow.ts index 9457183f..c6db8951 100644 --- a/api/api/v1/accounts/:id/follow.ts +++ b/api/api/v1/accounts/:id/follow.ts @@ -1,11 +1,9 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Relationship, User } from "@versia/kit/db"; +import { Relationship } 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 { ErrorSchema } from "~/types/api"; const schemas = { param: z.object({ @@ -37,6 +35,7 @@ const route = createRoute({ RolePermissions.ViewAccounts, ], }), + withUserParam, ] as const, responses: { 200: { @@ -47,14 +46,6 @@ const route = createRoute({ }, }, }, - 404: { - description: "User not found", - content: { - "application/json": { - schema: ErrorSchema, - }, - }, - }, }, request: { params: schemas.param, @@ -70,15 +61,9 @@ const route = createRoute({ 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"); - - const otherUser = await User.fromId(id); - - if (!otherUser) { - throw new ApiError(404, "User not found"); - } + const otherUser = context.get("user"); let relationship = await Relationship.fromOwnerAndSubject( user, diff --git a/api/api/v1/accounts/:id/followers.ts b/api/api/v1/accounts/:id/followers.ts index 805a2fc7..548f8770 100644 --- a/api/api/v1/accounts/:id/followers.ts +++ b/api/api/v1/accounts/:id/followers.ts @@ -1,11 +1,9 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; import { Timeline, User } from "@versia/kit/db"; import { RolePermissions, Users } from "@versia/kit/tables"; import { and, gt, gte, lt, sql } from "drizzle-orm"; import { z } from "zod"; -import { ApiError } from "~/classes/errors/api-error"; -import { ErrorSchema } from "~/types/api"; const schemas = { query: z.object({ @@ -34,6 +32,7 @@ const route = createRoute({ RolePermissions.ViewAccounts, ], }), + withUserParam, ] as const, request: { params: schemas.param, @@ -53,30 +52,15 @@ const route = createRoute({ }, }, }, - 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); + const otherUser = context.get("user"); // TODO: Add follower/following privacy settings - - if (!otherUser) { - throw new ApiError(404, "User not found"); - } - const { objects, link } = await Timeline.getUserTimeline( and( max_id ? lt(Users.id, max_id) : undefined, diff --git a/api/api/v1/accounts/:id/following.ts b/api/api/v1/accounts/:id/following.ts index f12e00c1..272f8b28 100644 --- a/api/api/v1/accounts/:id/following.ts +++ b/api/api/v1/accounts/:id/following.ts @@ -1,11 +1,9 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; import { Timeline, User } from "@versia/kit/db"; import { RolePermissions, Users } from "@versia/kit/tables"; import { and, gt, gte, lt, sql } from "drizzle-orm"; import { z } from "zod"; -import { ApiError } from "~/classes/errors/api-error"; -import { ErrorSchema } from "~/types/api"; const schemas = { query: z.object({ @@ -34,6 +32,7 @@ const route = createRoute({ RolePermissions.ViewAccounts, ], }), + withUserParam, ] as const, request: { params: schemas.param, @@ -54,27 +53,13 @@ const route = createRoute({ }, }, }, - 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) { - throw new ApiError(404, "User not found"); - } + const otherUser = context.get("user"); // TODO: Add follower/following privacy settings diff --git a/api/api/v1/accounts/:id/index.ts b/api/api/v1/accounts/:id/index.ts index 00ce43b5..bed7d8b2 100644 --- a/api/api/v1/accounts/:id/index.ts +++ b/api/api/v1/accounts/:id/index.ts @@ -1,10 +1,8 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; import { 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 route = createRoute({ method: "get", @@ -16,6 +14,7 @@ const route = createRoute({ auth: false, permissions: [RolePermissions.ViewAccounts], }), + withUserParam, ] as const, request: { params: z.object({ @@ -31,28 +30,14 @@ const route = createRoute({ }, }, }, - 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"); + app.openapi(route, (context) => { const { user } = context.get("auth"); + const otherUser = context.get("user"); - const foundUser = await User.fromId(id); - - if (!foundUser) { - throw new ApiError(404, "User not found"); - } - - return context.json(foundUser.toApi(user?.id === foundUser.id), 200); + return context.json(otherUser.toApi(user?.id === otherUser.id), 200); }), ); diff --git a/api/api/v1/accounts/:id/mute.ts b/api/api/v1/accounts/:id/mute.ts index 052f5f74..41b393f2 100644 --- a/api/api/v1/accounts/:id/mute.ts +++ b/api/api/v1/accounts/:id/mute.ts @@ -1,10 +1,8 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Relationship, User } from "@versia/kit/db"; +import { Relationship } 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({ @@ -35,6 +33,7 @@ const route = createRoute({ RolePermissions.ViewAccounts, ], }), + withUserParam, ] as const, request: { params: schemas.param, @@ -55,29 +54,15 @@ const route = createRoute({ }, }, }, - 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"); - - const otherUser = await User.fromId(id); - - if (!otherUser) { - throw new ApiError(404, "User not found"); - } + const otherUser = context.get("user"); const foundRelationship = await Relationship.fromOwnerAndSubject( user, diff --git a/api/api/v1/accounts/:id/note.ts b/api/api/v1/accounts/:id/note.ts index c6d1877b..5533b91c 100644 --- a/api/api/v1/accounts/:id/note.ts +++ b/api/api/v1/accounts/:id/note.ts @@ -1,10 +1,8 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Relationship, User } from "@versia/kit/db"; +import { Relationship } 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({ @@ -29,6 +27,7 @@ const route = createRoute({ RolePermissions.ViewAccounts, ], }), + withUserParam, ] as const, request: { params: schemas.param, @@ -49,28 +48,14 @@ const route = createRoute({ }, }, }, - 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"); - - const otherUser = await User.fromId(id); - - if (!otherUser) { - throw new ApiError(404, "User not found"); - } + const otherUser = context.get("user"); const foundRelationship = await Relationship.fromOwnerAndSubject( user, diff --git a/api/api/v1/accounts/:id/pin.ts b/api/api/v1/accounts/:id/pin.ts index 5276f79b..d33f7407 100644 --- a/api/api/v1/accounts/:id/pin.ts +++ b/api/api/v1/accounts/:id/pin.ts @@ -1,10 +1,8 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Relationship, User } from "@versia/kit/db"; +import { Relationship } 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 route = createRoute({ method: "post", @@ -20,6 +18,7 @@ const route = createRoute({ RolePermissions.ViewAccounts, ], }), + withUserParam, ] as const, request: { params: z.object({ @@ -35,27 +34,13 @@ const route = createRoute({ }, }, }, - 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) { - throw new ApiError(404, "User not found"); - } + const otherUser = context.get("user"); const foundRelationship = await Relationship.fromOwnerAndSubject( user, diff --git a/api/api/v1/accounts/:id/refetch.ts b/api/api/v1/accounts/:id/refetch.ts index 222b6c56..f5da0f71 100644 --- a/api/api/v1/accounts/:id/refetch.ts +++ b/api/api/v1/accounts/:id/refetch.ts @@ -1,4 +1,4 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; import { User } from "@versia/kit/db"; import { RolePermissions } from "@versia/kit/tables"; @@ -17,6 +17,7 @@ const route = createRoute({ scopes: ["write:accounts"], permissions: [RolePermissions.ViewAccounts], }), + withUserParam, ] as const, request: { params: z.object({ @@ -32,14 +33,6 @@ const route = createRoute({ }, }, }, - 404: { - description: "User not found", - content: { - "application/json": { - schema: ErrorSchema, - }, - }, - }, 400: { description: "User is local", content: { @@ -53,13 +46,7 @@ const route = createRoute({ export default apiRoute((app) => app.openapi(route, async (context) => { - const { id } = context.req.valid("param"); - - const otherUser = await User.fromId(id); - - if (!otherUser) { - throw new ApiError(404, "User not found"); - } + const otherUser = context.get("user"); if (otherUser.isLocal()) { throw new ApiError(400, "Cannot refetch a local user"); diff --git a/api/api/v1/accounts/:id/remove_from_followers.ts b/api/api/v1/accounts/:id/remove_from_followers.ts index e545ddf8..2ee7a465 100644 --- a/api/api/v1/accounts/:id/remove_from_followers.ts +++ b/api/api/v1/accounts/:id/remove_from_followers.ts @@ -1,10 +1,8 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Relationship, User } from "@versia/kit/db"; +import { Relationship } 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 route = createRoute({ method: "post", @@ -20,6 +18,7 @@ const route = createRoute({ RolePermissions.ViewAccounts, ], }), + withUserParam, ] as const, request: { params: z.object({ @@ -35,27 +34,13 @@ const route = createRoute({ }, }, }, - 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) { - throw new ApiError(404, "User not found"); - } + const otherUser = context.get("user"); const oppositeRelationship = await Relationship.fromOwnerAndSubject( otherUser, diff --git a/api/api/v1/accounts/:id/roles/:role_id/index.ts b/api/api/v1/accounts/:id/roles/:role_id/index.ts index 23c34339..af3840e5 100644 --- a/api/api/v1/accounts/:id/roles/:role_id/index.ts +++ b/api/api/v1/accounts/:id/roles/:role_id/index.ts @@ -1,6 +1,6 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Role, User } from "@versia/kit/db"; +import { Role } from "@versia/kit/db"; import { RolePermissions } from "@versia/kit/tables"; import { z } from "zod"; import { ApiError } from "~/classes/errors/api-error"; @@ -22,6 +22,7 @@ const routePost = createRoute({ auth: true, permissions: [RolePermissions.ManageRoles], }), + withUserParam, ] as const, request: { params: schemas.param, @@ -30,9 +31,8 @@ const routePost = createRoute({ 204: { description: "Role assigned", }, - 404: { - description: "User or role not found", + description: "Role not found", content: { "application/json": { schema: ErrorSchema, @@ -59,6 +59,7 @@ const routeDelete = createRoute({ auth: true, permissions: [RolePermissions.ManageRoles], }), + withUserParam, ] as const, request: { params: schemas.param, @@ -67,9 +68,8 @@ const routeDelete = createRoute({ 204: { description: "Role removed", }, - 404: { - description: "User or role not found", + description: "Role not found", content: { "application/json": { schema: ErrorSchema, @@ -90,19 +90,14 @@ const routeDelete = createRoute({ export default apiRoute((app) => { app.openapi(routePost, async (context) => { const { user } = context.get("auth"); - const { id, role_id } = context.req.valid("param"); + const { role_id } = context.req.valid("param"); + const targetUser = context.get("user"); - const targetUser = await User.fromId(id); const role = await Role.fromId(role_id); if (!role) { throw new ApiError(404, "Role not found"); } - - if (!targetUser) { - throw new ApiError(404, "User not found"); - } - // Priority check const userRoles = await Role.getUserRoles(user.id, user.data.isAdmin); @@ -125,19 +120,15 @@ export default apiRoute((app) => { app.openapi(routeDelete, async (context) => { const { user } = context.get("auth"); - const { id, role_id } = context.req.valid("param"); + const { role_id } = context.req.valid("param"); + const targetUser = context.get("user"); - const targetUser = await User.fromId(id); const role = await Role.fromId(role_id); if (!role) { throw new ApiError(404, "Role not found"); } - if (!targetUser) { - throw new ApiError(404, "User not found"); - } - // Priority check const userRoles = await Role.getUserRoles(user.id, user.data.isAdmin); diff --git a/api/api/v1/accounts/:id/roles/index.ts b/api/api/v1/accounts/:id/roles/index.ts index d3718d05..fedf9adc 100644 --- a/api/api/v1/accounts/:id/roles/index.ts +++ b/api/api/v1/accounts/:id/roles/index.ts @@ -1,9 +1,7 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Role, User } from "@versia/kit/db"; +import { Role } from "@versia/kit/db"; import { z } from "zod"; -import { ApiError } from "~/classes/errors/api-error"; -import { ErrorSchema } from "~/types/api"; const route = createRoute({ method: "get", @@ -13,6 +11,7 @@ const route = createRoute({ auth({ auth: false, }), + withUserParam, ] as const, request: { params: z.object({ @@ -28,26 +27,12 @@ const route = createRoute({ }, }, }, - 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 targetUser = await User.fromId(id); - - if (!targetUser) { - throw new ApiError(404, "User not found"); - } + const targetUser = context.get("user"); const roles = await Role.getUserRoles( targetUser.id, diff --git a/api/api/v1/accounts/:id/statuses.ts b/api/api/v1/accounts/:id/statuses.ts index 04b30cf6..71d24c22 100644 --- a/api/api/v1/accounts/:id/statuses.ts +++ b/api/api/v1/accounts/:id/statuses.ts @@ -1,11 +1,9 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Note, Timeline, User } from "@versia/kit/db"; +import { Note, 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"; -import { ApiError } from "~/classes/errors/api-error"; -import { ErrorSchema } from "~/types/api"; const schemas = { param: z.object({ @@ -53,6 +51,7 @@ const route = createRoute({ ], scopes: ["read:statuses"], }), + withUserParam, ] as const, request: { params: schemas.param, @@ -72,27 +71,13 @@ const route = createRoute({ }, }, }, - 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) { - throw new ApiError(404, "User not found"); - } + const otherUser = context.get("user"); const { max_id, @@ -110,7 +95,7 @@ export default apiRoute((app) => 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), + eq(Notes.authorId, otherUser.id), only_media ? sql`EXISTS (SELECT 1 FROM "Attachments" WHERE "Attachments"."noteId" = ${Notes.id})` : undefined, diff --git a/api/api/v1/accounts/:id/unblock.ts b/api/api/v1/accounts/:id/unblock.ts index 162478f7..fe892b08 100644 --- a/api/api/v1/accounts/:id/unblock.ts +++ b/api/api/v1/accounts/:id/unblock.ts @@ -1,10 +1,8 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Relationship, User } from "@versia/kit/db"; +import { Relationship } 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 route = createRoute({ method: "post", @@ -20,6 +18,7 @@ const route = createRoute({ RolePermissions.ViewAccounts, ], }), + withUserParam, ] as const, request: { params: z.object({ @@ -35,27 +34,13 @@ const route = createRoute({ }, }, }, - 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) { - throw new ApiError(404, "User not found"); - } + const otherUser = context.get("user"); const foundRelationship = await Relationship.fromOwnerAndSubject( user, diff --git a/api/api/v1/accounts/:id/unfollow.ts b/api/api/v1/accounts/:id/unfollow.ts index 2c24fe9e..19cf3b57 100644 --- a/api/api/v1/accounts/:id/unfollow.ts +++ b/api/api/v1/accounts/:id/unfollow.ts @@ -1,9 +1,8 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Relationship, User } from "@versia/kit/db"; +import { Relationship } 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 route = createRoute({ @@ -20,6 +19,7 @@ const route = createRoute({ RolePermissions.ViewAccounts, ], }), + withUserParam, ] as const, request: { params: z.object({ @@ -35,15 +35,6 @@ const route = createRoute({ }, }, }, - - 404: { - description: "User not found", - content: { - "application/json": { - schema: ErrorSchema, - }, - }, - }, 500: { description: "Failed to unfollow user during federation", content: { @@ -57,14 +48,8 @@ const route = createRoute({ 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) { - throw new ApiError(404, "User not found"); - } + const otherUser = context.get("user"); const foundRelationship = await Relationship.fromOwnerAndSubject( user, diff --git a/api/api/v1/accounts/:id/unmute.ts b/api/api/v1/accounts/:id/unmute.ts index 3977276c..c9ebe777 100644 --- a/api/api/v1/accounts/:id/unmute.ts +++ b/api/api/v1/accounts/:id/unmute.ts @@ -1,10 +1,8 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Relationship, User } from "@versia/kit/db"; +import { Relationship } 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 route = createRoute({ method: "post", @@ -20,6 +18,7 @@ const route = createRoute({ RolePermissions.ViewAccounts, ], }), + withUserParam, ] as const, request: { params: z.object({ @@ -35,28 +34,13 @@ const route = createRoute({ }, }, }, - - 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) { - throw new ApiError(404, "User not found"); - } + const otherUser = context.get("user"); const foundRelationship = await Relationship.fromOwnerAndSubject( user, diff --git a/api/api/v1/accounts/:id/unpin.ts b/api/api/v1/accounts/:id/unpin.ts index a729957f..ee4baf93 100644 --- a/api/api/v1/accounts/:id/unpin.ts +++ b/api/api/v1/accounts/:id/unpin.ts @@ -1,10 +1,8 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withUserParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Relationship, User } from "@versia/kit/db"; +import { Relationship } 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 route = createRoute({ method: "post", @@ -20,6 +18,7 @@ const route = createRoute({ RolePermissions.ViewAccounts, ], }), + withUserParam, ] as const, request: { params: z.object({ @@ -35,28 +34,13 @@ const route = createRoute({ }, }, }, - - 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) { - throw new ApiError(404, "User not found"); - } + const otherUser = context.get("user"); const foundRelationship = await Relationship.fromOwnerAndSubject( user, diff --git a/api/api/v1/statuses/:id/context.ts b/api/api/v1/statuses/:id/context.ts index 7e26d503..b21e58e4 100644 --- a/api/api/v1/statuses/:id/context.ts +++ b/api/api/v1/statuses/:id/context.ts @@ -1,9 +1,8 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withNoteParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; import { Note } 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 route = createRoute({ @@ -14,6 +13,7 @@ const route = createRoute({ auth: false, permissions: [RolePermissions.ViewNotes], }), + withNoteParam, ] as const, summary: "Get status context", request: { @@ -46,19 +46,12 @@ const route = createRoute({ export default apiRoute((app) => app.openapi(route, async (context) => { - const { id } = context.req.valid("param"); - const { user } = context.get("auth"); + const note = context.get("note"); - const foundStatus = await Note.fromId(id, user?.id); + const ancestors = await note.getAncestors(user ?? null); - if (!foundStatus) { - throw new ApiError(404, "Note not found"); - } - - const ancestors = await foundStatus.getAncestors(user ?? null); - - const descendants = await foundStatus.getDescendants(user ?? null); + const descendants = await note.getDescendants(user ?? null); return context.json( { diff --git a/api/api/v1/statuses/:id/favourite.test.ts b/api/api/v1/statuses/:id/favourite.test.ts index 381db73d..1c097343 100644 --- a/api/api/v1/statuses/:id/favourite.test.ts +++ b/api/api/v1/statuses/:id/favourite.test.ts @@ -32,7 +32,6 @@ describe("/api/v1/statuses/:id/favourite", () => { }, }, ); - expect(response.status).toBe(200); const json = (await response.json()) as ApiStatus; diff --git a/api/api/v1/statuses/:id/favourite.ts b/api/api/v1/statuses/:id/favourite.ts index 287b54c8..e89bd84e 100644 --- a/api/api/v1/statuses/:id/favourite.ts +++ b/api/api/v1/statuses/:id/favourite.ts @@ -1,10 +1,8 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withNoteParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; import { Note } 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 route = createRoute({ method: "post", @@ -18,6 +16,7 @@ const route = createRoute({ RolePermissions.ViewNotes, ], }), + withNoteParam, ] as const, request: { params: z.object({ @@ -33,29 +32,13 @@ const route = createRoute({ }, }, }, - - 404: { - description: "Record 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 note = await Note.fromId(id, user?.id); - - if (!(note && (await note?.isViewableByUser(user)))) { - throw new ApiError(404, "Note not found"); - } + const note = context.get("note"); await user.like(note); diff --git a/api/api/v1/statuses/:id/favourited_by.ts b/api/api/v1/statuses/:id/favourited_by.ts index e9b6efd9..5287b248 100644 --- a/api/api/v1/statuses/:id/favourited_by.ts +++ b/api/api/v1/statuses/:id/favourited_by.ts @@ -1,11 +1,9 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withNoteParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Note, Timeline, User } from "@versia/kit/db"; +import { Timeline, User } from "@versia/kit/db"; import { RolePermissions, Users } from "@versia/kit/tables"; import { and, gt, gte, lt, sql } from "drizzle-orm"; import { z } from "zod"; -import { ApiError } from "~/classes/errors/api-error"; -import { ErrorSchema } from "~/types/api"; const schemas = { query: z.object({ @@ -31,6 +29,7 @@ const route = createRoute({ RolePermissions.ViewNoteLikes, ], }), + withNoteParam, ] as const, request: { params: schemas.param, @@ -45,30 +44,13 @@ const route = createRoute({ }, }, }, - - 404: { - description: "Record not found", - content: { - "application/json": { - schema: ErrorSchema, - }, - }, - }, }, }); export default apiRoute((app) => app.openapi(route, async (context) => { const { max_id, since_id, min_id, limit } = context.req.valid("query"); - const { id } = context.req.valid("param"); - - const { user } = context.get("auth"); - - const note = await Note.fromId(id, user?.id); - - if (!(note && (await note?.isViewableByUser(user)))) { - throw new ApiError(404, "Note not found"); - } + const note = context.get("note"); const { objects, link } = await Timeline.getUserTimeline( and( diff --git a/api/api/v1/statuses/:id/index.ts b/api/api/v1/statuses/:id/index.ts index 6908b019..10ea9981 100644 --- a/api/api/v1/statuses/:id/index.ts +++ b/api/api/v1/statuses/:id/index.ts @@ -1,4 +1,4 @@ -import { apiRoute, auth, jsonOrForm } from "@/api"; +import { apiRoute, auth, jsonOrForm, withNoteParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; import { Attachment, Note } from "@versia/kit/db"; import { RolePermissions } from "@versia/kit/tables"; @@ -75,6 +75,7 @@ const routeGet = createRoute({ auth: false, permissions: [RolePermissions.ViewNotes], }), + withNoteParam, ] as const, request: { params: schemas.param, @@ -111,6 +112,7 @@ const routeDelete = createRoute({ RolePermissions.ViewNotes, ], }), + withNoteParam, ] as const, request: { params: schemas.param, @@ -156,6 +158,7 @@ const routePut = createRoute({ ], }), jsonOrForm(), + withNoteParam, ] as const, request: { params: schemas.param, @@ -190,14 +193,6 @@ const routePut = createRoute({ }, }, }, - 404: { - description: "Record not found", - content: { - "application/json": { - schema: ErrorSchema, - }, - }, - }, 422: { description: "Invalid media IDs", content: { @@ -211,27 +206,15 @@ const routePut = createRoute({ export default apiRoute((app) => { app.openapi(routeGet, async (context) => { - const { id } = context.req.valid("param"); const { user } = context.get("auth"); - - const note = await Note.fromId(id, user?.id); - - if (!(note && (await note?.isViewableByUser(user)))) { - throw new ApiError(404, "Note not found"); - } + const note = context.get("note"); return context.json(await note.toApi(user), 200); }); app.openapi(routeDelete, async (context) => { - const { id } = context.req.valid("param"); const { user } = context.get("auth"); - - const note = await Note.fromId(id, user?.id); - - if (!(note && (await note?.isViewableByUser(user)))) { - throw new ApiError(404, "Note not found"); - } + const note = context.get("note"); if (note.author.id !== user.id) { throw new ApiError(401, "Unauthorized"); @@ -246,14 +229,8 @@ export default apiRoute((app) => { }); app.openapi(routePut, async (context) => { - const { id } = context.req.valid("param"); const { user } = context.get("auth"); - - const note = await Note.fromId(id, user?.id); - - if (!(note && (await note?.isViewableByUser(user)))) { - throw new ApiError(404, "Note not found"); - } + const note = context.get("note"); if (note.author.id !== user.id) { throw new ApiError(401, "Unauthorized"); diff --git a/api/api/v1/statuses/:id/pin.ts b/api/api/v1/statuses/:id/pin.ts index 6bab4f78..899412e6 100644 --- a/api/api/v1/statuses/:id/pin.ts +++ b/api/api/v1/statuses/:id/pin.ts @@ -1,4 +1,4 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withNoteParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; import { Note, db } from "@versia/kit/db"; import { RolePermissions } from "@versia/kit/tables"; @@ -7,12 +7,6 @@ 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(), - }), -}; - const route = createRoute({ method: "post", path: "/api/v1/statuses/{id}/pin", @@ -25,9 +19,12 @@ const route = createRoute({ RolePermissions.ViewNotes, ], }), + withNoteParam, ] as const, request: { - params: schemas.param, + params: z.object({ + id: z.string().uuid(), + }), }, responses: { 200: { @@ -46,14 +43,6 @@ const route = createRoute({ }, }, }, - 404: { - description: "Record not found", - content: { - "application/json": { - schema: ErrorSchema, - }, - }, - }, 422: { description: "Already pinned", content: { @@ -67,16 +56,10 @@ const route = createRoute({ export default apiRoute((app) => app.openapi(route, async (context) => { - const { id } = context.req.valid("param"); const { user } = context.get("auth"); + const note = context.get("note"); - const foundStatus = await Note.fromId(id, user?.id); - - if (!foundStatus) { - throw new ApiError(404, "Note not found"); - } - - if (foundStatus.author.id !== user.id) { + if (note.author.id !== user.id) { throw new ApiError(401, "Unauthorized"); } @@ -84,7 +67,7 @@ export default apiRoute((app) => await db.query.UserToPinnedNotes.findFirst({ where: (userPinnedNote, { and, eq }): SQL | undefined => and( - eq(userPinnedNote.noteId, foundStatus.data.id), + eq(userPinnedNote.noteId, note.data.id), eq(userPinnedNote.userId, user.id), ), }) @@ -92,8 +75,8 @@ export default apiRoute((app) => throw new ApiError(422, "Already pinned"); } - await user.pin(foundStatus); + await user.pin(note); - return context.json(await foundStatus.toApi(user), 200); + return context.json(await note.toApi(user), 200); }), ); diff --git a/api/api/v1/statuses/:id/reblog.ts b/api/api/v1/statuses/:id/reblog.ts index 4ab0bfd0..6651fae4 100644 --- a/api/api/v1/statuses/:id/reblog.ts +++ b/api/api/v1/statuses/:id/reblog.ts @@ -1,4 +1,4 @@ -import { apiRoute, auth, jsonOrForm } from "@/api"; +import { apiRoute, auth, jsonOrForm, withNoteParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; import { Note } from "@versia/kit/db"; import { Notes, RolePermissions } from "@versia/kit/tables"; @@ -29,6 +29,7 @@ const route = createRoute({ ], }), jsonOrForm(), + withNoteParam, ] as const, request: { params: schemas.param, @@ -55,15 +56,6 @@ const route = createRoute({ }, }, }, - - 404: { - description: "Record not found", - content: { - "application/json": { - schema: ErrorSchema, - }, - }, - }, 422: { description: "Already reblogged", content: { @@ -85,15 +77,9 @@ const route = createRoute({ export default apiRoute((app) => app.openapi(route, async (context) => { - const { id } = context.req.valid("param"); const { visibility } = context.req.valid("json"); const { user } = context.get("auth"); - - const note = await Note.fromId(id, user.id); - - if (!(note && (await note?.isViewableByUser(user)))) { - throw new ApiError(404, "Note not found"); - } + const note = context.get("note"); const existingReblog = await Note.fromSql( and(eq(Notes.authorId, user.id), eq(Notes.reblogId, note.data.id)), diff --git a/api/api/v1/statuses/:id/reblogged_by.ts b/api/api/v1/statuses/:id/reblogged_by.ts index a2a3d476..72a06c5b 100644 --- a/api/api/v1/statuses/:id/reblogged_by.ts +++ b/api/api/v1/statuses/:id/reblogged_by.ts @@ -1,11 +1,9 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withNoteParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; -import { Note, Timeline, User } from "@versia/kit/db"; +import { Timeline, User } from "@versia/kit/db"; import { RolePermissions, Users } from "@versia/kit/tables"; import { and, gt, gte, lt, sql } from "drizzle-orm"; import { z } from "zod"; -import { ApiError } from "~/classes/errors/api-error"; -import { ErrorSchema } from "~/types/api"; const schemas = { param: z.object({ @@ -31,6 +29,7 @@ const route = createRoute({ RolePermissions.ViewNoteBoosts, ], }), + withNoteParam, ] as const, request: { params: schemas.param, @@ -45,29 +44,13 @@ const route = createRoute({ }, }, }, - - 404: { - description: "Record 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, min_id, since_id, limit } = context.req.valid("query"); - const { user } = context.get("auth"); - - const note = await Note.fromId(id, user.id); - - if (!(note && (await note?.isViewableByUser(user)))) { - throw new ApiError(404, "Note not found"); - } + const note = context.get("note"); const { objects, link } = await Timeline.getUserTimeline( and( diff --git a/api/api/v1/statuses/:id/source.ts b/api/api/v1/statuses/:id/source.ts index 486392ad..a54a53da 100644 --- a/api/api/v1/statuses/:id/source.ts +++ b/api/api/v1/statuses/:id/source.ts @@ -1,11 +1,8 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withNoteParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; import type { StatusSource as ApiStatusSource } from "@versia/client/types"; -import { Note } 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 route = createRoute({ method: "get", @@ -19,6 +16,7 @@ const route = createRoute({ RolePermissions.ViewNotes, ], }), + withNoteParam, ] as const, request: { params: z.object({ @@ -38,28 +36,12 @@ const route = createRoute({ }, }, }, - - 404: { - description: "Record 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 note = await Note.fromId(id, user.id); - - if (!(note && (await note?.isViewableByUser(user)))) { - throw new ApiError(404, "Note not found"); - } + app.openapi(route, (context) => { + const note = context.get("note"); return context.json( { diff --git a/api/api/v1/statuses/:id/unfavourite.ts b/api/api/v1/statuses/:id/unfavourite.ts index 7914a11e..09c75dd1 100644 --- a/api/api/v1/statuses/:id/unfavourite.ts +++ b/api/api/v1/statuses/:id/unfavourite.ts @@ -1,10 +1,8 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withNoteParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; import { Note } 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 route = createRoute({ method: "post", @@ -18,6 +16,7 @@ const route = createRoute({ RolePermissions.ViewNotes, ], }), + withNoteParam, ] as const, request: { params: z.object({ @@ -33,28 +32,13 @@ const route = createRoute({ }, }, }, - - 404: { - description: "Record 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 note = await Note.fromId(id, user.id); - - if (!(note && (await note?.isViewableByUser(user)))) { - throw new ApiError(404, "Note not found"); - } + const note = context.get("note"); await user.unlike(note); diff --git a/api/api/v1/statuses/:id/unpin.ts b/api/api/v1/statuses/:id/unpin.ts index 2849a251..bf5561fa 100644 --- a/api/api/v1/statuses/:id/unpin.ts +++ b/api/api/v1/statuses/:id/unpin.ts @@ -1,4 +1,4 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withNoteParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; import { Note } from "@versia/kit/db"; import { RolePermissions } from "@versia/kit/tables"; @@ -18,6 +18,7 @@ const route = createRoute({ RolePermissions.ViewNotes, ], }), + withNoteParam, ] as const, request: { params: z.object({ @@ -41,38 +42,24 @@ const route = createRoute({ }, }, }, - 404: { - description: "Record 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 note = context.get("note"); - const status = await Note.fromId(id, user.id); - - if (!status) { - throw new ApiError(404, "Note not found"); - } - - if (status.author.id !== user.id) { + if (note.author.id !== user.id) { throw new ApiError(401, "Unauthorized"); } - await user.unpin(status); + await user.unpin(note); - if (!status) { + if (!note) { throw new ApiError(404, "Note not found"); } - return context.json(await status.toApi(user), 200); + return context.json(await note.toApi(user), 200); }), ); diff --git a/api/api/v1/statuses/:id/unreblog.ts b/api/api/v1/statuses/:id/unreblog.ts index 471092b8..e56f2082 100644 --- a/api/api/v1/statuses/:id/unreblog.ts +++ b/api/api/v1/statuses/:id/unreblog.ts @@ -1,4 +1,4 @@ -import { apiRoute, auth } from "@/api"; +import { apiRoute, auth, withNoteParam } from "@/api"; import { createRoute } from "@hono/zod-openapi"; import { Note } from "@versia/kit/db"; import { Notes, RolePermissions } from "@versia/kit/tables"; @@ -19,6 +19,7 @@ const route = createRoute({ RolePermissions.ViewNotes, ], }), + withNoteParam, ] as const, request: { params: z.object({ @@ -34,15 +35,6 @@ const route = createRoute({ }, }, }, - - 404: { - description: "Record not found", - content: { - "application/json": { - schema: ErrorSchema, - }, - }, - }, 422: { description: "Not already reblogged", content: { @@ -58,13 +50,7 @@ export default apiRoute((app) => app.openapi(route, async (context) => { const { id } = context.req.valid("param"); const { user } = context.get("auth"); - - const note = await Note.fromId(id, user.id); - - // Check if user is authorized to view this status (if it's private) - if (!(note && (await note?.isViewableByUser(user)))) { - throw new ApiError(404, "Note not found"); - } + const note = context.get("note"); const existingReblog = await Note.fromSql( and(eq(Notes.authorId, user.id), eq(Notes.reblogId, note.data.id)), diff --git a/bun.lockb b/bun.lockb index 4b2e597375a4650e60a7a864b9e4a1e720eccade..53d081eeaa50cce762c84a371c100c0f3ff2df96 100755 GIT binary patch delta 34375 zcmeHw33yFc+xFfk=Ooz(F(${6B5EciL=q7v=6N0?f*=T)NMa_5PPE2utj17uR&7;P zV;(!8sG(?-lIox}RTb5u-+k}BHqkfrz3+d0-}V33f7w^=bFb%F&wAFl*Is+dKKG`7 z_FMk*yQ*mmxN#`+EVtMuEye|0!~Hg5LxCgZD#U8oUkc zqhu-B>7(Mur6CJHcnL(rt&)mT1^gAbGI%ri3Ggz>S>OQ3!@!lm?IhO+SA<+1%#7}# zHmvyta0T#D$=kr?A-_e=LSP{RtWmb)B+0R0mNXaEtYJoaQsPh)v^(@{%JhWf@d@aa zVW~-}?3{hbg-u=_xuOi&Bf+#AYxbye1~TL0Q99<=1DxfHKnMa%@C6vYv%OFaW;iN7 zJ#|8gqFhD$QGW+aha*$djKOjaGVN+39mmQc$ZYc6U}m%i%og|r@$AxQFe~%{nx1|# zh9?dhnUJ2L3{6bQ7?+TcJ`(Lp2WcZw9c*c6cV;{gY?K(xglQw|XG}^hT^8vaF%_3VN4btfANoGK=3HOs|b){IF5+Nn_y6l50RuZxz9; zmM@sqiLGwx-vu*$YDz*zVse5~uZF3A1J#6m&+O#HjQEUrMOg*~6DGr|{_vs749FZY z$zYC%STL*K9ZZLr!-A3%hmA}9qLwKq56>Kzgo1sd$J_mC$3si&>^f#6WPn+`)<{jx zOi4@19GRH1MPFt2bB?WRT8#kHcXCqFB)C#y^&@t_tO3w6P5U54ae=!)W<%bBOpb(4 z_HcbLo1&`BV{~ddcYeiR#`}QjJu!7$!tlg&B{bM<^4ehf50G3O%(CBUXmo)-(&5ay zgfK_r*Wl7%RpybNkTg62qa!VT9D)gxK5A^XVp2lJu}QKPXThlU=)97$}gxP4RDZ`A$nJ_LsEe(x*(ab3OTQJib`z`j(6OcOFn6 zXHE6Ee#Em|M05*t!fOO(AJhc14b$Q?M%5paFv%rlUoeZC-_jh;L3)f=<=R%Eyb#OG ztig$~mpH8ch?HT885tY^i}h7r4SGy$YmUI=^pR=t!^Vv3)6VRR^X(O-9#ZE?o(0B` z$c~e8LvVe_D!2i-0O_iLll7|Je(C`|!n<k-T{SKJ@FsQ3JAq&VH^|Qe&|KM(> z?9*LQszLq*GUwUvAYnIT;gcJ7mNL?F0zY;|Oyd3hq@Kpr{JAYPdJfY>CZaX1opkVGJxey|ox@>aA(+@6%iA z(997?qRf@kT^Q^+5G_A1Aip|UP>eu;C2R?P5pnviewH%q=BTo>|N;6U&* zV0Oh|86P3Jg4A1GaslJK6zs0|Gwlz8*%GgSv*<7f0oFWLMubW(0j>@G4}Hx9?(k zk(ge8LOOQplhCutUC?tU8<~_kbm$~SS(0KpUI1o%`&6@O=YlB@9Tz_=Aze`%*yGvc zoxm)>axmjDenuvQC`wdnnCS=^(zh~mteK!b9Qne~XPh~zc_8OtTL)%=1N2$ND`)+H zEa)zAWC~_fMM+PJPalOIFOK3eb;4*EU>hAj2C<4#sNRN`-I-=?BNCGmk`mK1ta$4X zXSG{=`lJ+NGKJsb@H=FJv5RD7$ET!Y8dsFxMw`WJGSMv1WH5KShUgeh)^*WlWx#&m za^R}avl=BPo9%KBdbZDpkg2yiay4XTxDw2Y&I8k*ui>|>l6_Y-+vgIP5qtGbCDy84 z)+hRSsZ}j6))BtKTr(Hz2!DJ1f|76A)scEcS+}}LpH#MiI#*v^_N+R6{j73Xs%A}& zIeO^|Yg<}*TV=M&W|jTqk7gY|1GDihJFBj~w)Hl@6CTJYzhamgp_g{~m9`E>)L{trgJh%9>;Qm4Dv+I0S+E(1I z4`|k0jn#9Txz*?OeEfc{JEPn-PlrArs=3-i&y8}csd_$s-`AbZ-Rc!R2ET3e+~#iW z1y4n3hC(~^Rn5cH&vd8Tt=0BY6zpKx9(vX2Ff9vGb4cEXv<(vN?UZc3-g?F8=4y96 zCfcnn^;VRYh_V^3e$?}$-D)-6*}|M{5|r02G9Yn8BDbcdB{xJ!Z*4hdNy``7fC zmTvWDJ-4M>tAXxkMxOerPT}4VIzzDQtG+da0cIK$F9Lf9rXA+mGE7?lsUsv+kB<&h zzt)|t-D-dy)7q^?V0eu*Vyd1# ze8}y{9?8e=INjNa!y%@VTl>jRQKF4%umv;yt^QfnB1}6C3CD0$n8V{qJ*G2zGq&3R07Y@bs+1nzGR!dv zQWJf7RHU{RAr3X9E(rIA5M>DQaQr0J4P35x>+zk!)Ms^P54XBkkLiIHt!$M)zDJnW z7ZP3A^nxy7j=hjN>Sww{YGtaRjiI;e@t4B&+@5ajS!h}t{!t{yeoA^kbflvsj&@D- zfMd+NNx2q9V4|D5Msl78oSv(Najw*Hg@7z&*D0mM(YHLamEhhSPID~s(Mz> z@9WkoV|~!fuw~mOLSnTrTVSYcgv30sS3Sh)^0*0)1h!dboNEpuIJ+SMLi$CmvrYKw^~<^!7ucK+}g_+S8QvH zx~MSqu6fjeq+ppHhTlT;*jQ=yMeiH*p13hg2Xm8 zrEQSdriP^6)t&KfNBxGd*W+47Y9kS1-=J4fsn;Q~Z%`@h)E$DY9xwPN+#7<~HLPzw zC1XrGQEKjp9!5)Z&B9&Q%z6zZ4pMW@Jqrnch~sc7-N-yOuyOa(q9DonBPz`C45Su@ z_fHXGu9_a-HcV};=MQ(Q({yKoTiXf8>@7vdo~ho^bD^sjYF5Tu=bSVH5_``q*=9(x zchQ$OA+d>&J8IJubv6dGHXR{OGB|9t4s&dT)K5P%JknMwOkXk*GZI!~vbB)uLftva zt>)`7_v9G3#q7keH7rcQx&0NM`%9cYmOzeg^wQE!37)NP`v~4v8LcG<1h)Z$hGX z9C-(YY3Cr}k2$vtLv!S(xV6661v#9IMSwONlH8k6nvbPq>_3_(I@zozjh1!m#=aPJm47MfA0#wtkMIZUVLA;A_BI$5iR2xf1tNMc)Z zMWQW*lBLFEG$hP%0a9CIE{Mdg4S_i#=`=`83JH_&7mz}D40EU$Lgw7j2qBIxj6Ae^ zDkOG2`t@qKHv|^moMF#Gx?c*_PtVVAs~vP_rd!qZm`t~}x6S?dXT!a*E1AaZ{cznm z-mNWxjy<9oIqiqk)5r-WQ!DBDu*hv^_N6gHXr($RN{VTLl1+O^x(I39{kS+B8%@i2 zOs5di?ngy-Qk0DQQhDs}qaZo-f{t(riA{*z8X0VX#LmI-3X|1kNUQ*MGwidxtIZFnz583CQK@^tKn)J$LAGg9;HttjXn6rx~Yn4=S<{>HN6RfLcM>E{gM+<0e8_W(x?gub?biN+?*5Q+bQ{cxk2|`K5W1X09Ibx_5{JJzaaSH<_KX=f3=(_II8tf) z{WuI!?Ia}jnmJvS)nlG@t8MjM{C=qCKkIh*4^@<*`k7}Vv3QDk&h6L*d6d3 z&xbq@^-R;3BmOTvW~SQ_H(XJMPsh!cU_7u=3^iAK!x zk?I!RIooZ!ov1IF(cCe3w4w~+DAKkegd+ilJZJHNW6ZtXT+l3r#0ECcTMj|u&Vi5+Rw#?pG0Nj5NkFQ#(h! zA#pmcm}2&zIrR>Ngd<*daXr3YxHklr3C&~*=2TS`OWK}#{(QG4QY}w72x>dqZdq!8j?@-hf0`W;>jN#AIlPq%f`ASTj#pV(#w=iBlp9hw8lui7qir zF^3(}V|2G#MbFjU4&E~|R~b7IVuP4R9?x`hb!SW~S{q1>jJS9V!HJOQ8joLShNcMyZ`)mJkPvPGMRcq!7cB-7yamkB5qJG{a7{*sb|y;sObD#q@%{VOk4F zX2Ws8Hbr*|w{`$J)&!H*pW)u)?+*kNBpMRUF=u9kX_Fx_GpvZw9PdD4GaBo4?H3t` zB6r3-IKdh~oFg+JbvM$XkdBWbh3IjZNpB()ZTg!ZuIJ~v)p@#eiCa54(bOAEV?rLZXtowgeii;O10nM1|NR;A*%gnL7XL{u@Okb5A>A&DYuQ%pmw9&m=!3KGsQ zF^%L%y|L8C?vuaFt^Eld3x{?_7Lik}mc@!feM*m6?q2`Z@~N}4dMS!Rs6cOk8`%c* zrGTv{rhPvn)?^350|Dyeq@L^n43TnSrXJHymJx-3k#M+V%rM4QnC;MDhA_5AF$;i+ z!q~{H2=;4Z`&Y&hmK!4~05d3H`#)ll+(E4=zO1YItBQEHgIYO}WtnX_ESg!ixd3@S zK>Gy%H!|fH0XD9XybR2?SP5`j2^14uIwI?&j;ePS)4T(4E6i;0wE**4CwaZ(4U+S~ zOurG}R+zmY?=ZAQGt2lfJCqJSk%s>c)BaO{{FzKg)_}tR3w{jXRur>B$BkHWT{HAR z@vky7Isw?knfhvj+7wTjRz))Xp9Va|-A<~XDA8Fq@_Cu2Fw^#XsV~fIyh{M%ugLg+ zhuP7;0&IgDG96g#2~wTn9h3y8GKz9b#*kT?-zDFc`l479cRQ;M#J~o!Hg+b&Runs+ z*AP#WV)$V#N=W;{%+mTo&(f9!)1;iVBUA4uWitIZRizHd2pHo!01>o#LULsa*vK|< zw}I-1qXF(O$mnWRL#{668j@>*xsfXgpRTH(v1hcAYO)WadxDv~x77bT><#;Yu=fNH z1vBX|$-^mNBh&8)DT4!zh(Zd(L6S5eYvLfvi55|m@lsuwJ;dFvYGpAnSVbd?xL|}k zcT;_hDRdg_nPj?5N9Le=M#_bms%K^V92x(gFq6#14|$#{r*H&}tuS*u=+bbZj3-n7 z5||CNMCuDO$H{W&*}$*Lcrw$y31qWb7tuWX2JT)_<>T61DAn56-+!f3uF-&ibF)>UaG%X7OHy3K(EY~hD9+QEkHcmXAzk4Vmbo9#2?Aj=gRmc zj2G|rLPiIWkufM%NZZ0p)hekk%xufopr_B*!932sC+*0LUnk|FnDHMXo+cmJa37V0 z=i>r4vAl_#X1B}qWCnLinN0JK!OZ^?Frz+`@?Hwq$c*17dB4Ep!psJ_06jC{(;wUlGb?ZvdiwbVOp{-w-LE!GM-2al9~%BH zBghQ?A!RZh{3&HJgLkD&X8b)VlUZ{+OC_06o{-7jQtzE*1!P1qDU<2I7fe-I$rUJI zD~jou4+)h6*8`UThf901ALMRQwm8cQ{9od%e<{(U3Q`_57%pq@Kf~z%{~1T7A9mSj z6p&q!1m>7XmIW@%j8BE$gR7Hq42Zk^avAfwG%U>Y$cOT{kxPL$ zfT{lo%o1({b8p)U=2n<#$Hzu_to;~F`4eeJru?agJaPF$Bz4=qcIH-@6hD9;`r_1<9G0Qt%+7)J&YXbD- ziD1s`PlIXyoL%k=GofHc&r8D>q`_P%&y(_tVA?H|yjb!}l9xzcCi!K_E5R(lDlj)P zhu)j^vS!3vP_RU6z&0_*E$6fMq2$!!}N!w zT_J91Oq}_MV1{SG9P{6SX?sEPB{0iFk+nehZL{*0t!7RXcQvMOlid>X@RmNWf)6cJ9qa|+0!0%utxFhAiz;vi$AkfebrtB%%8%#%< zl#7FDR}##YsUY?KVEidh;D;?(9n5@cgK_N7QW_&bN1fP9fYw%0!R*_KU^Iqet^qzQ^(Vlr>3MK5@MSRl{|cs^KN2zh6W}Zw)W)shp@(WU) z4dzyuSwOx8M4m6>i()i_BBY;}jP{4(B`8?3Tf$VP-+!lJW0= zS&;W6zc2X%Ft_{67nv%ExZybHCJo0y_kZ`ND$BS}mZ31Ste->gC%Po?(q4k<`yc=K z8-00RdN0h(^*iXv--DUkk5axM?a7S)2~5>Bss9hy97|RL_%LoL^#arJ|Nc!SGk;|W zGUMPK25=*@0K+9GfVmZBj;TaL`)H;g-b~^~W=kefz?Q_@Ufk|0O!4teC9|C#-&B(0 zq3}&64xq<3l^)+zGKL0kI&s>4d{fDsu^-=5dVEs}6C@ALk8dhHzNz&1rqbh^N{_mS z!(-_Ga{S|)N{??UJ-(@A+)U!$_V}h!S1e9=4)^$`5?5*1cyozYNgm%+k`2O}OOJ0V zVMIK>sr2}!(&L*-k8dhHzNz#-d!y<9@0&`zPl>xxdHD(Im(K{tCAE|I{i5n4-o2=H z%IkhfUHFNkFW%Vk$-DNmt)^W>@3!sLyEn7@bjK|q+_cS3uZ2e(#gTos>r{(#c{XoH zE!dm~`TWmk7crLB{-L-hE~Qc-RvD;UnVlt;{6@SeokAI_d)gv?aco#l?`?p{_`?++TK?k z7ki=#X1(%IuV|H?9mfAW%8NQCrZz6SVq^<;X;nGE0spI`|w^&6NwV>kGUPkc&z=O;h zTL-E0Mc8^+xg&H;QW_X7uF*w`H18_S@qCeT0N}<;2aM)Ju7?1Idq_Jz9=chycpole z5oEdh$|yej`U5J@f!a^%_!!!|vd#KSofEnVVgX%Dl2HR?lnYT~r7lkDDnS=5b%UfX z0J@fZc-#ZP5T2%Hm*~F3lSvJY4GdWHd|0 z_lLTOv2@`tqjF?aBSh6lm=C-JGgujxymC!MVX24mgdqvtsK^H6H>4FbD zaP+-~D89((1{TVW-Yi{2L$?Szjz~TrOK&XzU96*v>4>TbaR;LCr?djDn$xhdTbj3q z?pNqIO7}=z8-x!F^+UK=ji@IeT94IYW>e}w$5Dz$Y>gb-BRosG%rPi2xsXkGKvo~oRDMXnAGvXhLg~7lzu67eGq<8wAcU_IxB|lgpBHk z@IvTfWE>o8PY!^9nu^p|?v2amFjYOD_ZE?E<9djH7(2ir_inL3lP8^{N zdS}1?BBRD2yc$i%jn76i$0VQ_!fe7{r7jua;-YFET=+o6?Kc^fif~D3&c~|hA`K`d z?QTllSn9-Bx)?_rY=6qAbc9!;eTIPVNF5Hz%G1!rgYQaRCc<{Hi!T21GXB$<@|TR7 zfGExlEGQ>a<}eW$i$-B-_+U46lYpm1>5U#I#mfC2c5!K=$7@+wDex6MYXCkwwHinQl7S#(*Z>Ft8Us9n^N1Y=L;y{J zNT8Wr5v4YJRIT3z0v{tU3zP#Y0Dgc!YUl(i0xqBu5Fp|czXzo=0Njt0g6NkB5dL;eyp%~D_) zz!whrR%AAi13U#h4YU!}w|Lad>W*M5pb}6KZ~=C}0eAvlfH!a(E&c~^2e=E|1O5Wo zM=J0=f=pbATDZWPmSA zOai6>*}xP#{!z|T2uue&&`4aHOh7mT;Pd&Jzz`q-hzGb%;kslPFbEh73xPtV(z=1#&pfbSM%*z2K0X}VX6E-)1%fNBq2=FP82fPUAz(QaV zuo!rrJK1am<^T!62!MyLQ9wLU5(Qq1k+&D%f@=Y=7?=u71NhAC6d()80MdagIF@l` zbq%No1OXucpM>Vp=y`zu`Excf#}hmGJOma1F@PJGh@pA`I1ca`d_MM`0L(xfpSMl{ z#sPddoKK=(0?Gkp04{9~14n>kz?Z;rfXf@c_^=LG53B_~0N&(SUjWPj_(sSA)R>DK zzBl_6Fao+1AQ>11BmrZ9vA{Thufg;N`T$)4zTZ<1s0KUy5>*7K!ke(y#SuW^#OVULx6Z728aWO0>gm8 zzyN?Jbv%jV8CNXOAFu-&;0@g2d!>AZlkb&I1EvF6Kqjyb5$l8%pq#REftL43{qNd&3^RRR7VhT^~p zjKPnAWx&h8aDWRuKKZ;3;KD8!m<=?=aNuMATzdTu@EwF3Xz`DL4*|Yz!!_000N;jA z1IGY;f%=?g>HxI>zROh#CybiG$;EJE;8NATIlWv}5^Q77z;2AW}m_J9FRlrjC;j3mme?5or>i|zg z-vQ#_gA2-I;4hKC@sCw@f_DMCfjz)LfRBg22k<=cML-95VmJ=qiQi#>XL;)Zp4n9e zc*@2Xba95 zqoyqYUhruN@M2ID&3)l_p0Wy&h9kUgfjy1C?K;6u}NQkZTB~fRi zN1%Y5p+1{;d#^`qv6Am00zQq|_8B#A=rNBU{IzVJbz=KN^tj|vli|Dxmpn>)1eaCt z8xVvl-`M@(o?a8NMDzgThA!x`F88id}Nw6Vm* z;cH+J7Su4PiCG_CB;EhR>`om&ym8j1@{vX?x3hD^Wh8ZRz9We<+)mSxk*2_IM z8zpZP9@I#=DbitJQ$566*E~XOJ|5!qH4lGW+P(pGXjRm_1k#24{DJ><*M*x%hx#IG z>mpcot=A@v`*7>&wGUzu`Em`2FUwRJ|qE|Ayw-gmI?9{6xu! zlrrTzb%1$DP)I{G&u*dKfcfjfbpvf%MRbD{+5khW7|dG?cDJ*yS$g$Bk*)t&e>X{e zKP|BJDI+EXew54N2$H&TWYPiZvaj3TT9F2WP>cf<*)*_ud5KFmJp5hO2bSJR4gN6c z%CMJV*a(KG!EI6bCJJH4xMusF4~Z_`^h7%y2Iv?X{BzQ|A{j|tJON|*kMw{0aEISk zd~Z{yW2^=>#@;Pf-9&p>A7jd&S>s;w*qIu92b;dPh^x%Jh-x%3t6_bQX}}ou$&ip4 zLmm|CQxSCweb-&AD`&6p1e(P95Yo9_uXM{w+#9D)fpqkKd>t==D{G;`ux+?{%01I?6|@2fr0gbsLx)xcR2Qk?_aa4Y$KFj z873P10pHdarMlT&0m;R8$D0OdWn4HbeH&%iEDAs_>x)#aKKk>_k*zDzAK2XzgJBSA zeZp!;@1FjbdT;6az~G-_%%qR-ZDx+Wi$!@vS!HGZxwg4t*dN$8%(12J6YEfS^^EwO zO<;Yq%B$Sqa#Mb4$yuTy<`2x6BgJ1dus&YpQ{ndkzsx>2O^!6oi?v0MJ8%>ty8H<{ z>l;(!znH(ObGI*#Js6QaL=Ftpc(Igr);Fm>T)eu{>7#?6k#@*X`ujqRdEizx6aS~F z)`%7G5o&!8>w~qP`j^3NUwBX^>x)_bE2iYmd8>DbjKN-RmcnIyVk@ZGo`eg}_v-r~ z=~YqnE?V=B=mTc0@G_mF{ zqxa833bnq_)!}d@ZQzs>(~X(Mn3SxK+8cTkl?C?vH7~4sc81Tb;YPNGpT>*sBECK1c|5aC_W9`fKwlmcIsc$oy+p8T zcd8Mh6R6T<90xel1q2pU+o=U^f?XJ9I@n>J%~tLHu25GD~{CjJ$C^Xa;m#5 z)~ohLuEx?}>>Fp4@im>J3C3sKnvQie$OK&7lNe&rq)X*b&Tdl5NtHo?|K ztb&0$Ky2|qsuAKUrLjV@+efNf#8^9g#EKnudyd*(v~VEqmWU-qig^#nWqpAw-`Oa* zm^QDSQ4?d4a!KrQz{ed?+SBf|6%Q8mJkd1wzv@*oaPEK>y-FJP#=*a~7z{&~^%=0e zDOF1zIrh;87=)Ou8z6K~w1f3cF4cG03pL-l-46yh*0KEqMFBjhb;Vy^pa@ak3sw0@ zOrtbf6zsE?^EBFcqFCpJl1aONiFw%g;piN1w3U(GDGqtt8>`cWhlWB~-vg`i#PA9o zo?1B9&QeG45PV4V&`|#};UY;x{jCpzwO$agtxmI-7R$pVdVZ-`p`pCng;z0q7uWku zjazHkj*%V5oGo6rpq$#A|2H4oUXfbN?rc&31CFsaZQ9SCTFyR-2Al>PDmM_raUR&+ z@3*_#ZG+61V2ossNU^;b3Q+;~cl=?|p_KAs_ZsWQKClQ9*WeucYw_afi*~T%GAeuU zzxCDweNaI;UE%1BQoi zdRQrT!2<^R*L1WEcBNpq?ZAe%-nngKA2>Q7N|k_t_5GVEmqs3(S>|Hk1B3H6oe%k*INJq|jj>xnOetyKq%O`2Eybwgc@e&38P}_{J;3GFLN3#~wte&Byn@CU zPI65&UA#~lIa}XM>pnec>*;mt-+=*F+31WyJ87s3ZdlRJy??6FvFrW=#;`RekD4-O ze7}%@Ia9mtM+}$o=P`EsqM`#sIA{J>fDAct2WwImYAcXp=eOf?hK8CK|K_vYViD7pW1p)e_${RF+qrV zX8k9tyX9PpdJvN*rXi{8yiDqRzudbYe{gTc0|R4?m4jKJQF(OiKd;1%rFA3MEzFV} zpNj?@U);IX(4UNUZOm#95?{cB+D`PSfI9UO0U($4MK}M%q0s{|dTpvaA5o;;NOA2+ z%!pYP><*XpEwNII>SqqFII)Z`=hu)3I6o>@z+0&GJviI81~-Pbe&+ZC1E21=nuTJe z&Fk9am$tVSnlU_u*dZ<>X{GOAzEK&s{tsC_4&D|IoFeSZ``&5jTjsh z&S}M`r-=4PVXQCP{hoR0_@>4$&9Y&=8;Zjg7ei<;?#ejymJ_S|?UmJIVwXSqu6{3b zf_QcG_{>T7Vgrn^8q7qIqLdS9+KAdtdzVn_V|OPaM>X~ud$XI7djwab*5~j_onKTV zuwlP5MvSopGqP}skDd04GV3br7sdKI+rr~tCHEdW^EWdEqjrb|H83;F?yHJ2yLR?9 z?we=tcy&TtkMD0SficdBaZaQBAmS@xHZ9RlOs$C8Ti=E|H}yg<@7$AvS^LID?PJAS z7=~IOldHbw$LD{1aXyxKI93=ZjQrkQUG&-Q?OoHpTe_^8k<>U49Tazw)Mb63?&II* zzgM@xH(rK;F@P(GU>8~m%K?zf`rzH2tXqNAUtPJIc197DXG9hZLai_4ZMd3R{?5gx zyBdxR3+o$tzn1;rh<}M+zcpfvL(~m%kiM;t?$!3{`tzDo~55U?g)0dm4ugBhT_rVC}6~-&U;lS@DYRMNZGPsqYTJH3T$+*je3PQT*bE zG5GK9NNS*ndcxRUQTXAiSf)SsEW}z)`uIvui{Z*Yxz%CVTfXE1O+$7 z6hvd$D*k37r)n+()axR)GCUP&BX(Ctfj5gYw6OLHj+4Ejbd~#wTt1j=3eRAiphAo? z8RZKQ3v1X*dm0BAIcL6E#U5c5pojRg3MSt$(V!}Zh4nSak7s^)XuxlWomh?VP=fiT zh()(NEWRzKA*svy(B!z5pJyxSgQlPXq(KX@t||^)vc^V+|4?J~p_=`z$y}3%nupX) z*ER%J{&FrZpkN`0;nd{OP5e-w{mp054gXO88tqr%k%s@C(?@nS2PiHVd|e$a`S+ul z(V@nqc~q3DVGbo8c;rxG?*Hv^2vh1mwRz#eY}uHuAF6s0s)y_SkoCh+54&}`tWR~W z9?)Y}>F?K!wPC^1IH(a$6Cc@Gas)lf<`DhX_;#l_0Wu8G}y5t_{CtB1) zijyL?9@c{k#2H8~>szCTzT1}e(p+BHeQ2jo* zC$3S~cdpy-8_Kgzp12~fPNIORmWw}WK2tOZLeZ?xjUFt~<;wvZuGTj8KjT_dfS48p zf7W+MpYy8ya+g;>coY73$u9!;#KbyyP=65L2ibF6)|W&p+rAyt^-=XJ#j+T9aZ$8eo7S?o;0o_9!e8x5_b zd7^4?p-5w(In|XS6wL58`&FI+B?;F95VaEZeM3q3f+p!@YdytyQ*8CQ4YrTut+rVWznS> zJiQ-@sjZjzoT}U6uSm2>5xdO&3)D;_lj1sc-l%4Fo2tg=jft}Nv8jjhcDY%|TX_jB z?TZ}h*Li=ov!8dU_wsO#w@FnO=H+&?=X$D}M5Dg;pRkhz^nL_5f9# zp0_;CJ_ann&b+|E_MWQRJ}+~Ky_FqqHVsFl&8vl|F~WXG{eNQ8LG1*J6(mY0+Iz9$ zBNFYMQE}ch5(g5|`Enqeqev}!Jj)UINYkQh?(Z~@jFdagL*DPFEL!_0e0OigHr;=lxvX!m&Qyq5Dh&g%@75$(j)j6e6GQKa;68pepB`%wu~FBa^#ud-4^h%TQam%0Kj zbFB0eMZf`E8!@Pn*mwX};~sYDbr8icE{;0$QV!a`bNtISRNMXQRkF2CiRX?XqeEiL zG0aiIf zPPhn;v`^kFSAJNN@2-3Wnd^`RUvFAcY1yoD=OI^zyjNYjB;dtY7JGLVlaJdEX)p1S zC~Pb9dYrIVw+VYMN143Luk76?i^yIMx7ga#(N0{O?+D17(#uhPTd{M#xci6gg0E;^ z#xqcC_4O>FH7so^@uaG+XKJzeQhcd&A)9kjc|qiq_UxurDPtz-Eb>cx#%kG8oGGHp zc-GZ6O7W-|TgJ0PG4Habv2R(!A+8Z_c{29emHE{-_m?qgWj)1^_dF^Izp|eD{tHe? BZioN? delta 34366 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: { }); }; +type WithIdParam = { + in: { param: { id: string } }; + out: { param: { id: string } }; +}; + +/** + * Middleware to check if a note exists and is viewable by the user. + * + * Useful in /api/v1/statuses/:id/* routes + * @returns MiddlewareHandler + */ +export const withNoteParam = every( + zValidator("param", z.object({ id: z.string().uuid() }), handleZodError), + createMiddleware< + HonoEnv & { + Variables: { + note: Note; + }; + }, + string, + WithIdParam + >(async (context, next) => { + const { id } = context.req.valid("param"); + const { user } = context.get("auth"); + + const note = await Note.fromId(id, user?.id); + + if (!(note && (await note.isViewableByUser(user)))) { + throw new ApiError(404, "Note not found"); + } + + context.set("note", note); + + await next(); + }), +) as MiddlewareHandler< + HonoEnv & { + Variables: { + note: Note; + }; + } +>; + +/** + * Middleware to check if a user exists + * + * Useful in /api/v1/accounts/:id/* routes + * @returns MiddlewareHandler + */ +export const withUserParam = every( + zValidator("param", z.object({ id: z.string().uuid() }), handleZodError), + createMiddleware< + HonoEnv & { + Variables: { + user: User; + }; + }, + string, + WithIdParam + >(async (context, next) => { + const { id } = context.req.valid("param"); + const user = await User.fromId(id); + + if (!user) { + throw new ApiError(404, "User not found"); + } + + context.set("user", user); + + await next(); + }), +) as MiddlewareHandler< + HonoEnv & { + Variables: { + user: User; + }; + } +>; + // Helper function to parse form data async function parseFormData(context: Context): Promise<{ parsed: ParsedQs;