mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor(api): ♻️ Move from @hono/zod-openapi to hono-openapi
hono-openapi is easier to work with and generates better OpenAPI definitions
This commit is contained in:
parent
0576aff972
commit
58342e86e1
|
|
@ -1,80 +1,16 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute, handleZodError } from "@/api";
|
||||
import { Application, User } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { eq, or } from "drizzle-orm";
|
||||
import type { Context } from "hono";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { validator } from "hono-openapi/zod";
|
||||
import { setCookie } from "hono/cookie";
|
||||
import { SignJWT } from "jose";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
const schemas = {
|
||||
form: z.object({
|
||||
identifier: z
|
||||
.string()
|
||||
.email()
|
||||
.toLowerCase()
|
||||
.or(z.string().toLowerCase()),
|
||||
password: z.string().min(2).max(100),
|
||||
}),
|
||||
query: z.object({
|
||||
scope: z.string().optional(),
|
||||
redirect_uri: z.string().url().optional(),
|
||||
response_type: z.enum([
|
||||
"code",
|
||||
"token",
|
||||
"none",
|
||||
"id_token",
|
||||
"code id_token",
|
||||
"code token",
|
||||
"token id_token",
|
||||
"code token id_token",
|
||||
]),
|
||||
client_id: z.string(),
|
||||
state: z.string().optional(),
|
||||
code_challenge: z.string().optional(),
|
||||
code_challenge_method: z.enum(["plain", "S256"]).optional(),
|
||||
prompt: z
|
||||
.enum(["none", "login", "consent", "select_account"])
|
||||
.optional()
|
||||
.default("none"),
|
||||
max_age: z
|
||||
.number()
|
||||
.int()
|
||||
.optional()
|
||||
.default(60 * 60 * 24 * 7),
|
||||
}),
|
||||
};
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/auth/login",
|
||||
summary: "Login",
|
||||
description: "Login to the application",
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"multipart/form-data": {
|
||||
schema: schemas.form,
|
||||
},
|
||||
},
|
||||
},
|
||||
query: schemas.query,
|
||||
},
|
||||
responses: {
|
||||
302: {
|
||||
description: "Redirect to OAuth authorize, or error",
|
||||
headers: {
|
||||
"Set-Cookie": {
|
||||
description: "JWT cookie",
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const returnError = (
|
||||
context: Context,
|
||||
error: string,
|
||||
|
|
@ -101,7 +37,67 @@ const returnError = (
|
|||
};
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
app.post(
|
||||
"/api/auth/login",
|
||||
describeRoute({
|
||||
summary: "Login",
|
||||
description: "Login to the application",
|
||||
responses: {
|
||||
302: {
|
||||
description: "Redirect to OAuth authorize, or error",
|
||||
headers: {
|
||||
"Set-Cookie": {
|
||||
description: "JWT cookie",
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
scope: z.string().optional(),
|
||||
redirect_uri: z.string().url().optional(),
|
||||
response_type: z.enum([
|
||||
"code",
|
||||
"token",
|
||||
"none",
|
||||
"id_token",
|
||||
"code id_token",
|
||||
"code token",
|
||||
"token id_token",
|
||||
"code token id_token",
|
||||
]),
|
||||
client_id: z.string(),
|
||||
state: z.string().optional(),
|
||||
code_challenge: z.string().optional(),
|
||||
code_challenge_method: z.enum(["plain", "S256"]).optional(),
|
||||
prompt: z
|
||||
.enum(["none", "login", "consent", "select_account"])
|
||||
.optional()
|
||||
.default("none"),
|
||||
max_age: z
|
||||
.number()
|
||||
.int()
|
||||
.optional()
|
||||
.default(60 * 60 * 24 * 7),
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
validator(
|
||||
"form",
|
||||
z.object({
|
||||
identifier: z
|
||||
.string()
|
||||
.email()
|
||||
.toLowerCase()
|
||||
.or(z.string().toLowerCase()),
|
||||
password: z.string().min(2).max(100),
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const oidcConfig = config.plugins?.config?.["@versia/openid"] as
|
||||
| {
|
||||
forced: boolean;
|
||||
|
|
@ -147,7 +143,10 @@ export default apiRoute((app) =>
|
|||
if (
|
||||
!(
|
||||
user &&
|
||||
(await Bun.password.verify(password, user.data.password || ""))
|
||||
(await Bun.password.verify(
|
||||
password,
|
||||
user.data.password || "",
|
||||
))
|
||||
)
|
||||
) {
|
||||
return returnError(
|
||||
|
|
@ -205,7 +204,11 @@ export default apiRoute((app) =>
|
|||
|
||||
// Add all data that is not undefined except email and password
|
||||
for (const [key, value] of Object.entries(context.req.query())) {
|
||||
if (key !== "email" && key !== "password" && value !== undefined) {
|
||||
if (
|
||||
key !== "email" &&
|
||||
key !== "password" &&
|
||||
value !== undefined
|
||||
) {
|
||||
searchParams.append(key, String(value));
|
||||
}
|
||||
}
|
||||
|
|
@ -222,5 +225,6 @@ export default apiRoute((app) =>
|
|||
return context.redirect(
|
||||
`${config.frontend.routes.consent}?${searchParams.toString()}`,
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,21 +1,19 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute, handleZodError } from "@/api";
|
||||
import { db } from "@versia/kit/db";
|
||||
import { Applications, Tokens } from "@versia/kit/tables";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
const schemas = {
|
||||
query: z.object({
|
||||
redirect_uri: z.string().url(),
|
||||
client_id: z.string(),
|
||||
code: z.string(),
|
||||
}),
|
||||
};
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/auth/redirect",
|
||||
/**
|
||||
* OAuth Code flow
|
||||
*/
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/auth/redirect",
|
||||
describeRoute({
|
||||
summary: "OAuth Code flow",
|
||||
description:
|
||||
"Redirects to the application, or back to login if the code is invalid",
|
||||
|
|
@ -26,17 +24,19 @@ const route = createRoute({
|
|||
"Redirects to the application, or back to login if the code is invalid",
|
||||
},
|
||||
},
|
||||
request: {
|
||||
query: schemas.query,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* OAuth Code flow
|
||||
*/
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { redirect_uri, client_id, code } = context.req.valid("query");
|
||||
}),
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
redirect_uri: z.string().url(),
|
||||
client_id: z.string(),
|
||||
code: z.string(),
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { redirect_uri, client_id, code } =
|
||||
context.req.valid("query");
|
||||
|
||||
const redirectToLogin = (error: string): Response =>
|
||||
context.redirect(
|
||||
|
|
@ -49,7 +49,10 @@ export default apiRoute((app) =>
|
|||
const foundToken = await db
|
||||
.select()
|
||||
.from(Tokens)
|
||||
.leftJoin(Applications, eq(Tokens.applicationId, Applications.id))
|
||||
.leftJoin(
|
||||
Applications,
|
||||
eq(Tokens.applicationId, Applications.id),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(Tokens.code, code),
|
||||
|
|
@ -68,5 +71,6 @@ export default apiRoute((app) =>
|
|||
code,
|
||||
}).toString()}`,
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,42 +1,13 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute, handleZodError } from "@/api";
|
||||
import { User } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type { Context } from "hono";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
const schemas = {
|
||||
form: z.object({
|
||||
token: z.string().min(1),
|
||||
password: z.string().min(3).max(100),
|
||||
}),
|
||||
};
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/auth/reset",
|
||||
summary: "Reset password",
|
||||
description: "Reset password",
|
||||
responses: {
|
||||
302: {
|
||||
description: "Redirect to the password reset page with a message",
|
||||
},
|
||||
},
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"application/x-www-form-urlencoded": {
|
||||
schema: schemas.form,
|
||||
},
|
||||
"multipart/form-data": {
|
||||
schema: schemas.form,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const returnError = (
|
||||
context: Context,
|
||||
token: string,
|
||||
|
|
@ -60,10 +31,32 @@ const returnError = (
|
|||
};
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
app.post(
|
||||
"/api/auth/reset",
|
||||
describeRoute({
|
||||
summary: "Reset password",
|
||||
description: "Reset password",
|
||||
responses: {
|
||||
302: {
|
||||
description:
|
||||
"Redirect to the password reset page with a message",
|
||||
},
|
||||
},
|
||||
}),
|
||||
validator(
|
||||
"form",
|
||||
z.object({
|
||||
token: z.string().min(1),
|
||||
password: z.string().min(3).max(100),
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { token, password } = context.req.valid("form");
|
||||
|
||||
const user = await User.fromSql(eq(Users.passwordResetToken, token));
|
||||
const user = await User.fromSql(
|
||||
eq(Users.passwordResetToken, token),
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
return returnError(
|
||||
|
|
@ -82,5 +75,6 @@ export default apiRoute((app) =>
|
|||
return context.redirect(
|
||||
`${config.frontend.routes.password_reset}?success=true`,
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Relationship as RelationshipSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/block",
|
||||
summary: "Block account",
|
||||
description:
|
||||
"Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline)",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#block",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:blocks"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnBlocks,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Successfully blocked, or account was already blocked.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
if (!foundRelationship.data.blocking) {
|
||||
await foundRelationship.update({
|
||||
blocking: true,
|
||||
});
|
||||
}
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Relationship as RelationshipSchema,
|
||||
iso631,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/follow",
|
||||
summary: "Follow account",
|
||||
description:
|
||||
"Follow the given account. Can also be used to update whether to show reblogs or enable notifications.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#follow",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:follows"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnFollows,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Successfully followed, or account was already followed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
403: {
|
||||
description:
|
||||
"Trying to follow someone that you block or that blocks you",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ApiError.zodSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
reblogs: z.boolean().default(true).openapi({
|
||||
description:
|
||||
"Receive this account’s reblogs in home timeline?",
|
||||
example: true,
|
||||
}),
|
||||
notify: z.boolean().default(false).openapi({
|
||||
description:
|
||||
"Receive notifications when this account posts a status?",
|
||||
example: false,
|
||||
}),
|
||||
languages: z
|
||||
.array(iso631)
|
||||
.default([])
|
||||
.openapi({
|
||||
description:
|
||||
"Array of String (ISO 639-1 language two-letter code). Filter received statuses for these languages. If not provided, you will receive this account’s posts in all languages.",
|
||||
example: ["en", "fr"],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const { reblogs, notify, languages } = context.req.valid("json");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
let relationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
if (!relationship.data.following) {
|
||||
relationship = await user.followRequest(otherUser, {
|
||||
reblogs,
|
||||
notify,
|
||||
languages,
|
||||
});
|
||||
}
|
||||
|
||||
return context.json(relationship.toApi(), 200);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Account as AccountSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/{id}/followers",
|
||||
summary: "Get account’s followers",
|
||||
description:
|
||||
"Accounts which follow the given account, if network is not hidden by the account owner.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#followers",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
scopes: ["read:accounts"],
|
||||
permissions: [
|
||||
RolePermission.ViewAccountFollows,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
query: z.object({
|
||||
max_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||
}),
|
||||
since_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||
example: undefined,
|
||||
}),
|
||||
min_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||
example: undefined,
|
||||
}),
|
||||
limit: z.number().int().min(1).max(40).default(20).openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Accounts which follow the given account.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(AccountSchema),
|
||||
},
|
||||
},
|
||||
headers: z.object({
|
||||
link: z
|
||||
.string()
|
||||
.optional()
|
||||
.openapi({
|
||||
description: "Links to the next and previous pages",
|
||||
example:
|
||||
'<https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/followers?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/followers?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { max_id, since_id, min_id, limit } = context.req.valid("query");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
// TODO: Add follower/following privacy settings
|
||||
const { objects, link } = await Timeline.getUserTimeline(
|
||||
and(
|
||||
max_id ? lt(Users.id, max_id) : undefined,
|
||||
since_id ? gte(Users.id, since_id) : undefined,
|
||||
min_id ? gt(Users.id, min_id) : undefined,
|
||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${otherUser.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."following" = true)`,
|
||||
),
|
||||
limit,
|
||||
new URL(context.req.url),
|
||||
);
|
||||
|
||||
return context.json(
|
||||
await Promise.all(objects.map((object) => object.toApi())),
|
||||
200,
|
||||
{
|
||||
Link: link,
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Account as AccountSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/{id}/following",
|
||||
summary: "Get account’s following",
|
||||
description:
|
||||
"Accounts which the given account is following, if network is not hidden by the account owner.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#following",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
scopes: ["read:accounts"],
|
||||
permissions: [
|
||||
RolePermission.ViewAccountFollows,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
query: z.object({
|
||||
max_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||
}),
|
||||
since_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||
example: undefined,
|
||||
}),
|
||||
min_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||
example: undefined,
|
||||
}),
|
||||
limit: z.number().int().min(1).max(40).default(20).openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Accounts which the given account is following.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(AccountSchema),
|
||||
},
|
||||
},
|
||||
headers: z.object({
|
||||
link: z
|
||||
.string()
|
||||
.optional()
|
||||
.openapi({
|
||||
description: "Links to the next and previous pages",
|
||||
example:
|
||||
'<https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/following?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/following?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { max_id, since_id, min_id } = context.req.valid("query");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
// TODO: Add follower/following privacy settings
|
||||
|
||||
const { objects, link } = await Timeline.getUserTimeline(
|
||||
and(
|
||||
max_id ? lt(Users.id, max_id) : undefined,
|
||||
since_id ? gte(Users.id, since_id) : undefined,
|
||||
min_id ? gt(Users.id, min_id) : undefined,
|
||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${otherUser.id} AND "Relationships"."following" = true)`,
|
||||
),
|
||||
context.req.valid("query").limit,
|
||||
new URL(context.req.url),
|
||||
);
|
||||
|
||||
return context.json(
|
||||
await Promise.all(objects.map((object) => object.toApi())),
|
||||
200,
|
||||
{
|
||||
Link: link,
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Account as AccountSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/{id}",
|
||||
summary: "Get account",
|
||||
description: "View information about a profile.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#get",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
permissions: [RolePermission.ViewAccounts],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"The Account record will be returned. Note that acct of local users does not include the domain name.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: AccountSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
return context.json(otherUser.toApi(user?.id === otherUser.id), 200);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Relationship as RelationshipSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/mute",
|
||||
summary: "Mute account",
|
||||
description:
|
||||
"Mute the given account. Clients should filter statuses and notifications from this account, if received (e.g. due to a boost in the Home timeline).",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#mute",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:mutes"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnMutes,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
notifications: z.boolean().default(true).openapi({
|
||||
description:
|
||||
"Mute notifications in addition to statuses?",
|
||||
}),
|
||||
duration: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(60 * 60 * 24 * 365 * 5)
|
||||
.default(0)
|
||||
.openapi({
|
||||
description:
|
||||
"How long the mute should last, in seconds.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Successfully muted, or account was already muted. Note that you can call this API method again with notifications=false to update the relationship so that only statuses are muted.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
// TODO: Add duration support
|
||||
const { notifications } = context.req.valid("json");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
// TODO: Implement duration
|
||||
await foundRelationship.update({
|
||||
muting: true,
|
||||
mutingNotifications: notifications,
|
||||
});
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Relationship as RelationshipSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/note",
|
||||
summary: "Set private note on profile",
|
||||
description: "Sets a private note on a user.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#note",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:accounts"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnAccount,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
comment: RelationshipSchema.shape.note
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"The comment to be set on that user. Provide an empty string or leave out this parameter to clear the currently set note.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully updated profile note",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const { comment } = context.req.valid("json");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
await foundRelationship.update({
|
||||
note: comment ?? "",
|
||||
});
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Relationship as RelationshipSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/pin",
|
||||
summary: "Feature account on your profile",
|
||||
description:
|
||||
"Add the given account to the user’s featured profiles. (Featured profiles are currently shown on the user’s own public profile.)",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#pin",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:accounts"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnAccount,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated relationship",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
await foundRelationship.update({
|
||||
endorsed: true,
|
||||
});
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Account as AccountSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/refetch",
|
||||
summary: "Refetch account",
|
||||
description: "Refetch the given account's profile from the remote server",
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:accounts"],
|
||||
permissions: [RolePermission.ViewAccounts],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Refetched account data",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: AccountSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
400: {
|
||||
description: "User is local",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ApiError.zodSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const otherUser = context.get("user");
|
||||
|
||||
if (otherUser.isLocal()) {
|
||||
throw new ApiError(400, "Cannot refetch a local user");
|
||||
}
|
||||
|
||||
const newUser = await otherUser.updateFromRemote();
|
||||
|
||||
return context.json(newUser.toApi(false), 200);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Relationship as RelationshipSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/remove_from_followers",
|
||||
summary: "Remove account from followers",
|
||||
description: "Remove the given account from your followers.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#remove_from_followers",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:follows"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnFollows,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Successfully removed from followers, or account was already not following you",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const oppositeRelationship = await Relationship.fromOwnerAndSubject(
|
||||
otherUser,
|
||||
user,
|
||||
);
|
||||
|
||||
if (oppositeRelationship.data.following) {
|
||||
await oppositeRelationship.update({
|
||||
following: false,
|
||||
});
|
||||
}
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Role as RoleSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Role } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const routePost = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/roles/{role_id}",
|
||||
summary: "Assign role to account",
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageRoles],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
role_id: RoleSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
204: {
|
||||
description: "Role assigned",
|
||||
},
|
||||
404: ApiError.roleNotFound().schema,
|
||||
403: ApiError.forbidden().schema,
|
||||
},
|
||||
});
|
||||
|
||||
const routeDelete = createRoute({
|
||||
method: "delete",
|
||||
path: "/api/v1/accounts/{id}/roles/{role_id}",
|
||||
summary: "Remove role from user",
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageRoles],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
role_id: RoleSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
204: {
|
||||
description: "Role removed",
|
||||
},
|
||||
404: ApiError.roleNotFound().schema,
|
||||
403: ApiError.forbidden().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) => {
|
||||
app.openapi(routePost, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const { role_id } = context.req.valid("param");
|
||||
const targetUser = context.get("user");
|
||||
|
||||
const role = await Role.fromId(role_id);
|
||||
|
||||
if (!role) {
|
||||
throw ApiError.roleNotFound();
|
||||
}
|
||||
// Priority check
|
||||
const userRoles = await Role.getUserRoles(user.id, user.data.isAdmin);
|
||||
|
||||
const userHighestRole = userRoles.reduce((prev, current) =>
|
||||
prev.data.priority > current.data.priority ? prev : current,
|
||||
);
|
||||
|
||||
if (role.data.priority > userHighestRole.data.priority) {
|
||||
throw new ApiError(
|
||||
403,
|
||||
"Forbidden",
|
||||
`User with highest role priority ${userHighestRole.data.priority} cannot assign role with priority ${role.data.priority}`,
|
||||
);
|
||||
}
|
||||
|
||||
await role.linkUser(targetUser.id);
|
||||
|
||||
return context.body(null, 204);
|
||||
});
|
||||
|
||||
app.openapi(routeDelete, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const { role_id } = context.req.valid("param");
|
||||
const targetUser = context.get("user");
|
||||
|
||||
const role = await Role.fromId(role_id);
|
||||
|
||||
if (!role) {
|
||||
throw ApiError.roleNotFound();
|
||||
}
|
||||
|
||||
// Priority check
|
||||
const userRoles = await Role.getUserRoles(user.id, user.data.isAdmin);
|
||||
|
||||
const userHighestRole = userRoles.reduce((prev, current) =>
|
||||
prev.data.priority > current.data.priority ? prev : current,
|
||||
);
|
||||
|
||||
if (role.data.priority > userHighestRole.data.priority) {
|
||||
throw new ApiError(
|
||||
403,
|
||||
"Forbidden",
|
||||
`User with highest role priority ${userHighestRole.data.priority} cannot remove role with priority ${role.data.priority}`,
|
||||
);
|
||||
}
|
||||
|
||||
await role.unlinkUser(targetUser.id);
|
||||
|
||||
return context.body(null, 204);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Role as RoleSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { Role } from "@versia/kit/db";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/{id}/roles",
|
||||
summary: "List account roles",
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of roles",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(RoleSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) => {
|
||||
app.openapi(route, async (context) => {
|
||||
const targetUser = context.get("user");
|
||||
|
||||
const roles = await Role.getUserRoles(
|
||||
targetUser.id,
|
||||
targetUser.data.isAdmin,
|
||||
);
|
||||
|
||||
return context.json(
|
||||
roles.map((role) => role.toApi()),
|
||||
200,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Status as StatusSchema,
|
||||
zBoolean,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { Notes } from "@versia/kit/tables";
|
||||
import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/{id}/statuses",
|
||||
summary: "Get account’s statuses",
|
||||
description: "Statuses posted to the given account.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#statuses",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
permissions: [
|
||||
RolePermission.ViewNotes,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
scopes: ["read:statuses"],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
query: z.object({
|
||||
max_id: StatusSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||
}),
|
||||
since_id: StatusSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||
example: undefined,
|
||||
}),
|
||||
min_id: StatusSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||
example: undefined,
|
||||
}),
|
||||
limit: z.coerce.number().int().min(1).max(40).default(20).openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
only_media: zBoolean.default(false).openapi({
|
||||
description: "Filter out statuses without attachments.",
|
||||
}),
|
||||
exclude_replies: zBoolean.default(false).openapi({
|
||||
description:
|
||||
"Filter out statuses in reply to a different account.",
|
||||
}),
|
||||
exclude_reblogs: zBoolean.default(false).openapi({
|
||||
description: "Filter out boosts from the response.",
|
||||
}),
|
||||
pinned: zBoolean.default(false).openapi({
|
||||
description:
|
||||
"Filter for pinned statuses only. Pinned statuses do not receive special priority in the order of the returned results.",
|
||||
}),
|
||||
tagged: z.string().optional().openapi({
|
||||
description: "Filter for statuses using a specific hashtag.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Statuses posted to the given account.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(StatusSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const {
|
||||
max_id,
|
||||
min_id,
|
||||
since_id,
|
||||
limit,
|
||||
exclude_reblogs,
|
||||
only_media,
|
||||
exclude_replies,
|
||||
pinned,
|
||||
} = context.req.valid("query");
|
||||
|
||||
const { objects } = 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, otherUser.id),
|
||||
only_media
|
||||
? sql`EXISTS (SELECT 1 FROM "Medias" WHERE "Medias"."noteId" = ${Notes.id})`
|
||||
: undefined,
|
||||
pinned
|
||||
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = ${Notes.id} AND "UserToPinnedNotes"."userId" = ${otherUser.id})`
|
||||
: undefined,
|
||||
// Visibility check
|
||||
or(
|
||||
sql`EXISTS (SELECT 1 FROM "NoteToMentions" WHERE "NoteToMentions"."noteId" = ${Notes.id} AND "NoteToMentions"."userId" = ${otherUser.id})`,
|
||||
and(
|
||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Notes.authorId} AND "Relationships"."ownerId" = ${otherUser.id} AND "Relationships"."following" = true)`,
|
||||
inArray(Notes.visibility, ["public", "private"]),
|
||||
),
|
||||
inArray(Notes.visibility, ["public", "unlisted"]),
|
||||
),
|
||||
exclude_reblogs ? isNull(Notes.reblogId) : undefined,
|
||||
exclude_replies ? isNull(Notes.replyId) : undefined,
|
||||
),
|
||||
limit,
|
||||
new URL(context.req.url),
|
||||
user?.id,
|
||||
);
|
||||
|
||||
return context.json(
|
||||
await Promise.all(objects.map((note) => note.toApi(otherUser))),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Relationship as RelationshipSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/unblock",
|
||||
summary: "Unblock account",
|
||||
description: "Unblock the given account.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#unblock",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:blocks"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnBlocks,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Successfully unblocked, or account was already not blocked",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
if (foundRelationship.data.blocking) {
|
||||
await foundRelationship.update({
|
||||
blocking: false,
|
||||
});
|
||||
}
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Relationship as RelationshipSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/unfollow",
|
||||
summary: "Unfollow account",
|
||||
description: "Unfollow the given account.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#unfollow",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:follows"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnFollows,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Successfully unfollowed, or account was already not followed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
await user.unfollow(otherUser, foundRelationship);
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Relationship as RelationshipSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/unmute",
|
||||
summary: "Unmute account",
|
||||
description: "Unmute the given account.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#unmute",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:mutes"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnMutes,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully unmuted, or account was already unmuted",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
if (foundRelationship.data.muting) {
|
||||
await foundRelationship.update({
|
||||
muting: false,
|
||||
mutingNotifications: false,
|
||||
});
|
||||
}
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Relationship as RelationshipSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts/{id}/unpin",
|
||||
summary: "Unfeature account from profile",
|
||||
description: "Remove the given account from the user’s featured profiles.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#unpin",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:accounts"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnAccount,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
withUserParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Successfully unendorsed, or account was already not endorsed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
if (foundRelationship.data.endorsed) {
|
||||
await foundRelationship.update({
|
||||
endorsed: false,
|
||||
});
|
||||
}
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
}),
|
||||
);
|
||||
62
api/api/v1/accounts/[id]/block.ts
Normal file
62
api/api/v1/accounts/[id]/block.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { Relationship as RelationshipSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/accounts/:id/block",
|
||||
describeRoute({
|
||||
summary: "Block account",
|
||||
description:
|
||||
"Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline)",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#block",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Successfully blocked, or account was already blocked.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(RelationshipSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:blocks"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnBlocks,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
if (!foundRelationship.data.blocking) {
|
||||
await foundRelationship.update({
|
||||
blocking: true,
|
||||
});
|
||||
}
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
102
api/api/v1/accounts/[id]/follow.ts
Normal file
102
api/api/v1/accounts/[id]/follow.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||
import {
|
||||
Relationship as RelationshipSchema,
|
||||
iso631,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/accounts/:id/follow",
|
||||
describeRoute({
|
||||
summary: "Follow account",
|
||||
description:
|
||||
"Follow the given account. Can also be used to update whether to show reblogs or enable notifications.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#follow",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Successfully followed, or account was already followed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(RelationshipSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
403: {
|
||||
description:
|
||||
"Trying to follow someone that you block or that blocks you",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(ApiError.zodSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:follows"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnFollows,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
validator(
|
||||
"json",
|
||||
z.object({
|
||||
reblogs: z.boolean().default(true).openapi({
|
||||
description:
|
||||
"Receive this account’s reblogs in home timeline?",
|
||||
example: true,
|
||||
}),
|
||||
notify: z.boolean().default(false).openapi({
|
||||
description:
|
||||
"Receive notifications when this account posts a status?",
|
||||
example: false,
|
||||
}),
|
||||
languages: z
|
||||
.array(iso631)
|
||||
.default([])
|
||||
.openapi({
|
||||
description:
|
||||
"Array of String (ISO 639-1 language two-letter code). Filter received statuses for these languages. If not provided, you will receive this account’s posts in all languages.",
|
||||
example: ["en", "fr"],
|
||||
}),
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const { reblogs, notify, languages } = context.req.valid("json");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
let relationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
if (!relationship.data.following) {
|
||||
relationship = await user.followRequest(otherUser, {
|
||||
reblogs,
|
||||
notify,
|
||||
languages,
|
||||
});
|
||||
}
|
||||
|
||||
return context.json(relationship.toApi(), 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
111
api/api/v1/accounts/[id]/followers.ts
Normal file
111
api/api/v1/accounts/[id]/followers.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||
import { Account as AccountSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/accounts/:id/followers",
|
||||
describeRoute({
|
||||
summary: "Get account’s followers",
|
||||
description:
|
||||
"Accounts which follow the given account, if network is not hidden by the account owner.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#followers",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Accounts which follow the given account.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(AccountSchema)),
|
||||
},
|
||||
},
|
||||
headers: resolver(
|
||||
z.object({
|
||||
link: z
|
||||
.string()
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"Links to the next and previous pages",
|
||||
example:
|
||||
'<https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/followers?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/followers?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: false,
|
||||
scopes: ["read:accounts"],
|
||||
permissions: [
|
||||
RolePermission.ViewAccountFollows,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
max_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||
}),
|
||||
since_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||
example: undefined,
|
||||
}),
|
||||
min_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||
example: undefined,
|
||||
}),
|
||||
limit: z.number().int().min(1).max(40).default(20).openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { max_id, since_id, min_id, limit } =
|
||||
context.req.valid("query");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
// TODO: Add follower/following privacy settings
|
||||
const { objects, link } = await Timeline.getUserTimeline(
|
||||
and(
|
||||
max_id ? lt(Users.id, max_id) : undefined,
|
||||
since_id ? gte(Users.id, since_id) : undefined,
|
||||
min_id ? gt(Users.id, min_id) : undefined,
|
||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${otherUser.id} AND "Relationships"."ownerId" = ${Users.id} AND "Relationships"."following" = true)`,
|
||||
),
|
||||
limit,
|
||||
new URL(context.req.url),
|
||||
);
|
||||
|
||||
return context.json(
|
||||
await Promise.all(objects.map((object) => object.toApi())),
|
||||
200,
|
||||
{
|
||||
Link: link,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
112
api/api/v1/accounts/[id]/following.ts
Normal file
112
api/api/v1/accounts/[id]/following.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||
import { Account as AccountSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/accounts/:id/following",
|
||||
describeRoute({
|
||||
summary: "Get account’s following",
|
||||
description:
|
||||
"Accounts which the given account is following, if network is not hidden by the account owner.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#following",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Accounts which the given account is following.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(AccountSchema)),
|
||||
},
|
||||
},
|
||||
headers: resolver(
|
||||
z.object({
|
||||
link: z
|
||||
.string()
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"Links to the next and previous pages",
|
||||
example:
|
||||
'<https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/following?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/following?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
),
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: false,
|
||||
scopes: ["read:accounts"],
|
||||
permissions: [
|
||||
RolePermission.ViewAccountFollows,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
max_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||
}),
|
||||
since_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||
example: undefined,
|
||||
}),
|
||||
min_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||
example: undefined,
|
||||
}),
|
||||
limit: z.number().int().min(1).max(40).default(20).openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { max_id, since_id, min_id } = context.req.valid("query");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
// TODO: Add follower/following privacy settings
|
||||
|
||||
const { objects, link } = await Timeline.getUserTimeline(
|
||||
and(
|
||||
max_id ? lt(Users.id, max_id) : undefined,
|
||||
since_id ? gte(Users.id, since_id) : undefined,
|
||||
min_id ? gt(Users.id, min_id) : undefined,
|
||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Users.id} AND "Relationships"."ownerId" = ${otherUser.id} AND "Relationships"."following" = true)`,
|
||||
),
|
||||
context.req.valid("query").limit,
|
||||
new URL(context.req.url),
|
||||
);
|
||||
|
||||
return context.json(
|
||||
await Promise.all(objects.map((object) => object.toApi())),
|
||||
200,
|
||||
{
|
||||
Link: link,
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
47
api/api/v1/accounts/[id]/index.ts
Normal file
47
api/api/v1/accounts/[id]/index.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { Account as AccountSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/accounts/:id",
|
||||
describeRoute({
|
||||
summary: "Get account",
|
||||
description: "View information about a profile.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#get",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"The Account record will be returned. Note that acct of local users does not include the domain name.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(AccountSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: false,
|
||||
permissions: [RolePermission.ViewAccounts],
|
||||
}),
|
||||
(context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
return context.json(
|
||||
otherUser.toApi(user?.id === otherUser.id),
|
||||
200,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
84
api/api/v1/accounts/[id]/mute.ts
Normal file
84
api/api/v1/accounts/[id]/mute.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||
import { Relationship as RelationshipSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/accounts/:id/mute",
|
||||
describeRoute({
|
||||
summary: "Mute account",
|
||||
description:
|
||||
"Mute the given account. Clients should filter statuses and notifications from this account, if received (e.g. due to a boost in the Home timeline).",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#mute",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Successfully muted, or account was already muted. Note that you can call this API method again with notifications=false to update the relationship so that only statuses are muted.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(RelationshipSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:mutes"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnMutes,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
validator(
|
||||
"json",
|
||||
z.object({
|
||||
notifications: z.boolean().default(true).openapi({
|
||||
description: "Mute notifications in addition to statuses?",
|
||||
}),
|
||||
duration: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.max(60 * 60 * 24 * 365 * 5)
|
||||
.default(0)
|
||||
.openapi({
|
||||
description:
|
||||
"How long the mute should last, in seconds.",
|
||||
}),
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
// TODO: Add duration support
|
||||
const { notifications } = context.req.valid("json");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
// TODO: Implement duration
|
||||
await foundRelationship.update({
|
||||
muting: true,
|
||||
mutingNotifications: notifications,
|
||||
});
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
70
api/api/v1/accounts/[id]/note.ts
Normal file
70
api/api/v1/accounts/[id]/note.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||
import { Relationship as RelationshipSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/accounts/:id/note",
|
||||
describeRoute({
|
||||
summary: "Set private note on profile",
|
||||
description: "Sets a private note on a user.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#note",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Successfully updated profile note",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(RelationshipSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:accounts"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnAccount,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
validator(
|
||||
"json",
|
||||
z.object({
|
||||
comment: RelationshipSchema.shape.note.optional().openapi({
|
||||
description:
|
||||
"The comment to be set on that user. Provide an empty string or leave out this parameter to clear the currently set note.",
|
||||
}),
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const { comment } = context.req.valid("json");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
await foundRelationship.update({
|
||||
note: comment ?? "",
|
||||
});
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
54
api/api/v1/accounts/[id]/pin.ts
Normal file
54
api/api/v1/accounts/[id]/pin.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { Relationship as RelationshipSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/accounts/:id/pin",
|
||||
describeRoute({
|
||||
summary: "Feature account on your profile",
|
||||
description:
|
||||
"Add the given account to the user’s featured profiles. (Featured profiles are currently shown on the user’s own public profile.)",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#pin",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated relationship",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(RelationshipSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:accounts"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnAccount,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
await foundRelationship.update({
|
||||
endorsed: true,
|
||||
});
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
56
api/api/v1/accounts/[id]/refetch.ts
Normal file
56
api/api/v1/accounts/[id]/refetch.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { Account as AccountSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/accounts/:id/refetch",
|
||||
describeRoute({
|
||||
summary: "Refetch account",
|
||||
description:
|
||||
"Refetch the given account's profile from the remote server",
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Refetched account data",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(AccountSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
400: {
|
||||
description: "User is local",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(ApiError.zodSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:accounts"],
|
||||
permissions: [RolePermission.ViewAccounts],
|
||||
}),
|
||||
async (context) => {
|
||||
const otherUser = context.get("user");
|
||||
|
||||
if (otherUser.isLocal()) {
|
||||
throw new ApiError(400, "Cannot refetch a local user");
|
||||
}
|
||||
|
||||
const newUser = await otherUser.updateFromRemote();
|
||||
|
||||
return context.json(newUser.toApi(false), 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
66
api/api/v1/accounts/[id]/remove_from_followers.ts
Normal file
66
api/api/v1/accounts/[id]/remove_from_followers.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { Relationship as RelationshipSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/accounts/:id/remove_from_followers",
|
||||
describeRoute({
|
||||
summary: "Remove account from followers",
|
||||
description: "Remove the given account from your followers.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#remove_from_followers",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Successfully removed from followers, or account was already not following you",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(RelationshipSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:follows"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnFollows,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const oppositeRelationship = await Relationship.fromOwnerAndSubject(
|
||||
otherUser,
|
||||
user,
|
||||
);
|
||||
|
||||
if (oppositeRelationship.data.following) {
|
||||
await oppositeRelationship.update({
|
||||
following: false,
|
||||
});
|
||||
}
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
127
api/api/v1/accounts/[id]/roles/[role_id]/index.ts
Normal file
127
api/api/v1/accounts/[id]/roles/[role_id]/index.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Role as RoleSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Role } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) => {
|
||||
app.post(
|
||||
"/api/v1/accounts/:id/roles/:role_id",
|
||||
describeRoute({
|
||||
summary: "Assign role to account",
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
204: {
|
||||
description: "Role assigned",
|
||||
},
|
||||
404: ApiError.roleNotFound().schema,
|
||||
403: ApiError.forbidden().schema,
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
role_id: RoleSchema.shape.id,
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageRoles],
|
||||
}),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const { role_id } = context.req.valid("param");
|
||||
const targetUser = context.get("user");
|
||||
|
||||
const role = await Role.fromId(role_id);
|
||||
|
||||
if (!role) {
|
||||
throw ApiError.roleNotFound();
|
||||
}
|
||||
// Priority check
|
||||
const userRoles = await Role.getUserRoles(
|
||||
user.id,
|
||||
user.data.isAdmin,
|
||||
);
|
||||
|
||||
const userHighestRole = userRoles.reduce((prev, current) =>
|
||||
prev.data.priority > current.data.priority ? prev : current,
|
||||
);
|
||||
|
||||
if (role.data.priority > userHighestRole.data.priority) {
|
||||
throw new ApiError(
|
||||
403,
|
||||
"Forbidden",
|
||||
`User with highest role priority ${userHighestRole.data.priority} cannot assign role with priority ${role.data.priority}`,
|
||||
);
|
||||
}
|
||||
|
||||
await role.linkUser(targetUser.id);
|
||||
|
||||
return context.body(null, 204);
|
||||
},
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/api/v1/accounts/:id/roles/:role_id",
|
||||
describeRoute({
|
||||
summary: "Remove role from user",
|
||||
tags: ["Accounts"],
|
||||
}),
|
||||
withUserParam,
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: AccountSchema.shape.id,
|
||||
role_id: RoleSchema.shape.id,
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageRoles],
|
||||
}),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const { role_id } = context.req.valid("param");
|
||||
const targetUser = context.get("user");
|
||||
|
||||
const role = await Role.fromId(role_id);
|
||||
|
||||
if (!role) {
|
||||
throw ApiError.roleNotFound();
|
||||
}
|
||||
|
||||
// Priority check
|
||||
const userRoles = await Role.getUserRoles(
|
||||
user.id,
|
||||
user.data.isAdmin,
|
||||
);
|
||||
|
||||
const userHighestRole = userRoles.reduce((prev, current) =>
|
||||
prev.data.priority > current.data.priority ? prev : current,
|
||||
);
|
||||
|
||||
if (role.data.priority > userHighestRole.data.priority) {
|
||||
throw new ApiError(
|
||||
403,
|
||||
"Forbidden",
|
||||
`User with highest role priority ${userHighestRole.data.priority} cannot remove role with priority ${role.data.priority}`,
|
||||
);
|
||||
}
|
||||
|
||||
await role.unlinkUser(targetUser.id);
|
||||
|
||||
return context.body(null, 204);
|
||||
},
|
||||
);
|
||||
});
|
||||
43
api/api/v1/accounts/[id]/roles/index.ts
Normal file
43
api/api/v1/accounts/[id]/roles/index.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { Role as RoleSchema } from "@versia/client/schemas";
|
||||
import { Role } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
export default apiRoute((app) => {
|
||||
app.get(
|
||||
"/api/v1/accounts/:id/roles",
|
||||
describeRoute({
|
||||
summary: "List account roles",
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of roles",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(RoleSchema)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: false,
|
||||
}),
|
||||
async (context) => {
|
||||
const targetUser = context.get("user");
|
||||
|
||||
const roles = await Role.getUserRoles(
|
||||
targetUser.id,
|
||||
targetUser.data.isAdmin,
|
||||
);
|
||||
|
||||
return context.json(
|
||||
roles.map((role) => role.toApi()),
|
||||
200,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -95,7 +95,7 @@ describe("/api/v1/accounts/:id/statuses", () => {
|
|||
|
||||
expect(ok2).toBe(true);
|
||||
|
||||
const { data: data2, ok: ok3 } = await client0.getAccountStatuses(
|
||||
const { data: data3, ok: ok3 } = await client0.getAccountStatuses(
|
||||
users[1].id,
|
||||
{
|
||||
pinned: true,
|
||||
|
|
@ -103,7 +103,7 @@ describe("/api/v1/accounts/:id/statuses", () => {
|
|||
);
|
||||
|
||||
expect(ok3).toBe(true);
|
||||
expect(data2).toBeArrayOfSize(1);
|
||||
expect(data2[0].id).toBe(timeline[3].id);
|
||||
expect(data3).toBeArrayOfSize(1);
|
||||
expect(data3[0].id).toBe(timeline[3].id);
|
||||
});
|
||||
});
|
||||
142
api/api/v1/accounts/[id]/statuses.ts
Normal file
142
api/api/v1/accounts/[id]/statuses.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
|
||||
import { Status as StatusSchema, zBoolean } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { Notes } from "@versia/kit/tables";
|
||||
import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/accounts/:id/statuses",
|
||||
describeRoute({
|
||||
summary: "Get account’s statuses",
|
||||
description: "Statuses posted to the given account.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#statuses",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Statuses posted to the given account.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(StatusSchema)),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: false,
|
||||
permissions: [
|
||||
RolePermission.ViewNotes,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
scopes: ["read:statuses"],
|
||||
}),
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
max_id: StatusSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||
example: "8d35243d-b959-43e2-8bac-1a9d4eaea2aa",
|
||||
}),
|
||||
since_id: StatusSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be greater than this ID. In effect, sets a lower bound on results.",
|
||||
example: undefined,
|
||||
}),
|
||||
min_id: StatusSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||
example: undefined,
|
||||
}),
|
||||
limit: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(40)
|
||||
.default(20)
|
||||
.openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
only_media: zBoolean.default(false).openapi({
|
||||
description: "Filter out statuses without attachments.",
|
||||
}),
|
||||
exclude_replies: zBoolean.default(false).openapi({
|
||||
description:
|
||||
"Filter out statuses in reply to a different account.",
|
||||
}),
|
||||
exclude_reblogs: zBoolean.default(false).openapi({
|
||||
description: "Filter out boosts from the response.",
|
||||
}),
|
||||
pinned: zBoolean.default(false).openapi({
|
||||
description:
|
||||
"Filter for pinned statuses only. Pinned statuses do not receive special priority in the order of the returned results.",
|
||||
}),
|
||||
tagged: z.string().optional().openapi({
|
||||
description:
|
||||
"Filter for statuses using a specific hashtag.",
|
||||
}),
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const {
|
||||
max_id,
|
||||
min_id,
|
||||
since_id,
|
||||
limit,
|
||||
exclude_reblogs,
|
||||
only_media,
|
||||
exclude_replies,
|
||||
pinned,
|
||||
} = context.req.valid("query");
|
||||
|
||||
const { objects } = 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, otherUser.id),
|
||||
only_media
|
||||
? sql`EXISTS (SELECT 1 FROM "Medias" WHERE "Medias"."noteId" = ${Notes.id})`
|
||||
: undefined,
|
||||
pinned
|
||||
? sql`EXISTS (SELECT 1 FROM "UserToPinnedNotes" WHERE "UserToPinnedNotes"."noteId" = ${Notes.id} AND "UserToPinnedNotes"."userId" = ${otherUser.id})`
|
||||
: undefined,
|
||||
// Visibility check
|
||||
or(
|
||||
sql`EXISTS (SELECT 1 FROM "NoteToMentions" WHERE "NoteToMentions"."noteId" = ${Notes.id} AND "NoteToMentions"."userId" = ${otherUser.id})`,
|
||||
and(
|
||||
sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Notes.authorId} AND "Relationships"."ownerId" = ${otherUser.id} AND "Relationships"."following" = true)`,
|
||||
inArray(Notes.visibility, ["public", "private"]),
|
||||
),
|
||||
inArray(Notes.visibility, ["public", "unlisted"]),
|
||||
),
|
||||
exclude_reblogs ? isNull(Notes.reblogId) : undefined,
|
||||
exclude_replies ? isNull(Notes.replyId) : undefined,
|
||||
),
|
||||
limit,
|
||||
new URL(context.req.url),
|
||||
user?.id,
|
||||
);
|
||||
|
||||
return context.json(
|
||||
await Promise.all(objects.map((note) => note.toApi(otherUser))),
|
||||
200,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
61
api/api/v1/accounts/[id]/unblock.ts
Normal file
61
api/api/v1/accounts/[id]/unblock.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { Relationship as RelationshipSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/accounts/:id/unblock",
|
||||
describeRoute({
|
||||
summary: "Unblock account",
|
||||
description: "Unblock the given account.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#unblock",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Successfully unblocked, or account was already not blocked",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(RelationshipSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:blocks"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnBlocks,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
if (foundRelationship.data.blocking) {
|
||||
await foundRelationship.update({
|
||||
blocking: false,
|
||||
});
|
||||
}
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
57
api/api/v1/accounts/[id]/unfollow.ts
Normal file
57
api/api/v1/accounts/[id]/unfollow.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { Relationship as RelationshipSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/accounts/:id/unfollow",
|
||||
describeRoute({
|
||||
summary: "Unfollow account",
|
||||
description: "Unfollow the given account.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#unfollow",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Successfully unfollowed, or account was already not followed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(RelationshipSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:follows"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnFollows,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
await user.unfollow(otherUser, foundRelationship);
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
62
api/api/v1/accounts/[id]/unmute.ts
Normal file
62
api/api/v1/accounts/[id]/unmute.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { Relationship as RelationshipSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/accounts/:id/unmute",
|
||||
describeRoute({
|
||||
summary: "Unmute account",
|
||||
description: "Unmute the given account.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#unmute",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Successfully unmuted, or account was already unmuted",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(RelationshipSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:mutes"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnMutes,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
if (foundRelationship.data.muting) {
|
||||
await foundRelationship.update({
|
||||
muting: false,
|
||||
mutingNotifications: false,
|
||||
});
|
||||
}
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
62
api/api/v1/accounts/[id]/unpin.ts
Normal file
62
api/api/v1/accounts/[id]/unpin.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { apiRoute, auth, withUserParam } from "@/api";
|
||||
import { Relationship as RelationshipSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/accounts/:id/unpin",
|
||||
describeRoute({
|
||||
summary: "Unfeature account from profile",
|
||||
description:
|
||||
"Remove the given account from the user’s featured profiles.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#unpin",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Successfully unendorsed, or account was already not endorsed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(RelationshipSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
withUserParam,
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:accounts"],
|
||||
permissions: [
|
||||
RolePermission.ManageOwnAccount,
|
||||
RolePermission.ViewAccounts,
|
||||
],
|
||||
}),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const otherUser = context.get("user");
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
otherUser,
|
||||
);
|
||||
|
||||
if (foundRelationship.data.endorsed) {
|
||||
await foundRelationship.update({
|
||||
endorsed: false,
|
||||
});
|
||||
}
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { apiRoute, auth, qsQuery } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, handleZodError, qsQuery } from "@/api";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
FamiliarFollowers as FamiliarFollowersSchema,
|
||||
|
|
@ -8,12 +7,16 @@ import { RolePermission } from "@versia/client/schemas";
|
|||
import { User, db } from "@versia/kit/db";
|
||||
import type { Users } from "@versia/kit/tables";
|
||||
import { type InferSelectModel, sql } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { rateLimit } from "~/middlewares/rate-limit";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/familiar_followers",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/accounts/familiar_followers",
|
||||
describeRoute({
|
||||
summary: "Get familiar followers",
|
||||
description:
|
||||
"Obtain a list of all accounts that follow a given account, filtered for accounts you follow.",
|
||||
|
|
@ -21,17 +24,29 @@ const route = createRoute({
|
|||
url: "https://docs.joinmastodon.org/methods/accounts/#familiar_followers",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
responses: {
|
||||
200: {
|
||||
description: "Familiar followers",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(FamiliarFollowersSchema)),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
qsQuery(),
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["read:follows"],
|
||||
permissions: [RolePermission.ManageOwnFollows],
|
||||
}),
|
||||
rateLimit(5),
|
||||
qsQuery(),
|
||||
] as const,
|
||||
request: {
|
||||
query: z.object({
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
id: z
|
||||
.array(AccountSchema.shape.id)
|
||||
.min(1)
|
||||
|
|
@ -46,23 +61,9 @@ const route = createRoute({
|
|||
],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Familiar followers",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(FamiliarFollowersSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const { id: ids } = context.req.valid("query");
|
||||
|
||||
|
|
@ -72,7 +73,9 @@ export default apiRoute((app) =>
|
|||
id,
|
||||
accounts: await User.fromIds(
|
||||
(
|
||||
await db.execute(sql<InferSelectModel<typeof Users>>`
|
||||
await db.execute(sql<
|
||||
InferSelectModel<typeof Users>
|
||||
>`
|
||||
SELECT "Users"."id" FROM "Users"
|
||||
INNER JOIN "Relationships" AS "SelfFollowing"
|
||||
ON "SelfFollowing"."subjectId" = "Users"."id"
|
||||
|
|
@ -97,5 +100,6 @@ export default apiRoute((app) =>
|
|||
})),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { apiRoute, auth, jsonOrForm } from "@/api";
|
||||
import { apiRoute, auth, handleZodError, jsonOrForm } from "@/api";
|
||||
import { tempmailDomains } from "@/tempmail";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { zBoolean } from "@versia/client/schemas";
|
||||
import { User } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/config.ts";
|
||||
import { rateLimit } from "~/middlewares/rate-limit";
|
||||
|
|
@ -40,9 +42,10 @@ const schema = z.object({
|
|||
}),
|
||||
});
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/accounts",
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/accounts",
|
||||
describeRoute({
|
||||
summary: "Register an account",
|
||||
description:
|
||||
"Creates a user and account records. Returns an account access token for the app that initiated the request. The app should save this token for later, and should wait for the user to confirm their account by clicking a link in their email inbox.\n\nA relationship between the OAuth Application and created user account is stored.",
|
||||
|
|
@ -50,30 +53,6 @@ const route = createRoute({
|
|||
url: "https://docs.joinmastodon.org/methods/accounts/#create",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
scopes: ["write:accounts"],
|
||||
challenge: true,
|
||||
}),
|
||||
rateLimit(5),
|
||||
jsonOrForm(),
|
||||
] as const,
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema,
|
||||
},
|
||||
"multipart/form-data": {
|
||||
schema,
|
||||
},
|
||||
"application/x-www-form-urlencoded": {
|
||||
schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Token for the created account",
|
||||
|
|
@ -83,7 +62,8 @@ const route = createRoute({
|
|||
description: "Validation failed",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
schema: resolver(
|
||||
z.object({
|
||||
error: z.string(),
|
||||
details: z.object({
|
||||
username: z.array(
|
||||
|
|
@ -132,7 +112,10 @@ const route = createRoute({
|
|||
),
|
||||
locale: z.array(
|
||||
z.object({
|
||||
error: z.enum(["ERR_BLANK", "ERR_INVALID"]),
|
||||
error: z.enum([
|
||||
"ERR_BLANK",
|
||||
"ERR_INVALID",
|
||||
]),
|
||||
description: z.string(),
|
||||
}),
|
||||
),
|
||||
|
|
@ -147,14 +130,21 @@ const route = createRoute({
|
|||
),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
}),
|
||||
auth({
|
||||
auth: false,
|
||||
scopes: ["write:accounts"],
|
||||
challenge: true,
|
||||
}),
|
||||
rateLimit(5),
|
||||
jsonOrForm(),
|
||||
validator("json", schema, handleZodError),
|
||||
async (context) => {
|
||||
const form = context.req.valid("json");
|
||||
const { username, email, password, agreement, locale } =
|
||||
context.req.valid("json");
|
||||
|
|
@ -338,7 +328,9 @@ export default apiRoute((app) =>
|
|||
}
|
||||
|
||||
// If any errors are present, return them
|
||||
if (Object.values(errors.details).some((value) => value.length > 0)) {
|
||||
if (
|
||||
Object.values(errors.details).some((value) => value.length > 0)
|
||||
) {
|
||||
// Error is something like "Validation failed: Password can't be blank, Username must contain only letters, numbers and underscores, Agreement must be accepted"
|
||||
|
||||
const errorsText = Object.entries(errors.details)
|
||||
|
|
@ -368,5 +360,6 @@ export default apiRoute((app) =>
|
|||
});
|
||||
|
||||
return context.text("", 200);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,52 +1,53 @@
|
|||
import { apiRoute, auth, parseUserAddress } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, handleZodError, parseUserAddress } from "@/api";
|
||||
import { Account as AccountSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Instance, User } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/config.ts";
|
||||
import { rateLimit } from "~/middlewares/rate-limit";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/lookup",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/accounts/lookup",
|
||||
describeRoute({
|
||||
summary: "Lookup account ID from Webfinger address",
|
||||
description:
|
||||
"Quickly lookup a username to see if it is available, skipping WebFinger resolution.",
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
permissions: [RolePermission.Search],
|
||||
}),
|
||||
rateLimit(5),
|
||||
] as const,
|
||||
request: {
|
||||
query: z.object({
|
||||
acct: AccountSchema.shape.acct.openapi({
|
||||
description: "The username or Webfinger address to lookup.",
|
||||
example: "lexi@beta.versia.social",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Account",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: AccountSchema,
|
||||
schema: resolver(AccountSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
}),
|
||||
auth({
|
||||
auth: false,
|
||||
permissions: [RolePermission.Search],
|
||||
}),
|
||||
rateLimit(5),
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
acct: AccountSchema.shape.acct.openapi({
|
||||
description: "The username or Webfinger address to lookup.",
|
||||
example: "lexi@beta.versia.social",
|
||||
}),
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { acct } = context.req.valid("query");
|
||||
const { user } = context.get("auth");
|
||||
|
||||
|
|
@ -74,7 +75,10 @@ export default apiRoute((app) =>
|
|||
const instance = await Instance.resolveFromHost(domain);
|
||||
|
||||
if (!instance) {
|
||||
return context.json({ error: `Instance ${domain} not found` }, 404);
|
||||
return context.json(
|
||||
{ error: `Instance ${domain} not found` },
|
||||
404,
|
||||
);
|
||||
}
|
||||
|
||||
const account = await User.fromSql(
|
||||
|
|
@ -104,5 +108,6 @@ export default apiRoute((app) =>
|
|||
}
|
||||
|
||||
throw ApiError.accountNotFound();
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { apiRoute, auth, qsQuery } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, handleZodError, qsQuery } from "@/api";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Relationship as RelationshipSchema,
|
||||
|
|
@ -7,12 +6,16 @@ import {
|
|||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { rateLimit } from "~/middlewares/rate-limit";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/relationships",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/accounts/relationships",
|
||||
describeRoute({
|
||||
summary: "Check relationships to other accounts",
|
||||
description:
|
||||
"Find out whether a given account is followed, blocked, muted, etc.",
|
||||
|
|
@ -20,7 +23,19 @@ const route = createRoute({
|
|||
url: "https://docs.joinmastodon.org/methods/accounts/#relationships",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
responses: {
|
||||
200: {
|
||||
description: "Relationships",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(RelationshipSchema)),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
rateLimit(10),
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -28,9 +43,9 @@ const route = createRoute({
|
|||
permissions: [RolePermission.ManageOwnFollows],
|
||||
}),
|
||||
qsQuery(),
|
||||
] as const,
|
||||
request: {
|
||||
query: z.object({
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
id: z
|
||||
.array(AccountSchema.shape.id)
|
||||
.min(1)
|
||||
|
|
@ -50,23 +65,9 @@ const route = createRoute({
|
|||
example: false,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Relationships",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(RelationshipSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
|
||||
// TODO: Implement with_suspended
|
||||
|
|
@ -81,12 +82,14 @@ export default apiRoute((app) =>
|
|||
|
||||
relationships.sort(
|
||||
(a, b) =>
|
||||
ids.indexOf(a.data.subjectId) - ids.indexOf(b.data.subjectId),
|
||||
ids.indexOf(a.data.subjectId) -
|
||||
ids.indexOf(b.data.subjectId),
|
||||
);
|
||||
|
||||
return context.json(
|
||||
relationships.map((r) => r.toApi()),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,40 +1,60 @@
|
|||
import { apiRoute, auth, parseUserAddress } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, handleZodError, parseUserAddress } from "@/api";
|
||||
import { Account as AccountSchema, zBoolean } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { User } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { eq, ilike, not, or, sql } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import stringComparison from "string-comparison";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { rateLimit } from "~/middlewares/rate-limit";
|
||||
|
||||
export const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/search",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/accounts/search",
|
||||
describeRoute({
|
||||
summary: "Search for matching accounts",
|
||||
description: "Search for matching accounts by username or display name.",
|
||||
description:
|
||||
"Search for matching accounts by username or display name.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#search",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
responses: {
|
||||
200: {
|
||||
description: "Accounts",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(AccountSchema)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
rateLimit(5),
|
||||
auth({
|
||||
auth: false,
|
||||
permissions: [RolePermission.Search, RolePermission.ViewAccounts],
|
||||
scopes: ["read:accounts"],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: z.object({
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
q: AccountSchema.shape.username
|
||||
.or(AccountSchema.shape.acct)
|
||||
.openapi({
|
||||
description: "Search query for accounts.",
|
||||
example: "username",
|
||||
}),
|
||||
limit: z.coerce.number().int().min(1).max(80).default(40).openapi({
|
||||
limit: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(80)
|
||||
.default(40)
|
||||
.openapi({
|
||||
description: "Maximum number of results.",
|
||||
example: 40,
|
||||
}),
|
||||
|
|
@ -52,27 +72,18 @@ export const route = createRoute({
|
|||
example: false,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Accounts",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(AccountSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { q, limit, offset, resolve, following } =
|
||||
context.req.valid("query");
|
||||
const { user } = context.get("auth");
|
||||
|
||||
if (!user && following) {
|
||||
throw new ApiError(401, "Must be authenticated to use 'following'");
|
||||
throw new ApiError(
|
||||
401,
|
||||
"Must be authenticated to use 'following'",
|
||||
);
|
||||
}
|
||||
|
||||
const { username, domain } = parseUserAddress(q);
|
||||
|
|
@ -122,5 +133,6 @@ export default apiRoute((app) =>
|
|||
result.map((acct) => acct.toApi()),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,27 +1,42 @@
|
|||
import { apiRoute, auth, jsonOrForm } from "@/api";
|
||||
import { apiRoute, auth, handleZodError, jsonOrForm } from "@/api";
|
||||
import { mergeAndDeduplicate } from "@/lib";
|
||||
import { sanitizedHtmlStrip } from "@/sanitization";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Account as AccountSchema, zBoolean } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Emoji, Media, User } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { contentToHtml } from "~/classes/functions/status";
|
||||
import { config } from "~/config.ts";
|
||||
import { rateLimit } from "~/middlewares/rate-limit";
|
||||
|
||||
const route = createRoute({
|
||||
method: "patch",
|
||||
path: "/api/v1/accounts/update_credentials",
|
||||
export default apiRoute((app) =>
|
||||
app.patch(
|
||||
"/api/v1/accounts/update_credentials",
|
||||
describeRoute({
|
||||
summary: "Update account credentials",
|
||||
description: "Update the user’s display and preferences.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#update_credentials",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated user",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(AccountSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
rateLimit(5),
|
||||
auth({
|
||||
auth: true,
|
||||
|
|
@ -29,27 +44,20 @@ const route = createRoute({
|
|||
scopes: ["write:accounts"],
|
||||
}),
|
||||
jsonOrForm(),
|
||||
] as const,
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z
|
||||
validator(
|
||||
"json",
|
||||
z
|
||||
.object({
|
||||
display_name:
|
||||
AccountSchema.shape.display_name.openapi({
|
||||
description:
|
||||
"The display name to use for the profile.",
|
||||
display_name: AccountSchema.shape.display_name.openapi({
|
||||
description: "The display name to use for the profile.",
|
||||
example: "Lexi",
|
||||
}),
|
||||
username: AccountSchema.shape.username.openapi({
|
||||
description:
|
||||
"The username to use for the profile.",
|
||||
description: "The username to use for the profile.",
|
||||
example: "lexi",
|
||||
}),
|
||||
note: AccountSchema.shape.note.openapi({
|
||||
description:
|
||||
"The account bio. Markdown is supported.",
|
||||
description: "The account bio. Markdown is supported.",
|
||||
}),
|
||||
avatar: z
|
||||
.string()
|
||||
|
|
@ -100,11 +108,9 @@ const route = createRoute({
|
|||
"Whether manual approval of follow requests is required.",
|
||||
}),
|
||||
bot: AccountSchema.shape.bot.openapi({
|
||||
description:
|
||||
"Whether the account has a bot flag.",
|
||||
description: "Whether the account has a bot flag.",
|
||||
}),
|
||||
discoverable:
|
||||
AccountSchema.shape.discoverable.openapi({
|
||||
discoverable: AccountSchema.shape.discoverable.openapi({
|
||||
description:
|
||||
"Whether the account should be shown in the profile directory.",
|
||||
}),
|
||||
|
|
@ -127,50 +133,31 @@ const route = createRoute({
|
|||
source: z
|
||||
.object({
|
||||
privacy:
|
||||
AccountSchema.shape.source.unwrap()
|
||||
.shape.privacy,
|
||||
AccountSchema.shape.source.unwrap().shape
|
||||
.privacy,
|
||||
sensitive:
|
||||
AccountSchema.shape.source.unwrap()
|
||||
.shape.sensitive,
|
||||
AccountSchema.shape.source.unwrap().shape
|
||||
.sensitive,
|
||||
language:
|
||||
AccountSchema.shape.source.unwrap()
|
||||
.shape.language,
|
||||
AccountSchema.shape.source.unwrap().shape
|
||||
.language,
|
||||
})
|
||||
.partial(),
|
||||
fields_attributes: z
|
||||
.array(
|
||||
z.object({
|
||||
name: AccountSchema.shape.fields.element
|
||||
.shape.name,
|
||||
value: AccountSchema.shape.fields
|
||||
.element.shape.value,
|
||||
name: AccountSchema.shape.fields.element.shape
|
||||
.name,
|
||||
value: AccountSchema.shape.fields.element.shape
|
||||
.value,
|
||||
}),
|
||||
)
|
||||
.max(
|
||||
config.validation.accounts.max_field_count,
|
||||
),
|
||||
.max(config.validation.accounts.max_field_count),
|
||||
})
|
||||
.partial(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated user",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: AccountSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const {
|
||||
display_name,
|
||||
|
|
@ -365,5 +352,6 @@ export default apiRoute((app) =>
|
|||
}
|
||||
|
||||
return context.json(output.toApi(), 200);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,23 +1,19 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Account } from "@versia/client/schemas";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/accounts/verify_credentials",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/accounts/verify_credentials",
|
||||
describeRoute({
|
||||
summary: "Verify account credentials",
|
||||
description: "Test to make sure that the user token works.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/accounts/#verify_credentials",
|
||||
},
|
||||
tags: ["Accounts"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["read:accounts"],
|
||||
}),
|
||||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
// TODO: Implement CredentialAccount
|
||||
|
|
@ -25,20 +21,23 @@ const route = createRoute({
|
|||
"Note the extra source property, which is not visible on accounts other than your own. Also note that plain-text is used within source and HTML is used for their corresponding properties such as note and fields.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Account,
|
||||
schema: resolver(Account),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, (context) => {
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["read:accounts"],
|
||||
}),
|
||||
(context) => {
|
||||
// TODO: Add checks for disabled/unverified accounts
|
||||
const { user } = context.get("auth");
|
||||
|
||||
return context.json(user.toApi(true), 200);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,33 +1,49 @@
|
|||
import { apiRoute, jsonOrForm } from "@/api";
|
||||
import { apiRoute, handleZodError, jsonOrForm } from "@/api";
|
||||
import { randomString } from "@/math";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Application as ApplicationSchema,
|
||||
CredentialApplication as CredentialApplicationSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { Application } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { rateLimit } from "~/middlewares/rate-limit";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/apps",
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/apps",
|
||||
describeRoute({
|
||||
summary: "Create an application",
|
||||
description: "Create a new application to obtain OAuth2 credentials.",
|
||||
description:
|
||||
"Create a new application to obtain OAuth2 credentials.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/apps/#create",
|
||||
},
|
||||
tags: ["Apps"],
|
||||
middleware: [jsonOrForm(), rateLimit(4)],
|
||||
request: {
|
||||
body: {
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Store the client_id and client_secret in your cache, as these will be used to obtain OAuth tokens.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({
|
||||
schema: resolver(CredentialApplicationSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
jsonOrForm(),
|
||||
rateLimit(4),
|
||||
validator(
|
||||
"json",
|
||||
z.object({
|
||||
client_name: ApplicationSchema.shape.name,
|
||||
redirect_uris: ApplicationSchema.shape.redirect_uris.or(
|
||||
ApplicationSchema.shape.redirect_uri.transform(
|
||||
(u) => u.split("\n"),
|
||||
ApplicationSchema.shape.redirect_uri.transform((u) =>
|
||||
u.split("\n"),
|
||||
),
|
||||
),
|
||||
scopes: z
|
||||
|
|
@ -43,26 +59,9 @@ const route = createRoute({
|
|||
.optional()
|
||||
.or(z.literal("").transform(() => undefined)),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Store the client_id and client_secret in your cache, as these will be used to obtain OAuth tokens.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: CredentialApplicationSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { client_name, redirect_uris, scopes, website } =
|
||||
context.req.valid("json");
|
||||
|
||||
|
|
@ -76,5 +75,6 @@ export default apiRoute((app) =>
|
|||
});
|
||||
|
||||
return context.json(app.toApiCredential(), 200);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,42 +1,40 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Application as ApplicationSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Application } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/apps/verify_credentials",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/apps/verify_credentials",
|
||||
describeRoute({
|
||||
summary: "Verify your app works",
|
||||
description: "Confirm that the app’s OAuth2 credentials work.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/apps/#verify_credentials",
|
||||
},
|
||||
tags: ["Apps"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnApps],
|
||||
}),
|
||||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"If the Authorization header was provided with a valid token, you should see your app returned as an Application entity.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ApplicationSchema,
|
||||
schema: resolver(ApplicationSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnApps],
|
||||
}),
|
||||
async (context) => {
|
||||
const { token } = context.get("auth");
|
||||
|
||||
const application = await Application.getFromToken(
|
||||
|
|
@ -48,5 +46,6 @@ export default apiRoute((app) =>
|
|||
}
|
||||
|
||||
return context.json(application.toApi(), 200);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,30 +1,59 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, handleZodError } from "@/api";
|
||||
import { Account as AccountSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/blocks",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/blocks",
|
||||
describeRoute({
|
||||
summary: "View your blocks.",
|
||||
description: "View blocked users.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/blocks/#get",
|
||||
},
|
||||
tags: ["Blocks"],
|
||||
middleware: [
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of blocked users",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(AccountSchema)),
|
||||
},
|
||||
},
|
||||
headers: z.object({
|
||||
link: z
|
||||
.string()
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"Links to the next and previous pages",
|
||||
example:
|
||||
'<https://versia.social/api/v1/blocks?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/blocks?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["read:blocks"],
|
||||
permissions: [RolePermission.ManageOwnBlocks],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: z.object({
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
max_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||
|
|
@ -40,41 +69,21 @@ const route = createRoute({
|
|||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||
example: undefined,
|
||||
}),
|
||||
limit: z.coerce.number().int().min(1).max(80).default(40).openapi({
|
||||
limit: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(80)
|
||||
.default(40)
|
||||
.openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of blocked users",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(AccountSchema),
|
||||
},
|
||||
},
|
||||
headers: z.object({
|
||||
link: z
|
||||
.string()
|
||||
.optional()
|
||||
.openapi({
|
||||
description: "Links to the next and previous pages",
|
||||
example:
|
||||
'<https://versia.social/api/v1/blocks?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/blocks?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { max_id, since_id, min_id, limit } = context.req.valid("query");
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { max_id, since_id, min_id, limit } =
|
||||
context.req.valid("query");
|
||||
|
||||
const { user } = context.get("auth");
|
||||
|
||||
|
|
@ -96,5 +105,6 @@ export default apiRoute((app) =>
|
|||
Link: link,
|
||||
},
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,27 +1,24 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { generateChallenge } from "@/challenges";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Challenge } from "@versia/client/schemas";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/challenges",
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/challenges",
|
||||
describeRoute({
|
||||
summary: "Generate a challenge",
|
||||
description: "Generate a challenge to solve",
|
||||
tags: ["Challenges"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
}),
|
||||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description: "Challenge",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Challenge,
|
||||
schema: resolver(Challenge),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -29,15 +26,16 @@ const route = createRoute({
|
|||
description: "Challenges are disabled",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ApiError.zodSchema,
|
||||
schema: resolver(ApiError.zodSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
}),
|
||||
auth({
|
||||
auth: false,
|
||||
}),
|
||||
async (context) => {
|
||||
if (!config.validation.challenges) {
|
||||
throw new ApiError(400, "Challenges are disabled in config");
|
||||
}
|
||||
|
|
@ -51,5 +49,6 @@ export default apiRoute((app) =>
|
|||
},
|
||||
200,
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,42 +1,42 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { CustomEmoji as CustomEmojiSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Emoji } from "@versia/kit/db";
|
||||
import { Emojis } from "@versia/kit/tables";
|
||||
import { and, eq, isNull, or } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/custom_emojis",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/custom_emojis",
|
||||
describeRoute({
|
||||
summary: "View all custom emoji",
|
||||
description: "Returns custom emojis that are available on the server.",
|
||||
description:
|
||||
"Returns custom emojis that are available on the server.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/custom_emojis/#get",
|
||||
},
|
||||
tags: ["Emojis"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
permissions: [RolePermission.ViewEmojis],
|
||||
}),
|
||||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of custom emojis",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(CustomEmojiSchema),
|
||||
schema: resolver(z.array(CustomEmojiSchema)),
|
||||
},
|
||||
},
|
||||
},
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
}),
|
||||
auth({
|
||||
auth: false,
|
||||
permissions: [RolePermission.ViewEmojis],
|
||||
}),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const emojis = await Emoji.manyFromSql(
|
||||
|
|
@ -53,5 +53,6 @@ export default apiRoute((app) =>
|
|||
emojis.map((emoji) => emoji.toApi()),
|
||||
200,
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,276 +0,0 @@
|
|||
import { apiRoute, auth, jsonOrForm, withEmojiParam } from "@/api";
|
||||
import { mimeLookup } from "@/content_types";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { CustomEmoji as CustomEmojiSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
shortcode: CustomEmojiSchema.shape.shortcode,
|
||||
element: z
|
||||
.string()
|
||||
.url()
|
||||
.transform((a) => new URL(a))
|
||||
.openapi({
|
||||
description: "Emoji image URL",
|
||||
})
|
||||
.or(
|
||||
z
|
||||
.instanceof(File)
|
||||
.openapi({
|
||||
description:
|
||||
"Emoji image encoded using multipart/form-data",
|
||||
})
|
||||
.refine(
|
||||
(v) => v.size <= config.validation.emojis.max_bytes,
|
||||
`Emoji must be less than ${config.validation.emojis.max_bytes} bytes`,
|
||||
),
|
||||
),
|
||||
category: CustomEmojiSchema.shape.category.optional(),
|
||||
alt: CustomEmojiSchema.shape.description.optional(),
|
||||
global: CustomEmojiSchema.shape.global.default(false),
|
||||
})
|
||||
.partial();
|
||||
|
||||
const routeGet = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/emojis/{id}",
|
||||
summary: "Get emoji",
|
||||
description: "Retrieves a custom emoji from database by ID.",
|
||||
tags: ["Emojis"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ViewEmojis],
|
||||
}),
|
||||
withEmojiParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: CustomEmojiSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Emoji",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: CustomEmojiSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Emoji not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ApiError.zodSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
const routePatch = createRoute({
|
||||
method: "patch",
|
||||
path: "/api/v1/emojis/{id}",
|
||||
summary: "Modify emoji",
|
||||
description: "Edit image or metadata of an emoji.",
|
||||
tags: ["Emojis"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [
|
||||
RolePermission.ManageOwnEmojis,
|
||||
RolePermission.ViewEmojis,
|
||||
],
|
||||
}),
|
||||
jsonOrForm(),
|
||||
withEmojiParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: CustomEmojiSchema.shape.id,
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema,
|
||||
},
|
||||
"application/x-www-form-urlencoded": {
|
||||
schema,
|
||||
},
|
||||
"multipart/form-data": {
|
||||
schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Emoji modified",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: CustomEmojiSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
403: {
|
||||
description: "Insufficient permissions",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ApiError.zodSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Emoji not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ApiError.zodSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
const routeDelete = createRoute({
|
||||
method: "delete",
|
||||
path: "/api/v1/emojis/{id}",
|
||||
summary: "Delete emoji",
|
||||
description: "Delete a custom emoji from the database.",
|
||||
tags: ["Emojis"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [
|
||||
RolePermission.ManageOwnEmojis,
|
||||
RolePermission.ViewEmojis,
|
||||
],
|
||||
}),
|
||||
withEmojiParam,
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: CustomEmojiSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
204: {
|
||||
description: "Emoji deleted",
|
||||
},
|
||||
404: ApiError.emojiNotFound().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) => {
|
||||
app.openapi(routeGet, (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const emoji = context.get("emoji");
|
||||
|
||||
// Don't leak non-global emojis to non-admins
|
||||
if (
|
||||
!user.hasPermission(RolePermission.ManageEmojis) &&
|
||||
emoji.data.ownerId !== user.data.id
|
||||
) {
|
||||
throw ApiError.emojiNotFound();
|
||||
}
|
||||
|
||||
return context.json(emoji.toApi(), 200);
|
||||
});
|
||||
|
||||
app.openapi(routePatch, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const emoji = context.get("emoji");
|
||||
|
||||
// Check if user is admin
|
||||
if (
|
||||
!user.hasPermission(RolePermission.ManageEmojis) &&
|
||||
emoji.data.ownerId !== user.data.id
|
||||
) {
|
||||
throw new ApiError(
|
||||
403,
|
||||
"Cannot modify emoji not owned by you",
|
||||
`This emoji is either global (and you do not have the '${RolePermission.ManageEmojis}' permission) or not owned by you`,
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
global: emojiGlobal,
|
||||
alt,
|
||||
category,
|
||||
element,
|
||||
shortcode,
|
||||
} = context.req.valid("json");
|
||||
|
||||
if (!user.hasPermission(RolePermission.ManageEmojis) && emojiGlobal) {
|
||||
throw new ApiError(
|
||||
401,
|
||||
"Missing permissions",
|
||||
`'${RolePermission.ManageEmojis}' permission is needed to upload global emojis`,
|
||||
);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
// Check if emoji is an image
|
||||
const contentType =
|
||||
element instanceof File
|
||||
? element.type
|
||||
: await mimeLookup(element);
|
||||
|
||||
if (!contentType.startsWith("image/")) {
|
||||
throw new ApiError(
|
||||
422,
|
||||
"Invalid content type",
|
||||
`Emojis must be images (png, jpg, gif, etc.). Detected: ${contentType}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (element instanceof File) {
|
||||
await emoji.media.updateFromFile(element);
|
||||
} else {
|
||||
await emoji.media.updateFromUrl(element);
|
||||
}
|
||||
}
|
||||
|
||||
if (alt) {
|
||||
await emoji.media.updateMetadata({
|
||||
description: alt,
|
||||
});
|
||||
}
|
||||
|
||||
await emoji.update({
|
||||
shortcode,
|
||||
ownerId: emojiGlobal ? null : user.data.id,
|
||||
category,
|
||||
});
|
||||
|
||||
return context.json(emoji.toApi(), 200);
|
||||
});
|
||||
|
||||
app.openapi(routeDelete, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const emoji = context.get("emoji");
|
||||
|
||||
// Check if user is admin
|
||||
if (
|
||||
!user.hasPermission(RolePermission.ManageEmojis) &&
|
||||
emoji.data.ownerId !== user.data.id
|
||||
) {
|
||||
throw new ApiError(
|
||||
403,
|
||||
"Cannot delete emoji not owned by you",
|
||||
`This emoji is either global (and you do not have the '${RolePermission.ManageEmojis}' permission) or not owned by you`,
|
||||
);
|
||||
}
|
||||
|
||||
await emoji.delete();
|
||||
|
||||
return context.body(null, 204);
|
||||
});
|
||||
});
|
||||
258
api/api/v1/emojis/[id]/index.ts
Normal file
258
api/api/v1/emojis/[id]/index.ts
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
import {
|
||||
apiRoute,
|
||||
auth,
|
||||
handleZodError,
|
||||
jsonOrForm,
|
||||
withEmojiParam,
|
||||
} from "@/api";
|
||||
import { mimeLookup } from "@/content_types";
|
||||
import { CustomEmoji as CustomEmojiSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
export default apiRoute((app) => {
|
||||
app.get(
|
||||
"/api/v1/emojis/:id",
|
||||
describeRoute({
|
||||
summary: "Get emoji",
|
||||
description: "Retrieves a custom emoji from database by ID.",
|
||||
tags: ["Emojis"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Emoji",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(CustomEmojiSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Emoji not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(ApiError.zodSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ViewEmojis],
|
||||
}),
|
||||
withEmojiParam,
|
||||
(context) => {
|
||||
const { user } = context.get("auth");
|
||||
const emoji = context.get("emoji");
|
||||
|
||||
// Don't leak non-global emojis to non-admins
|
||||
if (
|
||||
!user.hasPermission(RolePermission.ManageEmojis) &&
|
||||
emoji.data.ownerId !== user.data.id
|
||||
) {
|
||||
throw ApiError.emojiNotFound();
|
||||
}
|
||||
|
||||
return context.json(emoji.toApi(), 200);
|
||||
},
|
||||
);
|
||||
|
||||
app.patch(
|
||||
"/api/v1/emojis/:id",
|
||||
describeRoute({
|
||||
summary: "Modify emoji",
|
||||
description: "Edit image or metadata of an emoji.",
|
||||
tags: ["Emojis"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Emoji modified",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(CustomEmojiSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
403: {
|
||||
description: "Insufficient permissions",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(ApiError.zodSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Emoji not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(ApiError.zodSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [
|
||||
RolePermission.ManageOwnEmojis,
|
||||
RolePermission.ViewEmojis,
|
||||
],
|
||||
}),
|
||||
jsonOrForm(),
|
||||
withEmojiParam,
|
||||
validator(
|
||||
"json",
|
||||
z
|
||||
.object({
|
||||
shortcode: CustomEmojiSchema.shape.shortcode,
|
||||
element: z
|
||||
.string()
|
||||
.url()
|
||||
.transform((a) => new URL(a))
|
||||
.openapi({
|
||||
description: "Emoji image URL",
|
||||
})
|
||||
.or(
|
||||
z
|
||||
.instanceof(File)
|
||||
.openapi({
|
||||
description:
|
||||
"Emoji image encoded using multipart/form-data",
|
||||
})
|
||||
.refine(
|
||||
(v) =>
|
||||
v.size <=
|
||||
config.validation.emojis.max_bytes,
|
||||
`Emoji must be less than ${config.validation.emojis.max_bytes} bytes`,
|
||||
),
|
||||
),
|
||||
category: CustomEmojiSchema.shape.category.optional(),
|
||||
alt: CustomEmojiSchema.shape.description.optional(),
|
||||
global: CustomEmojiSchema.shape.global.default(false),
|
||||
})
|
||||
.partial(),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const emoji = context.get("emoji");
|
||||
|
||||
// Check if user is admin
|
||||
if (
|
||||
!user.hasPermission(RolePermission.ManageEmojis) &&
|
||||
emoji.data.ownerId !== user.data.id
|
||||
) {
|
||||
throw new ApiError(
|
||||
403,
|
||||
"Cannot modify emoji not owned by you",
|
||||
`This emoji is either global (and you do not have the '${RolePermission.ManageEmojis}' permission) or not owned by you`,
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
global: emojiGlobal,
|
||||
alt,
|
||||
category,
|
||||
element,
|
||||
shortcode,
|
||||
} = context.req.valid("json");
|
||||
|
||||
if (
|
||||
!user.hasPermission(RolePermission.ManageEmojis) &&
|
||||
emojiGlobal
|
||||
) {
|
||||
throw new ApiError(
|
||||
401,
|
||||
"Missing permissions",
|
||||
`'${RolePermission.ManageEmojis}' permission is needed to upload global emojis`,
|
||||
);
|
||||
}
|
||||
|
||||
if (element) {
|
||||
// Check if emoji is an image
|
||||
const contentType =
|
||||
element instanceof File
|
||||
? element.type
|
||||
: await mimeLookup(element);
|
||||
|
||||
if (!contentType.startsWith("image/")) {
|
||||
throw new ApiError(
|
||||
422,
|
||||
"Invalid content type",
|
||||
`Emojis must be images (png, jpg, gif, etc.). Detected: ${contentType}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (element instanceof File) {
|
||||
await emoji.media.updateFromFile(element);
|
||||
} else {
|
||||
await emoji.media.updateFromUrl(element);
|
||||
}
|
||||
}
|
||||
|
||||
if (alt) {
|
||||
await emoji.media.updateMetadata({
|
||||
description: alt,
|
||||
});
|
||||
}
|
||||
|
||||
await emoji.update({
|
||||
shortcode,
|
||||
ownerId: emojiGlobal ? null : user.data.id,
|
||||
category,
|
||||
});
|
||||
|
||||
return context.json(emoji.toApi(), 200);
|
||||
},
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/api/v1/emojis/{id}",
|
||||
describeRoute({
|
||||
summary: "Delete emoji",
|
||||
description: "Delete a custom emoji from the database.",
|
||||
tags: ["Emojis"],
|
||||
responses: {
|
||||
204: {
|
||||
description: "Emoji deleted",
|
||||
},
|
||||
404: ApiError.emojiNotFound().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [
|
||||
RolePermission.ManageOwnEmojis,
|
||||
RolePermission.ViewEmojis,
|
||||
],
|
||||
}),
|
||||
withEmojiParam,
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const emoji = context.get("emoji");
|
||||
|
||||
// Check if user is admin
|
||||
if (
|
||||
!user.hasPermission(RolePermission.ManageEmojis) &&
|
||||
emoji.data.ownerId !== user.data.id
|
||||
) {
|
||||
throw new ApiError(
|
||||
403,
|
||||
"Cannot delete emoji not owned by you",
|
||||
`This emoji is either global (and you do not have the '${RolePermission.ManageEmojis}' permission) or not owned by you`,
|
||||
);
|
||||
}
|
||||
|
||||
await emoji.delete();
|
||||
|
||||
return context.body(null, 204);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -1,15 +1,47 @@
|
|||
import { apiRoute, auth, jsonOrForm } from "@/api";
|
||||
import { apiRoute, auth, handleZodError, jsonOrForm } from "@/api";
|
||||
import { mimeLookup } from "@/content_types";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { CustomEmoji as CustomEmojiSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Emoji, Media } from "@versia/kit/db";
|
||||
import { Emojis } from "@versia/kit/tables";
|
||||
import { and, eq, isNull, or } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
const schema = z.object({
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/emojis",
|
||||
describeRoute({
|
||||
summary: "Upload emoji",
|
||||
description: "Upload a new emoji to the server.",
|
||||
tags: ["Emojis"],
|
||||
responses: {
|
||||
201: {
|
||||
description: "Uploaded emoji",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(CustomEmojiSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [
|
||||
RolePermission.ManageOwnEmojis,
|
||||
RolePermission.ViewEmojis,
|
||||
],
|
||||
}),
|
||||
jsonOrForm(),
|
||||
validator(
|
||||
"json",
|
||||
z.object({
|
||||
shortcode: CustomEmojiSchema.shape.shortcode,
|
||||
element: z
|
||||
.string()
|
||||
|
|
@ -26,62 +58,19 @@ const schema = z.object({
|
|||
"Emoji image encoded using multipart/form-data",
|
||||
})
|
||||
.refine(
|
||||
(v) => v.size <= config.validation.emojis.max_bytes,
|
||||
(v) =>
|
||||
v.size <=
|
||||
config.validation.emojis.max_bytes,
|
||||
`Emoji must be less than ${config.validation.emojis.max_bytes} bytes`,
|
||||
),
|
||||
),
|
||||
category: CustomEmojiSchema.shape.category.optional(),
|
||||
alt: CustomEmojiSchema.shape.description.optional(),
|
||||
global: CustomEmojiSchema.shape.global.default(false),
|
||||
});
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/emojis",
|
||||
summary: "Upload emoji",
|
||||
description: "Upload a new emoji to the server.",
|
||||
tags: ["Emojis"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [
|
||||
RolePermission.ManageOwnEmojis,
|
||||
RolePermission.ViewEmojis,
|
||||
],
|
||||
}),
|
||||
jsonOrForm(),
|
||||
] as const,
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema,
|
||||
},
|
||||
"multipart/form-data": {
|
||||
schema,
|
||||
},
|
||||
"application/x-www-form-urlencoded": {
|
||||
schema,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
201: {
|
||||
description: "Uploaded emoji",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: CustomEmojiSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { shortcode, element, alt, global, category } =
|
||||
context.req.valid("json");
|
||||
const { user } = context.get("auth");
|
||||
|
|
@ -113,7 +102,9 @@ export default apiRoute((app) =>
|
|||
|
||||
// Check of emoji is an image
|
||||
const contentType =
|
||||
element instanceof File ? element.type : await mimeLookup(element);
|
||||
element instanceof File
|
||||
? element.type
|
||||
: await mimeLookup(element);
|
||||
|
||||
if (!contentType.startsWith("image/")) {
|
||||
throw new ApiError(
|
||||
|
|
@ -141,5 +132,6 @@ export default apiRoute((app) =>
|
|||
});
|
||||
|
||||
return context.json(emoji.toApi(), 201);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,29 +1,58 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, handleZodError } from "@/api";
|
||||
import { Status as StatusSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { Notes } from "@versia/kit/tables";
|
||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/favourites",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/favourites",
|
||||
describeRoute({
|
||||
summary: "View favourited statuses",
|
||||
description: "Statuses the user has favourited.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/favourites/#get",
|
||||
},
|
||||
tags: ["Favourites"],
|
||||
middleware: [
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of favourited statuses",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(StatusSchema)),
|
||||
},
|
||||
},
|
||||
headers: z.object({
|
||||
link: z
|
||||
.string()
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"Links to the next and previous pages",
|
||||
example:
|
||||
'<https://versia.social/api/v1/favourites?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/favourites?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnLikes],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: z.object({
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
max_id: StatusSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||
|
|
@ -39,45 +68,26 @@ const route = createRoute({
|
|||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||
example: undefined,
|
||||
}),
|
||||
limit: z.coerce.number().int().min(1).max(80).default(40).openapi({
|
||||
limit: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(80)
|
||||
.default(40)
|
||||
.openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of favourited statuses",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(StatusSchema),
|
||||
},
|
||||
},
|
||||
headers: z.object({
|
||||
link: z
|
||||
.string()
|
||||
.optional()
|
||||
.openapi({
|
||||
description: "Links to the next and previous pages",
|
||||
example:
|
||||
'<https://versia.social/api/v1/favourites?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/favourites?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { max_id, since_id, min_id, limit } = context.req.valid("query");
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { max_id, since_id, min_id, limit } =
|
||||
context.req.valid("query");
|
||||
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const { objects: favourites, link } = await Timeline.getNoteTimeline(
|
||||
const { objects: favourites, link } =
|
||||
await Timeline.getNoteTimeline(
|
||||
and(
|
||||
max_id ? lt(Notes.id, max_id) : undefined,
|
||||
since_id ? gte(Notes.id, since_id) : undefined,
|
||||
|
|
@ -96,5 +106,6 @@ export default apiRoute((app) =>
|
|||
Link: link,
|
||||
},
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Relationship as RelationshipSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship, User } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/follow_requests/{account_id}/authorize",
|
||||
summary: "Accept follow request",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/follow_requests/#accept",
|
||||
},
|
||||
tags: ["Follows"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnFollows],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
account_id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Your Relationship with this account should be updated so that you are followed_by this account.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const { account_id } = context.req.valid("param");
|
||||
|
||||
const account = await User.fromId(account_id);
|
||||
|
||||
if (!account) {
|
||||
throw ApiError.accountNotFound();
|
||||
}
|
||||
|
||||
const oppositeRelationship = await Relationship.fromOwnerAndSubject(
|
||||
account,
|
||||
user,
|
||||
);
|
||||
|
||||
await oppositeRelationship.update({
|
||||
requested: false,
|
||||
following: true,
|
||||
});
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
account,
|
||||
);
|
||||
|
||||
// Check if accepting remote follow
|
||||
if (account.isRemote()) {
|
||||
// Federate follow accept
|
||||
await user.sendFollowAccept(account);
|
||||
}
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Relationship as RelationshipSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship, User } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/follow_requests/{account_id}/reject",
|
||||
summary: "Reject follow request",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/follow_requests/#reject",
|
||||
},
|
||||
tags: ["Follows"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnFollows],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
account_id: AccountSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Your Relationship with this account should be unchanged.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: RelationshipSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const { account_id } = context.req.valid("param");
|
||||
|
||||
const account = await User.fromId(account_id);
|
||||
|
||||
if (!account) {
|
||||
throw ApiError.accountNotFound();
|
||||
}
|
||||
|
||||
const oppositeRelationship = await Relationship.fromOwnerAndSubject(
|
||||
account,
|
||||
user,
|
||||
);
|
||||
|
||||
await oppositeRelationship.update({
|
||||
requested: false,
|
||||
following: false,
|
||||
});
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
account,
|
||||
);
|
||||
|
||||
// Check if rejecting remote follow
|
||||
if (account.isRemote()) {
|
||||
// Federate follow reject
|
||||
await user.sendFollowReject(account);
|
||||
}
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
}),
|
||||
);
|
||||
82
api/api/v1/follow_requests/[account_id]/authorize.ts
Normal file
82
api/api/v1/follow_requests/[account_id]/authorize.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { apiRoute, auth, handleZodError } from "@/api";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Relationship as RelationshipSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship, User } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/follow_requests/:account_id/authorize",
|
||||
describeRoute({
|
||||
summary: "Accept follow request",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/follow_requests/#accept",
|
||||
},
|
||||
tags: ["Follows"],
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Your Relationship with this account should be updated so that you are followed_by this account.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(RelationshipSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnFollows],
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
account_id: AccountSchema.shape.id,
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
const { account_id } = context.req.valid("param");
|
||||
|
||||
const account = await User.fromId(account_id);
|
||||
|
||||
if (!account) {
|
||||
throw ApiError.accountNotFound();
|
||||
}
|
||||
|
||||
const oppositeRelationship = await Relationship.fromOwnerAndSubject(
|
||||
account,
|
||||
user,
|
||||
);
|
||||
|
||||
await oppositeRelationship.update({
|
||||
requested: false,
|
||||
following: true,
|
||||
});
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
account,
|
||||
);
|
||||
|
||||
// Check if accepting remote follow
|
||||
if (account.isRemote()) {
|
||||
// Federate follow accept
|
||||
await user.sendFollowAccept(account);
|
||||
}
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
83
api/api/v1/follow_requests/[account_id]/reject.ts
Normal file
83
api/api/v1/follow_requests/[account_id]/reject.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { apiRoute, auth, handleZodError } from "@/api";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Relationship as RelationshipSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Relationship, User } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/follow_requests/:account_id/reject",
|
||||
describeRoute({
|
||||
summary: "Reject follow request",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/follow_requests/#reject",
|
||||
},
|
||||
tags: ["Follows"],
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Your Relationship with this account should be unchanged.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(RelationshipSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.accountNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnFollows],
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
account_id: AccountSchema.shape.id,
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const { account_id } = context.req.valid("param");
|
||||
|
||||
const account = await User.fromId(account_id);
|
||||
|
||||
if (!account) {
|
||||
throw ApiError.accountNotFound();
|
||||
}
|
||||
|
||||
const oppositeRelationship = await Relationship.fromOwnerAndSubject(
|
||||
account,
|
||||
user,
|
||||
);
|
||||
|
||||
await oppositeRelationship.update({
|
||||
requested: false,
|
||||
following: false,
|
||||
});
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
user,
|
||||
account,
|
||||
);
|
||||
|
||||
// Check if rejecting remote follow
|
||||
if (account.isRemote()) {
|
||||
// Federate follow reject
|
||||
await user.sendFollowReject(account);
|
||||
}
|
||||
|
||||
return context.json(foundRelationship.toApi(), 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -1,29 +1,60 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, handleZodError } from "@/api";
|
||||
import { Account as AccountSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/follow_requests",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/follow_requests",
|
||||
describeRoute({
|
||||
summary: "View pending follow requests",
|
||||
description: "Get a list of follow requests that the user has received.",
|
||||
description:
|
||||
"Get a list of follow requests that the user has received.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/follow_requests/#get",
|
||||
},
|
||||
tags: ["Follows"],
|
||||
middleware: [
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"List of accounts that have requested to follow the user",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(AccountSchema)),
|
||||
},
|
||||
},
|
||||
headers: z.object({
|
||||
link: z
|
||||
.string()
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"Links to the next and previous pages",
|
||||
example:
|
||||
'<https://versia.social/api/v1/follow_requests?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/follow_requests?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnFollows],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: z.object({
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
max_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||
|
|
@ -39,42 +70,21 @@ const route = createRoute({
|
|||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||
example: undefined,
|
||||
}),
|
||||
limit: z.coerce.number().int().min(1).max(80).default(40).openapi({
|
||||
limit: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(80)
|
||||
.default(40)
|
||||
.openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"List of accounts that have requested to follow the user",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(AccountSchema),
|
||||
},
|
||||
},
|
||||
headers: z.object({
|
||||
link: z
|
||||
.string()
|
||||
.optional()
|
||||
.openapi({
|
||||
description: "Links to the next and previous pages",
|
||||
example:
|
||||
'<https://versia.social/api/v1/follow_requests?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/follow_requests?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { max_id, since_id, min_id, limit } = context.req.valid("query");
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { max_id, since_id, min_id, limit } =
|
||||
context.req.valid("query");
|
||||
|
||||
const { user } = context.get("auth");
|
||||
|
||||
|
|
@ -97,5 +107,6 @@ export default apiRoute((app) =>
|
|||
Link: link,
|
||||
},
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,29 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/frontend/config",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/frontend/config",
|
||||
describeRoute({
|
||||
summary: "Get frontend config",
|
||||
responses: {
|
||||
200: {
|
||||
description: "Frontend config",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.record(z.string(), z.any()).default({}),
|
||||
schema: resolver(
|
||||
z.record(z.string(), z.any()).default({}),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, (context) => {
|
||||
return context.json(config.frontend.settings, 200);
|
||||
}),
|
||||
(context) => {
|
||||
return context.json(config.frontend.settings, 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { ExtendedDescription as ExtendedDescriptionSchema } from "@versia/client/schemas";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { markdownParse } from "~/classes/functions/status";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/instance/extended_description",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/instance/extended_description",
|
||||
describeRoute({
|
||||
summary: "View extended description",
|
||||
description: "Obtain an extended description of this server",
|
||||
externalDocs: {
|
||||
|
|
@ -18,15 +20,13 @@ const route = createRoute({
|
|||
description: "Server extended description",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ExtendedDescriptionSchema,
|
||||
schema: resolver(ExtendedDescriptionSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
}),
|
||||
async (context) => {
|
||||
const content = await markdownParse(
|
||||
config.instance.extended_description_path?.content ??
|
||||
"This is a [Versia](https://versia.pub) server with the default extended description.",
|
||||
|
|
@ -42,5 +42,6 @@ export default apiRoute((app) =>
|
|||
},
|
||||
200,
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,20 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { apiRoute } from "@/api";
|
||||
import { proxyUrl } from "@/response";
|
||||
import { createRoute, type z } from "@hono/zod-openapi";
|
||||
import { InstanceV1 as InstanceV1Schema } from "@versia/client/schemas";
|
||||
import { Instance, Note, User } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, eq, isNull } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import type { z } from "zod";
|
||||
import { markdownParse } from "~/classes/functions/status";
|
||||
import { config } from "~/config.ts";
|
||||
import manifest from "~/package.json";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/instance",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/instance",
|
||||
describeRoute({
|
||||
summary: "View server information (v1)",
|
||||
description:
|
||||
"Obtain general information about the server. See api/v2/instance instead.",
|
||||
|
|
@ -20,25 +23,18 @@ const route = createRoute({
|
|||
url: "https://docs.joinmastodon.org/methods/instance/#v1",
|
||||
},
|
||||
tags: ["Instance"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
}),
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Instance information",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: InstanceV1Schema,
|
||||
schema: resolver(InstanceV1Schema),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
}),
|
||||
async (context) => {
|
||||
// Get software version from package.json
|
||||
const version = manifest.version;
|
||||
|
||||
|
|
@ -143,5 +139,6 @@ export default apiRoute((app) =>
|
|||
},
|
||||
contact_account: (contactAccount as User)?.toApi(),
|
||||
} satisfies z.infer<typeof InstanceV1Schema>);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,37 +1,32 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { apiRoute } from "@/api";
|
||||
import { PrivacyPolicy as PrivacyPolicySchema } from "@versia/client/schemas";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { markdownParse } from "~/classes/functions/status";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/instance/privacy_policy",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/instance/privacy_policy",
|
||||
describeRoute({
|
||||
summary: "View privacy policy",
|
||||
description: "Obtain the contents of this server’s privacy policy.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/instance/#privacy_policy",
|
||||
},
|
||||
tags: ["Instance"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
}),
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Server privacy policy",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: PrivacyPolicySchema,
|
||||
schema: resolver(PrivacyPolicySchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
}),
|
||||
async (context) => {
|
||||
const content = await markdownParse(
|
||||
config.instance.privacy_policy_path?.content ??
|
||||
"This instance has not provided any privacy policy.",
|
||||
|
|
@ -43,5 +38,6 @@ export default apiRoute((app) =>
|
|||
).toISOString(),
|
||||
content,
|
||||
});
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,36 +1,32 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute } from "@/api";
|
||||
import { Rule as RuleSchema } from "@versia/client/schemas";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/instance/rules",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/instance/rules",
|
||||
describeRoute({
|
||||
summary: "List of rules",
|
||||
description: "Rules that the users of this service should follow.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/instance/#rules",
|
||||
},
|
||||
tags: ["Instance"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
}),
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Instance rules",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(RuleSchema),
|
||||
schema: resolver(z.array(RuleSchema)),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, (context) => {
|
||||
}),
|
||||
(context) => {
|
||||
return context.json(
|
||||
config.instance.rules.map((r, index) => ({
|
||||
id: String(index),
|
||||
|
|
@ -38,5 +34,6 @@ export default apiRoute((app) =>
|
|||
hint: r.hint,
|
||||
})),
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { apiRoute } from "@/api";
|
||||
import { TermsOfService as TermsOfServiceSchema } from "@versia/client/schemas";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { markdownParse } from "~/classes/functions/status";
|
||||
import { config } from "~/config.ts";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/instance/terms_of_service",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/instance/terms_of_service",
|
||||
describeRoute({
|
||||
summary: "View terms of service",
|
||||
description:
|
||||
"Obtain the contents of this server’s terms of service, if configured.",
|
||||
|
|
@ -14,25 +16,18 @@ const route = createRoute({
|
|||
url: "https://docs.joinmastodon.org/methods/instance/#terms_of_service",
|
||||
},
|
||||
tags: ["Instance"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: false,
|
||||
}),
|
||||
],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Server terms of service",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: TermsOfServiceSchema,
|
||||
schema: resolver(TermsOfServiceSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
}),
|
||||
async (context) => {
|
||||
const content = await markdownParse(
|
||||
config.instance.tos_path?.content ??
|
||||
"This instance has not provided any terms of service.",
|
||||
|
|
@ -44,5 +39,6 @@ export default apiRoute((app) =>
|
|||
).toISOString(),
|
||||
content,
|
||||
});
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, handleZodError } from "@/api";
|
||||
import {
|
||||
Marker as MarkerSchema,
|
||||
Notification as NotificationSchema,
|
||||
|
|
@ -9,6 +8,9 @@ import { RolePermission } from "@versia/client/schemas";
|
|||
import { db } from "@versia/kit/db";
|
||||
import { Markers } from "@versia/kit/tables";
|
||||
import { type SQL, and, eq } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const MarkerResponseSchema = z.object({
|
||||
|
|
@ -16,23 +18,36 @@ const MarkerResponseSchema = z.object({
|
|||
home: MarkerSchema.optional(),
|
||||
});
|
||||
|
||||
const routeGet = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/markers",
|
||||
export default apiRoute((app) => {
|
||||
app.get(
|
||||
"/api/v1/markers",
|
||||
describeRoute({
|
||||
summary: "Get saved timeline positions",
|
||||
description: "Get current positions in timelines.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/markers/#get",
|
||||
},
|
||||
tags: ["Timelines"],
|
||||
middleware: [
|
||||
responses: {
|
||||
200: {
|
||||
description: "Markers",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(MarkerResponseSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnAccount],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: z.object({
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
"timeline[]": z
|
||||
.array(z.enum(["home", "notifications"]))
|
||||
.max(2)
|
||||
|
|
@ -43,67 +58,9 @@ const routeGet = createRoute({
|
|||
"Specify the timeline(s) for which markers should be fetched. Possible values: home, notifications. If not provided, an empty object will be returned.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Markers",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: MarkerResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
const routePost = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/markers",
|
||||
summary: "Save your position in a timeline",
|
||||
description: "Save current position in timeline.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/markers/#create",
|
||||
},
|
||||
tags: ["Timelines"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnAccount],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: z
|
||||
.object({
|
||||
"home[last_read_id]": StatusSchema.shape.id.openapi({
|
||||
description:
|
||||
"ID of the last status read in the home timeline.",
|
||||
example: "c62aa212-8198-4ce5-a388-2cc8344a84ef",
|
||||
}),
|
||||
"notifications[last_read_id]":
|
||||
NotificationSchema.shape.id.openapi({
|
||||
description: "ID of the last notification read.",
|
||||
}),
|
||||
})
|
||||
.partial(),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Markers",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: MarkerResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) => {
|
||||
app.openapi(routeGet, async (context) => {
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { "timeline[]": timeline } = context.req.valid("query");
|
||||
const { user } = context.get("auth");
|
||||
|
||||
|
|
@ -127,7 +84,10 @@ export default apiRoute((app) => {
|
|||
|
||||
const totalCount = await db.$count(
|
||||
Markers,
|
||||
and(eq(Markers.userId, user.id), eq(Markers.timeline, "home")),
|
||||
and(
|
||||
eq(Markers.userId, user.id),
|
||||
eq(Markers.timeline, "home"),
|
||||
),
|
||||
);
|
||||
|
||||
if (found?.noteId) {
|
||||
|
|
@ -166,9 +126,53 @@ export default apiRoute((app) => {
|
|||
}
|
||||
|
||||
return context.json(markers, 200);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
app.openapi(routePost, async (context) => {
|
||||
app.post(
|
||||
"/api/v1/markers",
|
||||
describeRoute({
|
||||
summary: "Save your position in a timeline",
|
||||
description: "Save current position in timeline.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/markers/#create",
|
||||
},
|
||||
tags: ["Timelines"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Markers",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(MarkerResponseSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnAccount],
|
||||
}),
|
||||
validator(
|
||||
"query",
|
||||
z
|
||||
.object({
|
||||
"home[last_read_id]": StatusSchema.shape.id.openapi({
|
||||
description:
|
||||
"ID of the last status read in the home timeline.",
|
||||
example: "c62aa212-8198-4ce5-a388-2cc8344a84ef",
|
||||
}),
|
||||
"notifications[last_read_id]":
|
||||
NotificationSchema.shape.id.openapi({
|
||||
description: "ID of the last notification read.",
|
||||
}),
|
||||
})
|
||||
.partial(),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const {
|
||||
"home[last_read_id]": homeId,
|
||||
"notifications[last_read_id]": notificationsId,
|
||||
|
|
@ -194,13 +198,18 @@ export default apiRoute((app) => {
|
|||
|
||||
const totalCount = await db.$count(
|
||||
Markers,
|
||||
and(eq(Markers.userId, user.id), eq(Markers.timeline, "home")),
|
||||
and(
|
||||
eq(Markers.userId, user.id),
|
||||
eq(Markers.timeline, "home"),
|
||||
),
|
||||
);
|
||||
|
||||
markers.home = {
|
||||
last_read_id: homeId,
|
||||
version: totalCount,
|
||||
updated_at: new Date(insertedMarker.createdAt).toISOString(),
|
||||
updated_at: new Date(
|
||||
insertedMarker.createdAt,
|
||||
).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -227,10 +236,13 @@ export default apiRoute((app) => {
|
|||
markers.notifications = {
|
||||
last_read_id: notificationsId,
|
||||
version: totalCount,
|
||||
updated_at: new Date(insertedMarker.createdAt).toISOString(),
|
||||
updated_at: new Date(
|
||||
insertedMarker.createdAt,
|
||||
).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
return context.json(markers, 200);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,154 +0,0 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Attachment as AttachmentSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Media } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const routePut = createRoute({
|
||||
method: "put",
|
||||
path: "/api/v1/media/{id}",
|
||||
summary: "Update media attachment",
|
||||
description:
|
||||
"Update a MediaAttachment’s parameters, before it is attached to a status and posted.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/media/#update",
|
||||
},
|
||||
tags: ["Media"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:media"],
|
||||
permissions: [RolePermission.ManageOwnMedia],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AttachmentSchema.shape.id,
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
"multipart/form-data": {
|
||||
schema: z
|
||||
.object({
|
||||
thumbnail: z.instanceof(File).openapi({
|
||||
description:
|
||||
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
|
||||
}),
|
||||
description: AttachmentSchema.shape.description,
|
||||
focus: z.string().openapi({
|
||||
description:
|
||||
"Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0. Used for media cropping on clients.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#focal-points",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.partial(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated attachment",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: AttachmentSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Attachment not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ApiError.zodSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
const routeGet = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/media/{id}",
|
||||
summary: "Get media attachment",
|
||||
description:
|
||||
"Get a media attachment, before it is attached to a status and posted, but after it is accepted for processing. Use this method to check that the full-sized media has finished processing.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/media/#get",
|
||||
},
|
||||
tags: ["Media"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnMedia],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: AttachmentSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Attachment",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: AttachmentSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Attachment not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ApiError.zodSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) => {
|
||||
app.openapi(routePut, async (context) => {
|
||||
const { id } = context.req.valid("param");
|
||||
|
||||
const media = await Media.fromId(id);
|
||||
|
||||
if (!media) {
|
||||
throw ApiError.mediaNotFound();
|
||||
}
|
||||
|
||||
const { description, thumbnail: thumbnailFile } =
|
||||
context.req.valid("form");
|
||||
|
||||
if (thumbnailFile) {
|
||||
await media.updateThumbnail(thumbnailFile);
|
||||
}
|
||||
|
||||
if (description) {
|
||||
await media.updateMetadata({
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
return context.json(media.toApi(), 200);
|
||||
});
|
||||
|
||||
app.openapi(routeGet, async (context) => {
|
||||
const { id } = context.req.valid("param");
|
||||
|
||||
const attachment = await Media.fromId(id);
|
||||
|
||||
if (!attachment) {
|
||||
throw ApiError.mediaNotFound();
|
||||
}
|
||||
|
||||
return context.json(attachment.toApi(), 200);
|
||||
});
|
||||
});
|
||||
154
api/api/v1/media/[id]/index.ts
Normal file
154
api/api/v1/media/[id]/index.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import { apiRoute, auth, handleZodError } from "@/api";
|
||||
import { Attachment as AttachmentSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Media } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) => {
|
||||
app.get(
|
||||
"/api/v1/media/:id",
|
||||
describeRoute({
|
||||
summary: "Get media attachment",
|
||||
description:
|
||||
"Get a media attachment, before it is attached to a status and posted, but after it is accepted for processing. Use this method to check that the full-sized media has finished processing.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/media/#get",
|
||||
},
|
||||
tags: ["Media"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Attachment",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(AttachmentSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Attachment not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(ApiError.zodSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnMedia],
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: AttachmentSchema.shape.id,
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { id } = context.req.valid("param");
|
||||
|
||||
const attachment = await Media.fromId(id);
|
||||
|
||||
if (!attachment) {
|
||||
throw ApiError.mediaNotFound();
|
||||
}
|
||||
|
||||
return context.json(attachment.toApi(), 200);
|
||||
},
|
||||
);
|
||||
|
||||
app.put(
|
||||
"/api/v1/media/:id",
|
||||
describeRoute({
|
||||
summary: "Update media attachment",
|
||||
description:
|
||||
"Update a MediaAttachment’s parameters, before it is attached to a status and posted.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/media/#update",
|
||||
},
|
||||
tags: ["Media"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "Updated attachment",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(AttachmentSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: {
|
||||
description: "Attachment not found",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(ApiError.zodSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:media"],
|
||||
permissions: [RolePermission.ManageOwnMedia],
|
||||
}),
|
||||
validator(
|
||||
"form",
|
||||
z
|
||||
.object({
|
||||
thumbnail: z.instanceof(File).openapi({
|
||||
description:
|
||||
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
|
||||
}),
|
||||
description: AttachmentSchema.shape.description,
|
||||
focus: z.string().openapi({
|
||||
description:
|
||||
"Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0. Used for media cropping on clients.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#focal-points",
|
||||
},
|
||||
}),
|
||||
})
|
||||
.partial(),
|
||||
handleZodError,
|
||||
),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: AttachmentSchema.shape.id,
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { id } = context.req.valid("param");
|
||||
|
||||
const media = await Media.fromId(id);
|
||||
|
||||
if (!media) {
|
||||
throw ApiError.mediaNotFound();
|
||||
}
|
||||
|
||||
const { description, thumbnail: thumbnailFile } =
|
||||
context.req.valid("form");
|
||||
|
||||
if (thumbnailFile) {
|
||||
await media.updateThumbnail(thumbnailFile);
|
||||
}
|
||||
|
||||
if (description) {
|
||||
await media.updateMetadata({
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
return context.json(media.toApi(), 200);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -1,13 +1,16 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, handleZodError } from "@/api";
|
||||
import { Attachment as AttachmentSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Media } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/media",
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/media",
|
||||
describeRoute({
|
||||
summary: "Upload media as an attachment (v1)",
|
||||
description:
|
||||
"Creates an attachment to be used with a new status. This method will return after the full sized media is done processing.",
|
||||
|
|
@ -16,18 +19,44 @@ const route = createRoute({
|
|||
url: "https://docs.joinmastodon.org/methods/media/#v1",
|
||||
},
|
||||
tags: ["Media"],
|
||||
middleware: [
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Attachment created successfully. Note that the MediaAttachment will be created even if the file is not understood correctly due to failed processing.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(AttachmentSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
413: {
|
||||
description: "File too large",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(ApiError.zodSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
415: {
|
||||
description: "Disallowed file type",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(ApiError.zodSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:media"],
|
||||
permissions: [RolePermission.ManageOwnMedia],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"multipart/form-data": {
|
||||
schema: z.object({
|
||||
validator(
|
||||
"form",
|
||||
z.object({
|
||||
file: z.instanceof(File).openapi({
|
||||
description:
|
||||
"The file to be attached, encoded using multipart form data. The file must have a MIME type.",
|
||||
|
|
@ -36,8 +65,7 @@ const route = createRoute({
|
|||
description:
|
||||
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
|
||||
}),
|
||||
description:
|
||||
AttachmentSchema.shape.description.optional(),
|
||||
description: AttachmentSchema.shape.description.optional(),
|
||||
focus: z
|
||||
.string()
|
||||
.optional()
|
||||
|
|
@ -49,43 +77,9 @@ const route = createRoute({
|
|||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Attachment created successfully. Note that the MediaAttachment will be created even if the file is not understood correctly due to failed processing.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: AttachmentSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
413: {
|
||||
description: "File too large",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ApiError.zodSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
415: {
|
||||
description: "Disallowed file type",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: ApiError.zodSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { file, thumbnail, description } = context.req.valid("form");
|
||||
|
||||
const attachment = await Media.fromFile(file, {
|
||||
|
|
@ -94,5 +88,6 @@ export default apiRoute((app) =>
|
|||
});
|
||||
|
||||
return context.json(attachment.toApi(), 200);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,30 +1,59 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, handleZodError } from "@/api";
|
||||
import { Account as AccountSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Timeline } from "@versia/kit/db";
|
||||
import { Users } from "@versia/kit/tables";
|
||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/mutes",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/mutes",
|
||||
describeRoute({
|
||||
summary: "View muted accounts",
|
||||
description: "View your mutes.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/mutes/#get",
|
||||
},
|
||||
tags: ["Mutes"],
|
||||
middleware: [
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of muted users",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(AccountSchema)),
|
||||
},
|
||||
},
|
||||
headers: z.object({
|
||||
link: z
|
||||
.string()
|
||||
.optional()
|
||||
.openapi({
|
||||
description:
|
||||
"Links to the next and previous pages",
|
||||
example:
|
||||
'<https://versia.social/api/v1/mutes?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/mutes?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["read:mutes"],
|
||||
permissions: [RolePermission.ManageOwnMutes],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: z.object({
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
max_id: AccountSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
"All results returned will be lesser than this ID. In effect, sets an upper bound on results.",
|
||||
|
|
@ -40,41 +69,21 @@ const route = createRoute({
|
|||
"Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward.",
|
||||
example: undefined,
|
||||
}),
|
||||
limit: z.coerce.number().int().min(1).max(80).default(40).openapi({
|
||||
limit: z.coerce
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(80)
|
||||
.default(40)
|
||||
.openapi({
|
||||
description: "Maximum number of results to return.",
|
||||
}),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "List of muted users",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(AccountSchema),
|
||||
},
|
||||
},
|
||||
headers: z.object({
|
||||
link: z
|
||||
.string()
|
||||
.optional()
|
||||
.openapi({
|
||||
description: "Links to the next and previous pages",
|
||||
example:
|
||||
'<https://versia.social/api/v1/mutes?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/mutes?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"',
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { max_id, since_id, limit, min_id } = context.req.valid("query");
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { max_id, since_id, limit, min_id } =
|
||||
context.req.valid("query");
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const { objects: mutes, link } = await Timeline.getUserTimeline(
|
||||
|
|
@ -95,5 +104,6 @@ export default apiRoute((app) =>
|
|||
Link: link,
|
||||
},
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Notification as NotificationSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Notification } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/notifications/{id}/dismiss",
|
||||
summary: "Dismiss a single notification",
|
||||
description: "Dismiss a single notification from the server.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/notifications/#dismiss",
|
||||
},
|
||||
tags: ["Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:notifications"],
|
||||
permissions: [RolePermission.ManageOwnNotifications],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: NotificationSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Notification with given ID successfully dismissed",
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
404: ApiError.notificationNotFound().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { id } = context.req.valid("param");
|
||||
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const notification = await Notification.fromId(id);
|
||||
|
||||
if (!notification || notification.data.notifiedId !== user.id) {
|
||||
throw ApiError.notificationNotFound();
|
||||
}
|
||||
|
||||
await notification.update({
|
||||
dismissed: true,
|
||||
});
|
||||
|
||||
return context.text("", 200);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { Notification as NotificationSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Notification } from "@versia/kit/db";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/notifications/{id}",
|
||||
summary: "Get a single notification",
|
||||
description: "View information about a notification with a given ID.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/notifications/#get",
|
||||
},
|
||||
tags: ["Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnNotifications],
|
||||
scopes: ["read:notifications"],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
params: z.object({
|
||||
id: NotificationSchema.shape.id,
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "A single Notification",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: NotificationSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.notificationNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
const { id } = context.req.valid("param");
|
||||
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const notification = await Notification.fromId(id, user.id);
|
||||
|
||||
if (!notification || notification.data.notifiedId !== user.id) {
|
||||
throw ApiError.notificationNotFound();
|
||||
}
|
||||
|
||||
return context.json(await notification.toApi(), 200);
|
||||
}),
|
||||
);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import type { z } from "@hono/zod-openapi";
|
||||
import type { Notification } from "@versia/client/schemas";
|
||||
import type { z } from "zod";
|
||||
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||
|
||||
const { users, deleteUsers } = await getTestUsers(2);
|
||||
59
api/api/v1/notifications/[id]/dismiss.ts
Normal file
59
api/api/v1/notifications/[id]/dismiss.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { apiRoute, auth, handleZodError } from "@/api";
|
||||
import { Notification as NotificationSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Notification } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/notifications/:id/dismiss",
|
||||
describeRoute({
|
||||
summary: "Dismiss a single notification",
|
||||
description: "Dismiss a single notification from the server.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/notifications/#dismiss",
|
||||
},
|
||||
tags: ["Notifications"],
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"Notification with given ID successfully dismissed",
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
404: ApiError.notificationNotFound().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
scopes: ["write:notifications"],
|
||||
permissions: [RolePermission.ManageOwnNotifications],
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: NotificationSchema.shape.id,
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { id } = context.req.valid("param");
|
||||
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const notification = await Notification.fromId(id);
|
||||
|
||||
if (!notification || notification.data.notifiedId !== user.id) {
|
||||
throw ApiError.notificationNotFound();
|
||||
}
|
||||
|
||||
await notification.update({
|
||||
dismissed: true,
|
||||
});
|
||||
|
||||
return context.text("", 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import type { z } from "@hono/zod-openapi";
|
||||
import type { Notification } from "@versia/client/schemas";
|
||||
import type { z } from "zod";
|
||||
import { generateClient, getTestUsers } from "~/tests/utils";
|
||||
|
||||
const { users, deleteUsers } = await getTestUsers(2);
|
||||
60
api/api/v1/notifications/[id]/index.ts
Normal file
60
api/api/v1/notifications/[id]/index.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { apiRoute, auth, handleZodError } from "@/api";
|
||||
import { Notification as NotificationSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { Notification } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/notifications/:id",
|
||||
describeRoute({
|
||||
summary: "Get a single notification",
|
||||
description:
|
||||
"View information about a notification with a given ID.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/notifications/#get",
|
||||
},
|
||||
tags: ["Notifications"],
|
||||
responses: {
|
||||
200: {
|
||||
description: "A single Notification",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(NotificationSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
404: ApiError.notificationNotFound().schema,
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnNotifications],
|
||||
scopes: ["read:notifications"],
|
||||
}),
|
||||
validator(
|
||||
"param",
|
||||
z.object({
|
||||
id: NotificationSchema.shape.id,
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { id } = context.req.valid("param");
|
||||
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const notification = await Notification.fromId(id, user.id);
|
||||
|
||||
if (!notification || notification.data.notifiedId !== user.id) {
|
||||
throw ApiError.notificationNotFound();
|
||||
}
|
||||
|
||||
return context.json(await notification.toApi(), 200);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -1,38 +1,36 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/notifications/clear",
|
||||
export default apiRoute((app) =>
|
||||
app.post(
|
||||
"/api/v1/notifications/clear",
|
||||
describeRoute({
|
||||
summary: "Dismiss all notifications",
|
||||
description: "Clear all notifications from the server.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/notifications/#clear",
|
||||
},
|
||||
tags: ["Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnNotifications],
|
||||
scopes: ["write:notifications"],
|
||||
}),
|
||||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description: "Notifications successfully cleared.",
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnNotifications],
|
||||
scopes: ["write:notifications"],
|
||||
}),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
|
||||
await user.clearAllNotifications();
|
||||
|
||||
return context.text("", 200);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import type { z } from "@hono/zod-openapi";
|
||||
import type { Notification } from "@versia/client/schemas";
|
||||
import type { z } from "zod";
|
||||
import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
|
||||
|
||||
const { users, deleteUsers } = await getTestUsers(2);
|
||||
|
|
|
|||
|
|
@ -1,45 +1,44 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, handleZodError, qsQuery } from "@/api";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const schemas = {
|
||||
query: z.object({
|
||||
"ids[]": z.array(z.string().uuid()),
|
||||
}),
|
||||
};
|
||||
|
||||
const route = createRoute({
|
||||
method: "delete",
|
||||
path: "/api/v1/notifications/destroy_multiple",
|
||||
export default apiRoute((app) =>
|
||||
app.delete(
|
||||
"/api/v1/notifications/destroy_multiple",
|
||||
describeRoute({
|
||||
summary: "Dismiss multiple notifications",
|
||||
tags: ["Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnNotifications],
|
||||
scopes: ["write:notifications"],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: schemas.query,
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Notifications dismissed",
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnNotifications],
|
||||
scopes: ["write:notifications"],
|
||||
}),
|
||||
qsQuery(),
|
||||
validator(
|
||||
"query",
|
||||
z.object({
|
||||
ids: z.array(z.string().uuid()),
|
||||
}),
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const { "ids[]": ids } = context.req.valid("query");
|
||||
const { ids } = context.req.valid("query");
|
||||
|
||||
await user.clearSomeNotifications(ids);
|
||||
|
||||
return context.text("", 200);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { apiRoute, auth, handleZodError } from "@/api";
|
||||
import {
|
||||
Account as AccountSchema,
|
||||
Notification as NotificationSchema,
|
||||
|
|
@ -9,18 +8,34 @@ import { RolePermission } from "@versia/client/schemas";
|
|||
import { Timeline } from "@versia/kit/db";
|
||||
import { Notifications } from "@versia/kit/tables";
|
||||
import { and, eq, gt, gte, inArray, lt, not, sql } from "drizzle-orm";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/notifications",
|
||||
export default apiRoute((app) =>
|
||||
app.get(
|
||||
"/api/v1/notifications",
|
||||
describeRoute({
|
||||
summary: "Get all notifications",
|
||||
description: "Notifications concerning the user.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/notifications/#get",
|
||||
},
|
||||
tags: ["Notifications"],
|
||||
middleware: [
|
||||
responses: {
|
||||
200: {
|
||||
description: "Notifications",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(z.array(NotificationSchema)),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [
|
||||
|
|
@ -28,9 +43,9 @@ const route = createRoute({
|
|||
RolePermission.ViewPrivateTimelines,
|
||||
],
|
||||
}),
|
||||
] as const,
|
||||
request: {
|
||||
query: z
|
||||
validator(
|
||||
"query",
|
||||
z
|
||||
.object({
|
||||
max_id: NotificationSchema.shape.id.optional().openapi({
|
||||
description:
|
||||
|
|
@ -75,30 +90,16 @@ const route = createRoute({
|
|||
// TODO: Implement
|
||||
include_filtered: zBoolean.default(false).openapi({
|
||||
description:
|
||||
"Whether to include notifications filtered by the user’s NotificationPolicy.",
|
||||
"Whether to include notifications filtered by the user's NotificationPolicy.",
|
||||
}),
|
||||
})
|
||||
.refine((val) => {
|
||||
// Can't use both exclude_types and types
|
||||
return !(val.exclude_types && val.types);
|
||||
}, "Can't use both exclude_types and types"),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: "Notifications",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(NotificationSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
handleZodError,
|
||||
),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const {
|
||||
|
|
@ -158,5 +159,6 @@ export default apiRoute((app) =>
|
|||
Link: link,
|
||||
},
|
||||
);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,25 +1,21 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Account } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "delete",
|
||||
path: "/api/v1/profile/avatar",
|
||||
export default apiRoute((app) =>
|
||||
app.delete(
|
||||
"/api/v1/profile/avatar",
|
||||
describeRoute({
|
||||
summary: "Delete profile avatar",
|
||||
description: "Deletes the avatar associated with the user’s profile.",
|
||||
description:
|
||||
"Deletes the avatar associated with the user’s profile.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/profile/#delete-profile-avatar",
|
||||
},
|
||||
tags: ["Profile"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnAccount],
|
||||
scopes: ["write:account"],
|
||||
}),
|
||||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
|
|
@ -27,22 +23,26 @@ const route = createRoute({
|
|||
content: {
|
||||
"application/json": {
|
||||
// TODO: Return a CredentialAccount
|
||||
schema: Account,
|
||||
schema: resolver(Account),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnAccount],
|
||||
scopes: ["write:account"],
|
||||
}),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
|
||||
await user.avatar?.delete();
|
||||
await user.reload();
|
||||
|
||||
return context.json(user.toApi(true), 200);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,46 +1,46 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { Account } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
const route = createRoute({
|
||||
method: "delete",
|
||||
path: "/api/v1/profile/header",
|
||||
export default apiRoute((app) =>
|
||||
app.delete(
|
||||
"/api/v1/profile/header",
|
||||
describeRoute({
|
||||
summary: "Delete profile header",
|
||||
description: "Deletes the header image associated with the user’s profile.",
|
||||
description:
|
||||
"Deletes the header image associated with the user’s profile.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/profile/#delete-profile-header",
|
||||
},
|
||||
tags: ["Profiles"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnAccount],
|
||||
scopes: ["write:account"],
|
||||
}),
|
||||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"The header was successfully deleted from the user’s profile. If there were no header associated with the profile, the response will still indicate a successful deletion.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: Account,
|
||||
schema: resolver(Account),
|
||||
},
|
||||
},
|
||||
},
|
||||
401: ApiError.missingAuthentication().schema,
|
||||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
});
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(route, async (context) => {
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.ManageOwnAccount],
|
||||
scopes: ["write:account"],
|
||||
}),
|
||||
async (context) => {
|
||||
const { user } = context.get("auth");
|
||||
|
||||
await user.header?.delete();
|
||||
await user.reload();
|
||||
return context.json(user.toApi(true), 200);
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,34 +1,28 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute, z } from "@hono/zod-openapi";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { PushSubscription } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(
|
||||
createRoute({
|
||||
method: "delete",
|
||||
path: "/api/v1/push/subscription",
|
||||
app.delete(
|
||||
"/api/v1/push/subscription",
|
||||
describeRoute({
|
||||
summary: "Remove current subscription",
|
||||
description: "Removes the current Web Push API subscription.",
|
||||
externalDocs: {
|
||||
url: "https://docs.joinmastodon.org/methods/push/#delete",
|
||||
},
|
||||
tags: ["Push Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.UsePushNotifications],
|
||||
scopes: ["push"],
|
||||
}),
|
||||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"PushSubscription successfully deleted or did not exist previously.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.object({}),
|
||||
schema: resolver(z.object({})),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -36,6 +30,11 @@ export default apiRoute((app) =>
|
|||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.UsePushNotifications],
|
||||
scopes: ["push"],
|
||||
}),
|
||||
async (context) => {
|
||||
const { token } = context.get("auth");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import { apiRoute, auth } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import { WebPushSubscription as WebPushSubscriptionSchema } from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { PushSubscription } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(
|
||||
createRoute({
|
||||
method: "get",
|
||||
path: "/api/v1/push/subscription",
|
||||
app.get(
|
||||
"/api/v1/push/subscription",
|
||||
describeRoute({
|
||||
summary: "Get current subscription",
|
||||
description:
|
||||
"View the PushSubscription currently associated with this access token.",
|
||||
|
|
@ -17,19 +17,12 @@ export default apiRoute((app) =>
|
|||
url: "https://docs.joinmastodon.org/methods/push/#get",
|
||||
},
|
||||
tags: ["Push Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.UsePushNotifications],
|
||||
scopes: ["push"],
|
||||
}),
|
||||
] as const,
|
||||
responses: {
|
||||
200: {
|
||||
description: "WebPushSubscription",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: WebPushSubscriptionSchema,
|
||||
schema: resolver(WebPushSubscriptionSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -37,6 +30,11 @@ export default apiRoute((app) =>
|
|||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.UsePushNotifications],
|
||||
scopes: ["push"],
|
||||
}),
|
||||
async (context) => {
|
||||
const { token } = context.get("auth");
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import { apiRoute } from "@/api";
|
||||
import { apiRoute, handleZodError } from "@/api";
|
||||
import { auth, jsonOrForm } from "@/api";
|
||||
import { createRoute } from "@hono/zod-openapi";
|
||||
import {
|
||||
WebPushSubscriptionInput,
|
||||
WebPushSubscription as WebPushSubscriptionSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { RolePermission } from "@versia/client/schemas";
|
||||
import { PushSubscription } from "@versia/kit/db";
|
||||
import { describeRoute } from "hono-openapi";
|
||||
import { resolver, validator } from "hono-openapi/zod";
|
||||
import { ApiError } from "~/classes/errors/api-error";
|
||||
|
||||
export default apiRoute((app) =>
|
||||
app.openapi(
|
||||
createRoute({
|
||||
method: "post",
|
||||
path: "/api/v1/push/subscription",
|
||||
app.post(
|
||||
"/api/v1/push/subscription",
|
||||
describeRoute({
|
||||
summary: "Subscribe to push notifications",
|
||||
description:
|
||||
"Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted.",
|
||||
|
|
@ -21,30 +21,13 @@ export default apiRoute((app) =>
|
|||
url: "https://docs.joinmastodon.org/methods/push/#create",
|
||||
},
|
||||
tags: ["Push Notifications"],
|
||||
middleware: [
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.UsePushNotifications],
|
||||
scopes: ["push"],
|
||||
}),
|
||||
jsonOrForm(),
|
||||
] as const,
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: WebPushSubscriptionInput,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description:
|
||||
"A new PushSubscription has been generated, which will send the requested alerts to your endpoint.",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: WebPushSubscriptionSchema,
|
||||
schema: resolver(WebPushSubscriptionSchema),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -52,6 +35,13 @@ export default apiRoute((app) =>
|
|||
422: ApiError.validationFailed().schema,
|
||||
},
|
||||
}),
|
||||
auth({
|
||||
auth: true,
|
||||
permissions: [RolePermission.UsePushNotifications],
|
||||
scopes: ["push"],
|
||||
}),
|
||||
jsonOrForm(),
|
||||
validator("json", WebPushSubscriptionInput, handleZodError),
|
||||
async (context) => {
|
||||
const { user, token } = context.get("auth");
|
||||
const { subscription, data, policy } = context.req.valid("json");
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue