Compare commits

..

No commits in common. "a6c9d6cd4f46ebe1e0f0fd201c27a6f51e5d60d1" and "79742f47dc8417b24e255284a9165399e40757f5" have entirely different histories.

509 changed files with 13845 additions and 7570 deletions

9
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,9 @@
# Bun doesn't run well on Musl but this seems to work
FROM oven/bun:1.2.15-alpine as base
# Switch to Bash by editing /etc/passwd
RUN apk add --no-cache libstdc++ git bash curl openssh cloc && \
sed -i -e 's|/bin/ash|/bin/bash|g' /etc/passwd
# Extract Node from its docker image (node:22-alpine)
COPY --from=node:22-alpine /usr/local/bin/node /usr/local/bin/node

View file

@ -0,0 +1,34 @@
{
"name": "versia Dev Container",
"dockerFile": "Dockerfile",
"runArgs": [
"-v",
"${localWorkspaceFolder}/config:/workspace/config",
"-v",
"${localWorkspaceFolder}/logs:/workspace/logs",
"-v",
"${localWorkspaceFolder}/uploads:/workspace/uploads",
"--network=host"
],
"mounts": [
"source=node_modules,target=/workspace/node_modules,type=bind,consistency=cached",
"type=bind,source=/home/${localEnv:USER}/.ssh,target=/root/.ssh,readonly"
],
"customizations": {
"vscode": {
"settings": {
"terminal.integrated.shell.linux": "/bin/bash"
},
"extensions": [
"biomejs.biome",
"ms-vscode-remote.remote-containers",
"oven.bun-vscode",
"vivaxy.vscode-conventional-commits",
"EditorConfig.EditorConfig",
"tamasfe.even-better-toml",
"YoavBls.pretty-ts-errors",
"eamodio.gitlens"
]
}
}
}

View file

@ -429,38 +429,50 @@ text = "No spam"
[logging]
# Available levels: trace, debug, info, warning, error, fatal
log_level = "info" # For console output
# Available levels: debug, info, warning, error, fatal
log_level = "debug"
log_file_path = "logs/versia.log"
[logging.types]
# Either pass a boolean
# requests = true
# Or a table with the following keys:
# requests_content = { level = "debug", log_file_path = "logs/requests.log" }
# Available types are: requests, responses, requests_content, filters
# [logging.file]
# path = "logs/versia.log"
# log_level = "info"
#
# [logging.file.rotation]
# max_size = 10_000_000 # 10 MB
# max_files = 10 # Keep 10 rotated files
#
# https://sentry.io support
# Uncomment to enable
# [logging.sentry]
# Sentry DSN for error logging
# dsn = "https://example.com"
# debug = false
# sample_rate = 1.0
# traces_sample_rate = 1.0
# Can also be regex
# trace_propagation_targets = []
# max_breadcrumbs = 100
# environment = "production"
# log_level = "info"
[authentication]
[plugins]
# Whether to automatically load all plugins in the plugins directory
autoload = true
# Override for autoload
[plugins.overrides]
enabled = []
disabled = []
[plugins.config."@versia/openid"]
# If enabled, Versia will require users to log in with an OpenID provider
forced_openid = false
forced = false
# Allow registration with OpenID providers
# If signups.registration is false, it will only be possible to register with OpenID
openid_registration = true
allow_registration = true
[authentication.keys]
[plugins.config."@versia/openid".keys]
# Run Versia Server with those values missing to generate a new key
public = "MCowBQYDK2VwAyEAfyZx8r98gVHtdH5EF1NYrBeChOXkt50mqiwKO2TX0f8="
private = "MC4CAQAwBQYDK2VwBCIEILDi1g7+bwNjBBvL4CRWHZpCFBR2m2OPCot62Wr+TCbq"
@ -472,7 +484,7 @@ private = "MC4CAQAwBQYDK2VwBCIEILDi1g7+bwNjBBvL4CRWHZpCFBR2m2OPCot62Wr+TCbq"
# The asterisk is important, as it allows for any query parameters to be passed
# Authentik for example uses regex so it can be set to (regex):
# <base_url>/oauth/sso/<provider_id>/callback.*
# [[authentication.openid_providers]]
# [[plugins.config."@versia/openid".providers]]
# name = "CPlusPatch ID"
# id = "cpluspatch-id"
# This MUST match the provider's issuer URI, including the trailing slash (or lack thereof)

View file

@ -13,10 +13,4 @@ const add = (a: number, b: number): number => a + b;
We always write TypeScript with double quotes and four spaces for indentation, so when your responses include TypeScript code, please follow those conventions.
Our codebase uses Drizzle as an ORM, which is exposed in the `@versia-server/kit/db` and `@versia-server/kit/tables` packages. This project uses a monorepo structure with Bun as the package manager.
The app has two modes: worker and API. The worker mode is used for background tasks, while the API mode serves HTTP requests. The entry point for the worker is `worker.ts`, and for the API, it is `api.ts`.
Run the typechecker with `bun run typecheck` to ensure that all TypeScript code is type-checked correctly. Run tests with `bun test` to ensure that all tests pass. Run the linter and formatter with `bun lint` to ensure that the code adheres to our style guidelines, and `bun lint --write` to automatically fix minor/formatting issues.
Cover all new functionality with tests, and ensure that all tests pass before submitting your code.
Our codebase uses Drizzle as an ORM, with custom abstractions in `classes/database/` for interacting with the database. The `@versia/kit/db` and `@versia/kit/tables` packages are aliases for these abstractions.

View file

@ -24,4 +24,4 @@ jobs:
- name: Run typechecks
run: |
bun run typecheck
bun run check

View file

@ -1,27 +0,0 @@
name: Check Circular Imports
on:
workflow_call:
jobs:
tests:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: recursive
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install NPM packages
run: |
bun install
- name: Run typechecks
run: |
bun run detect-circular

View file

@ -18,9 +18,6 @@ jobs:
tests:
uses: ./.github/workflows/tests.yml
detect-circular:
uses: ./.github/workflows/circular-imports.yml
build:
if: ${{ success() }}
needs: [lint, check, tests]

View file

@ -1,7 +0,0 @@
{
"detectiveOptions": {
"ts": {
"skipTypeImports": true
}
}
}

View file

@ -6,6 +6,7 @@
"cli",
"federation",
"config",
"plugin",
"worker",
"media",
"packages/client",

View file

@ -9,7 +9,7 @@
### Backend
- [x] 🚀 Upgraded Bun to `1.2.18`
- [x] 🚀 Upgraded Bun to `1.2.15`
# `0.8.0` • Federation 2: Electric Boogaloo

View file

@ -112,7 +112,7 @@ TypeScript errors should be ignored with `// @ts-expect-error` comments, as well
To scan for all TypeScript errors, run:
```sh
bun typecheck
bun check
```
### Commit messages
@ -153,4 +153,4 @@ If you find a bug, please open an issue on GitHub. Please make sure to include t
# License
Versia Server is licensed under the [AGPLv3 or later](https://www.gnu.org/licenses/agpl-3.0.en.html) license. By contributing to Versia, you agree to license your contributions under the same license.
Versia Server is licensed under the [AGPLv3 or later](https://www.gnu.org/licenses/agpl-3.0.en.html) license. By contributing to Versia, you agree to license your contributions under the same license.

View file

@ -1,5 +1,7 @@
# Node is required for building the project
FROM imbios/bun-node:latest-23-alpine AS base
FROM imbios/bun-node:1-20-alpine AS base
RUN apk add --no-cache libstdc++
# Install dependencies into temp directory
# This will cache them and speed up future builds
@ -20,19 +22,20 @@ COPY --from=install /temp/node_modules /temp/node_modules
# Build the project
WORKDIR /temp
RUN bun run build api
RUN bun run build
WORKDIR /temp/dist
# Copy production dependencies and source code into final image
FROM oven/bun:1.2.18-alpine
FROM oven/bun:1.2.15-alpine
# Install libstdc++ for Bun and create app directory
RUN mkdir -p /app
RUN apk add --no-cache libstdc++ && \
mkdir -p /app
COPY --from=build /temp/dist /app/dist
COPY entrypoint.sh /app
LABEL org.opencontainers.image.authors="Gaspard Wierzbinski (https://cpluspatch.com)"
LABEL org.opencontainers.image.authors="Gaspard Wierzbinski (https://cpluspatch.dev)"
LABEL org.opencontainers.image.source="https://github.com/versia-pub/server"
LABEL org.opencontainers.image.vendor="Versia Pub"
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
@ -48,4 +51,4 @@ WORKDIR /app
ENV NODE_ENV=production
ENTRYPOINT [ "/bin/sh", "/app/entrypoint.sh" ]
# Run migrations and start the server
CMD [ "bun", "run", "api.js" ]
CMD [ "bun", "run", "index.js" ]

View file

@ -1,5 +1,7 @@
# Node is required for building the project
FROM imbios/bun-node:latest-23-alpine AS base
FROM imbios/bun-node:1-20-alpine AS base
RUN apk add --no-cache libstdc++
# Install dependencies into temp directory
# This will cache them and speed up future builds
@ -20,19 +22,20 @@ COPY --from=install /temp/node_modules /temp/node_modules
# Build the project
WORKDIR /temp
RUN bun run build worker
RUN bun run build:worker
WORKDIR /temp/dist
# Copy production dependencies and source code into final image
FROM oven/bun:1.2.18-alpine
FROM oven/bun:1.2.15-alpine
# Install libstdc++ for Bun and create app directory
RUN mkdir -p /app
RUN apk add --no-cache libstdc++ && \
mkdir -p /app
COPY --from=build /temp/dist /app/dist
COPY entrypoint.sh /app
LABEL org.opencontainers.image.authors="Gaspard Wierzbinski (https://cpluspatch.com)"
LABEL org.opencontainers.image.authors="Gaspard Wierzbinski (https://cpluspatch.dev)"
LABEL org.opencontainers.image.source="https://github.com/versia-pub/server"
LABEL org.opencontainers.image.vendor="Versia Pub"
LABEL org.opencontainers.image.licenses="AGPL-3.0-or-later"
@ -44,8 +47,7 @@ ARG GIT_COMMIT
ENV GIT_COMMIT=$GIT_COMMIT
# CD to app
WORKDIR /app
WORKDIR /app/dist
ENV NODE_ENV=production
ENTRYPOINT [ "/bin/sh", "/app/entrypoint.sh" ]
# Run migrations and start the server
CMD [ "bun", "run", "worker.js" ]

View file

@ -1,9 +1,9 @@
import { afterAll, describe, expect, test } from "bun:test";
import { config } from "@versia-server/config";
import { Application } from "@versia-server/kit/db";
import { fakeRequest, getTestUsers } from "@versia-server/tests";
import { Application } from "@versia/kit/db";
import { randomUUIDv7 } from "bun";
import { randomString } from "@/math";
import { config } from "~/config.ts";
import { fakeRequest, getTestUsers } from "~/tests/utils";
const { users, deleteUsers, passwords } = await getTestUsers(1);

View file

@ -1,15 +1,16 @@
import { config } from "@versia-server/config";
import { ApiError } from "@versia-server/kit";
import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { Application, User } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { Application, User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { password as bunPassword } from "bun";
import { eq, or } from "drizzle-orm";
import type { Context } from "hono";
import { setCookie } from "hono/cookie";
import { describeRoute, validator } from "hono-openapi";
import { describeRoute } from "hono-openapi";
import { validator } from "hono-openapi/zod";
import { SignJWT } from "jose";
import { z } from "zod/v4";
import { z } from "zod";
import { apiRoute, handleZodError } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/config.ts";
const returnError = (
context: Context,
@ -58,7 +59,7 @@ export default apiRoute((app) =>
"query",
z.object({
scope: z.string().optional(),
redirect_uri: z.url().optional(),
redirect_uri: z.string().url().optional(),
response_type: z.enum([
"code",
"token",
@ -89,6 +90,7 @@ export default apiRoute((app) =>
"form",
z.object({
identifier: z
.string()
.email()
.toLowerCase()
.or(z.string().toLowerCase()),
@ -97,7 +99,30 @@ export default apiRoute((app) =>
handleZodError,
),
async (context) => {
if (config.authentication.forced_openid) {
const oidcConfig = config.plugins?.config?.["@versia/openid"] as
| {
forced: boolean;
providers: {
id: string;
name: string;
icon: string;
}[];
keys: {
private: string;
public: string;
};
}
| undefined;
if (!oidcConfig) {
return returnError(
context,
"invalid_request",
"The OpenID Connect plugin is not enabled on this instance. Cannot process login request.",
);
}
if (oidcConfig?.forced) {
return returnError(
context,
"invalid_request",
@ -143,6 +168,15 @@ export default apiRoute((app) =>
);
}
// Try and import the key
const privateKey = await crypto.subtle.importKey(
"pkcs8",
Buffer.from(oidcConfig?.keys?.private ?? "", "base64"),
"Ed25519",
false,
["sign"],
);
// Generate JWT
const jwt = await new SignJWT({
sub: user.id,
@ -153,7 +187,7 @@ export default apiRoute((app) =>
nbf: Math.floor(Date.now() / 1000),
})
.setProtectedHeader({ alg: "EdDSA" })
.sign(config.authentication.keys.private);
.sign(privateKey);
const application = await Application.fromClientId(client_id);

View file

@ -1,10 +1,11 @@
import { config } from "@versia-server/config";
import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { db } from "@versia-server/kit/db";
import { Applications, Tokens } from "@versia-server/kit/tables";
import { db } from "@versia/kit/db";
import { Applications, Tokens } from "@versia/kit/tables";
import { and, eq } from "drizzle-orm";
import { describeRoute, validator } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, handleZodError } from "@/api";
import { config } from "~/config.ts";
/**
* OAuth Code flow
@ -27,7 +28,7 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
redirect_uri: z.url(),
redirect_uri: z.string().url(),
client_id: z.string(),
code: z.string(),
}),

View file

@ -1,9 +1,9 @@
import { afterAll, describe, expect, test } from "bun:test";
import { config } from "@versia-server/config";
import { Application } from "@versia-server/kit/db";
import { fakeRequest, getTestUsers } from "@versia-server/tests";
import { Application } from "@versia/kit/db";
import { randomUUIDv7 } from "bun";
import { randomString } from "@/math";
import { config } from "~/config.ts";
import { fakeRequest, getTestUsers } from "~/tests/utils";
const { users, deleteUsers, passwords } = await getTestUsers(1);
const token = randomString(32, "hex");

View file

@ -1,12 +1,13 @@
import { config } from "@versia-server/config";
import { apiRoute, handleZodError } from "@versia-server/kit/api";
import { User } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { password as bunPassword } from "bun";
import { eq } from "drizzle-orm";
import type { Context } from "hono";
import { describeRoute, validator } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, handleZodError } from "@/api";
import { config } from "~/config.ts";
const returnError = (
context: Context,

View file

@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(2);

View file

@ -2,10 +2,11 @@ import {
Relationship as RelationshipSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
import { describeRoute, resolver } from "hono-openapi";
import { Relationship } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute, auth, withUserParam } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.post(

View file

@ -1,14 +1,10 @@
import { RolePermission } from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import {
apiRoute,
auth,
handleZodError,
withUserParam,
} from "@versia-server/kit/api";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
import { getFeed } from "@/rss";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.get(
@ -38,13 +34,12 @@ export default apiRoute((app) =>
RolePermission.ViewNotes,
RolePermission.ViewAccounts,
],
scopes: ["read:statuses"],
}),
validator(
"query",
z.object({
page: z.coerce.number().default(0).meta({
page: z.coerce.number().default(0).openapi({
description: "Page number to fetch. Defaults to 0.",
example: 2,
}),

View file

@ -1,14 +1,10 @@
import { RolePermission } from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import {
apiRoute,
auth,
handleZodError,
withUserParam,
} from "@versia-server/kit/api";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
import { getFeed } from "@/rss";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.get(
@ -37,13 +33,12 @@ export default apiRoute((app) =>
RolePermission.ViewNotes,
RolePermission.ViewAccounts,
],
scopes: ["read:statuses"],
}),
validator(
"query",
z.object({
page: z.coerce.number().default(0).meta({
page: z.coerce.number().default(0).openapi({
description: "Page number to fetch. Defaults to 0.",
example: 2,
}),

View file

@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(3);

View file

@ -3,16 +3,12 @@ import {
Relationship as RelationshipSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import {
apiRoute,
auth,
handleZodError,
withUserParam,
} from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { Relationship } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.post(
@ -61,12 +57,12 @@ export default apiRoute((app) =>
validator(
"json",
z.object({
reblogs: z.boolean().default(true).meta({
reblogs: z.boolean().default(true).openapi({
description:
"Receive this accounts reblogs in home timeline?",
example: true,
}),
notify: z.boolean().default(false).meta({
notify: z.boolean().default(false).openapi({
description:
"Receive notifications when this account posts a status?",
example: false,
@ -74,7 +70,7 @@ export default apiRoute((app) =>
languages: z
.array(iso631)
.default([])
.meta({
.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 accounts posts in all languages.",
example: ["en", "fr"],

View file

@ -1,5 +1,5 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(5);

View file

@ -2,18 +2,14 @@ import {
Account as AccountSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import {
apiRoute,
auth,
handleZodError,
withUserParam,
} from "@versia-server/kit/api";
import { Timeline } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { Timeline } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.get(
@ -38,7 +34,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
.meta({
.openapi({
description:
"Links to the next and previous pages",
example:
@ -65,22 +61,22 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
max_id: AccountSchema.shape.id.optional().meta({
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().meta({
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().meta({
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).meta({
limit: z.number().int().min(1).max(40).default(20).openapi({
description: "Maximum number of results to return.",
}),
}),

View file

@ -1,5 +1,5 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(5);

View file

@ -2,18 +2,14 @@ import {
Account as AccountSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import {
apiRoute,
auth,
handleZodError,
withUserParam,
} from "@versia-server/kit/api";
import { Timeline } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { Timeline } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.get(
@ -39,7 +35,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
.meta({
.openapi({
description:
"Links to the next and previous pages",
example:
@ -66,22 +62,22 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
max_id: AccountSchema.shape.id.optional().meta({
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().meta({
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().meta({
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).meta({
limit: z.number().int().min(1).max(40).default(20).openapi({
description: "Maximum number of results to return.",
}),
}),

View file

@ -1,9 +1,5 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import {
generateClient,
getTestStatuses,
getTestUsers,
} from "@versia-server/tests";
import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(5);
const timeline = (await getTestStatuses(5, users[0])).toReversed();

View file

@ -2,9 +2,10 @@ import {
Account as AccountSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { describeRoute, resolver } from "hono-openapi";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute, auth, withUserParam } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.get(

View file

@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(2);

View file

@ -2,20 +2,16 @@ import {
Relationship as RelationshipSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import {
apiRoute,
auth,
handleZodError,
withUserParam,
} from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
import { Relationship } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
import {
RelationshipJobType,
relationshipQueue,
} from "@versia-server/kit/queues/relationships";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
} from "~/classes/queues/relationships";
export default apiRoute((app) =>
app.post(
@ -55,7 +51,7 @@ export default apiRoute((app) =>
validator(
"json",
z.object({
notifications: z.boolean().default(true).meta({
notifications: z.boolean().default(true).openapi({
description: "Mute notifications in addition to statuses?",
}),
duration: z
@ -64,7 +60,7 @@ export default apiRoute((app) =>
.min(0)
.max(60 * 60 * 24 * 365 * 5)
.default(0)
.meta({
.openapi({
description:
"How long the mute should last, in seconds. 0 means indefinite.",
}),

View file

@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(2);

View file

@ -2,16 +2,12 @@ import {
Relationship as RelationshipSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import {
apiRoute,
auth,
handleZodError,
withUserParam,
} from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { Relationship } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.post(
@ -49,7 +45,7 @@ export default apiRoute((app) =>
validator(
"json",
z.object({
comment: RelationshipSchema.shape.note.optional().meta({
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.",
}),

View file

@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(2);

View file

@ -2,9 +2,10 @@ import {
Relationship as RelationshipSchema,
RolePermission,
} from "@versia/client/schemas";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
import { describeRoute, resolver } from "hono-openapi";
import { Relationship } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute, auth, withUserParam } from "@/api";
export default apiRoute((app) =>
app.post(

View file

@ -2,10 +2,11 @@ import {
Account as AccountSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { User } from "@versia-server/kit/db";
import { describeRoute, resolver } from "hono-openapi";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute, auth, withUserParam } from "@/api";
import { User } from "~/classes/database/user";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.post(

View file

@ -1,5 +1,5 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(2);

View file

@ -2,10 +2,11 @@ import {
Relationship as RelationshipSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
import { describeRoute, resolver } from "hono-openapi";
import { Relationship } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute, auth, withUserParam } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.post(

View file

@ -1,8 +1,8 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { RolePermission } from "@versia/client/schemas";
import { Role } from "@versia-server/kit/db";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { Role } from "@versia/kit/db";
import { randomUUIDv7 } from "bun";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(2);
let role: Role;

View file

@ -3,16 +3,12 @@ import {
RolePermission,
Role as RoleSchema,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import {
apiRoute,
auth,
handleZodError,
withUserParam,
} from "@versia-server/kit/api";
import { Role } from "@versia-server/kit/db";
import { describeRoute, validator } from "hono-openapi";
import { z } from "zod/v4";
import { Role } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) => {
app.post(

View file

@ -1,8 +1,8 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { RolePermission } from "@versia/client/schemas";
import { Role } from "@versia-server/kit/db";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { Role } from "@versia/kit/db";
import { randomUUIDv7 } from "bun";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(2);
let role: Role;

View file

@ -1,8 +1,9 @@
import { Role as RoleSchema } from "@versia/client/schemas";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Role } from "@versia-server/kit/db";
import { describeRoute, resolver } from "hono-openapi";
import { z } from "zod/v4";
import { Role } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, withUserParam } from "@/api";
export default apiRoute((app) => {
app.get(

View file

@ -3,7 +3,7 @@ import {
generateClient,
getTestStatuses,
getTestUsers,
} from "@versia-server/tests";
} from "~/tests/utils.ts";
const { users, deleteUsers } = await getTestUsers(5);
const timeline = (await getTestStatuses(5, users[1])).toReversed();

View file

@ -3,18 +3,14 @@ import {
Status as StatusSchema,
zBoolean,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import {
apiRoute,
auth,
handleZodError,
withUserParam,
} from "@versia-server/kit/api";
import { Timeline } from "@versia-server/kit/db";
import { Notes } from "@versia-server/kit/tables";
import { Timeline } from "@versia/kit/db";
import { Notes } from "@versia/kit/tables";
import { and, eq, gt, gte, inArray, isNull, lt, or, sql } from "drizzle-orm";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError, withUserParam } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.get(
@ -46,45 +42,50 @@ export default apiRoute((app) =>
RolePermission.ViewNotes,
RolePermission.ViewAccounts,
],
scopes: ["read:statuses"],
}),
validator(
"query",
z.object({
max_id: StatusSchema.shape.id.optional().meta({
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().meta({
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().meta({
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).meta({
description: "Maximum number of results to return.",
}),
only_media: zBoolean.default(false).meta({
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).meta({
exclude_replies: zBoolean.default(false).openapi({
description:
"Filter out statuses in reply to a different account.",
}),
exclude_reblogs: zBoolean.default(false).meta({
exclude_reblogs: zBoolean.default(false).openapi({
description: "Filter out boosts from the response.",
}),
pinned: zBoolean.default(false).meta({
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().meta({
tagged: z.string().optional().openapi({
description:
"Filter for statuses using a specific hashtag.",
}),

View file

@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(2);

View file

@ -2,10 +2,11 @@ import {
Relationship as RelationshipSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
import { describeRoute, resolver } from "hono-openapi";
import { Relationship } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute, auth, withUserParam } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.post(

View file

@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(3);

View file

@ -2,10 +2,11 @@ import {
Relationship as RelationshipSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
import { describeRoute, resolver } from "hono-openapi";
import { Relationship } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute, auth, withUserParam } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.post(

View file

@ -1,5 +1,5 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(2);

View file

@ -2,10 +2,11 @@ import {
Relationship as RelationshipSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
import { describeRoute, resolver } from "hono-openapi";
import { Relationship } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute, auth, withUserParam } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.post(

View file

@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(2);

View file

@ -2,10 +2,11 @@ import {
Relationship as RelationshipSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, withUserParam } from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
import { describeRoute, resolver } from "hono-openapi";
import { Relationship } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute, auth, withUserParam } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.post(

View file

@ -1,5 +1,5 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils.ts";
const { users, deleteUsers } = await getTestUsers(5);

View file

@ -3,19 +3,15 @@ import {
FamiliarFollowers as FamiliarFollowersSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import {
apiRoute,
auth,
handleZodError,
qsQuery,
} from "@versia-server/kit/api";
import { db, User } from "@versia-server/kit/db";
import type { Users } from "@versia-server/kit/tables";
import { db, User } from "@versia/kit/db";
import type { Users } from "@versia/kit/tables";
import { type InferSelectModel, sql } from "drizzle-orm";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError, qsQuery } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
import { rateLimit } from "~/middlewares/rate-limit";
export default apiRoute((app) =>
app.get(
@ -55,8 +51,8 @@ export default apiRoute((app) =>
.array(AccountSchema.shape.id)
.min(1)
.max(10)
.or(AccountSchema.shape.id)
.meta({
.or(AccountSchema.shape.id.transform((v) => [v]))
.openapi({
description:
"Find familiar followers for the provided account IDs.",
example: [
@ -69,11 +65,11 @@ export default apiRoute((app) =>
),
async (context) => {
const { user } = context.get("auth");
const { id } = context.req.valid("query");
const { id: ids } = context.req.valid("query");
// Find followers of the accounts in "ids", that you also follow
const finalUsers = await Promise.all(
(Array.isArray(id) ? id : [id]).map(async (id) => ({
ids.map(async (id) => ({
id,
accounts: await User.fromIds(
(

View file

@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(5);

View file

@ -1,9 +1,9 @@
import { afterEach, describe, expect, test } from "bun:test";
import { db } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { generateClient, getSolvedChallenge } from "@versia-server/tests";
import { db } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { eq } from "drizzle-orm";
import { randomString } from "@/math";
import { generateClient, getSolvedChallenge } from "~/tests/utils";
const username = randomString(10, "hex");
const username2 = randomString(10, "hex");

View file

@ -1,48 +1,42 @@
import { Account as AccountSchema, zBoolean } from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { ApiError } from "@versia-server/kit";
import {
apiRoute,
auth,
handleZodError,
jsonOrForm,
qsQuery,
} from "@versia-server/kit/api";
import { User } from "@versia-server/kit/db";
import { searchManager } from "@versia-server/kit/search";
import { Users } from "@versia-server/kit/tables";
import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import { describeRoute, resolver, validator } from "hono-openapi";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import ISO6391 from "iso-639-1";
import { z } from "zod/v4";
import { z } from "zod";
import { apiRoute, auth, handleZodError, jsonOrForm, qsQuery } from "@/api";
import { tempmailDomains } from "@/tempmail";
import { rateLimit } from "../../../../middlewares/rate-limit.ts";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/config.ts";
import { rateLimit } from "~/middlewares/rate-limit";
const schema = z.object({
username: z.string().meta({
username: z.string().openapi({
description: "The desired username for the account",
example: "alice",
}),
email: z.string().toLowerCase().meta({
email: z.string().toLowerCase().openapi({
description:
"The email address to be used for login. Transformed to lowercase.",
example: "alice@gmail.com",
}),
password: z.string().meta({
password: z.string().openapi({
description: "The password to be used for login",
example: "hunter2",
}),
agreement: zBoolean.meta({
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().meta({
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().meta({
reason: z.string().optional().openapi({
description:
"If registrations require manual approval, this text will be reviewed by moderators.",
}),
@ -85,8 +79,8 @@ export default apiRoute((app) => {
.array(AccountSchema.shape.id)
.min(1)
.max(40)
.or(AccountSchema.shape.id)
.meta({
.or(AccountSchema.shape.id.transform((v) => [v]))
.openapi({
description: "The IDs of the Accounts in the database.",
example: [
"f137ce6f-ff5e-4998-b20f-0361ba9be007",
@ -97,10 +91,10 @@ export default apiRoute((app) => {
handleZodError,
),
async (context) => {
const { id } = context.req.valid("query");
const { id: ids } = context.req.valid("query");
// Find accounts by IDs
const accounts = await User.fromIds(Array.isArray(id) ? id : [id]);
const accounts = await User.fromIds(ids);
return context.json(
accounts.map((account) => account.toApi()),
@ -419,14 +413,11 @@ export default apiRoute((app) => {
);
}
const user = await User.register(username, {
await User.register(username, {
password,
email,
});
// Add to search index
await searchManager.addUser(user);
return context.text("", 200);
},
);

View file

@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(5);

View file

@ -2,16 +2,16 @@ import {
Account as AccountSchema,
RolePermission,
} from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Instance, User } from "@versia-server/kit/db";
import { parseUserAddress } from "@versia-server/kit/parsers";
import { Users } from "@versia-server/kit/tables";
import { Instance, User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError, parseUserAddress } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/config.ts";
import { rateLimit } from "~/middlewares/rate-limit";
export default apiRoute((app) =>
app.get(
@ -42,7 +42,7 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
acct: AccountSchema.shape.acct.meta({
acct: AccountSchema.shape.acct.openapi({
description: "The username or Webfinger address to lookup.",
example: "lexi@beta.versia.social",
}),

View file

@ -1,8 +1,8 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { db } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { db } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { eq } from "drizzle-orm";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(5);

View file

@ -4,17 +4,13 @@ import {
RolePermission,
zBoolean,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import {
apiRoute,
auth,
handleZodError,
qsQuery,
} from "@versia-server/kit/api";
import { Relationship } from "@versia-server/kit/db";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
import { Relationship } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError, qsQuery } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
import { rateLimit } from "~/middlewares/rate-limit";
export default apiRoute((app) =>
app.get(
@ -54,8 +50,8 @@ export default apiRoute((app) =>
.array(AccountSchema.shape.id)
.min(1)
.max(10)
.or(AccountSchema.shape.id)
.meta({
.or(AccountSchema.shape.id.transform((v) => [v]))
.openapi({
description:
"Check relationships for the provided account IDs.",
example: [
@ -63,7 +59,7 @@ export default apiRoute((app) =>
"8424c654-5d03-4a1b-bec8-4e87db811b5d",
],
}),
with_suspended: zBoolean.default(false).meta({
with_suspended: zBoolean.default(false).openapi({
description:
"Whether relationships should be returned for suspended users",
example: false,
@ -75,16 +71,17 @@ export default apiRoute((app) =>
const { user } = context.get("auth");
// TODO: Implement with_suspended
const { id } = context.req.valid("query");
const { id: ids } = context.req.valid("query");
const relationships = await Relationship.fromOwnerAndSubjects(
user,
Array.isArray(id) ? id : [id],
ids,
);
relationships.sort(
(a, b) =>
id.indexOf(a.data.subjectId) - id.indexOf(b.data.subjectId),
ids.indexOf(a.data.subjectId) -
ids.indexOf(b.data.subjectId),
);
return context.json(

View file

@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(5);

View file

@ -3,16 +3,16 @@ import {
RolePermission,
zBoolean,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { User } from "@versia-server/kit/db";
import { parseUserAddress } from "@versia-server/kit/parsers";
import { Users } from "@versia-server/kit/tables";
import { User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { eq, ilike, not, or, sql } from "drizzle-orm";
import { describeRoute, resolver, validator } from "hono-openapi";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import stringComparison from "string-comparison";
import { z } from "zod/v4";
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
import { z } from "zod";
import { apiRoute, auth, handleZodError, parseUserAddress } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
import { rateLimit } from "~/middlewares/rate-limit";
export default apiRoute((app) =>
app.get(
@ -47,24 +47,30 @@ export default apiRoute((app) =>
z.object({
q: AccountSchema.shape.username
.or(AccountSchema.shape.acct)
.meta({
.openapi({
description: "Search query for accounts.",
example: "username",
}),
limit: z.coerce.number().int().min(1).max(80).default(40).meta({
description: "Maximum number of results.",
example: 40,
}),
offset: z.coerce.number().int().default(0).meta({
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).meta({
resolve: zBoolean.default(false).openapi({
description:
"Attempt WebFinger lookup. Use this when q is an exact address.",
example: false,
}),
following: zBoolean.default(false).meta({
following: zBoolean.default(false).openapi({
description: "Limit the search to users you are following.",
example: false,
}),

View file

@ -1,6 +1,6 @@
import { afterAll, describe, expect, test } from "bun:test";
import { config } from "@versia-server/config";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { config } from "~/config.ts";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(1);

View file

@ -3,24 +3,20 @@ import {
RolePermission,
zBoolean,
} from "@versia/client/schemas";
import * as VersiaEntities from "@versia/sdk/entities";
import { config } from "@versia-server/config";
import { ApiError } from "@versia-server/kit";
import {
apiRoute,
auth,
handleZodError,
jsonOrForm,
} from "@versia-server/kit/api";
import { Emoji, Media, User } from "@versia-server/kit/db";
import { versiaTextToHtml } from "@versia-server/kit/parsers";
import { Users } from "@versia-server/kit/tables";
import { Emoji, Media, User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError, jsonOrForm } from "@/api";
import { mergeAndDeduplicate } from "@/lib";
import { sanitizedHtmlStrip } from "@/sanitization";
import { rateLimit } from "../../../../../middlewares/rate-limit.ts";
import { ApiError } from "~/classes/errors/api-error";
import { contentToHtml } from "~/classes/functions/status";
import { config } from "~/config.ts";
import { rateLimit } from "~/middlewares/rate-limit";
import * as VersiaEntities from "~/packages/sdk/entities";
export default apiRoute((app) =>
app.patch(
@ -57,7 +53,7 @@ export default apiRoute((app) =>
z
.object({
display_name: AccountSchema.shape.display_name
.meta({
.openapi({
description:
"The display name to use for the profile.",
example: "Lexi",
@ -74,7 +70,7 @@ export default apiRoute((app) =>
"Display name contains blocked words",
),
username: AccountSchema.shape.username
.meta({
.openapi({
description: "The username to use for the profile.",
example: "lexi",
})
@ -94,7 +90,7 @@ export default apiRoute((app) =>
"Username is disallowed",
),
note: AccountSchema.shape.note
.meta({
.openapi({
description:
"The account bio. Markdown is supported.",
})
@ -107,60 +103,72 @@ export default apiRoute((app) =>
"Bio contains blocked words",
),
avatar: z
.string()
.url()
.meta({
.transform((a) => new URL(a))
.openapi({
description: "Avatar image URL",
})
.or(
z
.file()
.max(
config.validation.accounts.max_avatar_bytes,
.instanceof(File)
.refine(
(v) =>
v.size <=
config.validation.accounts
.max_avatar_bytes,
`Avatar must be less than ${config.validation.accounts.max_avatar_bytes} bytes`,
)
.meta({
.openapi({
description:
"Avatar image encoded using multipart/form-data",
}),
),
header: z
.string()
.url()
.meta({
.transform((v) => new URL(v))
.openapi({
description: "Header image URL",
})
.or(
z
.file()
.max(
config.validation.accounts.max_header_bytes,
.instanceof(File)
.refine(
(v) =>
v.size <=
config.validation.accounts
.max_header_bytes,
`Header must be less than ${config.validation.accounts.max_header_bytes} bytes`,
)
.meta({
.openapi({
description:
"Header image encoded using multipart/form-data",
}),
),
locked: AccountSchema.shape.locked.meta({
locked: AccountSchema.shape.locked.openapi({
description:
"Whether manual approval of follow requests is required.",
}),
bot: AccountSchema.shape.bot.meta({
bot: AccountSchema.shape.bot.openapi({
description: "Whether the account has a bot flag.",
}),
discoverable: AccountSchema.shape.discoverable
.unwrap()
.meta({
.openapi({
description:
"Whether the account should be shown in the profile directory.",
}),
hide_collections: zBoolean.meta({
hide_collections: zBoolean.openapi({
description:
"Whether to hide followers and followed accounts.",
}),
indexable: zBoolean.meta({
indexable: zBoolean.openapi({
description:
"Whether public posts should be searchable to anyone.",
}),
// TODO: Implement :(
attribution_domains: z.array(z.string()).meta({
attribution_domains: z.array(z.string()).openapi({
description:
"Domains of websites allowed to credit the account.",
example: ["cnn.com", "myblog.com"],
@ -234,7 +242,7 @@ export default apiRoute((app) =>
if (note) {
self.source.note = note;
self.note = await versiaTextToHtml(
self.note = await contentToHtml(
new VersiaEntities.TextContentFormat({
"text/markdown": {
content: note,
@ -274,9 +282,9 @@ export default apiRoute((app) =>
user.avatar = await Media.fromFile(avatar);
}
} else if (user.avatar) {
await user.avatar.updateFromUrl(new URL(avatar));
await user.avatar.updateFromUrl(avatar);
} else {
user.avatar = await Media.fromUrl(new URL(avatar));
user.avatar = await Media.fromUrl(avatar);
}
}
@ -288,9 +296,9 @@ export default apiRoute((app) =>
user.header = await Media.fromFile(header);
}
} else if (user.header) {
await user.header.updateFromUrl(new URL(header));
await user.header.updateFromUrl(header);
} else {
user.header = await Media.fromUrl(new URL(header));
user.header = await Media.fromUrl(header);
}
}
@ -321,7 +329,7 @@ export default apiRoute((app) =>
self.source.fields = [];
for (const field of fields_attributes) {
// Can be Markdown or plaintext, also has emojis
const parsedName = await versiaTextToHtml(
const parsedName = await contentToHtml(
new VersiaEntities.TextContentFormat({
"text/markdown": {
content: field.name,
@ -332,7 +340,7 @@ export default apiRoute((app) =>
true,
);
const parsedValue = await versiaTextToHtml(
const parsedValue = await contentToHtml(
new VersiaEntities.TextContentFormat({
"text/markdown": {
content: field.value,

View file

@ -1,6 +1,6 @@
import { afterAll, describe, expect, test } from "bun:test";
import { config } from "@versia-server/config";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { config } from "~/config";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(1);

View file

@ -1,7 +1,8 @@
import { Account } from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth } from "@versia-server/kit/api";
import { describeRoute, resolver } from "hono-openapi";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute, auth } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.get(

View file

@ -2,14 +2,15 @@ import {
Application as ApplicationSchema,
CredentialApplication as CredentialApplicationSchema,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, handleZodError, jsonOrForm } from "@versia-server/kit/api";
import { Application } from "@versia-server/kit/db";
import { Application } from "@versia/kit/db";
import { randomUUIDv7 } from "bun";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, handleZodError, jsonOrForm } from "@/api";
import { randomString } from "@/math";
import { rateLimit } from "../../../../middlewares/rate-limit.ts";
import { ApiError } from "~/classes/errors/api-error";
import { rateLimit } from "~/middlewares/rate-limit";
export default apiRoute((app) =>
app.post(
@ -42,20 +43,22 @@ export default apiRoute((app) =>
z.object({
client_name: ApplicationSchema.shape.name,
redirect_uris: ApplicationSchema.shape.redirect_uris.or(
ApplicationSchema.shape.redirect_uri,
ApplicationSchema.shape.redirect_uri.transform((u) =>
u.split("\n"),
),
),
scopes: z.string().default("read").meta({
description: "Space separated list of scopes.",
type: "string",
}),
scopes: z
.string()
.default("read")
.transform((s) => s.split(" "))
.openapi({
description: "Space separated list of scopes.",
}),
// Allow empty websites because Traewelling decides to give an empty
// value instead of not providing anything at all
website: ApplicationSchema.shape.website
.optional()
.or(z.literal(""))
.meta({
type: "string",
}),
.or(z.literal("").transform(() => undefined)),
}),
handleZodError,
),
@ -66,11 +69,9 @@ export default apiRoute((app) =>
const app = await Application.insert({
id: randomUUIDv7(),
name: client_name,
redirectUri: Array.isArray(redirect_uris)
? redirect_uris.join("\n")
: redirect_uris,
scopes,
website: website || undefined,
redirectUri: redirect_uris.join("\n"),
scopes: scopes.join(" "),
website,
clientId: randomString(32, "base64url"),
secret: randomString(64, "base64url"),
});

View file

@ -2,10 +2,11 @@ import {
Application as ApplicationSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth } from "@versia-server/kit/api";
import { Application } from "@versia-server/kit/db";
import { describeRoute, resolver } from "hono-openapi";
import { Application } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute, auth } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.get(

View file

@ -1,5 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(3);

View file

@ -2,13 +2,14 @@ import {
Account as AccountSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Timeline } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { Timeline } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.get(
@ -32,7 +33,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
.meta({
.openapi({
description:
"Links to the next and previous pages",
example:
@ -55,24 +56,30 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
max_id: AccountSchema.shape.id.optional().meta({
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().meta({
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().meta({
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.coerce.number().int().min(1).max(80).default(40).meta({
description: "Maximum number of results to return.",
}),
limit: z.coerce
.number()
.int()
.min(1)
.max(80)
.default(40)
.openapi({
description: "Maximum number of results to return.",
}),
}),
handleZodError,
),

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { generateClient } from "@versia-server/tests";
import { generateClient } from "~/tests/utils";
// /api/v1/challenges
describe("/api/v1/challenges", () => {

View file

@ -1,9 +1,10 @@
import { Challenge } from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth } from "@versia-server/kit/api";
import { describeRoute, resolver } from "hono-openapi";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute, auth } from "@/api";
import { generateChallenge } from "@/challenges";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/config.ts";
export default apiRoute((app) =>
app.post(

View file

@ -1,8 +1,8 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { db } from "@versia-server/kit/db";
import { Emojis } from "@versia-server/kit/tables";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { db } from "@versia/kit/db";
import { Emojis } from "@versia/kit/tables";
import { inArray } from "drizzle-orm";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(2);

View file

@ -2,13 +2,14 @@ import {
CustomEmoji as CustomEmojiSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth } from "@versia-server/kit/api";
import { Emoji } from "@versia-server/kit/db";
import { Emojis } from "@versia-server/kit/tables";
import { Emoji } from "@versia/kit/db";
import { Emojis } from "@versia/kit/tables";
import { and, eq, isNull, or } from "drizzle-orm";
import { describeRoute, resolver } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.get(

View file

@ -1,8 +1,8 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { db } from "@versia-server/kit/db";
import { Emojis } from "@versia-server/kit/tables";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { db } from "@versia/kit/db";
import { Emojis } from "@versia/kit/tables";
import { inArray } from "drizzle-orm";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(2);
let id = "";

View file

@ -2,18 +2,19 @@ import {
CustomEmoji as CustomEmojiSchema,
RolePermission,
} from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { ApiError } from "@versia-server/kit";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import {
apiRoute,
auth,
handleZodError,
jsonOrForm,
withEmojiParam,
} from "@versia-server/kit/api";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
} from "@/api";
import { mimeLookup } from "@/content_types";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/config.ts";
export default apiRoute((app) => {
app.get(
@ -122,18 +123,25 @@ export default apiRoute((app) => {
"Shortcode contains blocked words",
),
element: z
.string()
.url()
.meta({
.transform((a) => new URL(a))
.openapi({
description: "Emoji image URL",
})
.or(
z
.file()
.max(config.validation.emojis.max_bytes)
.meta({
.instanceof(File)
.openapi({
description:
"Emoji image encoded using multipart/form-data",
}),
})
.refine(
(v) =>
v.size <=
config.validation.emojis.max_bytes,
`Emoji must be less than ${config.validation.emojis.max_bytes} bytes`,
),
),
category: CustomEmojiSchema.shape.category.optional(),
alt: CustomEmojiSchema.shape.description
@ -187,7 +195,7 @@ export default apiRoute((app) => {
const contentType =
element instanceof File
? element.type
: await mimeLookup(new URL(element));
: await mimeLookup(element);
if (!contentType.startsWith("image/")) {
throw new ApiError(
@ -200,7 +208,7 @@ export default apiRoute((app) => {
if (element instanceof File) {
await emoji.media.updateFromFile(element);
} else {
await emoji.media.updateFromUrl(new URL(element));
await emoji.media.updateFromUrl(element);
}
}

View file

@ -1,9 +1,9 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { db } from "@versia-server/kit/db";
import { Emojis } from "@versia-server/kit/tables";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { db } from "@versia/kit/db";
import { Emojis } from "@versia/kit/tables";
import { inArray } from "drizzle-orm";
import sharp from "sharp";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(3);

View file

@ -2,21 +2,17 @@ import {
CustomEmoji as CustomEmojiSchema,
RolePermission,
} from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { ApiError } from "@versia-server/kit";
import {
apiRoute,
auth,
handleZodError,
jsonOrForm,
} from "@versia-server/kit/api";
import { Emoji, Media } from "@versia-server/kit/db";
import { Emojis } from "@versia-server/kit/tables";
import { Emoji, Media } from "@versia/kit/db";
import { Emojis } from "@versia/kit/tables";
import { randomUUIDv7 } from "bun";
import { and, eq, isNull, or } from "drizzle-orm";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError, jsonOrForm } from "@/api";
import { mimeLookup } from "@/content_types";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/config.ts";
export default apiRoute((app) =>
app.post(
@ -59,15 +55,25 @@ export default apiRoute((app) =>
"Shortcode contains blocked words",
),
element: z
.string()
.url()
.meta({
.transform((a) => new URL(a))
.openapi({
description: "Emoji image URL",
})
.or(
z.file().max(config.validation.emojis.max_bytes).meta({
description:
"Emoji image encoded using multipart/form-data",
}),
z
.instanceof(File)
.openapi({
description:
"Emoji image encoded using multipart/form-data",
})
.refine(
(v) =>
v.size <=
config.validation.emojis.max_bytes,
`Emoji must be less than ${config.validation.emojis.max_bytes} bytes`,
),
),
category: CustomEmojiSchema.shape.category.optional(),
alt: CustomEmojiSchema.shape.description
@ -112,7 +118,7 @@ export default apiRoute((app) =>
const contentType =
element instanceof File
? element.type
: await mimeLookup(new URL(element));
: await mimeLookup(element);
if (!contentType.startsWith("image/")) {
throw new ApiError(
@ -127,7 +133,7 @@ export default apiRoute((app) =>
? await Media.fromFile(element, {
description: alt ?? undefined,
})
: await Media.fromUrl(new URL(element), {
: await Media.fromUrl(element, {
description: alt ?? undefined,
});

View file

@ -1,11 +1,12 @@
import { RolePermission, Status as StatusSchema } from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Timeline } from "@versia-server/kit/db";
import { Notes } from "@versia-server/kit/tables";
import { Timeline } from "@versia/kit/db";
import { Notes } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.get(
@ -29,7 +30,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
.meta({
.openapi({
description:
"Links to the next and previous pages",
example:
@ -51,24 +52,30 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
max_id: StatusSchema.shape.id.optional().meta({
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().meta({
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().meta({
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(80).default(40).meta({
description: "Maximum number of results to return.",
}),
limit: z.coerce
.number()
.int()
.min(1)
.max(80)
.default(40)
.openapi({
description: "Maximum number of results to return.",
}),
}),
handleZodError,
),

View file

@ -3,11 +3,12 @@ import {
Relationship as RelationshipSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Relationship, User } from "@versia-server/kit/db";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { Relationship, User } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.post(

View file

@ -3,11 +3,12 @@ import {
Relationship as RelationshipSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Relationship, User } from "@versia-server/kit/db";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { Relationship, User } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.post(

View file

@ -2,13 +2,14 @@ import {
Account as AccountSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Timeline } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { Timeline } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.get(
@ -34,7 +35,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
.meta({
.openapi({
description:
"Links to the next and previous pages",
example:
@ -56,24 +57,30 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
max_id: AccountSchema.shape.id.optional().meta({
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().meta({
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().meta({
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.coerce.number().int().min(1).max(80).default(40).meta({
description: "Maximum number of results to return.",
}),
limit: z.coerce
.number()
.int()
.min(1)
.max(80)
.default(40)
.openapi({
description: "Maximum number of results to return.",
}),
}),
handleZodError,
),

View file

@ -1,7 +1,8 @@
import { config } from "@versia-server/config";
import { apiRoute } from "@versia-server/kit/api";
import { describeRoute, resolver } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute } from "@/api";
import { config } from "~/config.ts";
export default apiRoute((app) =>
app.get(

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { generateClient } from "@versia-server/tests";
import { generateClient } from "~/tests/utils";
// /api/v1/instance/extended_description
describe("/api/v1/instance/extended_description", () => {

View file

@ -1,8 +1,9 @@
import { ExtendedDescription as ExtendedDescriptionSchema } from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { apiRoute } from "@versia-server/kit/api";
import { markdownToHtml } from "@versia-server/kit/markdown";
import { describeRoute, resolver } from "hono-openapi";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute } from "@/api";
import { markdownParse } from "~/classes/functions/status";
import { config } from "~/config.ts";
export default apiRoute((app) =>
app.get(
@ -26,7 +27,7 @@ export default apiRoute((app) =>
},
}),
async (context) => {
const content = await markdownToHtml(
const content = await markdownParse(
config.instance.extended_description_path?.content ??
"This is a [Versia](https://versia.pub) server with the default extended description.",
);

View file

@ -1,13 +1,14 @@
import { InstanceV1 as InstanceV1Schema } from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { apiRoute } from "@versia-server/kit/api";
import { Instance, Note, User } from "@versia-server/kit/db";
import { markdownToHtml } from "@versia-server/kit/markdown";
import { Users } from "@versia-server/kit/tables";
import { Instance, Note, User } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { and, eq, isNull } from "drizzle-orm";
import { describeRoute, resolver } from "hono-openapi";
import type { z } from "zod/v4";
import manifest from "../../../../../../package.json" with { type: "json" };
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import type { z } from "zod";
import { apiRoute } from "@/api";
import { markdownParse } from "~/classes/functions/status";
import { config } from "~/config.ts";
import manifest from "~/package.json" with { type: "json" };
export default apiRoute((app) =>
app.get(
@ -48,7 +49,18 @@ export default apiRoute((app) =>
const knownDomainsCount = await Instance.getCount();
const content = await markdownToHtml(
const oidcConfig = config.plugins?.config?.["@versia/openid"] as
| {
forced?: boolean;
providers?: {
id: string;
name: string;
icon?: string;
}[];
}
| undefined;
const content = await markdownParse(
config.instance.extended_description_path?.content ??
"This is a [Versia](https://versia.pub) server with the default extended description.",
);
@ -110,15 +122,15 @@ export default apiRoute((app) =>
},
version: "4.3.0-alpha.3+glitch",
versia_version: version,
// TODO: Put into plugin directly
sso: {
forced: config.authentication.forced_openid,
providers: config.authentication.openid_providers.map(
(p) => ({
forced: oidcConfig?.forced ?? false,
providers:
oidcConfig?.providers?.map((p) => ({
name: p.name,
icon: p.icon?.href,
icon: p.icon,
id: p.id,
}),
),
})) ?? [],
},
contact_account: (contactAccount as User)?.toApi(),
} satisfies z.infer<typeof InstanceV1Schema>);

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { generateClient } from "@versia-server/tests";
import { generateClient } from "~/tests/utils";
// /api/v1/instance/privacy_policy
describe("/api/v1/instance/privacy_policy", () => {

View file

@ -1,8 +1,9 @@
import { PrivacyPolicy as PrivacyPolicySchema } from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { apiRoute } from "@versia-server/kit/api";
import { markdownToHtml } from "@versia-server/kit/markdown";
import { describeRoute, resolver } from "hono-openapi";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute } from "@/api";
import { markdownParse } from "~/classes/functions/status";
import { config } from "~/config.ts";
export default apiRoute((app) =>
app.get(
@ -26,7 +27,7 @@ export default apiRoute((app) =>
},
}),
async (context) => {
const content = await markdownToHtml(
const content = await markdownParse(
config.instance.privacy_policy_path?.content ??
"This instance has not provided any privacy policy.",
);

View file

@ -1,6 +1,6 @@
import { describe, expect, test } from "bun:test";
import { config } from "@versia-server/config";
import { generateClient } from "@versia-server/tests";
import { config } from "~/config.ts";
import { generateClient } from "~/tests/utils";
// /api/v1/instance/rules
describe("/api/v1/instance/rules", () => {

View file

@ -1,8 +1,9 @@
import { Rule as RuleSchema } from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { apiRoute } from "@versia-server/kit/api";
import { describeRoute, resolver } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute } from "@/api";
import { config } from "~/config.ts";
export default apiRoute((app) =>
app.get(

View file

@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { generateClient } from "@versia-server/tests";
import { generateClient } from "~/tests/utils";
// /api/v1/instance/terms_of_service
describe("/api/v1/instance/terms_of_service", () => {

View file

@ -1,8 +1,9 @@
import { TermsOfService as TermsOfServiceSchema } from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { apiRoute } from "@versia-server/kit/api";
import { markdownToHtml } from "@versia-server/kit/markdown";
import { describeRoute, resolver } from "hono-openapi";
import { describeRoute } from "hono-openapi";
import { resolver } from "hono-openapi/zod";
import { apiRoute } from "@/api";
import { markdownParse } from "~/classes/functions/status";
import { config } from "~/config.ts";
export default apiRoute((app) =>
app.get(
@ -27,7 +28,7 @@ export default apiRoute((app) =>
},
}),
async (context) => {
const content = await markdownToHtml(
const content = await markdownParse(
config.instance.tos_path?.content ??
"This instance has not provided any terms of service.",
);

View file

@ -1,9 +1,5 @@
import { afterAll, describe, expect, test } from "bun:test";
import {
generateClient,
getTestStatuses,
getTestUsers,
} from "@versia-server/tests";
import { generateClient, getTestStatuses, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(1);
const timeline = await getTestStatuses(10, users[0]);

View file

@ -4,14 +4,15 @@ import {
RolePermission,
Status as StatusSchema,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { db } from "@versia-server/kit/db";
import { Markers } from "@versia-server/kit/tables";
import { db } from "@versia/kit/db";
import { Markers } from "@versia/kit/tables";
import { randomUUIDv7 } from "bun";
import { and, eq, type SQL } from "drizzle-orm";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
const MarkerResponseSchema = z.object({
notifications: MarkerSchema.optional(),
@ -51,9 +52,9 @@ export default apiRoute((app) => {
"timeline[]": z
.array(z.enum(["home", "notifications"]))
.max(2)
.or(z.enum(["home", "notifications"]))
.or(z.enum(["home", "notifications"]).transform((t) => [t]))
.optional()
.meta({
.openapi({
description:
"Specify the timeline(s) for which markers should be fetched. Possible values: home, notifications. If not provided, an empty object will be returned.",
}),
@ -61,17 +62,13 @@ export default apiRoute((app) => {
handleZodError,
),
async (context) => {
const { "timeline[]": queryTimeline } = context.req.valid("query");
const { "timeline[]": timeline } = context.req.valid("query");
const { user } = context.get("auth");
if (!queryTimeline) {
if (!timeline) {
return context.json({}, 200);
}
const timeline = Array.isArray(queryTimeline)
? queryTimeline
: [queryTimeline];
const markers: z.infer<typeof MarkerResponseSchema> = {
home: undefined,
notifications: undefined,
@ -163,13 +160,13 @@ export default apiRoute((app) => {
"query",
z
.object({
"home[last_read_id]": StatusSchema.shape.id.meta({
"home[last_read_id]": StatusSchema.shape.id.openapi({
description:
"ID of the last status read in the home timeline.",
example: "c62aa212-8198-4ce5-a388-2cc8344a84ef",
}),
"notifications[last_read_id]":
NotificationSchema.shape.id.meta({
NotificationSchema.shape.id.openapi({
description: "ID of the last notification read.",
}),
})

View file

@ -2,12 +2,13 @@ import {
Attachment as AttachmentSchema,
RolePermission,
} from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Media } from "@versia-server/kit/db";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { Media } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/config";
export default apiRoute((app) => {
app.get(
@ -105,7 +106,7 @@ export default apiRoute((app) => {
"form",
z
.object({
thumbnail: z.file().meta({
thumbnail: z.instanceof(File).openapi({
description:
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
}),
@ -113,7 +114,7 @@ export default apiRoute((app) => {
.unwrap()
.max(config.validation.media.max_description_characters)
.optional(),
focus: z.string().meta({
focus: z.string().openapi({
description:
"Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0. Used for media cropping on clients.",
externalDocs: {

View file

@ -2,12 +2,13 @@ import {
Attachment as AttachmentSchema,
RolePermission,
} from "@versia/client/schemas";
import { config } from "@versia-server/config";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Media } from "@versia-server/kit/db";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { Media } from "@versia/kit/db";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
import { config } from "~/config";
export default apiRoute((app) =>
app.post(
@ -59,11 +60,11 @@ export default apiRoute((app) =>
validator(
"form",
z.object({
file: z.file().meta({
file: z.instanceof(File).openapi({
description:
"The file to be attached, encoded using multipart form data. The file must have a MIME type.",
}),
thumbnail: z.file().optional().meta({
thumbnail: z.instanceof(File).optional().openapi({
description:
"The custom thumbnail of the media to be attached, encoded using multipart form data.",
}),
@ -74,7 +75,7 @@ export default apiRoute((app) =>
focus: z
.string()
.optional()
.meta({
.openapi({
description:
"Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0. Used for media cropping on clients.",
externalDocs: {

View file

@ -1,5 +1,5 @@
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { generateClient, getTestUsers } from "@versia-server/tests";
import { generateClient, getTestUsers } from "~/tests/utils";
const { users, deleteUsers } = await getTestUsers(3);

View file

@ -2,13 +2,14 @@ import {
Account as AccountSchema,
RolePermission,
} from "@versia/client/schemas";
import { ApiError } from "@versia-server/kit";
import { apiRoute, auth, handleZodError } from "@versia-server/kit/api";
import { Timeline } from "@versia-server/kit/db";
import { Users } from "@versia-server/kit/tables";
import { Timeline } from "@versia/kit/db";
import { Users } from "@versia/kit/tables";
import { and, gt, gte, lt, sql } from "drizzle-orm";
import { describeRoute, resolver, validator } from "hono-openapi";
import { z } from "zod/v4";
import { describeRoute } from "hono-openapi";
import { resolver, validator } from "hono-openapi/zod";
import { z } from "zod";
import { apiRoute, auth, handleZodError } from "@/api";
import { ApiError } from "~/classes/errors/api-error";
export default apiRoute((app) =>
app.get(
@ -32,7 +33,7 @@ export default apiRoute((app) =>
link: z
.string()
.optional()
.meta({
.openapi({
description:
"Links to the next and previous pages",
example:
@ -55,24 +56,30 @@ export default apiRoute((app) =>
validator(
"query",
z.object({
max_id: AccountSchema.shape.id.optional().meta({
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().meta({
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().meta({
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.coerce.number().int().min(1).max(80).default(40).meta({
description: "Maximum number of results to return.",
}),
limit: z.coerce
.number()
.int()
.min(1)
.max(80)
.default(40)
.openapi({
description: "Maximum number of results to return.",
}),
}),
handleZodError,
),

Some files were not shown because too many files have changed in this diff Show more