mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor(api): 🏷️ Port all /api/v1/accounts to use new schemas
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s
Some checks failed
Mirror to Codeberg / Mirror (push) Failing after 0s
This commit is contained in:
parent
a0ce18337a
commit
e3e285571e
|
|
@ -180,7 +180,7 @@ export default apiRoute((app) =>
|
||||||
// Generate JWT
|
// Generate JWT
|
||||||
const jwt = await new SignJWT({
|
const jwt = await new SignJWT({
|
||||||
sub: user.id,
|
sub: user.id,
|
||||||
iss: new URL(config.http.base_url).origin,
|
iss: config.http.base_url.origin,
|
||||||
aud: client_id,
|
aud: client_id,
|
||||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
||||||
iat: Math.floor(Date.now() / 1000),
|
iat: Math.floor(Date.now() / 1000),
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,26 @@
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import {
|
||||||
|
accountNotFound,
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
reusedResponses,
|
||||||
|
withUserParam,
|
||||||
|
} from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/accounts/{id}/block",
|
path: "/api/v1/accounts/{id}/block",
|
||||||
summary: "Block user",
|
summary: "Block account",
|
||||||
description: "Block a user",
|
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: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -22,17 +34,20 @@ const route = createRoute({
|
||||||
] as const,
|
] as const,
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Updated relationship",
|
description:
|
||||||
|
"Successfully blocked, or account was already blocked.",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: RelationshipSchema,
|
schema: RelationshipSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
404: accountNotFound,
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid(),
|
id: AccountSchema.shape.id,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,28 @@
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import {
|
||||||
|
accountNotFound,
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
reusedResponses,
|
||||||
|
withUserParam,
|
||||||
|
} from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { iso631 } from "~/classes/schemas/common";
|
import { iso631 } from "~/classes/schemas/common";
|
||||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||||
|
import { ErrorSchema } from "~/types/api";
|
||||||
const schemas = {
|
|
||||||
param: z.object({
|
|
||||||
id: z.string().uuid(),
|
|
||||||
}),
|
|
||||||
json: z
|
|
||||||
.object({
|
|
||||||
reblogs: z.coerce.boolean().optional(),
|
|
||||||
notify: z.coerce.boolean().optional(),
|
|
||||||
languages: z.array(iso631).optional(),
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.default({ reblogs: true, notify: false, languages: [] }),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/accounts/{id}/follow",
|
path: "/api/v1/accounts/{id}/follow",
|
||||||
summary: "Follow user",
|
summary: "Follow account",
|
||||||
description: "Follow a user",
|
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: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -37,20 +36,53 @@ const route = createRoute({
|
||||||
] as const,
|
] as const,
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Updated relationship",
|
description:
|
||||||
|
"Successfully followed, or account was already followed",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: RelationshipSchema,
|
schema: RelationshipSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
403: {
|
||||||
|
description:
|
||||||
|
"Trying to follow someone that you block or that blocks you",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: ErrorSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
404: accountNotFound,
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
request: {
|
request: {
|
||||||
params: schemas.param,
|
params: z.object({
|
||||||
|
id: AccountSchema.shape.id,
|
||||||
|
}),
|
||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: schemas.json,
|
schema: z.object({
|
||||||
|
reblogs: z.boolean().default(true).openapi({
|
||||||
|
description:
|
||||||
|
"Receive this account’s reblogs in home timeline?",
|
||||||
|
example: true,
|
||||||
|
}),
|
||||||
|
notify: z.boolean().default(false).openapi({
|
||||||
|
description:
|
||||||
|
"Receive notifications when this account posts a status?",
|
||||||
|
example: false,
|
||||||
|
}),
|
||||||
|
languages: z
|
||||||
|
.array(iso631)
|
||||||
|
.default([])
|
||||||
|
.openapi({
|
||||||
|
description:
|
||||||
|
"Array of String (ISO 639-1 language two-letter code). Filter received statuses for these languages. If not provided, you will receive this account’s posts in all languages.",
|
||||||
|
example: ["en", "fr"],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,27 @@
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import {
|
||||||
|
accountNotFound,
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
reusedResponses,
|
||||||
|
withUserParam,
|
||||||
|
} from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Timeline } from "@versia/kit/db";
|
import { Timeline } from "@versia/kit/db";
|
||||||
import { RolePermissions, Users } from "@versia/kit/tables";
|
import { RolePermissions, Users } from "@versia/kit/tables";
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { Account } from "~/classes/schemas/account";
|
import { Account } from "~/classes/schemas/account";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
const schemas = {
|
|
||||||
query: z.object({
|
|
||||||
max_id: z.string().uuid().optional(),
|
|
||||||
since_id: z.string().uuid().optional(),
|
|
||||||
min_id: z.string().uuid().optional(),
|
|
||||||
limit: z.coerce.number().int().min(1).max(40).optional().default(20),
|
|
||||||
}),
|
|
||||||
param: z.object({
|
|
||||||
id: z.string().uuid(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/accounts/{id}/followers",
|
path: "/api/v1/accounts/{id}/followers",
|
||||||
summary: "Get account followers",
|
summary: "Get account’s followers",
|
||||||
description:
|
description:
|
||||||
"Gets an paginated list of accounts that follow the specified account",
|
"Accounts which follow the given account, if network is not hidden by the account owner.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#followers",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: false,
|
auth: false,
|
||||||
|
|
@ -35,23 +34,53 @@ const route = createRoute({
|
||||||
withUserParam,
|
withUserParam,
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: schemas.param,
|
params: z.object({
|
||||||
query: schemas.query,
|
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: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "A list of accounts that follow the specified account",
|
description: "Accounts which follow the given account.",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.array(Account),
|
schema: z.array(Account),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
headers: {
|
headers: z.object({
|
||||||
Link: {
|
link: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.openapi({
|
||||||
description: "Links to the next and previous pages",
|
description: "Links to the next and previous pages",
|
||||||
|
example: `<https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/followers?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/followers?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"`,
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
404: accountNotFound,
|
||||||
|
422: reusedResponses[422],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,27 @@
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import {
|
||||||
|
accountNotFound,
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
reusedResponses,
|
||||||
|
withUserParam,
|
||||||
|
} from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Timeline } from "@versia/kit/db";
|
import { Timeline } from "@versia/kit/db";
|
||||||
import { RolePermissions, Users } from "@versia/kit/tables";
|
import { RolePermissions, Users } from "@versia/kit/tables";
|
||||||
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
import { and, gt, gte, lt, sql } from "drizzle-orm";
|
||||||
import { Account } from "~/classes/schemas/account";
|
import { Account } from "~/classes/schemas/account";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
const schemas = {
|
|
||||||
query: z.object({
|
|
||||||
max_id: z.string().uuid().optional(),
|
|
||||||
since_id: z.string().uuid().optional(),
|
|
||||||
min_id: z.string().uuid().optional(),
|
|
||||||
limit: z.coerce.number().int().min(1).max(40).optional().default(20),
|
|
||||||
}),
|
|
||||||
param: z.object({
|
|
||||||
id: z.string().uuid(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/accounts/{id}/following",
|
path: "/api/v1/accounts/{id}/following",
|
||||||
summary: "Get account following",
|
summary: "Get account’s following",
|
||||||
description:
|
description:
|
||||||
"Gets an paginated list of accounts that the specified account follows",
|
"Accounts which the given account is following, if network is not hidden by the account owner.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#following",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: false,
|
auth: false,
|
||||||
|
|
@ -35,24 +34,53 @@ const route = createRoute({
|
||||||
withUserParam,
|
withUserParam,
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: schemas.param,
|
params: z.object({
|
||||||
query: schemas.query,
|
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: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description:
|
description: "Accounts which the given account is following.",
|
||||||
"A list of accounts that the specified account follows",
|
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.array(Account),
|
schema: z.array(Account),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
headers: {
|
headers: z.object({
|
||||||
Link: {
|
link: z
|
||||||
description: "Link to the next page of results",
|
.string()
|
||||||
},
|
.optional()
|
||||||
|
.openapi({
|
||||||
|
description: "Links to the next and previous pages",
|
||||||
|
example: `<https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/following?limit=2&max_id=359ae97f-78dd-43e7-8e13-1d8e1d7829b5>; rel="next", <https://versia.social/api/v1/accounts/46be88d3-25b4-4edc-8be9-c28c4ac5ea95/following?limit=2&since_id=75e9f5a9-f455-48eb-8f60-435b4a088bc0>; rel="prev"`,
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/api/guidelines/#pagination",
|
||||||
},
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
|
404: accountNotFound,
|
||||||
|
422: reusedResponses[422],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,24 @@
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import {
|
||||||
|
accountNotFound,
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
reusedResponses,
|
||||||
|
withUserParam,
|
||||||
|
} from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
import { Account } from "~/classes/schemas/account";
|
import { Account } from "~/classes/schemas/account";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/accounts/{id}",
|
path: "/api/v1/accounts/{id}",
|
||||||
summary: "Get account data",
|
summary: "Get account",
|
||||||
description: "Gets the specified account data",
|
description: "View information about a profile.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#get",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: false,
|
auth: false,
|
||||||
|
|
@ -17,18 +28,21 @@ const route = createRoute({
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid(),
|
id: AccountSchema.shape.id,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Account data",
|
description:
|
||||||
|
"The Account record will be returned. Note that acct of local users does not include the domain name.",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: Account,
|
schema: Account,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
404: accountNotFound,
|
||||||
|
422: reusedResponses[422],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,26 @@
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import {
|
||||||
|
accountNotFound,
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
reusedResponses,
|
||||||
|
withUserParam,
|
||||||
|
} from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||||
|
|
||||||
const schemas = {
|
|
||||||
param: z.object({
|
|
||||||
id: z.string().uuid(),
|
|
||||||
}),
|
|
||||||
json: z.object({
|
|
||||||
notifications: z.boolean().optional(),
|
|
||||||
duration: z
|
|
||||||
.number()
|
|
||||||
.int()
|
|
||||||
.min(60)
|
|
||||||
.max(60 * 60 * 24 * 365 * 5)
|
|
||||||
.optional(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/accounts/{id}/mute",
|
path: "/api/v1/accounts/{id}/mute",
|
||||||
summary: "Mute user",
|
summary: "Mute account",
|
||||||
description: "Mute a user",
|
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: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -36,24 +33,44 @@ const route = createRoute({
|
||||||
withUserParam,
|
withUserParam,
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: schemas.param,
|
params: z.object({
|
||||||
|
id: AccountSchema.shape.id,
|
||||||
|
}),
|
||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: schemas.json,
|
schema: z.object({
|
||||||
|
notifications: z.boolean().default(true).openapi({
|
||||||
|
description:
|
||||||
|
"Mute notifications in addition to statuses?",
|
||||||
|
}),
|
||||||
|
duration: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(0)
|
||||||
|
.max(60 * 60 * 24 * 365 * 5)
|
||||||
|
.default(0)
|
||||||
|
.openapi({
|
||||||
|
description:
|
||||||
|
"How long the mute should last, in seconds.",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Updated relationship",
|
description:
|
||||||
|
"Successfully muted, or account was already muted. Note that you can call this API method again with notifications=false to update the relationship so that only statuses are muted.",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: RelationshipSchema,
|
schema: RelationshipSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
404: accountNotFound,
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -72,7 +89,7 @@ export default apiRoute((app) =>
|
||||||
// TODO: Implement duration
|
// TODO: Implement duration
|
||||||
await foundRelationship.update({
|
await foundRelationship.update({
|
||||||
muting: true,
|
muting: true,
|
||||||
mutingNotifications: notifications ?? true,
|
mutingNotifications: notifications,
|
||||||
});
|
});
|
||||||
|
|
||||||
return context.json(foundRelationship.toApi(), 200);
|
return context.json(foundRelationship.toApi(), 200);
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,25 @@
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import {
|
||||||
|
accountNotFound,
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
reusedResponses,
|
||||||
|
withUserParam,
|
||||||
|
} from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||||
|
|
||||||
const schemas = {
|
|
||||||
param: z.object({
|
|
||||||
id: z.string().uuid(),
|
|
||||||
}),
|
|
||||||
json: z.object({
|
|
||||||
comment: z.string().min(0).max(5000).trim().optional(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/accounts/{id}/note",
|
path: "/api/v1/accounts/{id}/note",
|
||||||
summary: "Set note",
|
summary: "Set private note on profile",
|
||||||
description: "Set a note on a user's profile, visible only to you",
|
description: "Sets a private note on a user.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#note",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -30,24 +32,35 @@ const route = createRoute({
|
||||||
withUserParam,
|
withUserParam,
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: schemas.param,
|
params: z.object({
|
||||||
|
id: AccountSchema.shape.id,
|
||||||
|
}),
|
||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: schemas.json,
|
schema: z.object({
|
||||||
|
comment: RelationshipSchema.shape.note
|
||||||
|
.optional()
|
||||||
|
.openapi({
|
||||||
|
description:
|
||||||
|
"The comment to be set on that user. Provide an empty string or leave out this parameter to clear the currently set note.",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Updated relationship",
|
description: "Successfully updated profile note",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: RelationshipSchema,
|
schema: RelationshipSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
404: accountNotFound,
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -63,7 +76,7 @@ export default apiRoute((app) =>
|
||||||
);
|
);
|
||||||
|
|
||||||
await foundRelationship.update({
|
await foundRelationship.update({
|
||||||
note: comment,
|
note: comment ?? "",
|
||||||
});
|
});
|
||||||
|
|
||||||
return context.json(foundRelationship.toApi(), 200);
|
return context.json(foundRelationship.toApi(), 200);
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,19 @@ import { apiRoute, auth, withUserParam } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/accounts/{id}/pin",
|
path: "/api/v1/accounts/{id}/pin",
|
||||||
summary: "Pin user",
|
summary: "Feature account on your profile",
|
||||||
description: "Pin a user to 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: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -22,7 +28,7 @@ const route = createRoute({
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid(),
|
id: AccountSchema.shape.id,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,23 @@
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import {
|
||||||
|
accountNotFound,
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
reusedResponses,
|
||||||
|
withUserParam,
|
||||||
|
} from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { Account } from "~/classes/schemas/account";
|
import { Account } from "~/classes/schemas/account";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { ErrorSchema } from "~/types/api";
|
import { ErrorSchema } from "~/types/api";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/accounts/{id}/refetch",
|
path: "/api/v1/accounts/{id}/refetch",
|
||||||
summary: "Refetch user",
|
summary: "Refetch account",
|
||||||
description: "Refetch a user's profile from the remote server",
|
description: "Refetch the given account's profile from the remote server",
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -20,12 +28,12 @@ const route = createRoute({
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid(),
|
id: AccountSchema.shape.id,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Updated user data",
|
description: "Refetched account data",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: Account,
|
schema: Account,
|
||||||
|
|
@ -40,6 +48,8 @@ const route = createRoute({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
404: accountNotFound,
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,25 @@
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import {
|
||||||
|
accountNotFound,
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
reusedResponses,
|
||||||
|
withUserParam,
|
||||||
|
} from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/accounts/{id}/remove_from_followers",
|
path: "/api/v1/accounts/{id}/remove_from_followers",
|
||||||
summary: "Remove user from followers",
|
summary: "Remove account from followers",
|
||||||
description: "Remove a user from your followers",
|
description: "Remove the given account from your followers.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#remove_from_followers",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -22,18 +33,21 @@ const route = createRoute({
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid(),
|
id: AccountSchema.shape.id,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Updated relationship",
|
description:
|
||||||
|
"Successfully removed from followers, or account was already not following you",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: RelationshipSchema,
|
schema: RelationshipSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
404: accountNotFound,
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,15 @@ import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Role } from "@versia/kit/db";
|
import { Role } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
|
import { Role as RoleSchema } from "~/classes/schemas/versia";
|
||||||
import { ErrorSchema } from "~/types/api";
|
import { ErrorSchema } from "~/types/api";
|
||||||
|
|
||||||
const schemas = {
|
|
||||||
param: z.object({
|
|
||||||
id: z.string().uuid(),
|
|
||||||
role_id: z.string().uuid(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const routePost = createRoute({
|
const routePost = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/accounts/{id}/roles/{role_id}",
|
path: "/api/v1/accounts/{id}/roles/{role_id}",
|
||||||
summary: "Assign role to user",
|
summary: "Assign role to account",
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -24,7 +20,10 @@ const routePost = createRoute({
|
||||||
withUserParam,
|
withUserParam,
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: schemas.param,
|
params: z.object({
|
||||||
|
id: AccountSchema.shape.id,
|
||||||
|
role_id: RoleSchema.shape.id,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
204: {
|
204: {
|
||||||
|
|
@ -61,7 +60,10 @@ const routeDelete = createRoute({
|
||||||
withUserParam,
|
withUserParam,
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: schemas.param,
|
params: z.object({
|
||||||
|
id: AccountSchema.shape.id,
|
||||||
|
role_id: RoleSchema.shape.id,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
204: {
|
204: {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import { apiRoute, auth, withUserParam } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Role } from "@versia/kit/db";
|
import { Role } from "@versia/kit/db";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { Role as RoleSchema } from "~/classes/schemas/versia.ts";
|
import { Role as RoleSchema } from "~/classes/schemas/versia.ts";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/accounts/{id}/roles",
|
path: "/api/v1/accounts/{id}/roles",
|
||||||
summary: "List user roles",
|
summary: "List account roles",
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: false,
|
auth: false,
|
||||||
|
|
@ -15,7 +17,7 @@ const route = createRoute({
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid(),
|
id: AccountSchema.shape.id,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,27 @@
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import {
|
||||||
|
accountNotFound,
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
reusedResponses,
|
||||||
|
withUserParam,
|
||||||
|
} from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Timeline } from "@versia/kit/db";
|
import { Timeline } from "@versia/kit/db";
|
||||||
import { Notes, RolePermissions } from "@versia/kit/tables";
|
import { Notes, RolePermissions } from "@versia/kit/tables";
|
||||||
import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
|
import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
|
||||||
import { Status } from "~/classes/schemas/status";
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
|
import { Status as StatusSchema } from "~/classes/schemas/status";
|
||||||
const schemas = {
|
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||||
param: z.object({
|
|
||||||
id: z.string().uuid(),
|
|
||||||
}),
|
|
||||||
query: z.object({
|
|
||||||
max_id: z.string().uuid().optional(),
|
|
||||||
since_id: z.string().uuid().optional(),
|
|
||||||
min_id: z.string().uuid().optional(),
|
|
||||||
limit: z.coerce.number().int().min(1).max(40).optional().default(20),
|
|
||||||
only_media: z
|
|
||||||
.string()
|
|
||||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
|
||||||
.optional(),
|
|
||||||
exclude_replies: z
|
|
||||||
.string()
|
|
||||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
|
||||||
.optional(),
|
|
||||||
exclude_reblogs: z
|
|
||||||
.string()
|
|
||||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
|
||||||
.optional(),
|
|
||||||
pinned: z
|
|
||||||
.string()
|
|
||||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
|
||||||
.optional(),
|
|
||||||
tagged: z
|
|
||||||
.string()
|
|
||||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
|
||||||
.optional(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/accounts/{id}/statuses",
|
path: "/api/v1/accounts/{id}/statuses",
|
||||||
summary: "Get account statuses",
|
summary: "Get account’s statuses",
|
||||||
description: "Gets an paginated list of statuses by the specified account",
|
description: "Statuses posted to the given account.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#statuses",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: false,
|
auth: false,
|
||||||
|
|
@ -54,23 +34,58 @@ const route = createRoute({
|
||||||
withUserParam,
|
withUserParam,
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: schemas.param,
|
params: z.object({
|
||||||
query: schemas.query,
|
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: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "A list of statuses by the specified account",
|
description: "Statuses posted to the given account.",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.array(Status),
|
schema: z.array(StatusSchema),
|
||||||
},
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
Link: {
|
|
||||||
description: "Links to the next and previous pages",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
404: accountNotFound,
|
||||||
|
422: reusedResponses[422],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -90,7 +105,7 @@ export default apiRoute((app) =>
|
||||||
pinned,
|
pinned,
|
||||||
} = context.req.valid("query");
|
} = context.req.valid("query");
|
||||||
|
|
||||||
const { objects, link } = await Timeline.getNoteTimeline(
|
const { objects } = await Timeline.getNoteTimeline(
|
||||||
and(
|
and(
|
||||||
max_id ? lt(Notes.id, max_id) : undefined,
|
max_id ? lt(Notes.id, max_id) : undefined,
|
||||||
since_id ? gte(Notes.id, since_id) : undefined,
|
since_id ? gte(Notes.id, since_id) : undefined,
|
||||||
|
|
@ -122,9 +137,6 @@ export default apiRoute((app) =>
|
||||||
return context.json(
|
return context.json(
|
||||||
await Promise.all(objects.map((note) => note.toApi(otherUser))),
|
await Promise.all(objects.map((note) => note.toApi(otherUser))),
|
||||||
200,
|
200,
|
||||||
{
|
|
||||||
link,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,25 @@
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import {
|
||||||
|
accountNotFound,
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
reusedResponses,
|
||||||
|
withUserParam,
|
||||||
|
} from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/accounts/{id}/unblock",
|
path: "/api/v1/accounts/{id}/unblock",
|
||||||
summary: "Unblock user",
|
summary: "Unblock account",
|
||||||
description: "Unblock a user",
|
description: "Unblock the given account.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#unblock",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -22,18 +33,21 @@ const route = createRoute({
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid(),
|
id: AccountSchema.shape.id,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Updated relationship",
|
description:
|
||||||
|
"Successfully unblocked, or account was already not blocked",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: RelationshipSchema,
|
schema: RelationshipSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
404: accountNotFound,
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,25 @@
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import {
|
||||||
|
accountNotFound,
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
reusedResponses,
|
||||||
|
withUserParam,
|
||||||
|
} from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||||
import { ErrorSchema } from "~/types/api";
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/accounts/{id}/unfollow",
|
path: "/api/v1/accounts/{id}/unfollow",
|
||||||
summary: "Unfollow user",
|
summary: "Unfollow account",
|
||||||
description: "Unfollow a user",
|
description: "Unfollow the given account.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#unfollow",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -23,26 +33,21 @@ const route = createRoute({
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid(),
|
id: AccountSchema.shape.id,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Updated relationship",
|
description:
|
||||||
|
"Successfully unfollowed, or account was already not followed",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: RelationshipSchema,
|
schema: RelationshipSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
500: {
|
404: accountNotFound,
|
||||||
description: "Failed to unfollow user during federation",
|
...reusedResponses,
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: ErrorSchema,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,25 @@
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import {
|
||||||
|
accountNotFound,
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
reusedResponses,
|
||||||
|
withUserParam,
|
||||||
|
} from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/accounts/{id}/unmute",
|
path: "/api/v1/accounts/{id}/unmute",
|
||||||
summary: "Unmute user",
|
summary: "Unmute account",
|
||||||
description: "Unmute a user",
|
description: "Unmute the given account.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#unmute",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -22,18 +33,20 @@ const route = createRoute({
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid(),
|
id: AccountSchema.shape.id,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Updated relationship",
|
description: "Successfully unmuted, or account was already unmuted",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: RelationshipSchema,
|
schema: RelationshipSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
404: accountNotFound,
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,25 @@
|
||||||
import { apiRoute, auth, withUserParam } from "@/api";
|
import {
|
||||||
|
accountNotFound,
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
reusedResponses,
|
||||||
|
withUserParam,
|
||||||
|
} from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/accounts/{id}/unpin",
|
path: "/api/v1/accounts/{id}/unpin",
|
||||||
summary: "Unpin user",
|
summary: "Unfeature account from profile",
|
||||||
description: "Unpin a user from your profile",
|
description: "Remove the given account from the user’s featured profiles.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#unpin",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -22,18 +33,21 @@ const route = createRoute({
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
id: z.string().uuid(),
|
id: AccountSchema.shape.id,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Updated relationship",
|
description:
|
||||||
|
"Successfully unendorsed, or account was already not endorsed",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: RelationshipSchema,
|
schema: RelationshipSchema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
404: accountNotFound,
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,10 @@
|
||||||
import { apiRoute, auth, qsQuery } from "@/api";
|
import { apiRoute, auth, qsQuery, reusedResponses } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { User, db } from "@versia/kit/db";
|
import { User, db } from "@versia/kit/db";
|
||||||
import { RolePermissions, type Users } from "@versia/kit/tables";
|
import { RolePermissions, type Users } from "@versia/kit/tables";
|
||||||
import { type InferSelectModel, sql } from "drizzle-orm";
|
import { type InferSelectModel, sql } from "drizzle-orm";
|
||||||
import { Account } from "~/classes/schemas/account";
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
|
import { FamiliarFollowers as FamiliarFollowersSchema } from "~/classes/schemas/familiar-followers";
|
||||||
const schemas = {
|
|
||||||
query: z.object({
|
|
||||||
id: z
|
|
||||||
.array(z.string().uuid())
|
|
||||||
.min(1)
|
|
||||||
.max(10)
|
|
||||||
.or(z.string().uuid())
|
|
||||||
.transform((v) => (Array.isArray(v) ? v : [v])),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
|
@ -22,6 +12,10 @@ const route = createRoute({
|
||||||
summary: "Get familiar followers",
|
summary: "Get familiar followers",
|
||||||
description:
|
description:
|
||||||
"Obtain a list of all accounts that follow a given account, filtered for accounts you follow.",
|
"Obtain a list of all accounts that follow a given account, filtered for accounts you follow.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#familiar_followers",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -31,22 +25,32 @@ const route = createRoute({
|
||||||
qsQuery(),
|
qsQuery(),
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
query: schemas.query,
|
query: z.object({
|
||||||
|
id: z
|
||||||
|
.array(AccountSchema.shape.id)
|
||||||
|
.min(1)
|
||||||
|
.max(10)
|
||||||
|
.or(AccountSchema.shape.id.transform((v) => [v]))
|
||||||
|
.openapi({
|
||||||
|
description:
|
||||||
|
"Find familiar followers for the provided account IDs.",
|
||||||
|
example: [
|
||||||
|
"f137ce6f-ff5e-4998-b20f-0361ba9be007",
|
||||||
|
"8424c654-5d03-4a1b-bec8-4e87db811b5d",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Familiar followers",
|
description: "Familiar followers",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.array(
|
schema: z.array(FamiliarFollowersSchema),
|
||||||
z.object({
|
|
||||||
id: z.string().uuid(),
|
|
||||||
accounts: z.array(Account),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ describe("/api/v1/accounts/id", () => {
|
||||||
|
|
||||||
test("should return 404 for non-existent user", async () => {
|
test("should return 404 for non-existent user", async () => {
|
||||||
const response = await fakeRequest(
|
const response = await fakeRequest(
|
||||||
`/api/v1/accounts/id?username=${users[0].data.username}-nonexistent`,
|
"/api/v1/accounts/id?username=nonexistent",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(404);
|
expect(response.status).toBe(404);
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,18 @@
|
||||||
import { apiRoute, auth } from "@/api";
|
import { accountNotFound, apiRoute, auth, reusedResponses } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { User } from "@versia/kit/db";
|
import { User } from "@versia/kit/db";
|
||||||
import { RolePermissions, Users } from "@versia/kit/tables";
|
import { RolePermissions, Users } from "@versia/kit/tables";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { Account } from "~/classes/schemas/account";
|
import { Account } from "~/classes/schemas/account";
|
||||||
import { ErrorSchema } from "~/types/api";
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
|
|
||||||
const schemas = {
|
|
||||||
query: z.object({
|
|
||||||
username: z.string().min(1).max(512).toLowerCase(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/accounts/id",
|
path: "/api/v1/accounts/id",
|
||||||
summary: "Get account by username",
|
summary: "Get account by username",
|
||||||
description: "Get an account by username",
|
description: "Get an account by username",
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: false,
|
auth: false,
|
||||||
|
|
@ -25,7 +20,11 @@ const route = createRoute({
|
||||||
}),
|
}),
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
query: schemas.query,
|
query: z.object({
|
||||||
|
username: AccountSchema.shape.username.transform((v) =>
|
||||||
|
v.toLowerCase(),
|
||||||
|
),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
|
|
@ -36,14 +35,8 @@ const route = createRoute({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
404: {
|
404: accountNotFound,
|
||||||
description: "Not found",
|
422: reusedResponses[422],
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: ErrorSchema,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,26 +7,48 @@ import { and, eq, isNull } from "drizzle-orm";
|
||||||
import ISO6391 from "iso-639-1";
|
import ISO6391 from "iso-639-1";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/packages/config-manager";
|
||||||
|
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||||
|
|
||||||
const schemas = {
|
const schema = z.object({
|
||||||
json: z.object({
|
username: z.string().openapi({
|
||||||
username: z.string(),
|
description: "The desired username for the account",
|
||||||
email: z.string().toLowerCase(),
|
example: "alice",
|
||||||
password: z.string().optional(),
|
|
||||||
agreement: z
|
|
||||||
.string()
|
|
||||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
|
||||||
.or(z.boolean()),
|
|
||||||
locale: z.string(),
|
|
||||||
reason: z.string(),
|
|
||||||
}),
|
}),
|
||||||
};
|
email: z.string().toLowerCase().openapi({
|
||||||
|
description:
|
||||||
|
"The email address to be used for login. Transformed to lowercase.",
|
||||||
|
example: "alice@gmail.com",
|
||||||
|
}),
|
||||||
|
password: z.string().openapi({
|
||||||
|
description: "The password to be used for login",
|
||||||
|
example: "hunter2",
|
||||||
|
}),
|
||||||
|
agreement: zBoolean.openapi({
|
||||||
|
description:
|
||||||
|
"Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE.",
|
||||||
|
example: true,
|
||||||
|
}),
|
||||||
|
locale: z.string().openapi({
|
||||||
|
description:
|
||||||
|
"The language of the confirmation email that will be sent. ISO 639-1 code.",
|
||||||
|
example: "en",
|
||||||
|
}),
|
||||||
|
reason: z.string().optional().openapi({
|
||||||
|
description:
|
||||||
|
"If registrations require manual approval, this text will be reviewed by moderators.",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "post",
|
method: "post",
|
||||||
path: "/api/v1/accounts",
|
path: "/api/v1/accounts",
|
||||||
summary: "Create account",
|
summary: "Register an account",
|
||||||
description: "Register a new account",
|
description:
|
||||||
|
"Creates a user and account records. Returns an account access token for the app that initiated the request. The app should save this token for later, and should wait for the user to confirm their account by clicking a link in their email inbox.\n\nA relationship between the OAuth Application and created user account is stored.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#create",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: false,
|
auth: false,
|
||||||
|
|
@ -34,18 +56,18 @@ const route = createRoute({
|
||||||
challenge: true,
|
challenge: true,
|
||||||
}),
|
}),
|
||||||
jsonOrForm(),
|
jsonOrForm(),
|
||||||
],
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: schemas.json,
|
schema: schema,
|
||||||
},
|
},
|
||||||
"multipart/form-data": {
|
"multipart/form-data": {
|
||||||
schema: schemas.json,
|
schema: schema,
|
||||||
},
|
},
|
||||||
"application/x-www-form-urlencoded": {
|
"application/x-www-form-urlencoded": {
|
||||||
schema: schemas.json,
|
schema: schema,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -113,7 +135,10 @@ const route = createRoute({
|
||||||
),
|
),
|
||||||
reason: z.array(
|
reason: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
error: z.enum(["ERR_BLANK"]),
|
error: z.enum([
|
||||||
|
"ERR_BLANK",
|
||||||
|
"ERR_TOO_LONG",
|
||||||
|
]),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
|
@ -288,6 +313,14 @@ export default apiRoute((app) =>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if reason is too long
|
||||||
|
if ((form.reason?.length ?? 0) > 10_000) {
|
||||||
|
errors.details.reason.push({
|
||||||
|
error: "ERR_TOO_LONG",
|
||||||
|
description: `is too long (maximum is ${10_000} characters)`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// If any errors are present, return them
|
// If 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"
|
// Error is something like "Validation failed: Password can't be blank, Username must contain only letters, numbers and underscores, Agreement must be accepted"
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ describe("/api/v1/accounts/lookup", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should automatically lowercase the acct", async () => {
|
test("should require exact case", async () => {
|
||||||
const response = await fakeRequest(
|
const response = await fakeRequest(
|
||||||
`/api/v1/accounts/lookup?acct=${users[0].data.username.toUpperCase()}`,
|
`/api/v1/accounts/lookup?acct=${users[0].data.username.toUpperCase()}`,
|
||||||
{
|
{
|
||||||
|
|
@ -44,17 +44,6 @@ describe("/api/v1/accounts/lookup", () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(404);
|
||||||
|
|
||||||
const data = (await response.json()) as ApiAccount[];
|
|
||||||
expect(data).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
id: users[0].id,
|
|
||||||
username: users[0].data.username,
|
|
||||||
display_name: users[0].data.displayName,
|
|
||||||
avatar: expect.any(String),
|
|
||||||
header: expect.any(String),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,25 @@
|
||||||
import { apiRoute, auth, parseUserAddress } from "@/api";
|
import {
|
||||||
|
accountNotFound,
|
||||||
|
apiRoute,
|
||||||
|
auth,
|
||||||
|
parseUserAddress,
|
||||||
|
reusedResponses,
|
||||||
|
} from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Instance, User } from "@versia/kit/db";
|
import { Instance, User } from "@versia/kit/db";
|
||||||
import { RolePermissions, Users } from "@versia/kit/tables";
|
import { RolePermissions, Users } from "@versia/kit/tables";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { Account } from "~/classes/schemas/account";
|
import { Account } from "~/classes/schemas/account";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/packages/config-manager";
|
||||||
import { ErrorSchema } from "~/types/api";
|
|
||||||
|
|
||||||
const schemas = {
|
|
||||||
query: z.object({
|
|
||||||
acct: z.string().min(1).max(512).toLowerCase(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/accounts/lookup",
|
path: "/api/v1/accounts/lookup",
|
||||||
summary: "Lookup account",
|
summary: "Lookup account ID from Webfinger address",
|
||||||
description: "Lookup an account by acct",
|
description:
|
||||||
|
"Quickly lookup a username to see if it is available, skipping WebFinger resolution.",
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: false,
|
auth: false,
|
||||||
|
|
@ -26,7 +27,12 @@ const route = createRoute({
|
||||||
}),
|
}),
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
query: schemas.query,
|
query: z.object({
|
||||||
|
acct: AccountSchema.shape.acct.openapi({
|
||||||
|
description: "The username or Webfinger address to lookup.",
|
||||||
|
example: "lexi@beta.versia.social",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
|
|
@ -37,22 +43,8 @@ const route = createRoute({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
404: {
|
404: accountNotFound,
|
||||||
description: "Not found",
|
422: reusedResponses[422],
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: ErrorSchema,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
422: {
|
|
||||||
description: "Invalid parameter",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: ErrorSchema,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -64,12 +56,8 @@ export default apiRoute((app) =>
|
||||||
// Check if acct is matching format username@domain.com or @username@domain.com
|
// Check if acct is matching format username@domain.com or @username@domain.com
|
||||||
const { username, domain } = parseUserAddress(acct);
|
const { username, domain } = parseUserAddress(acct);
|
||||||
|
|
||||||
if (!username) {
|
|
||||||
throw new Error("Invalid username");
|
|
||||||
}
|
|
||||||
|
|
||||||
// User is local
|
// User is local
|
||||||
if (!domain || domain === new URL(config.http.base_url).host) {
|
if (!domain || domain === config.http.base_url.host) {
|
||||||
const account = await User.fromSql(
|
const account = await User.fromSql(
|
||||||
and(eq(Users.username, username), isNull(Users.instanceId)),
|
and(eq(Users.username, username), isNull(Users.instanceId)),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,21 @@
|
||||||
import { apiRoute, auth, qsQuery } from "@/api";
|
import { apiRoute, auth, qsQuery, reusedResponses } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { Relationship } from "@versia/kit/db";
|
import { Relationship } from "@versia/kit/db";
|
||||||
import { RolePermissions } from "@versia/kit/tables";
|
import { RolePermissions } from "@versia/kit/tables";
|
||||||
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
import { Relationship as RelationshipSchema } from "~/classes/schemas/relationship";
|
||||||
|
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||||
const schemas = {
|
|
||||||
query: z.object({
|
|
||||||
id: z.array(z.string().uuid()).min(1).max(10).or(z.string().uuid()),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/accounts/relationships",
|
path: "/api/v1/accounts/relationships",
|
||||||
summary: "Get relationships",
|
summary: "Check relationships to other accounts",
|
||||||
description: "Get relationships by account ID",
|
description:
|
||||||
|
"Find out whether a given account is followed, blocked, muted, etc.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#relationships",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -24,7 +25,26 @@ const route = createRoute({
|
||||||
qsQuery(),
|
qsQuery(),
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
query: schemas.query,
|
query: z.object({
|
||||||
|
id: z
|
||||||
|
.array(AccountSchema.shape.id)
|
||||||
|
.min(1)
|
||||||
|
.max(10)
|
||||||
|
.or(AccountSchema.shape.id.transform((v) => [v]))
|
||||||
|
.openapi({
|
||||||
|
description:
|
||||||
|
"Check relationships for the provided account IDs.",
|
||||||
|
example: [
|
||||||
|
"f137ce6f-ff5e-4998-b20f-0361ba9be007",
|
||||||
|
"8424c654-5d03-4a1b-bec8-4e87db811b5d",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
with_suspended: zBoolean.default(false).openapi({
|
||||||
|
description:
|
||||||
|
"Whether relationships should be returned for suspended users",
|
||||||
|
example: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
|
|
@ -35,12 +55,15 @@ const route = createRoute({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.openapi(route, async (context) => {
|
app.openapi(route, async (context) => {
|
||||||
const { user } = context.get("auth");
|
const { user } = context.get("auth");
|
||||||
|
|
||||||
|
// TODO: Implement with_suspended
|
||||||
const { id } = context.req.valid("query");
|
const { id } = context.req.valid("query");
|
||||||
|
|
||||||
const ids = Array.isArray(id) ? id : [id];
|
const ids = Array.isArray(id) ? id : [id];
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,22 @@
|
||||||
import { apiRoute, auth, parseUserAddress, userAddressValidator } from "@/api";
|
import { apiRoute, auth, parseUserAddress } from "@/api";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
import { User } from "@versia/kit/db";
|
import { User } from "@versia/kit/db";
|
||||||
import { RolePermissions, Users } from "@versia/kit/tables";
|
import { RolePermissions, Users } from "@versia/kit/tables";
|
||||||
import { eq, ilike, not, or, sql } from "drizzle-orm";
|
import { eq, ilike, not, or, sql } from "drizzle-orm";
|
||||||
import stringComparison from "string-comparison";
|
import stringComparison from "string-comparison";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { Account } from "~/classes/schemas/account";
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
|
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||||
const schemas = {
|
|
||||||
query: z.object({
|
|
||||||
q: z.string().min(1).max(512).regex(userAddressValidator),
|
|
||||||
limit: z.coerce.number().int().min(1).max(80).default(40),
|
|
||||||
offset: z.coerce.number().int().optional(),
|
|
||||||
resolve: z
|
|
||||||
.string()
|
|
||||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
|
||||||
.optional(),
|
|
||||||
following: z
|
|
||||||
.string()
|
|
||||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
|
||||||
.optional(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const route = createRoute({
|
export const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/accounts/search",
|
path: "/api/v1/accounts/search",
|
||||||
summary: "Search accounts",
|
summary: "Search for matching accounts",
|
||||||
description: "Search for accounts",
|
description: "Search for matching accounts by username or display name.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#search",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: false,
|
auth: false,
|
||||||
|
|
@ -36,14 +25,38 @@ export const route = createRoute({
|
||||||
}),
|
}),
|
||||||
] as const,
|
] as const,
|
||||||
request: {
|
request: {
|
||||||
query: schemas.query,
|
query: z.object({
|
||||||
|
q: AccountSchema.shape.username
|
||||||
|
.or(AccountSchema.shape.acct)
|
||||||
|
.openapi({
|
||||||
|
description: "Search query for accounts.",
|
||||||
|
example: "username",
|
||||||
|
}),
|
||||||
|
limit: z.coerce.number().int().min(1).max(80).default(40).openapi({
|
||||||
|
description: "Maximum number of results.",
|
||||||
|
example: 40,
|
||||||
|
}),
|
||||||
|
offset: z.coerce.number().int().default(0).openapi({
|
||||||
|
description: "Skip the first n results.",
|
||||||
|
example: 0,
|
||||||
|
}),
|
||||||
|
resolve: zBoolean.default(false).openapi({
|
||||||
|
description:
|
||||||
|
"Attempt WebFinger lookup. Use this when q is an exact address.",
|
||||||
|
example: false,
|
||||||
|
}),
|
||||||
|
following: zBoolean.default(false).openapi({
|
||||||
|
description: "Limit the search to users you are following.",
|
||||||
|
example: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Accounts",
|
description: "Accounts",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: z.array(Account),
|
schema: z.array(AccountSchema),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { apiRoute, auth, jsonOrForm } from "@/api";
|
import { apiRoute, auth, jsonOrForm, reusedResponses } from "@/api";
|
||||||
import { mergeAndDeduplicate } from "@/lib";
|
import { mergeAndDeduplicate } from "@/lib";
|
||||||
import { sanitizedHtmlStrip } from "@/sanitization";
|
import { sanitizedHtmlStrip } from "@/sanitization";
|
||||||
import { createRoute, z } from "@hono/zod-openapi";
|
import { createRoute, z } from "@hono/zod-openapi";
|
||||||
|
|
@ -7,73 +7,19 @@ import { RolePermissions, Users } from "@versia/kit/tables";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import { contentToHtml } from "~/classes/functions/status";
|
import { contentToHtml } from "~/classes/functions/status";
|
||||||
import { Account } from "~/classes/schemas/account";
|
import { Account as AccountSchema } from "~/classes/schemas/account";
|
||||||
|
import { zBoolean } from "~/packages/config-manager/config.type";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/packages/config-manager/index.ts";
|
||||||
import { ErrorSchema } from "~/types/api";
|
|
||||||
|
|
||||||
const schemas = {
|
|
||||||
json: z
|
|
||||||
.object({
|
|
||||||
display_name: Account.shape.display_name,
|
|
||||||
username: Account.shape.username,
|
|
||||||
note: Account.shape.note,
|
|
||||||
avatar: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(1)
|
|
||||||
.max(2000)
|
|
||||||
.url()
|
|
||||||
.transform((a) => new URL(a))
|
|
||||||
.or(
|
|
||||||
z
|
|
||||||
.instanceof(File)
|
|
||||||
.refine(
|
|
||||||
(v) => v.size <= config.validation.max_avatar_size,
|
|
||||||
`Avatar must be less than ${config.validation.max_avatar_size} bytes`,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
header: z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(1)
|
|
||||||
.max(2000)
|
|
||||||
.url()
|
|
||||||
.transform((v) => new URL(v))
|
|
||||||
.or(
|
|
||||||
z
|
|
||||||
.instanceof(File)
|
|
||||||
.refine(
|
|
||||||
(v) => v.size <= config.validation.max_header_size,
|
|
||||||
`Header must be less than ${config.validation.max_header_size} bytes`,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
locked: Account.shape.locked,
|
|
||||||
bot: Account.shape.bot,
|
|
||||||
discoverable: Account.shape.discoverable,
|
|
||||||
source: z
|
|
||||||
.object({
|
|
||||||
privacy: Account.shape.source.unwrap().shape.privacy,
|
|
||||||
sensitive: Account.shape.source.unwrap().shape.sensitive,
|
|
||||||
language: Account.shape.source.unwrap().shape.language,
|
|
||||||
})
|
|
||||||
.partial(),
|
|
||||||
fields_attributes: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
name: Account.shape.fields.element.shape.name,
|
|
||||||
value: Account.shape.fields.element.shape.value,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.max(config.validation.max_field_count),
|
|
||||||
})
|
|
||||||
.partial(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "patch",
|
method: "patch",
|
||||||
path: "/api/v1/accounts/update_credentials",
|
path: "/api/v1/accounts/update_credentials",
|
||||||
summary: "Update credentials",
|
summary: "Update account credentials",
|
||||||
description: "Update user credentials",
|
description: "Update the user’s display and preferences.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#update_credentials",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -86,7 +32,121 @@ const route = createRoute({
|
||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: schemas.json,
|
schema: z
|
||||||
|
.object({
|
||||||
|
display_name:
|
||||||
|
AccountSchema.shape.display_name.openapi({
|
||||||
|
description:
|
||||||
|
"The display name to use for the profile.",
|
||||||
|
example: "Lexi",
|
||||||
|
}),
|
||||||
|
username: AccountSchema.shape.username.openapi({
|
||||||
|
description:
|
||||||
|
"The username to use for the profile.",
|
||||||
|
example: "lexi",
|
||||||
|
}),
|
||||||
|
note: AccountSchema.shape.note.openapi({
|
||||||
|
description:
|
||||||
|
"The account bio. Markdown is supported.",
|
||||||
|
}),
|
||||||
|
avatar: z
|
||||||
|
.string()
|
||||||
|
.url()
|
||||||
|
.transform((a) => new URL(a))
|
||||||
|
.openapi({
|
||||||
|
description: "Avatar image URL",
|
||||||
|
})
|
||||||
|
.or(
|
||||||
|
z
|
||||||
|
.instanceof(File)
|
||||||
|
.refine(
|
||||||
|
(v) =>
|
||||||
|
v.size <=
|
||||||
|
config.validation
|
||||||
|
.max_avatar_size,
|
||||||
|
`Avatar must be less than ${config.validation.max_avatar_size} bytes`,
|
||||||
|
)
|
||||||
|
.openapi({
|
||||||
|
description:
|
||||||
|
"Avatar image encoded using multipart/form-data",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
header: z
|
||||||
|
.string()
|
||||||
|
.url()
|
||||||
|
.transform((v) => new URL(v))
|
||||||
|
.openapi({
|
||||||
|
description: "Header image URL",
|
||||||
|
})
|
||||||
|
.or(
|
||||||
|
z
|
||||||
|
.instanceof(File)
|
||||||
|
.refine(
|
||||||
|
(v) =>
|
||||||
|
v.size <=
|
||||||
|
config.validation
|
||||||
|
.max_header_size,
|
||||||
|
`Header must be less than ${config.validation.max_header_size} bytes`,
|
||||||
|
)
|
||||||
|
.openapi({
|
||||||
|
description:
|
||||||
|
"Header image encoded using multipart/form-data",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
locked: AccountSchema.shape.locked.openapi({
|
||||||
|
description:
|
||||||
|
"Whether manual approval of follow requests is required.",
|
||||||
|
}),
|
||||||
|
bot: AccountSchema.shape.bot.openapi({
|
||||||
|
description:
|
||||||
|
"Whether the account has a bot flag.",
|
||||||
|
}),
|
||||||
|
discoverable:
|
||||||
|
AccountSchema.shape.discoverable.openapi({
|
||||||
|
description:
|
||||||
|
"Whether the account should be shown in the profile directory.",
|
||||||
|
}),
|
||||||
|
// TODO: Implement :(
|
||||||
|
hide_collections: zBoolean.openapi({
|
||||||
|
description:
|
||||||
|
"Whether to hide followers and followed accounts.",
|
||||||
|
}),
|
||||||
|
// TODO: Implement :(
|
||||||
|
indexable: zBoolean.openapi({
|
||||||
|
description:
|
||||||
|
"Whether public posts should be searchable to anyone.",
|
||||||
|
}),
|
||||||
|
// TODO: Implement :(
|
||||||
|
attribution_domains: z.array(z.string()).openapi({
|
||||||
|
description:
|
||||||
|
"Domains of websites allowed to credit the account.",
|
||||||
|
example: ["cnn.com", "myblog.com"],
|
||||||
|
}),
|
||||||
|
source: z
|
||||||
|
.object({
|
||||||
|
privacy:
|
||||||
|
AccountSchema.shape.source.unwrap()
|
||||||
|
.shape.privacy,
|
||||||
|
sensitive:
|
||||||
|
AccountSchema.shape.source.unwrap()
|
||||||
|
.shape.sensitive,
|
||||||
|
language:
|
||||||
|
AccountSchema.shape.source.unwrap()
|
||||||
|
.shape.language,
|
||||||
|
})
|
||||||
|
.partial(),
|
||||||
|
fields_attributes: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
name: AccountSchema.shape.fields.element
|
||||||
|
.shape.name,
|
||||||
|
value: AccountSchema.shape.fields
|
||||||
|
.element.shape.value,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.max(config.validation.max_field_count),
|
||||||
|
})
|
||||||
|
.partial(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -96,27 +156,11 @@ const route = createRoute({
|
||||||
description: "Updated user",
|
description: "Updated user",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: Account,
|
schema: AccountSchema,
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
422: {
|
|
||||||
description: "Validation error",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: ErrorSchema,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
500: {
|
|
||||||
description: "Couldn't edit user",
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: ErrorSchema,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
import { apiRoute, auth } from "@/api";
|
import { apiRoute, auth, reusedResponses } from "@/api";
|
||||||
import { createRoute } from "@hono/zod-openapi";
|
import { createRoute } from "@hono/zod-openapi";
|
||||||
import { Account } from "~/classes/schemas/account";
|
import { Account } from "~/classes/schemas/account";
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/api/v1/accounts/verify_credentials",
|
path: "/api/v1/accounts/verify_credentials",
|
||||||
summary: "Verify credentials",
|
summary: "Verify account credentials",
|
||||||
description: "Get your own account information",
|
description: "Test to make sure that the user token works.",
|
||||||
|
externalDocs: {
|
||||||
|
url: "https://docs.joinmastodon.org/methods/accounts/#verify_credentials",
|
||||||
|
},
|
||||||
|
tags: ["Accounts"],
|
||||||
middleware: [
|
middleware: [
|
||||||
auth({
|
auth({
|
||||||
auth: true,
|
auth: true,
|
||||||
|
|
@ -15,13 +19,16 @@ const route = createRoute({
|
||||||
] as const,
|
] as const,
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
description: "Account",
|
// TODO: Implement CredentialAccount
|
||||||
|
description:
|
||||||
|
"Note the extra source property, which is not visible on accounts other than your own. Also note that plain-text is used within source and HTML is used for their corresponding properties such as note and fields.",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: Account,
|
schema: Account,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...reusedResponses,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -336,7 +336,7 @@ describe("/api/v1/statuses", () => {
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
status: `Hello, @${users[1].data.username}@${
|
status: `Hello, @${users[1].data.username}@${
|
||||||
new URL(config.http.base_url).host
|
config.http.base_url.host
|
||||||
}!`,
|
}!`,
|
||||||
local_only: "true",
|
local_only: "true",
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
// TODO: fill in more values
|
// TODO: fill in more values
|
||||||
return context.json({
|
return context.json({
|
||||||
domain: new URL(config.http.base_url).hostname,
|
domain: config.http.base_url.hostname,
|
||||||
title: config.instance.name,
|
title: config.instance.name,
|
||||||
version: "4.3.0-alpha.3+glitch",
|
version: "4.3.0-alpha.3+glitch",
|
||||||
versia_version: version,
|
versia_version: version,
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ export default apiRoute((app) =>
|
||||||
// This fixes reverse proxy errors
|
// This fixes reverse proxy errors
|
||||||
const reqUrl = new URL(context.req.url);
|
const reqUrl = new URL(context.req.url);
|
||||||
if (
|
if (
|
||||||
new URL(config.http.base_url).protocol === "https:" &&
|
config.http.base_url.protocol === "https:" &&
|
||||||
reqUrl.protocol === "http:"
|
reqUrl.protocol === "http:"
|
||||||
) {
|
) {
|
||||||
reqUrl.protocol = "https:";
|
reqUrl.protocol = "https:";
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ const route = createRoute({
|
||||||
|
|
||||||
export default apiRoute((app) =>
|
export default apiRoute((app) =>
|
||||||
app.openapi(route, (context) => {
|
app.openapi(route, (context) => {
|
||||||
const baseUrl = new URL(config.http.base_url);
|
const baseUrl = config.http.base_url;
|
||||||
return context.json(
|
return context.json(
|
||||||
{
|
{
|
||||||
issuer: baseUrl.origin.toString(),
|
issuer: baseUrl.origin.toString(),
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ export default apiRoute((app) =>
|
||||||
],
|
],
|
||||||
versions: ["0.4.0"],
|
versions: ["0.4.0"],
|
||||||
},
|
},
|
||||||
host: new URL(config.http.base_url).host,
|
host: config.http.base_url.host,
|
||||||
name: config.instance.name,
|
name: config.instance.name,
|
||||||
description: config.instance.description,
|
description: config.instance.description,
|
||||||
public_key: {
|
public_key: {
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export default apiRoute((app) =>
|
||||||
|
|
||||||
const requestedUser = resource.split("acct:")[1];
|
const requestedUser = resource.split("acct:")[1];
|
||||||
|
|
||||||
const host = new URL(config.http.base_url).host;
|
const host = config.http.base_url.host;
|
||||||
|
|
||||||
const { username, domain } = parseUserAddress(requestedUser);
|
const { username, domain } = parseUserAddress(requestedUser);
|
||||||
|
|
||||||
|
|
@ -96,7 +96,7 @@ export default apiRoute((app) =>
|
||||||
try {
|
try {
|
||||||
activityPubUrl = await manager.webFinger(
|
activityPubUrl = await manager.webFinger(
|
||||||
user.data.username,
|
user.data.username,
|
||||||
new URL(config.http.base_url).host,
|
config.http.base_url.host,
|
||||||
"application/activity+json",
|
"application/activity+json",
|
||||||
config.federation.bridge.url?.toString(),
|
config.federation.bridge.url?.toString(),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -784,9 +784,7 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
||||||
replacedContent = replacedContent.replace(
|
replacedContent = replacedContent.replace(
|
||||||
createRegExp(
|
createRegExp(
|
||||||
exactly(
|
exactly(
|
||||||
`@${mention.username}@${
|
`@${mention.username}@${config.http.base_url.host}`,
|
||||||
new URL(config.http.base_url).host
|
|
||||||
}`,
|
|
||||||
),
|
),
|
||||||
[global],
|
[global],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ export const parseTextMentions = async (
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrlHost = new URL(config.http.base_url).host;
|
const baseUrlHost = config.http.base_url.host;
|
||||||
const isLocal = (host?: string): boolean => host === baseUrlHost || !host;
|
const isLocal = (host?: string): boolean => host === baseUrlHost || !host;
|
||||||
|
|
||||||
// Find local and matching users
|
// Find local and matching users
|
||||||
|
|
@ -301,7 +301,7 @@ export const replaceTextMentions = (text: string, mentions: User[]): string => {
|
||||||
return mentions.reduce((finalText, mention) => {
|
return mentions.reduce((finalText, mention) => {
|
||||||
const { username, instance } = mention.data;
|
const { username, instance } = mention.data;
|
||||||
const uri = mention.getUri();
|
const uri = mention.getUri();
|
||||||
const baseHost = new URL(config.http.base_url).host;
|
const baseHost = config.http.base_url.host;
|
||||||
const linkTemplate = (displayText: string): string =>
|
const linkTemplate = (displayText: string): string =>
|
||||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${uri}">${displayText}</a>`;
|
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${uri}">${displayText}</a>`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { userAddressValidator } from "@/api.ts";
|
||||||
import { z } from "@hono/zod-openapi";
|
import { z } from "@hono/zod-openapi";
|
||||||
import type { Account as ApiAccount } from "@versia/client/types";
|
import type { Account as ApiAccount } from "@versia/client/types";
|
||||||
import { config } from "~/packages/config-manager";
|
import { config } from "~/packages/config-manager";
|
||||||
|
|
@ -126,7 +127,6 @@ export const Account = z.object({
|
||||||
.min(3)
|
.min(3)
|
||||||
.trim()
|
.trim()
|
||||||
.max(config.validation.max_username_size)
|
.max(config.validation.max_username_size)
|
||||||
.toLowerCase()
|
|
||||||
.regex(
|
.regex(
|
||||||
/^[a-z0-9_-]+$/,
|
/^[a-z0-9_-]+$/,
|
||||||
"Username can only contain letters, numbers, underscores and hyphens",
|
"Username can only contain letters, numbers, underscores and hyphens",
|
||||||
|
|
@ -142,7 +142,12 @@ export const Account = z.object({
|
||||||
url: "https://docs.joinmastodon.org/entities/Account/#username",
|
url: "https://docs.joinmastodon.org/entities/Account/#username",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
acct: z.string().openapi({
|
acct: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.trim()
|
||||||
|
.regex(userAddressValidator, "Invalid user address")
|
||||||
|
.openapi({
|
||||||
description:
|
description:
|
||||||
"The Webfinger account URI. Equal to username for local users, or username@domain for remote users.",
|
"The Webfinger account URI. Equal to username for local users, or username@domain for remote users.",
|
||||||
example: "lexi@beta.versia.social",
|
example: "lexi@beta.versia.social",
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ export const Relationship = z
|
||||||
description: "Are you featuring this user on your profile?",
|
description: "Are you featuring this user on your profile?",
|
||||||
example: false,
|
example: false,
|
||||||
}),
|
}),
|
||||||
note: z.string().openapi({
|
note: z.string().min(0).max(5000).trim().openapi({
|
||||||
description: "This user’s profile bio",
|
description: "This user’s profile bio",
|
||||||
example: "they also like Kerbal Space Program",
|
example: "they also like Kerbal Space Program",
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { config } from "~/packages/config-manager";
|
||||||
|
|
||||||
export const urlCheck = createMiddleware(async (context, next) => {
|
export const urlCheck = createMiddleware(async (context, next) => {
|
||||||
// Check that request URL matches base_url
|
// Check that request URL matches base_url
|
||||||
const baseUrl = new URL(config.http.base_url);
|
const baseUrl = config.http.base_url;
|
||||||
|
|
||||||
if (new URL(context.req.url).origin !== baseUrl.origin) {
|
if (new URL(context.req.url).origin !== baseUrl.origin) {
|
||||||
return context.json(
|
return context.json(
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ describe("/oauth/authorize", () => {
|
||||||
test("should authorize and redirect with valid inputs", async () => {
|
test("should authorize and redirect with valid inputs", async () => {
|
||||||
const jwt = await new SignJWT({
|
const jwt = await new SignJWT({
|
||||||
sub: users[0].id,
|
sub: users[0].id,
|
||||||
iss: new URL(config.http.base_url).origin,
|
iss: config.http.base_url.origin,
|
||||||
aud: application.data.clientId,
|
aud: application.data.clientId,
|
||||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
||||||
iat: Math.floor(Date.now() / 1000),
|
iat: Math.floor(Date.now() / 1000),
|
||||||
|
|
@ -109,7 +109,7 @@ describe("/oauth/authorize", () => {
|
||||||
test("should return error for missing required fields in JWT", async () => {
|
test("should return error for missing required fields in JWT", async () => {
|
||||||
const jwt = await new SignJWT({
|
const jwt = await new SignJWT({
|
||||||
sub: users[0].id,
|
sub: users[0].id,
|
||||||
iss: new URL(config.http.base_url).origin,
|
iss: config.http.base_url.origin,
|
||||||
aud: application.data.clientId,
|
aud: application.data.clientId,
|
||||||
})
|
})
|
||||||
.setProtectedHeader({ alg: "EdDSA" })
|
.setProtectedHeader({ alg: "EdDSA" })
|
||||||
|
|
@ -150,7 +150,7 @@ describe("/oauth/authorize", () => {
|
||||||
sub: "non-existent-user",
|
sub: "non-existent-user",
|
||||||
aud: application.data.clientId,
|
aud: application.data.clientId,
|
||||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
||||||
iss: new URL(config.http.base_url).origin,
|
iss: config.http.base_url.origin,
|
||||||
iat: Math.floor(Date.now() / 1000),
|
iat: Math.floor(Date.now() / 1000),
|
||||||
nbf: Math.floor(Date.now() / 1000),
|
nbf: Math.floor(Date.now() / 1000),
|
||||||
})
|
})
|
||||||
|
|
@ -190,7 +190,7 @@ describe("/oauth/authorize", () => {
|
||||||
sub: "23e42862-d5df-49a8-95b5-52d8c6a11aea",
|
sub: "23e42862-d5df-49a8-95b5-52d8c6a11aea",
|
||||||
aud: application.data.clientId,
|
aud: application.data.clientId,
|
||||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
||||||
iss: new URL(config.http.base_url).origin,
|
iss: config.http.base_url.origin,
|
||||||
iat: Math.floor(Date.now() / 1000),
|
iat: Math.floor(Date.now() / 1000),
|
||||||
nbf: Math.floor(Date.now() / 1000),
|
nbf: Math.floor(Date.now() / 1000),
|
||||||
})
|
})
|
||||||
|
|
@ -233,7 +233,7 @@ describe("/oauth/authorize", () => {
|
||||||
|
|
||||||
const jwt = await new SignJWT({
|
const jwt = await new SignJWT({
|
||||||
sub: users[0].id,
|
sub: users[0].id,
|
||||||
iss: new URL(config.http.base_url).origin,
|
iss: config.http.base_url.origin,
|
||||||
aud: application.data.clientId,
|
aud: application.data.clientId,
|
||||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
||||||
iat: Math.floor(Date.now() / 1000),
|
iat: Math.floor(Date.now() / 1000),
|
||||||
|
|
@ -278,7 +278,7 @@ describe("/oauth/authorize", () => {
|
||||||
const jwt = await new SignJWT({
|
const jwt = await new SignJWT({
|
||||||
sub: users[0].id,
|
sub: users[0].id,
|
||||||
aud: "invalid-client-id",
|
aud: "invalid-client-id",
|
||||||
iss: new URL(config.http.base_url).origin,
|
iss: config.http.base_url.origin,
|
||||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
||||||
iat: Math.floor(Date.now() / 1000),
|
iat: Math.floor(Date.now() / 1000),
|
||||||
nbf: Math.floor(Date.now() / 1000),
|
nbf: Math.floor(Date.now() / 1000),
|
||||||
|
|
@ -319,7 +319,7 @@ describe("/oauth/authorize", () => {
|
||||||
test("should return error for invalid redirect_uri", async () => {
|
test("should return error for invalid redirect_uri", async () => {
|
||||||
const jwt = await new SignJWT({
|
const jwt = await new SignJWT({
|
||||||
sub: users[0].id,
|
sub: users[0].id,
|
||||||
iss: new URL(config.http.base_url).origin,
|
iss: config.http.base_url.origin,
|
||||||
aud: application.data.clientId,
|
aud: application.data.clientId,
|
||||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
||||||
iat: Math.floor(Date.now() / 1000),
|
iat: Math.floor(Date.now() / 1000),
|
||||||
|
|
@ -361,7 +361,7 @@ describe("/oauth/authorize", () => {
|
||||||
test("should return error for invalid scope", async () => {
|
test("should return error for invalid scope", async () => {
|
||||||
const jwt = await new SignJWT({
|
const jwt = await new SignJWT({
|
||||||
sub: users[0].id,
|
sub: users[0].id,
|
||||||
iss: new URL(config.http.base_url).origin,
|
iss: config.http.base_url.origin,
|
||||||
aud: application.data.clientId,
|
aud: application.data.clientId,
|
||||||
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
exp: Math.floor(Date.now() / 1000) + 60 * 60,
|
||||||
iat: Math.floor(Date.now() / 1000),
|
iat: Math.floor(Date.now() / 1000),
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ describe("API Tests", () => {
|
||||||
|
|
||||||
// Now automatically mitigated by the server
|
// Now automatically mitigated by the server
|
||||||
/* test("try sending a request with a different origin", async () => {
|
/* test("try sending a request with a different origin", async () => {
|
||||||
if (new URL(config.http.base_url).protocol === "http:") {
|
if (config.http.base_url.protocol === "http:") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ describe("API Tests", () => {
|
||||||
expect(account.fields).toEqual([]);
|
expect(account.fields).toEqual([]);
|
||||||
expect(account.source?.fields).toEqual([]);
|
expect(account.source?.fields).toEqual([]);
|
||||||
expect(account.source?.privacy).toBe("public");
|
expect(account.source?.privacy).toBe("public");
|
||||||
expect(account.source?.language).toBeNull();
|
expect(account.source?.language).toBe("en");
|
||||||
expect(account.source?.note).toBe("");
|
expect(account.source?.note).toBe("");
|
||||||
expect(account.source?.sensitive).toBe(false);
|
expect(account.source?.sensitive).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,8 @@ export const getTestUsers = async (
|
||||||
const password = randomString(32, "hex");
|
const password = randomString(32, "hex");
|
||||||
|
|
||||||
const user = await User.fromDataLocal({
|
const user = await User.fromDataLocal({
|
||||||
username: `test-${randomString(32, "hex")}`,
|
username: `test-${randomString(8, "hex")}`,
|
||||||
email: `${randomString(32, "hex")}@test.com`,
|
email: `${randomString(16, "hex")}@test.com`,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
30
utils/api.ts
30
utils/api.ts
|
|
@ -29,7 +29,35 @@ import { fromZodError } from "zod-validation-error";
|
||||||
import { ApiError } from "~/classes/errors/api-error";
|
import { ApiError } from "~/classes/errors/api-error";
|
||||||
import type { AuthData } from "~/classes/functions/user";
|
import type { AuthData } from "~/classes/functions/user";
|
||||||
import { config } from "~/packages/config-manager/index.ts";
|
import { config } from "~/packages/config-manager/index.ts";
|
||||||
import type { HonoEnv } from "~/types/api";
|
import { ErrorSchema, type HonoEnv } from "~/types/api";
|
||||||
|
|
||||||
|
export const reusedResponses = {
|
||||||
|
401: {
|
||||||
|
description: "Invalid or missing Authorization header.",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: ErrorSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
422: {
|
||||||
|
description: "Invalid values in request",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: ErrorSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const accountNotFound = {
|
||||||
|
description: "Account does not exist",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: ErrorSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const apiRoute = (fn: (app: OpenAPIHono<HonoEnv>) => void): typeof fn =>
|
export const apiRoute = (fn: (app: OpenAPIHono<HonoEnv>) => void): typeof fn =>
|
||||||
fn;
|
fn;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue