feat(api): Add support for batch account data API

This commit is contained in:
Jesse Wierzbinski 2025-05-26 18:41:45 +02:00
parent 287f428a83
commit 7bd07801f2
No known key found for this signature in database
6 changed files with 141 additions and 8 deletions

View file

@ -5,6 +5,7 @@
### API ### API
- [x] 🥺 Emoji Reactions are now available! You can react to any note with custom emojis. - [x] 🥺 Emoji Reactions are now available! You can react to any note with custom emojis.
- [x] 🔎 Added support for [batch account data API](https://docs.joinmastodon.org/methods/accounts/#index).
# `0.8.0` • Federation 2: Electric Boogaloo # `0.8.0` • Federation 2: Electric Boogaloo

View file

@ -0,0 +1,52 @@
import { afterAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(5);
afterAll(async () => {
await deleteUsers();
});
describe("/api/v1/accounts", () => {
test("should return accounts", async () => {
await using client = await generateClient();
const { data, ok } = await client.getAccounts(users.map((u) => u.id));
expect(ok).toBe(true);
expect(data).toEqual(
expect.arrayContaining(
users.map((u) =>
expect.objectContaining({
id: u.id,
username: u.data.username,
acct: u.data.username,
}),
),
),
);
});
test("should skip nonexistent accounts", async () => {
await using client = await generateClient();
const { data, ok } = await client.getAccounts([
...users.map((u) => u.id),
"00000000-0000-0000-0000-000000000000",
]);
expect(ok).toBe(true);
expect(data).toEqual(
expect.arrayContaining(
users.map((u) =>
expect.objectContaining({
id: u.id,
username: u.data.username,
acct: u.data.username,
}),
),
),
);
expect(data).toHaveLength(users.length);
});
});

View file

@ -1,4 +1,4 @@
import { zBoolean } from "@versia/client/schemas"; import { Account as AccountSchema, zBoolean } from "@versia/client/schemas";
import { User } from "@versia/kit/db"; import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables"; import { Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm"; import { and, eq, isNull } from "drizzle-orm";
@ -6,7 +6,7 @@ import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod"; import { resolver, validator } from "hono-openapi/zod";
import ISO6391 from "iso-639-1"; import ISO6391 from "iso-639-1";
import { z } from "zod"; import { z } from "zod";
import { apiRoute, auth, handleZodError, jsonOrForm } from "@/api"; import { apiRoute, auth, handleZodError, jsonOrForm, qsQuery } from "@/api";
import { tempmailDomains } from "@/tempmail"; import { tempmailDomains } from "@/tempmail";
import { ApiError } from "~/classes/errors/api-error"; import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/config.ts"; import { config } from "~/config.ts";
@ -42,7 +42,67 @@ const schema = z.object({
}), }),
}); });
export default apiRoute((app) => export default apiRoute((app) => {
app.get(
"/api/v1/accounts",
describeRoute({
summary: "Get multiple accounts",
description: "View information about multiple profiles.",
externalDocs: {
url: "https://docs.joinmastodon.org/methods/accounts/#index",
},
tags: ["Accounts"],
responses: {
200: {
description:
"Account records for the requested confirmed and approved accounts. There can be fewer records than requested if the accounts do not exist or are not confirmed.",
content: {
"application/json": {
schema: resolver(z.array(AccountSchema)),
},
},
},
422: ApiError.validationFailed().schema,
},
}),
qsQuery(),
auth({
auth: false,
scopes: [],
challenge: false,
}),
rateLimit(40),
validator(
"query",
z.object({
id: z
.array(AccountSchema.shape.id)
.min(1)
.max(40)
.or(AccountSchema.shape.id.transform((v) => [v]))
.openapi({
description: "The IDs of the Accounts in the database.",
example: [
"f137ce6f-ff5e-4998-b20f-0361ba9be007",
"8424c654-5d03-4a1b-bec8-4e87db811b5d",
],
}),
}),
handleZodError,
),
async (context) => {
const { id: ids } = context.req.valid("query");
// Find accounts by IDs
const accounts = await User.fromIds(ids);
return context.json(
accounts.map((account) => account.toApi()),
200,
);
},
);
app.post( app.post(
"/api/v1/accounts", "/api/v1/accounts",
describeRoute({ describeRoute({
@ -360,5 +420,5 @@ export default apiRoute((app) =>
return context.text("", 200); return context.text("", 200);
}, },
), );
); });

View file

@ -71,9 +71,7 @@ export default apiRoute((app) =>
const { user } = context.get("auth"); const { user } = context.get("auth");
// TODO: Implement with_suspended // TODO: Implement with_suspended
const { id } = context.req.valid("query"); const { id: ids } = context.req.valid("query");
const ids = Array.isArray(id) ? id : [id];
const relationships = await Relationship.fromOwnerAndSubjects( const relationships = await Relationship.fromOwnerAndSubjects(
user, user,

View file

@ -703,6 +703,28 @@ export class Client extends BaseClient {
); );
} }
/**
* GET /api/v1/accounts
*
* @param ids The account IDs.
* @return An array of accounts.
*/
public getAccounts(
ids: string[],
extra?: RequestInit,
): Promise<Output<z.infer<typeof Account>[]>> {
const params = new URLSearchParams();
for (const id of ids) {
params.append("id[]", id);
}
return this.get<z.infer<typeof Account>[]>(
`/api/v1/accounts?${params.toString()}`,
extra,
);
}
/** /**
* GET /api/v1/accounts/id * GET /api/v1/accounts/id
* *