diff --git a/.github/workflows/circular-imports.yml b/.github/workflows/circular-imports.yml new file mode 100644 index 00000000..f85882d6 --- /dev/null +++ b/.github/workflows/circular-imports.yml @@ -0,0 +1,27 @@ +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 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 8b1155b0..edbd8bc5 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -18,6 +18,9 @@ jobs: tests: uses: ./.github/workflows/tests.yml + detect-circular: + uses: ./.github/workflows/circular-imports.yml + build: if: ${{ success() }} needs: [lint, check, tests] diff --git a/.madgerc b/.madgerc new file mode 100644 index 00000000..b407c6b4 --- /dev/null +++ b/.madgerc @@ -0,0 +1,7 @@ +{ + "detectiveOptions": { + "ts": { + "skipTypeImports": true + } + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f87dc67e..9880297b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 check +bun typecheck ``` ### 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. \ No newline at end of file +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. diff --git a/packages/api/index.ts b/api.ts similarity index 81% rename from packages/api/index.ts rename to api.ts index 93cd93c8..3a700fbc 100644 --- a/packages/api/index.ts +++ b/api.ts @@ -1,8 +1,8 @@ import process from "node:process"; +import { appFactory } from "@versia-server/api"; import { config } from "@versia-server/config"; import { Youch } from "youch"; import { createServer } from "@/server"; -import { appFactory } from "./app.ts"; process.on("SIGINT", () => { process.exit(); @@ -14,6 +14,6 @@ process.on("uncaughtException", async (error) => { console.error(await youch.toANSI(error)); }); -await import("./setup.ts"); +await import("@versia-server/api/setup"); createServer(config, await appFactory()); diff --git a/build.ts b/build.ts new file mode 100644 index 00000000..de2ceb77 --- /dev/null +++ b/build.ts @@ -0,0 +1,55 @@ +import process from "node:process"; +import { $, build, file, write } from "bun"; +import manifest from "./package.json" with { type: "json" }; + +console.log("Building..."); + +await $`rm -rf dist && mkdir dist`; + +const type = process.argv[2] as "api" | "worker"; + +if (type !== "api" && type !== "worker") { + throw new Error("Invalid build type. Use 'api' or 'worker'."); +} + +const packages = Object.keys(manifest.dependencies) + .filter((dep) => dep.startsWith("@versia")) + .filter((dep) => dep !== "@versia-server/tests"); + +await build({ + entrypoints: [`./${type}.ts`], + outdir: "dist", + target: "bun", + splitting: true, + minify: true, + external: [...packages], +}); + +console.log("Copying files..."); + +// Copy each package into dist/node_modules +for (const pkg of packages) { + const directory = pkg.split("/")[1] || pkg; + await $`mkdir -p dist/node_modules/${pkg}`; + // Copy the built package files + await $`cp -rL packages/${directory}/{dist,package.json} dist/node_modules/${pkg}`; + + // Rewrite package.json "exports" field to point to the dist directory and use .js extension + const packageJsonPath = `dist/node_modules/${pkg}/package.json`; + const packageJson = await file(packageJsonPath).json(); + for (const [key, value] of Object.entries(packageJson.exports) as [ + string, + { import?: string }, + ][]) { + if (value.import) { + packageJson.exports[key] = { + import: value.import + .replace("./", "./dist/") + .replace(/\.ts$/, ".js"), + }; + } + } + await write(packageJsonPath, JSON.stringify(packageJson, null, 4)); +} + +console.log("Build complete!"); diff --git a/bun.lock b/bun.lock index 9bfcb008..449e7baf 100644 --- a/bun.lock +++ b/bun.lock @@ -16,10 +16,12 @@ "@inquirer/confirm": "catalog:", "@scalar/hono-api-reference": "catalog:", "@sentry/bun": "catalog:", + "@versia-server/api": "workspace:*", "@versia-server/config": "workspace:*", "@versia-server/kit": "workspace:*", "@versia-server/logging": "workspace:*", "@versia-server/tests": "workspace:*", + "@versia-server/worker": "workspace:*", "@versia/client": "workspace:*", "@versia/sdk": "workspace:*", "altcha-lib": "catalog:", @@ -108,7 +110,7 @@ "ip-matching": "catalog:", "iso-639-1": "catalog:", "jose": "catalog:", - "magic-regexp": "catalog:", + "oauth4webapi": "catalog:", "qs": "catalog:", "sharp": "catalog:", "string-comparison": "catalog:", @@ -145,20 +147,7 @@ "zod-validation-error": "catalog:", }, }, - "packages/logging": { - "name": "@versia-server/logging", - "version": "0.0.1", - "dependencies": { - "@logtape/file": "catalog:", - "@logtape/logtape": "catalog:", - "@logtape/otel": "catalog:", - "@logtape/sentry": "catalog:", - "@sentry/bun": "catalog:", - "@versia-server/config": "workspace:*", - "chalk": "catalog:", - }, - }, - "packages/plugin-kit": { + "packages/kit": { "name": "@versia-server/kit", "version": "0.0.0", "dependencies": { @@ -185,12 +174,26 @@ "mitt": "catalog:", "qs": "catalog:", "sharp": "catalog:", + "sonic-channel": "catalog:", "web-push": "catalog:", "zod": "catalog:", "zod-to-json-schema": "catalog:", "zod-validation-error": "catalog:", }, }, + "packages/logging": { + "name": "@versia-server/logging", + "version": "0.0.1", + "dependencies": { + "@logtape/file": "catalog:", + "@logtape/logtape": "catalog:", + "@logtape/otel": "catalog:", + "@logtape/sentry": "catalog:", + "@sentry/bun": "catalog:", + "@versia-server/config": "workspace:*", + "chalk": "catalog:", + }, + }, "packages/sdk": { "name": "@versia/sdk", "version": "0.0.1", @@ -792,7 +795,7 @@ "@versia-server/config": ["@versia-server/config@workspace:packages/config"], - "@versia-server/kit": ["@versia-server/kit@workspace:packages/plugin-kit"], + "@versia-server/kit": ["@versia-server/kit@workspace:packages/kit"], "@versia-server/logging": ["@versia-server/logging@workspace:packages/logging"], diff --git a/classes/search/search-manager.ts b/classes/search/search-manager.ts index 65fcbb7f..e69de29b 100644 --- a/classes/search/search-manager.ts +++ b/classes/search/search-manager.ts @@ -1,309 +0,0 @@ -/** - * @file search-manager.ts - * @description Sonic search integration for indexing and searching accounts and statuses - */ - -import { config } from "@versia-server/config"; -import { db, Note, User } from "@versia-server/kit/db"; -import { sonicLogger } from "@versia-server/logging"; -import type { SQL, ValueOrArray } from "drizzle-orm"; -import { - Ingest as SonicChannelIngest, - Search as SonicChannelSearch, -} from "sonic-channel"; - -/** - * Enum for Sonic index types - */ -export enum SonicIndexType { - Accounts = "accounts", - Statuses = "statuses", -} - -/** - * Class for managing Sonic search operations - */ -export class SonicSearchManager { - private searchChannel: SonicChannelSearch; - private ingestChannel: SonicChannelIngest; - private connected = false; - - /** - * @param config Configuration for Sonic - */ - public constructor() { - if (!config.search.sonic) { - throw new Error("Sonic configuration is missing"); - } - - this.searchChannel = new SonicChannelSearch({ - host: config.search.sonic.host, - port: config.search.sonic.port, - auth: config.search.sonic.password, - }); - - this.ingestChannel = new SonicChannelIngest({ - host: config.search.sonic.host, - port: config.search.sonic.port, - auth: config.search.sonic.password, - }); - } - - /** - * Connect to Sonic - */ - public async connect(silent = false): Promise { - if (!config.search.enabled) { - !silent && sonicLogger.info`Sonic search is disabled`; - return; - } - - if (this.connected) { - return; - } - - !silent && sonicLogger.info`Connecting to Sonic...`; - - // Connect to Sonic - await new Promise((resolve, reject) => { - this.searchChannel.connect({ - connected: (): void => { - !silent && - sonicLogger.info`Connected to Sonic Search Channel`; - resolve(true); - }, - disconnected: (): void => - sonicLogger.error`Disconnected from Sonic Search Channel. You might be using an incorrect password.`, - timeout: (): void => - sonicLogger.error`Sonic Search Channel connection timed out`, - retrying: (): void => - sonicLogger.warn`Retrying connection to Sonic Search Channel`, - error: (error): void => { - sonicLogger.error`Failed to connect to Sonic Search Channel: ${error}`; - reject(error); - }, - }); - }); - - await new Promise((resolve, reject) => { - this.ingestChannel.connect({ - connected: (): void => { - !silent && - sonicLogger.info`Connected to Sonic Ingest Channel`; - resolve(true); - }, - disconnected: (): void => - sonicLogger.error`Disconnected from Sonic Ingest Channel`, - timeout: (): void => - sonicLogger.error`Sonic Ingest Channel connection timed out`, - retrying: (): void => - sonicLogger.warn`Retrying connection to Sonic Ingest Channel`, - error: (error): void => { - sonicLogger.error`Failed to connect to Sonic Ingest Channel: ${error}`; - reject(error); - }, - }); - }); - - try { - await Promise.all([ - this.searchChannel.ping(), - this.ingestChannel.ping(), - ]); - this.connected = true; - !silent && sonicLogger.info`Connected to Sonic`; - } catch (error) { - sonicLogger.fatal`Error while connecting to Sonic: ${error}`; - throw error; - } - } - - /** - * Add a user to Sonic - * @param user User to add - */ - public async addUser(user: User): Promise { - if (!config.search.enabled) { - return; - } - - try { - await this.ingestChannel.push( - SonicIndexType.Accounts, - "users", - user.id, - `${user.data.username} ${user.data.displayName} ${user.data.note}`, - ); - } catch (error) { - sonicLogger.error`Failed to add user to Sonic: ${error}`; - } - } - - /** - * Get a batch of accounts from the database - * @param n Batch number - * @param batchSize Size of the batch - */ - private static getNthDatabaseAccountBatch( - n: number, - batchSize = 1000, - ): Promise[]> { - return db.query.Users.findMany({ - offset: n * batchSize, - limit: batchSize, - columns: { - id: true, - username: true, - displayName: true, - note: true, - createdAt: true, - }, - orderBy: (user, { asc }): ValueOrArray => asc(user.createdAt), - }); - } - - /** - * Get a batch of statuses from the database - * @param n Batch number - * @param batchSize Size of the batch - */ - private static getNthDatabaseStatusBatch( - n: number, - batchSize = 1000, - ): Promise[]> { - return db.query.Notes.findMany({ - offset: n * batchSize, - limit: batchSize, - columns: { - id: true, - content: true, - createdAt: true, - }, - orderBy: (status, { asc }): ValueOrArray => - asc(status.createdAt), - }); - } - - /** - * Rebuild search indexes - * @param indexes Indexes to rebuild - * @param batchSize Size of each batch - * @param progressCallback Callback for progress updates - */ - public async rebuildSearchIndexes( - indexes: SonicIndexType[], - batchSize = 100, - progressCallback?: (progress: number) => void, - ): Promise { - for (const index of indexes) { - if (index === SonicIndexType.Accounts) { - await this.rebuildAccountsIndex(batchSize, progressCallback); - } else if (index === SonicIndexType.Statuses) { - await this.rebuildStatusesIndex(batchSize, progressCallback); - } - } - } - - /** - * Rebuild accounts index - * @param batchSize Size of each batch - * @param progressCallback Callback for progress updates - */ - private async rebuildAccountsIndex( - batchSize: number, - progressCallback?: (progress: number) => void, - ): Promise { - const accountCount = await User.getCount(); - const batchCount = Math.ceil(accountCount / batchSize); - - for (let i = 0; i < batchCount; i++) { - const accounts = - await SonicSearchManager.getNthDatabaseAccountBatch( - i, - batchSize, - ); - await Promise.all( - accounts.map((account) => - this.ingestChannel.push( - SonicIndexType.Accounts, - "users", - account.id as string, - `${account.username} ${account.displayName} ${account.note}`, - ), - ), - ); - progressCallback?.((i + 1) / batchCount); - } - } - - /** - * Rebuild statuses index - * @param batchSize Size of each batch - * @param progressCallback Callback for progress updates - */ - private async rebuildStatusesIndex( - batchSize: number, - progressCallback?: (progress: number) => void, - ): Promise { - const statusCount = await Note.getCount(); - const batchCount = Math.ceil(statusCount / batchSize); - - for (let i = 0; i < batchCount; i++) { - const statuses = await SonicSearchManager.getNthDatabaseStatusBatch( - i, - batchSize, - ); - await Promise.all( - statuses.map((status) => - this.ingestChannel.push( - SonicIndexType.Statuses, - "notes", - status.id as string, - status.content as string, - ), - ), - ); - progressCallback?.((i + 1) / batchCount); - } - } - - /** - * Search for accounts - * @param query Search query - * @param limit Maximum number of results - * @param offset Offset for pagination - */ - public searchAccounts( - query: string, - limit = 10, - offset = 0, - ): Promise { - return this.searchChannel.query( - SonicIndexType.Accounts, - "users", - query, - { limit, offset }, - ); - } - - /** - * Search for statuses - * @param query Search query - * @param limit Maximum number of results - * @param offset Offset for pagination - */ - public searchStatuses( - query: string, - limit = 10, - offset = 0, - ): Promise { - return this.searchChannel.query( - SonicIndexType.Statuses, - "notes", - query, - { limit, offset }, - ); - } -} - -export const searchManager = new SonicSearchManager(); diff --git a/cli/index.ts b/cli/index.ts index daa7a402..4db09ca8 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -4,8 +4,8 @@ import { helpPlugin } from "@clerc/plugin-help"; import { notFoundPlugin } from "@clerc/plugin-not-found"; import { versionPlugin } from "@clerc/plugin-version"; import { setupDatabase } from "@versia-server/kit/db"; +import { searchManager } from "@versia-server/kit/search"; import { Clerc } from "clerc"; -import { searchManager } from "~/classes/search/search-manager.ts"; import pkg from "../package.json" with { type: "json" }; import { rebuildIndexCommand } from "./index/rebuild.ts"; import { refetchInstanceCommand } from "./instance/refetch.ts"; diff --git a/cli/index/rebuild.ts b/cli/index/rebuild.ts index 4209e8c3..b3b6d424 100644 --- a/cli/index/rebuild.ts +++ b/cli/index/rebuild.ts @@ -1,12 +1,9 @@ import { config } from "@versia-server/config"; +import { SonicIndexType, searchManager } from "@versia-server/kit/search"; // @ts-expect-error - Root import is required or the Clec type definitions won't work // biome-ignore lint/correctness/noUnusedImports: Root import is required or the Clec type definitions won't work import { defineCommand, type Root } from "clerc"; import ora from "ora"; -import { - SonicIndexType, - searchManager, -} from "~/classes/search/search-manager.ts"; export const rebuildIndexCommand = defineCommand( { diff --git a/cli/user/create.ts b/cli/user/create.ts index 3e2b56aa..1a116cb1 100644 --- a/cli/user/create.ts +++ b/cli/user/create.ts @@ -1,5 +1,6 @@ import { config } from "@versia-server/config"; import { User } from "@versia-server/kit/db"; +import { searchManager } from "@versia-server/kit/search"; import { Users } from "@versia-server/kit/tables"; import chalk from "chalk"; // @ts-expect-error - Root import is required or the Clec type definitions won't work @@ -54,6 +55,9 @@ export const createUserCommand = defineCommand( isAdmin: admin, }); + // Add to search index + await searchManager.addUser(user); + if (!user) { throw new Error("Failed to create user."); } diff --git a/config/config b/config/config new file mode 120000 index 00000000..3ca249e0 --- /dev/null +++ b/config/config @@ -0,0 +1 @@ +../config \ No newline at end of file diff --git a/package.json b/package.json index 4766f263..3e128e94 100644 --- a/package.json +++ b/package.json @@ -118,14 +118,15 @@ "scripts": { "lint": "biome check .", "cloc": "cloc . --exclude-dir node_modules,dist,.output,.nuxt,meta,logs --exclude-ext sql,log,pem", - "wc": "find server database *.ts docs packages types utils drizzle tests -type f -print0 | wc -m --files0-from=-", "cli": "bun run cli/index.ts", - "check": "bunx tsc -p .", + "typecheck": "bunx tsc -p .", "test": "bun test", - "run-api": "bun run packages/api/build.ts && cd dist && ln -s ../config config && bun run packages/api/index.js", - "run-worker": "bun run packages/worker/build.ts && cd dist && ln -s ../config config && bun run packages/worker/index.js", - "dev": "bun run --hot packages/api/index.ts", - "worker:dev": "bun run --hot packages/worker/index.ts" + "build": "bun run --filter \"*\" build && bun run build.ts", + "detect-circular": "bunx madge --circular --extensions ts ./", + "run-api": "bun run build && bun run build.ts api && cd dist && ln -s ../config config && bun run api.js", + "run-worker": "bun run build && bun run build.ts worker && cd dist && ln -s ../config config && bun run worker.js", + "dev": "bun run --hot api.ts", + "worker:dev": "bun run --hot worker.ts" }, "trustedDependencies": [ "@biomejs/biome", @@ -171,6 +172,8 @@ "@versia-server/kit": "workspace:*", "@versia-server/tests": "workspace:*", "@versia-server/logging": "workspace:*", + "@versia-server/api": "workspace:*", + "@versia-server/worker": "workspace:*", "@versia/client": "workspace:*", "@versia/sdk": "workspace:*", "altcha-lib": "catalog:", diff --git a/packages/api/app.ts b/packages/api/app.ts index d9c5322d..58a3726f 100644 --- a/packages/api/app.ts +++ b/packages/api/app.ts @@ -1,4 +1,4 @@ -import { resolve } from "node:path"; +import { join } from "node:path"; import { Scalar } from "@scalar/hono-api-reference"; import { config } from "@versia-server/config"; import { ApiError } from "@versia-server/kit"; @@ -113,7 +113,7 @@ export const appFactory = async (): Promise> => { const loader = new PluginLoader(); const plugins = await loader.loadPlugins( - resolve("./plugins"), + join(import.meta.dir, "plugins"), config.plugins?.autoload ?? true, config.plugins?.overrides.enabled, config.plugins?.overrides.disabled, diff --git a/packages/api/build.ts b/packages/api/build.ts index c38fa947..51177502 100644 --- a/packages/api/build.ts +++ b/packages/api/build.ts @@ -1,5 +1,6 @@ import { readdir } from "node:fs/promises"; import { $, build } from "bun"; +import manifest from "./package.json" with { type: "json" }; import { routes } from "./routes.ts"; console.log("Building..."); @@ -11,10 +12,7 @@ const pluginDirs = await readdir("plugins", { withFileTypes: true }); await build({ entrypoints: [ - "packages/api/index.ts", - // HACK: Include to avoid cyclical import errors - "packages/config/index.ts", - "cli/index.ts", + ...Object.values(manifest.exports).map((entry) => entry.import), // Force Bun to include endpoints ...Object.values(routes), // Include all plugins @@ -25,43 +23,24 @@ await build({ outdir: "dist", target: "bun", splitting: true, - minify: false, - external: ["acorn", "@bull-board/ui"], + minify: true, + external: [ + ...Object.keys(manifest.dependencies).filter((dep) => + dep.startsWith("@versia"), + ), + "@bull-board/ui", + ], }); console.log("Copying files..."); -// Fix Bun build mistake -await $`sed -i 's/ProxiableUrl, url, sensitiveString, keyPair, exportedConfig/url, sensitiveString, keyPair, exportedConfig/g' dist/packages/config/*.js`; - -// Copy Drizzle stuff -await $`mkdir -p dist/packages/plugin-kit/tables`; -await $`cp -rL packages/plugin-kit/tables/migrations dist/packages/plugin-kit/tables`; - // Copy plugin manifests await $`cp plugins/openid/manifest.json dist/plugins/openid/manifest.json`; await $`mkdir -p dist/node_modules`; -// Copy Sharp to dist -await $`mkdir -p dist/node_modules/@img`; -await $`cp -rL node_modules/@img/sharp-libvips-linux* dist/node_modules/@img`; -await $`cp -rL node_modules/@img/sharp-linux* dist/node_modules/@img`; - -// Copy acorn to dist -await $`cp -rL node_modules/acorn dist/node_modules/acorn`; - // Copy bull-board to dist await $`mkdir -p dist/node_modules/@bull-board`; -await $`cp -rL node_modules/@bull-board/ui dist/node_modules/@bull-board/ui`; - -// Copy the Bee Movie script from pages -await $`cp beemovie.txt dist/beemovie.txt`; - -// Copy package.json -await $`cp package.json dist/package.json`; - -// Fixes issues with sharp -await $`cp -rL node_modules/detect-libc dist/node_modules/`; +await $`cp -rL ../../node_modules/@bull-board/ui dist/node_modules/@bull-board/ui`; console.log("Build complete!"); diff --git a/packages/api/package.json b/packages/api/package.json index 398db16f..2ce799d3 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -36,11 +36,19 @@ "scripts": { "dev": "bun run --hot index.ts", "build": "bun run build.ts", - "schema:generate": "bun run classes/config/to-json-schema.ts > config/config.schema.json && bun run packages/plugin-kit/json-schema.ts > packages/plugin-kit/manifest.schema.json", + "schema:generate": "bun run classes/config/to-json-schema.ts > config/config.schema.json && bun run packages/kit/json-schema.ts > packages/kit/manifest.schema.json", "docs:dev": "vitepress dev docs", "docs:build": "vitepress build docs", "docs:preview": "vitepress preview docs" }, + "exports": { + ".": { + "import": "./app.ts" + }, + "./setup": { + "import": "./setup.ts" + } + }, "dependencies": { "@versia-server/config": "workspace:*", "@versia-server/tests": "workspace:*", @@ -65,10 +73,10 @@ "hono-rate-limiter": "catalog:", "ip-matching": "catalog:", "qs": "catalog:", - "magic-regexp": "catalog:", "altcha-lib": "catalog:", "@hono/zod-validator": "catalog:", "zod-validation-error": "catalog:", - "confbox": "catalog:" + "confbox": "catalog:", + "oauth4webapi": "catalog:" } } diff --git a/plugins/openid/errors.ts b/packages/api/plugins/openid/errors.ts similarity index 100% rename from plugins/openid/errors.ts rename to packages/api/plugins/openid/errors.ts diff --git a/plugins/openid/index.ts b/packages/api/plugins/openid/index.ts similarity index 99% rename from plugins/openid/index.ts rename to packages/api/plugins/openid/index.ts index c06fc361..a0d7383c 100644 --- a/plugins/openid/index.ts +++ b/packages/api/plugins/openid/index.ts @@ -1,5 +1,5 @@ import { RolePermission } from "@versia/client/schemas"; -import { keyPair, sensitiveString, url } from "@versia-server/config/schema"; +import { keyPair, sensitiveString, url } from "@versia-server/config"; import { ApiError, Hooks, Plugin } from "@versia-server/kit"; import { User } from "@versia-server/kit/db"; import { getCookie } from "hono/cookie"; diff --git a/plugins/openid/manifest.json b/packages/api/plugins/openid/manifest.json similarity index 87% rename from plugins/openid/manifest.json rename to packages/api/plugins/openid/manifest.json index e6ac5dff..f6219620 100644 --- a/plugins/openid/manifest.json +++ b/packages/api/plugins/openid/manifest.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/versia-pub/server/refs/heads/main/packages/plugin-kit/manifest.schema.json", + "$schema": "https://raw.githubusercontent.com/versia-pub/server/refs/heads/main/packages/kit/manifest.schema.json", "name": "@versia/openid", "description": "OpenID authentication.", "version": "0.1.0", diff --git a/plugins/openid/routes/authorize.test.ts b/packages/api/plugins/openid/routes/authorize.test.ts similarity index 100% rename from plugins/openid/routes/authorize.test.ts rename to packages/api/plugins/openid/routes/authorize.test.ts diff --git a/plugins/openid/routes/authorize.ts b/packages/api/plugins/openid/routes/authorize.ts similarity index 100% rename from plugins/openid/routes/authorize.ts rename to packages/api/plugins/openid/routes/authorize.ts diff --git a/plugins/openid/routes/jwks.test.ts b/packages/api/plugins/openid/routes/jwks.test.ts similarity index 100% rename from plugins/openid/routes/jwks.test.ts rename to packages/api/plugins/openid/routes/jwks.test.ts diff --git a/plugins/openid/routes/jwks.ts b/packages/api/plugins/openid/routes/jwks.ts similarity index 100% rename from plugins/openid/routes/jwks.ts rename to packages/api/plugins/openid/routes/jwks.ts diff --git a/plugins/openid/routes/oauth/callback.ts b/packages/api/plugins/openid/routes/oauth/callback.ts similarity index 98% rename from plugins/openid/routes/oauth/callback.ts rename to packages/api/plugins/openid/routes/oauth/callback.ts index ec2f771b..4f0a0965 100644 --- a/plugins/openid/routes/oauth/callback.ts +++ b/packages/api/plugins/openid/routes/oauth/callback.ts @@ -5,6 +5,7 @@ import { import { ApiError } from "@versia-server/kit"; import { handleZodError } from "@versia-server/kit/api"; import { db, Media, Token, User } from "@versia-server/kit/db"; +import { searchManager } from "@versia-server/kit/search"; import { OpenIdAccounts, Users } from "@versia-server/kit/tables"; import { randomUUIDv7 } from "bun"; import { and, eq, isNull, type SQL } from "drizzle-orm"; @@ -242,6 +243,9 @@ export default (plugin: PluginType): void => { avatar: avatar ?? undefined, }); + // Add to search index + await searchManager.addUser(user); + // Link account await db.insert(OpenIdAccounts).values({ id: randomUUIDv7(), diff --git a/plugins/openid/routes/oauth/revoke.test.ts b/packages/api/plugins/openid/routes/oauth/revoke.test.ts similarity index 100% rename from plugins/openid/routes/oauth/revoke.test.ts rename to packages/api/plugins/openid/routes/oauth/revoke.test.ts diff --git a/plugins/openid/routes/oauth/revoke.ts b/packages/api/plugins/openid/routes/oauth/revoke.ts similarity index 100% rename from plugins/openid/routes/oauth/revoke.ts rename to packages/api/plugins/openid/routes/oauth/revoke.ts diff --git a/plugins/openid/routes/oauth/sso.ts b/packages/api/plugins/openid/routes/oauth/sso.ts similarity index 100% rename from plugins/openid/routes/oauth/sso.ts rename to packages/api/plugins/openid/routes/oauth/sso.ts diff --git a/plugins/openid/routes/oauth/token.test.ts b/packages/api/plugins/openid/routes/oauth/token.test.ts similarity index 100% rename from plugins/openid/routes/oauth/token.test.ts rename to packages/api/plugins/openid/routes/oauth/token.test.ts diff --git a/plugins/openid/routes/oauth/token.ts b/packages/api/plugins/openid/routes/oauth/token.ts similarity index 100% rename from plugins/openid/routes/oauth/token.ts rename to packages/api/plugins/openid/routes/oauth/token.ts diff --git a/plugins/openid/routes/sso/:id/index.test.ts b/packages/api/plugins/openid/routes/sso/:id/index.test.ts similarity index 100% rename from plugins/openid/routes/sso/:id/index.test.ts rename to packages/api/plugins/openid/routes/sso/:id/index.test.ts diff --git a/plugins/openid/routes/sso/:id/index.ts b/packages/api/plugins/openid/routes/sso/:id/index.ts similarity index 100% rename from plugins/openid/routes/sso/:id/index.ts rename to packages/api/plugins/openid/routes/sso/:id/index.ts diff --git a/plugins/openid/routes/sso/index.test.ts b/packages/api/plugins/openid/routes/sso/index.test.ts similarity index 100% rename from plugins/openid/routes/sso/index.test.ts rename to packages/api/plugins/openid/routes/sso/index.test.ts diff --git a/plugins/openid/routes/sso/index.ts b/packages/api/plugins/openid/routes/sso/index.ts similarity index 100% rename from plugins/openid/routes/sso/index.ts rename to packages/api/plugins/openid/routes/sso/index.ts diff --git a/plugins/openid/utils.ts b/packages/api/plugins/openid/utils.ts similarity index 100% rename from plugins/openid/utils.ts rename to packages/api/plugins/openid/utils.ts diff --git a/packages/api/routes.ts b/packages/api/routes.ts index 085c982c..ccc79e7b 100644 --- a/packages/api/routes.ts +++ b/packages/api/routes.ts @@ -1,8 +1,10 @@ +import { join } from "node:path"; import { FileSystemRouter } from "bun"; + // Returns the route filesystem path when given a URL export const routeMatcher = new FileSystemRouter({ style: "nextjs", - dir: "packages/api/routes", + dir: join(import.meta.dir, "routes"), fileExtensions: [".ts", ".js"], }); diff --git a/packages/api/routes/api/v1/accounts/index.ts b/packages/api/routes/api/v1/accounts/index.ts index 601277ca..66f0e5ef 100644 --- a/packages/api/routes/api/v1/accounts/index.ts +++ b/packages/api/routes/api/v1/accounts/index.ts @@ -9,6 +9,7 @@ import { 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 { and, eq, isNull } from "drizzle-orm"; import { describeRoute } from "hono-openapi"; @@ -419,11 +420,14 @@ export default apiRoute((app) => { ); } - await User.register(username, { + const user = await User.register(username, { password, email, }); + // Add to search index + await searchManager.addUser(user); + return context.text("", 200); }, ); diff --git a/packages/api/routes/api/v1/statuses/[id]/favourite.ts b/packages/api/routes/api/v1/statuses/[id]/favourite.ts index 64d555c5..535a9e30 100644 --- a/packages/api/routes/api/v1/statuses/[id]/favourite.ts +++ b/packages/api/routes/api/v1/statuses/[id]/favourite.ts @@ -39,7 +39,7 @@ export default apiRoute((app) => const { user } = context.get("auth"); const note = context.get("note"); - await user.like(note); + await note.like(user); await note.reload(user.id); diff --git a/packages/api/routes/api/v1/statuses/[id]/reactions/[name].ts b/packages/api/routes/api/v1/statuses/[id]/reactions/[name].ts index 810b6f02..6ced1821 100644 --- a/packages/api/routes/api/v1/statuses/[id]/reactions/[name].ts +++ b/packages/api/routes/api/v1/statuses/[id]/reactions/[name].ts @@ -110,7 +110,7 @@ export default apiRoute((app) => { emoji = unicodeEmoji; } - await user.react(note, emoji); + await note.react(user, emoji); // Reload note to get updated reactions await note.reload(user.id); @@ -204,7 +204,7 @@ export default apiRoute((app) => { emoji = unicodeEmoji; } - await user.unreact(note, emoji); + await note.unreact(user, emoji); // Reload note to get updated reactions await note.reload(user.id); diff --git a/packages/api/routes/api/v1/statuses/[id]/reblog.ts b/packages/api/routes/api/v1/statuses/[id]/reblog.ts index 97631350..66e8a8c8 100644 --- a/packages/api/routes/api/v1/statuses/[id]/reblog.ts +++ b/packages/api/routes/api/v1/statuses/[id]/reblog.ts @@ -55,7 +55,7 @@ export default apiRoute((app) => const { user } = context.get("auth"); const note = context.get("note"); - const reblog = await user.reblog(note, visibility); + const reblog = await note.reblog(user, visibility); return context.json(await reblog.toApi(user), 200); }, diff --git a/packages/api/routes/api/v1/statuses/[id]/unfavourite.ts b/packages/api/routes/api/v1/statuses/[id]/unfavourite.ts index 107203dc..5c016461 100644 --- a/packages/api/routes/api/v1/statuses/[id]/unfavourite.ts +++ b/packages/api/routes/api/v1/statuses/[id]/unfavourite.ts @@ -40,7 +40,7 @@ export default apiRoute((app) => const { user } = context.get("auth"); const note = context.get("note"); - await user.unlike(note); + await note.unlike(user); await note.reload(user.id); diff --git a/packages/api/routes/api/v1/statuses/[id]/unreblog.ts b/packages/api/routes/api/v1/statuses/[id]/unreblog.ts index bfb7225a..b19ed7af 100644 --- a/packages/api/routes/api/v1/statuses/[id]/unreblog.ts +++ b/packages/api/routes/api/v1/statuses/[id]/unreblog.ts @@ -40,7 +40,7 @@ export default apiRoute((app) => const { user } = context.get("auth"); const note = context.get("note"); - await user.unreblog(note); + await note.unreblog(user); const newNote = await Note.fromId(note.data.id, user.id); diff --git a/packages/api/routes/api/v2/search/index.ts b/packages/api/routes/api/v2/search/index.ts index 0d2656a2..da2eb7a4 100644 --- a/packages/api/routes/api/v2/search/index.ts +++ b/packages/api/routes/api/v2/search/index.ts @@ -11,12 +11,12 @@ import { ApiError } from "@versia-server/kit"; import { apiRoute, auth, handleZodError } from "@versia-server/kit/api"; import { db, Note, User } from "@versia-server/kit/db"; import { parseUserAddress } from "@versia-server/kit/parsers"; +import { searchManager } from "@versia-server/kit/search"; import { Instances, Notes, Users } from "@versia-server/kit/tables"; import { and, eq, inArray, isNull, sql } from "drizzle-orm"; import { describeRoute } from "hono-openapi"; import { resolver, validator } from "hono-openapi/zod"; import { z } from "zod"; -import { searchManager } from "~/classes/search/search-manager"; export default apiRoute((app) => app.get( diff --git a/packages/api/setup.ts b/packages/api/setup.ts index 09571c79..80dfc4cf 100644 --- a/packages/api/setup.ts +++ b/packages/api/setup.ts @@ -1,8 +1,8 @@ import { config } from "@versia-server/config"; import { Note, setupDatabase } from "@versia-server/kit/db"; import { connection } from "@versia-server/kit/redis"; +import { searchManager } from "@versia-server/kit/search"; import { serverLogger } from "@versia-server/logging"; -import { searchManager } from "../../classes/search/search-manager.ts"; const timeAtStart = performance.now(); diff --git a/packages/client/build.ts b/packages/client/build.ts new file mode 100644 index 00000000..908e5d50 --- /dev/null +++ b/packages/client/build.ts @@ -0,0 +1,19 @@ +import { $, build } from "bun"; +import manifest from "./package.json" with { type: "json" }; + +console.log("Building..."); + +await $`rm -rf dist && mkdir dist`; + +await build({ + entrypoints: Object.values(manifest.exports).map((entry) => entry.import), + outdir: "dist", + target: "bun", + splitting: true, + minify: true, + external: [ + ...Object.keys(manifest.dependencies).filter((dep) => + dep.startsWith("@versia"), + ), + ], +}); diff --git a/packages/client/package.json b/packages/client/package.json index 652dc49b..54d0b718 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -7,6 +7,9 @@ "name": "Jesse Wierzbinski (CPlusPatch)", "url": "https://cpluspatch.com" }, + "scripts": { + "build": "bun run build.ts" + }, "readme": "README.md", "repository": { "type": "git", @@ -41,12 +44,10 @@ }, "exports": { ".": { - "import": "./index.ts", - "default": "./index.ts" + "import": "./index.ts" }, "./schemas": { - "import": "./schemas.ts", - "default": "./schemas.ts" + "import": "./schemas.ts" } }, "funding": { diff --git a/packages/config/build.ts b/packages/config/build.ts new file mode 100644 index 00000000..908e5d50 --- /dev/null +++ b/packages/config/build.ts @@ -0,0 +1,19 @@ +import { $, build } from "bun"; +import manifest from "./package.json" with { type: "json" }; + +console.log("Building..."); + +await $`rm -rf dist && mkdir dist`; + +await build({ + entrypoints: Object.values(manifest.exports).map((entry) => entry.import), + outdir: "dist", + target: "bun", + splitting: true, + minify: true, + external: [ + ...Object.keys(manifest.dependencies).filter((dep) => + dep.startsWith("@versia"), + ), + ], +}); diff --git a/packages/config/index.ts b/packages/config/index.ts index ae380eba..6d859989 100644 --- a/packages/config/index.ts +++ b/packages/config/index.ts @@ -1,9 +1,829 @@ -import { env, file } from "bun"; +import { RolePermission } from "@versia/client/schemas"; +import { type BunFile, env, file } from "bun"; import chalk from "chalk"; import { parseTOML } from "confbox"; -import type { z } from "zod"; +import ISO6391 from "iso-639-1"; +import { types as mimeTypes } from "mime-types"; +import { generateVAPIDKeys } from "web-push"; +import { z } from "zod"; import { fromZodError } from "zod-validation-error"; -import { ConfigSchema } from "./schema.ts"; + +export class ProxiableUrl extends URL { + private isAllowedOrigin(): boolean { + const allowedOrigins: URL[] = [exportedConfig.http.base_url].concat( + exportedConfig.s3?.public_url ?? [], + ); + + return allowedOrigins.some((origin) => + this.hostname.endsWith(origin.hostname), + ); + } + + public get proxied(): string { + // Don't proxy from CDN and self, since those sources are trusted + if (this.isAllowedOrigin()) { + return this.href; + } + + const urlAsBase64Url = Buffer.from(this.href).toString("base64url"); + + return new URL( + `/media/proxy/${urlAsBase64Url}`, + exportedConfig.http.base_url, + ).href; + } +} + +export const DEFAULT_ROLES = [ + RolePermission.ManageOwnNotes, + RolePermission.ViewNotes, + RolePermission.ViewNoteLikes, + RolePermission.ViewNoteBoosts, + RolePermission.ManageOwnAccount, + RolePermission.ViewAccountFollows, + RolePermission.ManageOwnLikes, + RolePermission.ManageOwnBoosts, + RolePermission.ViewAccounts, + RolePermission.ManageOwnEmojis, + RolePermission.ViewReactions, + RolePermission.ManageOwnReactions, + RolePermission.ViewEmojis, + RolePermission.ManageOwnMedia, + RolePermission.ManageOwnBlocks, + RolePermission.ManageOwnFilters, + RolePermission.ManageOwnMutes, + RolePermission.ManageOwnReports, + RolePermission.ManageOwnSettings, + RolePermission.ManageOwnNotifications, + RolePermission.ManageOwnFollows, + RolePermission.ManageOwnApps, + RolePermission.Search, + RolePermission.UsePushNotifications, + RolePermission.ViewPublicTimelines, + RolePermission.ViewPrivateTimelines, + RolePermission.OAuth, +]; + +export const ADMIN_ROLES = [ + ...DEFAULT_ROLES, + RolePermission.ManageNotes, + RolePermission.ManageAccounts, + RolePermission.ManageLikes, + RolePermission.ManageBoosts, + RolePermission.ManageEmojis, + RolePermission.ManageReactions, + RolePermission.ManageMedia, + RolePermission.ManageBlocks, + RolePermission.ManageFilters, + RolePermission.ManageMutes, + RolePermission.ManageReports, + RolePermission.ManageSettings, + RolePermission.ManageRoles, + RolePermission.ManageNotifications, + RolePermission.ManageFollows, + RolePermission.Impersonate, + RolePermission.IgnoreRateLimits, + RolePermission.ManageInstance, + RolePermission.ManageInstanceFederation, + RolePermission.ManageInstanceSettings, +]; + +export enum MediaBackendType { + Local = "local", + S3 = "s3", +} + +// Need to declare this here instead of importing it otherwise we get cyclical import errors +export const iso631 = z.enum(ISO6391.getAllCodes() as [string, ...string[]]); + +export const urlPath = z + .string() + .trim() + .min(1) + // Remove trailing slashes, but keep the root slash + .transform((arg) => (arg === "/" ? arg : arg.replace(/\/$/, ""))); + +export const url = z + .string() + .trim() + .min(1) + .refine((arg) => URL.canParse(arg), "Invalid url") + .transform((arg) => new ProxiableUrl(arg)); + +export const unixPort = z + .number() + .int() + .min(1) + .max(2 ** 16 - 1); + +const fileFromPathString = (text: string): BunFile => file(text.slice(5)); + +// Not using .ip() because we allow CIDR ranges and wildcards and such +const ip = z + .string() + .describe("An IPv6/v4 address or CIDR range. Wildcards are also allowed"); + +const regex = z + .string() + .transform((arg) => new RegExp(arg)) + .describe("JavaScript regular expression"); + +export const sensitiveString = z + .string() + .refine( + (text) => + text.startsWith("PATH:") ? fileFromPathString(text).exists() : true, + (text) => ({ + message: `Path ${ + fileFromPathString(text).name + } does not exist, is a directory or is not accessible`, + }), + ) + .transform((text) => + text.startsWith("PATH:") ? fileFromPathString(text).text() : text, + ) + .describe("You can use PATH:/path/to/file to load this value from a file"); + +export const filePathString = z + .string() + .transform((s) => file(s)) + .refine( + (file) => file.exists(), + (file) => ({ + message: `Path ${file.name} does not exist, is a directory or is not accessible`, + }), + ) + .transform(async (file) => ({ + content: await file.text(), + file, + })) + .describe("This value must be a file path"); + +export const keyPair = z + .strictObject({ + public: sensitiveString.optional(), + private: sensitiveString.optional(), + }) + .optional() + .transform(async (k, ctx) => { + if (!(k?.public && k?.private)) { + const keys = await crypto.subtle.generateKey("Ed25519", true, [ + "sign", + "verify", + ]); + + const privateKey = Buffer.from( + await crypto.subtle.exportKey("pkcs8", keys.privateKey), + ).toString("base64"); + + const publicKey = Buffer.from( + await crypto.subtle.exportKey("spki", keys.publicKey), + ).toString("base64"); + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Public and private keys are not set. Here are generated keys for you to copy.\n\nPublic: ${publicKey}\nPrivate: ${privateKey}`, + }); + + return z.NEVER; + } + + let publicKey: CryptoKey; + let privateKey: CryptoKey; + + try { + publicKey = await crypto.subtle.importKey( + "spki", + Buffer.from(k.public, "base64"), + "Ed25519", + true, + ["verify"], + ); + } catch { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Public key is invalid", + }); + + return z.NEVER; + } + + try { + privateKey = await crypto.subtle.importKey( + "pkcs8", + Buffer.from(k.private, "base64"), + "Ed25519", + true, + ["sign"], + ); + } catch { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Private key is invalid", + }); + + return z.NEVER; + } + + return { + public: publicKey, + private: privateKey, + }; + }); + +export const vapidKeyPair = z + .strictObject({ + public: sensitiveString.optional(), + private: sensitiveString.optional(), + }) + .optional() + .transform((k, ctx) => { + if (!(k?.public && k?.private)) { + const keys = generateVAPIDKeys(); + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `VAPID keys are not set. Here are generated keys for you to copy.\n\nPublic: ${keys.publicKey}\nPrivate: ${keys.privateKey}`, + }); + + return z.NEVER; + } + + return k; + }); + +export const hmacKey = sensitiveString.transform(async (text, ctx) => { + if (!text) { + const key = await crypto.subtle.generateKey( + { + name: "HMAC", + hash: "SHA-256", + }, + true, + ["sign"], + ); + + const exported = await crypto.subtle.exportKey("raw", key); + + const base64 = Buffer.from(exported).toString("base64"); + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `HMAC key is not set. Here is a generated key for you to copy: ${base64}`, + }); + + return z.NEVER; + } + + try { + await crypto.subtle.importKey( + "raw", + Buffer.from(text, "base64"), + { + name: "HMAC", + hash: "SHA-256", + }, + true, + ["sign"], + ); + } catch { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "HMAC key is invalid", + }); + + return z.NEVER; + } + + return text; +}); + +export const ConfigSchema = z + .strictObject({ + postgres: z + .strictObject({ + host: z.string().min(1).default("localhost"), + port: unixPort.default(5432), + username: z.string().min(1), + password: sensitiveString.default(""), + database: z.string().min(1).default("versia"), + replicas: z + .array( + z.strictObject({ + host: z.string().min(1), + port: unixPort.default(5432), + username: z.string().min(1), + password: sensitiveString.default(""), + database: z.string().min(1).default("versia"), + }), + ) + .describe("Additional read-only replicas") + .default([]), + }) + .describe("PostgreSQL database configuration"), + redis: z + .strictObject({ + queue: z + .strictObject({ + host: z.string().min(1).default("localhost"), + port: unixPort.default(6379), + password: sensitiveString.default(""), + database: z.number().int().default(0), + }) + .describe("A Redis database used for managing queues."), + cache: z + .strictObject({ + host: z.string().min(1).default("localhost"), + port: unixPort.default(6379), + password: sensitiveString.default(""), + database: z.number().int().default(1), + }) + .optional() + .describe( + "A Redis database used for caching SQL queries. Optional.", + ), + }) + .describe("Redis configuration. Used for queues and caching."), + search: z + .strictObject({ + enabled: z + .boolean() + .default(false) + .describe("Enable indexing and searching?"), + sonic: z + .strictObject({ + host: z.string().min(1).default("localhost"), + port: unixPort.default(7700), + password: sensitiveString, + }) + .describe("Sonic database configuration") + .optional(), + }) + .refine( + (o) => !o.enabled || o.sonic, + "When search is enabled, Sonic configuration must be set", + ) + .describe("Search and indexing configuration"), + registration: z.strictObject({ + allow: z + .boolean() + .default(true) + .describe("Can users sign up freely?"), + require_approval: z.boolean().default(false), + message: z + .string() + .optional() + .describe( + "Message to show to users when registration is disabled", + ), + }), + http: z.strictObject({ + base_url: url.describe( + "URL that the instance will be accessible at", + ), + bind: z.string().min(1).default("0.0.0.0"), + bind_port: unixPort.default(8080), + banned_ips: z.array(ip).default([]), + banned_user_agents: z.array(regex).default([]), + proxy_address: url + .optional() + .describe("URL to an eventual HTTP proxy") + .refine(async (url) => { + if (!url) { + return true; + } + + // Test the proxy + const response = await fetch( + "https://api.ipify.org?format=json", + { + proxy: url.origin, + }, + ); + + return response.ok; + }, "The HTTP proxy address is not reachable"), + tls: z + .strictObject({ + key: filePathString, + cert: filePathString, + passphrase: sensitiveString.optional(), + ca: filePathString.optional(), + }) + .describe( + "TLS configuration. You should probably be using a reverse proxy instead of this", + ) + .optional(), + }), + frontend: z.strictObject({ + enabled: z.boolean().default(true), + path: z.string().default(env.VERSIA_FRONTEND_PATH || "frontend"), + routes: z.strictObject({ + home: urlPath.default("/"), + login: urlPath.default("/oauth/authorize"), + consent: urlPath.default("/oauth/consent"), + register: urlPath.default("/register"), + password_reset: urlPath.default("/oauth/reset"), + }), + settings: z.record(z.string(), z.any()).default({}), + }), + email: z + .strictObject({ + send_emails: z.boolean().default(false), + smtp: z + .strictObject({ + server: z.string().min(1), + port: unixPort.default(465), + username: z.string().min(1), + password: sensitiveString.optional(), + tls: z.boolean().default(true), + }) + .optional(), + }) + .refine( + (o) => o.send_emails || !o.smtp, + "When send_emails is enabled, SMTP configuration must be set", + ), + media: z.strictObject({ + backend: z + .nativeEnum(MediaBackendType) + .default(MediaBackendType.Local), + uploads_path: z.string().min(1).default("uploads"), + conversion: z.strictObject({ + convert_images: z.boolean().default(false), + convert_to: z.string().default("image/webp"), + convert_vectors: z.boolean().default(false), + }), + }), + s3: z + .strictObject({ + endpoint: url, + access_key: sensitiveString, + secret_access_key: sensitiveString, + region: z.string().optional(), + bucket_name: z.string().optional(), + public_url: url.describe( + "Public URL that uploaded media will be accessible at", + ), + path: z.string().optional(), + path_style: z.boolean().default(true), + }) + .optional(), + validation: z.strictObject({ + accounts: z.strictObject({ + max_displayname_characters: z + .number() + .int() + .nonnegative() + .default(50), + max_username_characters: z + .number() + .int() + .nonnegative() + .default(30), + max_bio_characters: z + .number() + .int() + .nonnegative() + .default(5000), + max_avatar_bytes: z + .number() + .int() + .nonnegative() + .default(5_000_000), + max_header_bytes: z + .number() + .int() + .nonnegative() + .default(5_000_000), + disallowed_usernames: z + .array(regex) + .default([ + "well-known", + "about", + "activities", + "api", + "auth", + "dev", + "inbox", + "internal", + "main", + "media", + "nodeinfo", + "notice", + "oauth", + "objects", + "proxy", + "push", + "registration", + "relay", + "settings", + "status", + "tag", + "users", + "web", + "search", + "mfa", + ]), + max_field_count: z.number().int().default(10), + max_field_name_characters: z.number().int().default(1000), + max_field_value_characters: z.number().int().default(1000), + max_pinned_notes: z.number().int().default(20), + }), + notes: z.strictObject({ + max_characters: z.number().int().nonnegative().default(5000), + allowed_url_schemes: z + .array(z.string()) + .default([ + "http", + "https", + "ftp", + "dat", + "dweb", + "gopher", + "hyper", + "ipfs", + "ipns", + "irc", + "xmpp", + "ircs", + "magnet", + "mailto", + "mumble", + "ssb", + "gemini", + ]), + max_attachments: z.number().int().default(16), + }), + media: z.strictObject({ + max_bytes: z.number().int().nonnegative().default(40_000_000), + max_description_characters: z + .number() + .int() + .nonnegative() + .default(1000), + allowed_mime_types: z + .array(z.string()) + .default(Object.values(mimeTypes)), + }), + emojis: z.strictObject({ + max_bytes: z.number().int().nonnegative().default(1_000_000), + max_shortcode_characters: z + .number() + .int() + .nonnegative() + .default(100), + max_description_characters: z + .number() + .int() + .nonnegative() + .default(1_000), + }), + polls: z.strictObject({ + max_options: z.number().int().nonnegative().default(20), + max_option_characters: z + .number() + .int() + .nonnegative() + .default(500), + min_duration_seconds: z + .number() + .int() + .nonnegative() + .default(60), + max_duration_seconds: z + .number() + .int() + .nonnegative() + .default(100 * 24 * 60 * 60), + }), + emails: z.strictObject({ + disallow_tempmail: z + .boolean() + .default(false) + .describe("Blocks over 10,000 common tempmail domains"), + disallowed_domains: z.array(regex).default([]), + }), + challenges: z + .strictObject({ + difficulty: z.number().int().positive().default(50000), + expiration: z.number().int().positive().default(300), + key: hmacKey, + }) + .optional() + .describe( + "CAPTCHA challenge configuration. Challenges are disabled if not provided.", + ), + filters: z + .strictObject({ + note_content: z.array(regex).default([]), + emoji_shortcode: z.array(regex).default([]), + username: z.array(regex).default([]), + displayname: z.array(regex).default([]), + bio: z.array(regex).default([]), + }) + .describe( + "Block content that matches these regular expressions", + ), + }), + notifications: z.strictObject({ + push: z + .strictObject({ + vapid_keys: vapidKeyPair, + subject: z + .string() + .optional() + .describe( + "Subject field embedded in the push notification. Example: 'mailto:contact@example.com'", + ), + }) + .describe( + "Web Push Notifications configuration. Leave out to disable.", + ) + .optional(), + }), + defaults: z.strictObject({ + visibility: z + .enum(["public", "unlisted", "private", "direct"]) + .default("public"), + language: z.string().default("en"), + avatar: url.optional(), + header: url.optional(), + placeholder_style: z + .string() + .default("thumbs") + .describe("A style name from https://www.dicebear.com/styles"), + }), + federation: z.strictObject({ + blocked: z.array(z.string()).default([]), + followers_only: z.array(z.string()).default([]), + discard: z.strictObject({ + reports: z.array(z.string()).default([]), + deletes: z.array(z.string()).default([]), + updates: z.array(z.string()).default([]), + media: z.array(z.string()).default([]), + follows: z.array(z.string()).default([]), + likes: z.array(z.string()).default([]), + reactions: z.array(z.string()).default([]), + banners: z.array(z.string()).default([]), + avatars: z.array(z.string()).default([]), + }), + bridge: z + .strictObject({ + software: z.enum(["versia-ap"]).or(z.string()), + allowed_ips: z.array(ip).default([]), + token: sensitiveString, + url, + }) + .optional(), + }), + queues: z.record( + z.enum(["delivery", "inbox", "fetch", "push", "media"]), + z.strictObject({ + remove_after_complete_seconds: z + .number() + .int() + .nonnegative() + // 1 year + .default(60 * 60 * 24 * 365), + remove_after_failure_seconds: z + .number() + .int() + .nonnegative() + // 1 year + .default(60 * 60 * 24 * 365), + }), + ), + instance: z.strictObject({ + name: z.string().min(1).default("Versia Server"), + description: z.string().min(1).default("A Versia instance"), + extended_description_path: filePathString.optional(), + tos_path: filePathString.optional(), + privacy_policy_path: filePathString.optional(), + branding: z.strictObject({ + logo: url.optional(), + banner: url.optional(), + }), + languages: z + .array(iso631) + .describe("Primary instance languages. ISO 639-1 codes."), + contact: z.strictObject({ + email: z + .string() + .email() + .describe("Email to contact the instance administration"), + }), + rules: z + .array( + z.strictObject({ + text: z + .string() + .min(1) + .max(255) + .describe("Short description of the rule"), + hint: z + .string() + .min(1) + .max(4096) + .optional() + .describe( + "Longer version of the rule with additional information", + ), + }), + ) + .default([]), + keys: keyPair, + }), + permissions: z.strictObject({ + anonymous: z + .array(z.nativeEnum(RolePermission)) + .default(DEFAULT_ROLES), + default: z + .array(z.nativeEnum(RolePermission)) + .default(DEFAULT_ROLES), + admin: z.array(z.nativeEnum(RolePermission)).default(ADMIN_ROLES), + }), + logging: z.strictObject({ + file: z + .strictObject({ + path: z.string().default("logs/versia.log"), + rotation: z + .strictObject({ + max_size: z + .number() + .int() + .nonnegative() + .default(10_000_000), // 10 MB + max_files: z + .number() + .int() + .nonnegative() + .default(10), + }) + .optional(), + log_level: z + .enum([ + "trace", + "debug", + "info", + "warning", + "error", + "fatal", + ]) + .default("info"), + }) + .optional(), + sentry: z + .strictObject({ + dsn: url, + debug: z.boolean().default(false), + sample_rate: z.number().min(0).max(1.0).default(1.0), + traces_sample_rate: z.number().min(0).max(1.0).default(1.0), + trace_propagation_targets: z.array(z.string()).default([]), + max_breadcrumbs: z.number().default(100), + environment: z.string().optional(), + log_level: z + .enum([ + "trace", + "debug", + "info", + "warning", + "error", + "fatal", + ]) + .default("info"), + }) + .optional(), + log_level: z + .enum(["trace", "debug", "info", "warning", "error", "fatal"]) + .default("info"), + }), + debug: z + .strictObject({ + federation: z.boolean().default(false), + }) + .optional(), + plugins: z.strictObject({ + autoload: z.boolean().default(true), + overrides: z + .strictObject({ + enabled: z.array(z.string()).default([]), + disabled: z.array(z.string()).default([]), + }) + .refine( + // Only one of enabled or disabled can be set + (arg) => + arg.enabled.length === 0 || arg.disabled.length === 0, + "Only one of enabled or disabled can be set", + ), + config: z.record(z.string(), z.any()).optional(), + }), + }) + .refine( + // If media backend is S3, s3 config must be set + (arg) => arg.media.backend === MediaBackendType.Local || !!arg.s3, + "When media backend is S3, S3 configuration must be set", + ); const CONFIG_LOCATION = env.CONFIG_LOCATION ?? "./config/config.toml"; const configFile = file(CONFIG_LOCATION); @@ -15,7 +835,7 @@ if (!(await configFile.exists())) { } const configText = await configFile.text(); -const config = await parseTOML>(configText); +const config = parseTOML>(configText); const parsed = await ConfigSchema.safeParseAsync(config); @@ -38,5 +858,4 @@ if (!parsed.success) { const exportedConfig = parsed.data; -export { ProxiableUrl } from "./url.ts"; export { exportedConfig as config }; diff --git a/packages/config/package.json b/packages/config/package.json index 5dc26f86..3e053c12 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -4,14 +4,12 @@ "type": "module", "version": "0.0.1", "private": true, + "scripts": { + "build": "bun run build.ts" + }, "exports": { ".": { - "import": "./index.ts", - "default": "./index.ts" - }, - "./schema": { - "import": "./schema.ts", - "default": "./schema.ts" + "import": "./index.ts" } }, "dependencies": { diff --git a/packages/config/schema.ts b/packages/config/schema.ts deleted file mode 100644 index 65b8bd17..00000000 --- a/packages/config/schema.ts +++ /dev/null @@ -1,798 +0,0 @@ -import { RolePermission } from "@versia/client/schemas"; -import { type BunFile, env, file } from "bun"; -import ISO6391 from "iso-639-1"; -import { types as mimeTypes } from "mime-types"; -import { generateVAPIDKeys } from "web-push"; -import { z } from "zod"; -import { ProxiableUrl } from "./url.ts"; - -export const DEFAULT_ROLES = [ - RolePermission.ManageOwnNotes, - RolePermission.ViewNotes, - RolePermission.ViewNoteLikes, - RolePermission.ViewNoteBoosts, - RolePermission.ManageOwnAccount, - RolePermission.ViewAccountFollows, - RolePermission.ManageOwnLikes, - RolePermission.ManageOwnBoosts, - RolePermission.ViewAccounts, - RolePermission.ManageOwnEmojis, - RolePermission.ViewReactions, - RolePermission.ManageOwnReactions, - RolePermission.ViewEmojis, - RolePermission.ManageOwnMedia, - RolePermission.ManageOwnBlocks, - RolePermission.ManageOwnFilters, - RolePermission.ManageOwnMutes, - RolePermission.ManageOwnReports, - RolePermission.ManageOwnSettings, - RolePermission.ManageOwnNotifications, - RolePermission.ManageOwnFollows, - RolePermission.ManageOwnApps, - RolePermission.Search, - RolePermission.UsePushNotifications, - RolePermission.ViewPublicTimelines, - RolePermission.ViewPrivateTimelines, - RolePermission.OAuth, -]; - -export const ADMIN_ROLES = [ - ...DEFAULT_ROLES, - RolePermission.ManageNotes, - RolePermission.ManageAccounts, - RolePermission.ManageLikes, - RolePermission.ManageBoosts, - RolePermission.ManageEmojis, - RolePermission.ManageReactions, - RolePermission.ManageMedia, - RolePermission.ManageBlocks, - RolePermission.ManageFilters, - RolePermission.ManageMutes, - RolePermission.ManageReports, - RolePermission.ManageSettings, - RolePermission.ManageRoles, - RolePermission.ManageNotifications, - RolePermission.ManageFollows, - RolePermission.Impersonate, - RolePermission.IgnoreRateLimits, - RolePermission.ManageInstance, - RolePermission.ManageInstanceFederation, - RolePermission.ManageInstanceSettings, -]; - -export enum MediaBackendType { - Local = "local", - S3 = "s3", -} - -// Need to declare this here instead of importing it otherwise we get cyclical import errors -export const iso631 = z.enum(ISO6391.getAllCodes() as [string, ...string[]]); - -export const urlPath = z - .string() - .trim() - .min(1) - // Remove trailing slashes, but keep the root slash - .transform((arg) => (arg === "/" ? arg : arg.replace(/\/$/, ""))); - -export const url = z - .string() - .trim() - .min(1) - .refine((arg) => URL.canParse(arg), "Invalid url") - .transform((arg) => new ProxiableUrl(arg)); - -export const unixPort = z - .number() - .int() - .min(1) - .max(2 ** 16 - 1); - -const fileFromPathString = (text: string): BunFile => file(text.slice(5)); - -// Not using .ip() because we allow CIDR ranges and wildcards and such -const ip = z - .string() - .describe("An IPv6/v4 address or CIDR range. Wildcards are also allowed"); - -const regex = z - .string() - .transform((arg) => new RegExp(arg)) - .describe("JavaScript regular expression"); - -export const sensitiveString = z - .string() - .refine( - (text) => - text.startsWith("PATH:") ? fileFromPathString(text).exists() : true, - (text) => ({ - message: `Path ${ - fileFromPathString(text).name - } does not exist, is a directory or is not accessible`, - }), - ) - .transform((text) => - text.startsWith("PATH:") ? fileFromPathString(text).text() : text, - ) - .describe("You can use PATH:/path/to/file to load this value from a file"); - -export const filePathString = z - .string() - .transform((s) => file(s)) - .refine( - (file) => file.exists(), - (file) => ({ - message: `Path ${file.name} does not exist, is a directory or is not accessible`, - }), - ) - .transform(async (file) => ({ - content: await file.text(), - file, - })) - .describe("This value must be a file path"); - -export const keyPair = z - .strictObject({ - public: sensitiveString.optional(), - private: sensitiveString.optional(), - }) - .optional() - .transform(async (k, ctx) => { - if (!(k?.public && k?.private)) { - const keys = await crypto.subtle.generateKey("Ed25519", true, [ - "sign", - "verify", - ]); - - const privateKey = Buffer.from( - await crypto.subtle.exportKey("pkcs8", keys.privateKey), - ).toString("base64"); - - const publicKey = Buffer.from( - await crypto.subtle.exportKey("spki", keys.publicKey), - ).toString("base64"); - - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Public and private keys are not set. Here are generated keys for you to copy.\n\nPublic: ${publicKey}\nPrivate: ${privateKey}`, - }); - - return z.NEVER; - } - - let publicKey: CryptoKey; - let privateKey: CryptoKey; - - try { - publicKey = await crypto.subtle.importKey( - "spki", - Buffer.from(k.public, "base64"), - "Ed25519", - true, - ["verify"], - ); - } catch { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Public key is invalid", - }); - - return z.NEVER; - } - - try { - privateKey = await crypto.subtle.importKey( - "pkcs8", - Buffer.from(k.private, "base64"), - "Ed25519", - true, - ["sign"], - ); - } catch { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "Private key is invalid", - }); - - return z.NEVER; - } - - return { - public: publicKey, - private: privateKey, - }; - }); - -export const vapidKeyPair = z - .strictObject({ - public: sensitiveString.optional(), - private: sensitiveString.optional(), - }) - .optional() - .transform((k, ctx) => { - if (!(k?.public && k?.private)) { - const keys = generateVAPIDKeys(); - - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `VAPID keys are not set. Here are generated keys for you to copy.\n\nPublic: ${keys.publicKey}\nPrivate: ${keys.privateKey}`, - }); - - return z.NEVER; - } - - return k; - }); - -export const hmacKey = sensitiveString.transform(async (text, ctx) => { - if (!text) { - const key = await crypto.subtle.generateKey( - { - name: "HMAC", - hash: "SHA-256", - }, - true, - ["sign"], - ); - - const exported = await crypto.subtle.exportKey("raw", key); - - const base64 = Buffer.from(exported).toString("base64"); - - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `HMAC key is not set. Here is a generated key for you to copy: ${base64}`, - }); - - return z.NEVER; - } - - try { - await crypto.subtle.importKey( - "raw", - Buffer.from(text, "base64"), - { - name: "HMAC", - hash: "SHA-256", - }, - true, - ["sign"], - ); - } catch { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: "HMAC key is invalid", - }); - - return z.NEVER; - } - - return text; -}); - -export const ConfigSchema = z - .strictObject({ - postgres: z - .strictObject({ - host: z.string().min(1).default("localhost"), - port: unixPort.default(5432), - username: z.string().min(1), - password: sensitiveString.default(""), - database: z.string().min(1).default("versia"), - replicas: z - .array( - z.strictObject({ - host: z.string().min(1), - port: unixPort.default(5432), - username: z.string().min(1), - password: sensitiveString.default(""), - database: z.string().min(1).default("versia"), - }), - ) - .describe("Additional read-only replicas") - .default([]), - }) - .describe("PostgreSQL database configuration"), - redis: z - .strictObject({ - queue: z - .strictObject({ - host: z.string().min(1).default("localhost"), - port: unixPort.default(6379), - password: sensitiveString.default(""), - database: z.number().int().default(0), - }) - .describe("A Redis database used for managing queues."), - cache: z - .strictObject({ - host: z.string().min(1).default("localhost"), - port: unixPort.default(6379), - password: sensitiveString.default(""), - database: z.number().int().default(1), - }) - .optional() - .describe( - "A Redis database used for caching SQL queries. Optional.", - ), - }) - .describe("Redis configuration. Used for queues and caching."), - search: z - .strictObject({ - enabled: z - .boolean() - .default(false) - .describe("Enable indexing and searching?"), - sonic: z - .strictObject({ - host: z.string().min(1).default("localhost"), - port: unixPort.default(7700), - password: sensitiveString, - }) - .describe("Sonic database configuration") - .optional(), - }) - .refine( - (o) => !o.enabled || o.sonic, - "When search is enabled, Sonic configuration must be set", - ) - .describe("Search and indexing configuration"), - registration: z.strictObject({ - allow: z - .boolean() - .default(true) - .describe("Can users sign up freely?"), - require_approval: z.boolean().default(false), - message: z - .string() - .optional() - .describe( - "Message to show to users when registration is disabled", - ), - }), - http: z.strictObject({ - base_url: url.describe( - "URL that the instance will be accessible at", - ), - bind: z.string().min(1).default("0.0.0.0"), - bind_port: unixPort.default(8080), - banned_ips: z.array(ip).default([]), - banned_user_agents: z.array(regex).default([]), - proxy_address: url - .optional() - .describe("URL to an eventual HTTP proxy") - .refine(async (url) => { - if (!url) { - return true; - } - - // Test the proxy - const response = await fetch( - "https://api.ipify.org?format=json", - { - proxy: url.origin, - }, - ); - - return response.ok; - }, "The HTTP proxy address is not reachable"), - tls: z - .strictObject({ - key: filePathString, - cert: filePathString, - passphrase: sensitiveString.optional(), - ca: filePathString.optional(), - }) - .describe( - "TLS configuration. You should probably be using a reverse proxy instead of this", - ) - .optional(), - }), - frontend: z.strictObject({ - enabled: z.boolean().default(true), - path: z.string().default(env.VERSIA_FRONTEND_PATH || "frontend"), - routes: z.strictObject({ - home: urlPath.default("/"), - login: urlPath.default("/oauth/authorize"), - consent: urlPath.default("/oauth/consent"), - register: urlPath.default("/register"), - password_reset: urlPath.default("/oauth/reset"), - }), - settings: z.record(z.string(), z.any()).default({}), - }), - email: z - .strictObject({ - send_emails: z.boolean().default(false), - smtp: z - .strictObject({ - server: z.string().min(1), - port: unixPort.default(465), - username: z.string().min(1), - password: sensitiveString.optional(), - tls: z.boolean().default(true), - }) - .optional(), - }) - .refine( - (o) => o.send_emails || !o.smtp, - "When send_emails is enabled, SMTP configuration must be set", - ), - media: z.strictObject({ - backend: z - .nativeEnum(MediaBackendType) - .default(MediaBackendType.Local), - uploads_path: z.string().min(1).default("uploads"), - conversion: z.strictObject({ - convert_images: z.boolean().default(false), - convert_to: z.string().default("image/webp"), - convert_vectors: z.boolean().default(false), - }), - }), - s3: z - .strictObject({ - endpoint: url, - access_key: sensitiveString, - secret_access_key: sensitiveString, - region: z.string().optional(), - bucket_name: z.string().optional(), - public_url: url.describe( - "Public URL that uploaded media will be accessible at", - ), - path: z.string().optional(), - path_style: z.boolean().default(true), - }) - .optional(), - validation: z.strictObject({ - accounts: z.strictObject({ - max_displayname_characters: z - .number() - .int() - .nonnegative() - .default(50), - max_username_characters: z - .number() - .int() - .nonnegative() - .default(30), - max_bio_characters: z - .number() - .int() - .nonnegative() - .default(5000), - max_avatar_bytes: z - .number() - .int() - .nonnegative() - .default(5_000_000), - max_header_bytes: z - .number() - .int() - .nonnegative() - .default(5_000_000), - disallowed_usernames: z - .array(regex) - .default([ - "well-known", - "about", - "activities", - "api", - "auth", - "dev", - "inbox", - "internal", - "main", - "media", - "nodeinfo", - "notice", - "oauth", - "objects", - "proxy", - "push", - "registration", - "relay", - "settings", - "status", - "tag", - "users", - "web", - "search", - "mfa", - ]), - max_field_count: z.number().int().default(10), - max_field_name_characters: z.number().int().default(1000), - max_field_value_characters: z.number().int().default(1000), - max_pinned_notes: z.number().int().default(20), - }), - notes: z.strictObject({ - max_characters: z.number().int().nonnegative().default(5000), - allowed_url_schemes: z - .array(z.string()) - .default([ - "http", - "https", - "ftp", - "dat", - "dweb", - "gopher", - "hyper", - "ipfs", - "ipns", - "irc", - "xmpp", - "ircs", - "magnet", - "mailto", - "mumble", - "ssb", - "gemini", - ]), - max_attachments: z.number().int().default(16), - }), - media: z.strictObject({ - max_bytes: z.number().int().nonnegative().default(40_000_000), - max_description_characters: z - .number() - .int() - .nonnegative() - .default(1000), - allowed_mime_types: z - .array(z.string()) - .default(Object.values(mimeTypes)), - }), - emojis: z.strictObject({ - max_bytes: z.number().int().nonnegative().default(1_000_000), - max_shortcode_characters: z - .number() - .int() - .nonnegative() - .default(100), - max_description_characters: z - .number() - .int() - .nonnegative() - .default(1_000), - }), - polls: z.strictObject({ - max_options: z.number().int().nonnegative().default(20), - max_option_characters: z - .number() - .int() - .nonnegative() - .default(500), - min_duration_seconds: z - .number() - .int() - .nonnegative() - .default(60), - max_duration_seconds: z - .number() - .int() - .nonnegative() - .default(100 * 24 * 60 * 60), - }), - emails: z.strictObject({ - disallow_tempmail: z - .boolean() - .default(false) - .describe("Blocks over 10,000 common tempmail domains"), - disallowed_domains: z.array(regex).default([]), - }), - challenges: z - .strictObject({ - difficulty: z.number().int().positive().default(50000), - expiration: z.number().int().positive().default(300), - key: hmacKey, - }) - .optional() - .describe( - "CAPTCHA challenge configuration. Challenges are disabled if not provided.", - ), - filters: z - .strictObject({ - note_content: z.array(regex).default([]), - emoji_shortcode: z.array(regex).default([]), - username: z.array(regex).default([]), - displayname: z.array(regex).default([]), - bio: z.array(regex).default([]), - }) - .describe( - "Block content that matches these regular expressions", - ), - }), - notifications: z.strictObject({ - push: z - .strictObject({ - vapid_keys: vapidKeyPair, - subject: z - .string() - .optional() - .describe( - "Subject field embedded in the push notification. Example: 'mailto:contact@example.com'", - ), - }) - .describe( - "Web Push Notifications configuration. Leave out to disable.", - ) - .optional(), - }), - defaults: z.strictObject({ - visibility: z - .enum(["public", "unlisted", "private", "direct"]) - .default("public"), - language: z.string().default("en"), - avatar: url.optional(), - header: url.optional(), - placeholder_style: z - .string() - .default("thumbs") - .describe("A style name from https://www.dicebear.com/styles"), - }), - federation: z.strictObject({ - blocked: z.array(z.string()).default([]), - followers_only: z.array(z.string()).default([]), - discard: z.strictObject({ - reports: z.array(z.string()).default([]), - deletes: z.array(z.string()).default([]), - updates: z.array(z.string()).default([]), - media: z.array(z.string()).default([]), - follows: z.array(z.string()).default([]), - likes: z.array(z.string()).default([]), - reactions: z.array(z.string()).default([]), - banners: z.array(z.string()).default([]), - avatars: z.array(z.string()).default([]), - }), - bridge: z - .strictObject({ - software: z.enum(["versia-ap"]).or(z.string()), - allowed_ips: z.array(ip).default([]), - token: sensitiveString, - url, - }) - .optional(), - }), - queues: z.record( - z.enum(["delivery", "inbox", "fetch", "push", "media"]), - z.strictObject({ - remove_after_complete_seconds: z - .number() - .int() - .nonnegative() - // 1 year - .default(60 * 60 * 24 * 365), - remove_after_failure_seconds: z - .number() - .int() - .nonnegative() - // 1 year - .default(60 * 60 * 24 * 365), - }), - ), - instance: z.strictObject({ - name: z.string().min(1).default("Versia Server"), - description: z.string().min(1).default("A Versia instance"), - extended_description_path: filePathString.optional(), - tos_path: filePathString.optional(), - privacy_policy_path: filePathString.optional(), - branding: z.strictObject({ - logo: url.optional(), - banner: url.optional(), - }), - languages: z - .array(iso631) - .describe("Primary instance languages. ISO 639-1 codes."), - contact: z.strictObject({ - email: z - .string() - .email() - .describe("Email to contact the instance administration"), - }), - rules: z - .array( - z.strictObject({ - text: z - .string() - .min(1) - .max(255) - .describe("Short description of the rule"), - hint: z - .string() - .min(1) - .max(4096) - .optional() - .describe( - "Longer version of the rule with additional information", - ), - }), - ) - .default([]), - keys: keyPair, - }), - permissions: z.strictObject({ - anonymous: z - .array(z.nativeEnum(RolePermission)) - .default(DEFAULT_ROLES), - default: z - .array(z.nativeEnum(RolePermission)) - .default(DEFAULT_ROLES), - admin: z.array(z.nativeEnum(RolePermission)).default(ADMIN_ROLES), - }), - logging: z.strictObject({ - file: z - .strictObject({ - path: z.string().default("logs/versia.log"), - rotation: z - .strictObject({ - max_size: z - .number() - .int() - .nonnegative() - .default(10_000_000), // 10 MB - max_files: z - .number() - .int() - .nonnegative() - .default(10), - }) - .optional(), - log_level: z - .enum([ - "trace", - "debug", - "info", - "warning", - "error", - "fatal", - ]) - .default("info"), - }) - .optional(), - sentry: z - .strictObject({ - dsn: url, - debug: z.boolean().default(false), - sample_rate: z.number().min(0).max(1.0).default(1.0), - traces_sample_rate: z.number().min(0).max(1.0).default(1.0), - trace_propagation_targets: z.array(z.string()).default([]), - max_breadcrumbs: z.number().default(100), - environment: z.string().optional(), - log_level: z - .enum([ - "trace", - "debug", - "info", - "warning", - "error", - "fatal", - ]) - .default("info"), - }) - .optional(), - log_level: z - .enum(["trace", "debug", "info", "warning", "error", "fatal"]) - .default("info"), - }), - debug: z - .strictObject({ - federation: z.boolean().default(false), - }) - .optional(), - plugins: z.strictObject({ - autoload: z.boolean().default(true), - overrides: z - .strictObject({ - enabled: z.array(z.string()).default([]), - disabled: z.array(z.string()).default([]), - }) - .refine( - // Only one of enabled or disabled can be set - (arg) => - arg.enabled.length === 0 || arg.disabled.length === 0, - "Only one of enabled or disabled can be set", - ), - config: z.record(z.string(), z.any()).optional(), - }), - }) - .refine( - // If media backend is S3, s3 config must be set - (arg) => arg.media.backend === MediaBackendType.Local || !!arg.s3, - "When media backend is S3, S3 configuration must be set", - ); diff --git a/packages/config/to-json-schema.ts b/packages/config/to-json-schema.ts index 6af0d8ff..6c4cf474 100644 --- a/packages/config/to-json-schema.ts +++ b/packages/config/to-json-schema.ts @@ -1,5 +1,5 @@ import { zodToJsonSchema } from "zod-to-json-schema"; -import { ConfigSchema } from "./schema.ts"; +import { ConfigSchema } from "./index.ts"; const jsonSchema = zodToJsonSchema(ConfigSchema, {}); diff --git a/packages/config/url.ts b/packages/config/url.ts deleted file mode 100644 index ae5a6093..00000000 --- a/packages/config/url.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { config } from "./index.ts"; - -export class ProxiableUrl extends URL { - private isAllowedOrigin(): boolean { - const allowedOrigins: URL[] = [config.http.base_url].concat( - config.s3?.public_url ?? [], - ); - - return allowedOrigins.some((origin) => - this.hostname.endsWith(origin.hostname), - ); - } - - public get proxied(): string { - // Don't proxy from CDN and self, since those sources are trusted - if (this.isAllowedOrigin()) { - return this.href; - } - - const urlAsBase64Url = Buffer.from(this.href).toString("base64url"); - - return new URL(`/media/proxy/${urlAsBase64Url}`, config.http.base_url) - .href; - } -} diff --git a/packages/plugin-kit/api-error.ts b/packages/kit/api-error.ts similarity index 100% rename from packages/plugin-kit/api-error.ts rename to packages/kit/api-error.ts diff --git a/packages/plugin-kit/api.ts b/packages/kit/api.ts similarity index 100% rename from packages/plugin-kit/api.ts rename to packages/kit/api.ts diff --git a/packages/kit/build.ts b/packages/kit/build.ts new file mode 100644 index 00000000..84dfd501 --- /dev/null +++ b/packages/kit/build.ts @@ -0,0 +1,41 @@ +import { $, build } from "bun"; +import manifest from "./package.json" with { type: "json" }; + +console.log("Building..."); + +await $`rm -rf dist && mkdir dist`; + +await build({ + entrypoints: Object.values(manifest.exports).map((entry) => entry.import), + outdir: "dist", + target: "bun", + splitting: true, + minify: true, + external: [ + ...Object.keys(manifest.dependencies).filter((dep) => + dep.startsWith("@versia"), + ), + "acorn", + ], +}); + +console.log("Copying files..."); + +// Copy Drizzle stuff +// Copy to dist instead of dist/tables because the built files are at the top-level +await $`cp -rL tables/migrations dist`; + +await $`mkdir -p dist/node_modules`; + +// Copy Sharp to dist +await $`mkdir -p dist/node_modules/@img`; +await $`cp -rL ../../node_modules/@img/sharp-libvips-linux* dist/node_modules/@img`; +await $`cp -rL ../../node_modules/@img/sharp-linux* dist/node_modules/@img`; + +// Copy acorn to dist +await $`cp -rL ../../node_modules/acorn dist/node_modules/acorn`; + +// Fixes issues with sharp +await $`cp -rL ../../node_modules/detect-libc dist/node_modules/`; + +console.log("Build complete!"); diff --git a/packages/plugin-kit/db/application.ts b/packages/kit/db/application.ts similarity index 97% rename from packages/plugin-kit/db/application.ts rename to packages/kit/db/application.ts index 16d89779..c2a8df76 100644 --- a/packages/plugin-kit/db/application.ts +++ b/packages/kit/db/application.ts @@ -2,8 +2,6 @@ import type { Application as ApplicationSchema, CredentialApplication, } from "@versia/client/schemas"; -import { db, Token } from "@versia-server/kit/db"; -import { Applications } from "@versia-server/kit/tables"; import { desc, eq, @@ -13,7 +11,10 @@ import { type SQL, } from "drizzle-orm"; import type { z } from "zod"; +import { db } from "../tables/db.ts"; +import { Applications } from "../tables/schema.ts"; import { BaseInterface } from "./base.ts"; +import { Token } from "./token.ts"; type ApplicationType = InferSelectModel; diff --git a/packages/plugin-kit/db/base.ts b/packages/kit/db/base.ts similarity index 100% rename from packages/plugin-kit/db/base.ts rename to packages/kit/db/base.ts diff --git a/packages/plugin-kit/db/emoji.ts b/packages/kit/db/emoji.ts similarity index 97% rename from packages/plugin-kit/db/emoji.ts rename to packages/kit/db/emoji.ts index c607c3a2..5dfcccc0 100644 --- a/packages/plugin-kit/db/emoji.ts +++ b/packages/kit/db/emoji.ts @@ -5,8 +5,6 @@ import { } from "@versia/client/schemas"; import * as VersiaEntities from "@versia/sdk/entities"; import type { ImageContentFormatSchema } from "@versia/sdk/schemas"; -import { db, type Instance, Media } from "@versia-server/kit/db"; -import { Emojis, type Instances, type Medias } from "@versia-server/kit/tables"; import { randomUUIDv7 } from "bun"; import { and, @@ -19,7 +17,11 @@ import { type SQL, } from "drizzle-orm"; import type { z } from "zod"; +import { db } from "../tables/db.ts"; +import { Emojis, type Instances, type Medias } from "../tables/schema.ts"; import { BaseInterface } from "./base.ts"; +import type { Instance } from "./instance.ts"; +import { Media } from "./media.ts"; type EmojiType = InferSelectModel & { media: InferSelectModel; diff --git a/packages/plugin-kit/db/index.ts b/packages/kit/db/index.ts similarity index 100% rename from packages/plugin-kit/db/index.ts rename to packages/kit/db/index.ts diff --git a/packages/plugin-kit/db/instance.ts b/packages/kit/db/instance.ts similarity index 96% rename from packages/plugin-kit/db/instance.ts rename to packages/kit/db/instance.ts index cb83accc..55008ad5 100644 --- a/packages/plugin-kit/db/instance.ts +++ b/packages/kit/db/instance.ts @@ -1,8 +1,6 @@ import * as VersiaEntities from "@versia/sdk/entities"; +import { FederationRequester } from "@versia/sdk/http"; import { config } from "@versia-server/config"; -import { ApiError } from "@versia-server/kit"; -import { db } from "@versia-server/kit/db"; -import { Instances } from "@versia-server/kit/tables"; import { federationMessagingLogger, federationResolversLogger, @@ -17,8 +15,11 @@ import { inArray, type SQL, } from "drizzle-orm"; +import { ApiError } from "../api-error.ts"; +import { db } from "../tables/db.ts"; +import { Instances } from "../tables/schema.ts"; import { BaseInterface } from "./base.ts"; -import { User } from "./user.ts"; +import type { User } from "./user.ts"; type InstanceType = InferSelectModel; @@ -146,10 +147,10 @@ export class Instance extends BaseInterface { const wellKnownUrl = new URL("/.well-known/versia", origin); try { - const metadata = await User.federationRequester.fetchEntity( - wellKnownUrl, - VersiaEntities.InstanceMetadata, - ); + const metadata = await new FederationRequester( + config.instance.keys.private, + config.http.base_url, + ).fetchEntity(wellKnownUrl, VersiaEntities.InstanceMetadata); return { metadata, protocol: "versia" }; } catch { diff --git a/packages/plugin-kit/db/like.ts b/packages/kit/db/like.ts similarity index 98% rename from packages/plugin-kit/db/like.ts rename to packages/kit/db/like.ts index 457aca37..041bfd04 100644 --- a/packages/plugin-kit/db/like.ts +++ b/packages/kit/db/like.ts @@ -1,12 +1,5 @@ import * as VersiaEntities from "@versia/sdk/entities"; import { config } from "@versia-server/config"; -import { db } from "@versia-server/kit/db"; -import { - Likes, - type Notes, - Notifications, - type Users, -} from "@versia-server/kit/tables"; import { and, desc, @@ -16,6 +9,13 @@ import { inArray, type SQL, } from "drizzle-orm"; +import { db } from "../tables/db.ts"; +import { + Likes, + type Notes, + Notifications, + type Users, +} from "../tables/schema.ts"; import { BaseInterface } from "./base.ts"; import { User } from "./user.ts"; diff --git a/packages/plugin-kit/db/media.ts b/packages/kit/db/media.ts similarity index 97% rename from packages/plugin-kit/db/media.ts rename to packages/kit/db/media.ts index de967fd4..eb21ab3f 100644 --- a/packages/plugin-kit/db/media.ts +++ b/packages/kit/db/media.ts @@ -5,11 +5,7 @@ import type { ContentFormatSchema, ImageContentFormatSchema, } from "@versia/sdk/schemas"; -import { config, ProxiableUrl } from "@versia-server/config"; -import { MediaBackendType } from "@versia-server/config/schema"; -import { ApiError } from "@versia-server/kit"; -import { db } from "@versia-server/kit/db"; -import { Medias } from "@versia-server/kit/tables"; +import { config, MediaBackendType, ProxiableUrl } from "@versia-server/config"; import { randomUUIDv7, S3Client, SHA256, write } from "bun"; import { desc, @@ -23,7 +19,10 @@ import sharp from "sharp"; import type { z } from "zod"; import { mimeLookup } from "@/content_types.ts"; import { getMediaHash } from "../../../classes/media/media-hasher.ts"; -import { MediaJobType, mediaQueue } from "../queues/media.ts"; +import { ApiError } from "../api-error.ts"; +import { MediaJobType, mediaQueue } from "../queues/media/queue.ts"; +import { db } from "../tables/db.ts"; +import { Medias } from "../tables/schema.ts"; import { BaseInterface } from "./base.ts"; type MediaType = InferSelectModel; diff --git a/packages/plugin-kit/db/note.ts b/packages/kit/db/note.ts similarity index 79% rename from packages/plugin-kit/db/note.ts rename to packages/kit/db/note.ts index 19a90283..bad30145 100644 --- a/packages/plugin-kit/db/note.ts +++ b/packages/kit/db/note.ts @@ -1,18 +1,11 @@ -import type { NoteReactionWithAccounts, Status } from "@versia/client/schemas"; +import type { + NoteReactionWithAccounts, + Status as StatusSchema, +} from "@versia/client/schemas"; import * as VersiaEntities from "@versia/sdk/entities"; +import { FederationRequester } from "@versia/sdk/http"; import type { NonTextContentFormatSchema } from "@versia/sdk/schemas"; import { config } from "@versia-server/config"; -import { db, Instance, type Reaction } from "@versia-server/kit/db"; -import { versiaTextToHtml } from "@versia-server/kit/parsers"; -import { uuid } from "@versia-server/kit/regex"; -import { - EmojiToNote, - Likes, - MediasToNotes, - Notes, - NoteToMentions, - Users, -} from "@versia-server/kit/tables"; import { randomUUIDv7 } from "bun"; import { and, @@ -30,11 +23,26 @@ import { createRegExp, exactly, global } from "magic-regexp"; import type { z } from "zod"; import { mergeAndDeduplicate } from "@/lib.ts"; import { sanitizedHtmlStrip } from "@/sanitization"; -import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts"; +import { versiaTextToHtml } from "../parsers.ts"; +import { DeliveryJobType, deliveryQueue } from "../queues/delivery/queue.ts"; +import { uuid } from "../regex.ts"; +import { db } from "../tables/db.ts"; +import { + EmojiToNote, + Likes, + MediasToNotes, + Notes, + NoteToMentions, + Notifications, + Users, +} from "../tables/schema.ts"; import { Application } from "./application.ts"; import { BaseInterface } from "./base.ts"; import { Emoji } from "./emoji.ts"; +import { Instance } from "./instance.ts"; +import { Like } from "./like.ts"; import { Media } from "./media.ts"; +import { Reaction } from "./reaction.ts"; import { transformOutputToUserWithRelations, User, @@ -475,6 +483,315 @@ export class Note extends BaseInterface { ); } + /** + * Reblog a note. + * + * If the note is already reblogged, it will return the existing reblog. Also creates a notification for the author of the note. + * @param reblogger The user reblogging the note + * @param visibility The visibility of the reblog + * @param uri The URI of the reblog, if it is remote + * @returns The reblog object created or the existing reblog + */ + public async reblog( + reblogger: User, + visibility: z.infer, + uri?: URL, + ): Promise { + const existingReblog = await Note.fromSql( + and(eq(Notes.authorId, reblogger.id), eq(Notes.reblogId, this.id)), + undefined, + reblogger.id, + ); + + if (existingReblog) { + return existingReblog; + } + + const newReblog = await Note.insert({ + id: randomUUIDv7(), + authorId: reblogger.id, + reblogId: this.id, + visibility, + sensitive: false, + updatedAt: new Date().toISOString(), + applicationId: null, + uri: uri?.href, + }); + + await this.recalculateReblogCount(); + + // Refetch the note *again* to get the proper value of .reblogged + await newReblog.reload(reblogger?.id); + + if (!newReblog) { + throw new Error("Failed to reblog"); + } + + if (this.author.local) { + // Notify the user that their post has been reblogged + await this.author.notify("reblog", reblogger, newReblog); + } + + if (reblogger.local) { + const federatedUsers = await reblogger.federateToFollowers( + newReblog.toVersiaShare(), + ); + + if ( + this.remote && + !federatedUsers.find((u) => u.id === this.author.id) + ) { + await reblogger.federateToUser( + newReblog.toVersiaShare(), + this.author, + ); + } + } + + return newReblog; + } + + /** + * Unreblog a note. + * + * If the note is not reblogged, it will return without doing anything. Also removes any notifications for this reblog. + * @param unreblogger The user unreblogging the note + * @returns + */ + public async unreblog(unreblogger: User): Promise { + const reblogToDelete = await Note.fromSql( + and( + eq(Notes.authorId, unreblogger.id), + eq(Notes.reblogId, this.id), + ), + undefined, + unreblogger.id, + ); + + if (!reblogToDelete) { + return; + } + + await reblogToDelete.delete(); + + await this.recalculateReblogCount(); + + if (this.author.local) { + // Remove any eventual notifications for this reblog + await db + .delete(Notifications) + .where( + and( + eq(Notifications.accountId, this.id), + eq(Notifications.type, "reblog"), + eq(Notifications.notifiedId, unreblogger.id), + eq(Notifications.noteId, this.id), + ), + ); + } + + if (this.local) { + const federatedUsers = await unreblogger.federateToFollowers( + reblogToDelete.toVersiaUnshare(), + ); + + if ( + this.remote && + !federatedUsers.find((u) => u.id === this.author.id) + ) { + await unreblogger.federateToUser( + reblogToDelete.toVersiaUnshare(), + this.author, + ); + } + } + } + + /** + * Like a note. + * + * If the note is already liked, it will return the existing like. Also creates a notification for the author of the note. + * @param liker The user liking the note + * @param uri The URI of the like, if it is remote + * @returns The like object created or the existing like + */ + public async like(liker: User, uri?: URL): Promise { + // Check if the user has already liked the note + const existingLike = await Like.fromSql( + and(eq(Likes.likerId, liker.id), eq(Likes.likedId, this.id)), + ); + + if (existingLike) { + return existingLike; + } + + const newLike = await Like.insert({ + id: randomUUIDv7(), + likerId: liker.id, + likedId: this.id, + uri: uri?.href, + }); + + await this.recalculateLikeCount(); + + if (this.author.local) { + // Notify the user that their post has been favourited + await this.author.notify("favourite", liker, this); + } + + if (liker.local) { + const federatedUsers = await liker.federateToFollowers( + newLike.toVersia(), + ); + + if ( + this.remote && + !federatedUsers.find((u) => u.id === this.author.id) + ) { + await liker.federateToUser(newLike.toVersia(), this.author); + } + } + + return newLike; + } + + /** + * Unlike a note. + * + * If the note is not liked, it will return without doing anything. Also removes any notifications for this like. + * @param unliker The user unliking the note + * @returns + */ + public async unlike(unliker: User): Promise { + const likeToDelete = await Like.fromSql( + and(eq(Likes.likerId, unliker.id), eq(Likes.likedId, this.id)), + ); + + if (!likeToDelete) { + return; + } + + await likeToDelete.delete(); + + await this.recalculateLikeCount(); + + if (this.author.local) { + // Remove any eventual notifications for this like + await likeToDelete.clearRelatedNotifications(); + } + + if (unliker.local) { + const federatedUsers = await unliker.federateToFollowers( + likeToDelete.unlikeToVersia(unliker), + ); + + if ( + this.remote && + !federatedUsers.find((u) => u.id === this.author.id) + ) { + await unliker.federateToUser( + likeToDelete.unlikeToVersia(unliker), + this.author, + ); + } + } + } + + /** + * Add an emoji reaction to a note + * @param reacter - The author of the reaction + * @param emoji - The emoji to react with (Emoji object for custom emojis, or Unicode emoji) + * @returns The created reaction + */ + public async react(reacter: User, emoji: Emoji | string): Promise { + const existingReaction = await Reaction.fromEmoji(emoji, reacter, this); + + if (existingReaction) { + return; // Reaction already exists, don't create duplicate + } + + // Create the reaction + const reaction = await Reaction.insert({ + id: randomUUIDv7(), + authorId: reacter.id, + noteId: this.id, + emojiText: emoji instanceof Emoji ? null : emoji, + emojiId: emoji instanceof Emoji ? emoji.id : null, + }); + + await this.reload(reacter.id); + + if (this.author.local) { + // Notify the user that their post has been reacted to + await this.author.notify("reaction", reacter, this); + } + + if (reacter.local) { + const federatedUsers = await reacter.federateToFollowers( + reaction.toVersia(), + ); + + if ( + this.remote && + !federatedUsers.find((u) => u.id === this.author.id) + ) { + await reacter.federateToUser(reaction.toVersia(), this.author); + } + } + } + + /** + * Remove an emoji reaction from a note + * @param unreacter - The author of the reaction + * @param emoji - The emoji to remove reaction for (Emoji object for custom emojis, or Unicode emoji) + */ + public async unreact( + unreacter: User, + emoji: Emoji | string, + ): Promise { + const reactionToDelete = await Reaction.fromEmoji( + emoji, + unreacter, + this, + ); + + if (!reactionToDelete) { + return; // Reaction doesn't exist, nothing to delete + } + + await reactionToDelete.delete(); + + if (this.author.local) { + // Remove any eventual notifications for this reaction + await db + .delete(Notifications) + .where( + and( + eq(Notifications.accountId, unreacter.id), + eq(Notifications.type, "reaction"), + eq(Notifications.notifiedId, this.data.authorId), + eq(Notifications.noteId, this.id), + ), + ); + } + + if (unreacter.local) { + const federatedUsers = await unreacter.federateToFollowers( + reactionToDelete.toVersiaUnreact(), + ); + + if ( + this.remote && + !federatedUsers.find((u) => u.id === this.author.id) + ) { + await unreacter.federateToUser( + reactionToDelete.toVersiaUnreact(), + this.author, + ); + } + } + } + /** * Get the children of this note (replies) * @param userId - The ID of the user requesting the note (used to check visibility of the note) @@ -637,10 +954,10 @@ export class Note extends BaseInterface { ): Promise { if (versiaNote instanceof URL) { // No bridge support for notes yet - const note = await User.federationRequester.fetchEntity( - versiaNote, - VersiaEntities.Note, - ); + const note = await new FederationRequester( + config.instance.keys.private, + config.http.base_url, + ).fetchEntity(versiaNote, VersiaEntities.Note); return Note.fromVersia(note); } @@ -805,7 +1122,7 @@ export class Note extends BaseInterface { */ public async toApi( userFetching?: User | null, - ): Promise> { + ): Promise> { const data = this.data; // Convert mentions of local users from @username@host to @username diff --git a/packages/plugin-kit/db/notification.ts b/packages/kit/db/notification.ts similarity index 95% rename from packages/plugin-kit/db/notification.ts rename to packages/kit/db/notification.ts index 4680ea3a..2d954212 100644 --- a/packages/plugin-kit/db/notification.ts +++ b/packages/kit/db/notification.ts @@ -1,6 +1,4 @@ import type { Notification as NotificationSchema } from "@versia/client/schemas"; -import { db, Note, User } from "@versia-server/kit/db"; -import { Notifications } from "@versia-server/kit/tables"; import { desc, eq, @@ -10,8 +8,15 @@ import { type SQL, } from "drizzle-orm"; import type { z } from "zod"; +import { db } from "../tables/db.ts"; +import { Notifications } from "../tables/schema.ts"; import { BaseInterface } from "./base.ts"; -import { transformOutputToUserWithRelations, userRelations } from "./user.ts"; +import { Note } from "./note.ts"; +import { + transformOutputToUserWithRelations, + User, + userRelations, +} from "./user.ts"; export type NotificationType = InferSelectModel & { status: typeof Note.$type | null; diff --git a/packages/plugin-kit/db/pushsubscription.ts b/packages/kit/db/pushsubscription.ts similarity index 96% rename from packages/plugin-kit/db/pushsubscription.ts rename to packages/kit/db/pushsubscription.ts index 6faaa715..682dd496 100644 --- a/packages/plugin-kit/db/pushsubscription.ts +++ b/packages/kit/db/pushsubscription.ts @@ -1,6 +1,4 @@ import type { WebPushSubscription as WebPushSubscriptionSchema } from "@versia/client/schemas"; -import { db, type Token, type User } from "@versia-server/kit/db"; -import { PushSubscriptions, Tokens } from "@versia-server/kit/tables"; import { desc, eq, @@ -10,7 +8,11 @@ import { type SQL, } from "drizzle-orm"; import type { z } from "zod"; +import { db } from "../tables/db.ts"; +import { PushSubscriptions, Tokens } from "../tables/schema.ts"; import { BaseInterface } from "./base.ts"; +import type { Token } from "./token.ts"; +import type { User } from "./user.ts"; type PushSubscriptionType = InferSelectModel; diff --git a/packages/plugin-kit/db/reaction.ts b/packages/kit/db/reaction.ts similarity index 96% rename from packages/plugin-kit/db/reaction.ts rename to packages/kit/db/reaction.ts index c6b051d0..1f55373e 100644 --- a/packages/plugin-kit/db/reaction.ts +++ b/packages/kit/db/reaction.ts @@ -1,7 +1,5 @@ import * as VersiaEntities from "@versia/sdk/entities"; import { config } from "@versia-server/config"; -import { db, Emoji, Instance, type Note, User } from "@versia-server/kit/db"; -import { type Notes, Reactions, type Users } from "@versia-server/kit/tables"; import { randomUUIDv7 } from "bun"; import { and, @@ -13,7 +11,13 @@ import { isNull, type SQL, } from "drizzle-orm"; +import { db } from "../tables/db.ts"; +import { type Notes, Reactions, type Users } from "../tables/schema.ts"; import { BaseInterface } from "./base.ts"; +import { Emoji } from "./emoji.ts"; +import { Instance } from "./instance.ts"; +import type { Note } from "./note.ts"; +import { User } from "./user.ts"; type ReactionType = InferSelectModel & { emoji: typeof Emoji.$type | null; diff --git a/packages/plugin-kit/db/relationship.ts b/packages/kit/db/relationship.ts similarity index 98% rename from packages/plugin-kit/db/relationship.ts rename to packages/kit/db/relationship.ts index 4c27618d..8256cd85 100644 --- a/packages/plugin-kit/db/relationship.ts +++ b/packages/kit/db/relationship.ts @@ -1,6 +1,4 @@ import type { Relationship as RelationshipSchema } from "@versia/client/schemas"; -import { db } from "@versia-server/kit/db"; -import { Relationships, Users } from "@versia-server/kit/tables"; import { randomUUIDv7 } from "bun"; import { and, @@ -13,6 +11,8 @@ import { sql, } from "drizzle-orm"; import { z } from "zod"; +import { db } from "../tables/db.ts"; +import { Relationships, Users } from "../tables/schema.ts"; import { BaseInterface } from "./base.ts"; import type { User } from "./user.ts"; diff --git a/packages/plugin-kit/db/role.ts b/packages/kit/db/role.ts similarity index 98% rename from packages/plugin-kit/db/role.ts rename to packages/kit/db/role.ts index 333c6ec0..ae4080a3 100644 --- a/packages/plugin-kit/db/role.ts +++ b/packages/kit/db/role.ts @@ -3,8 +3,6 @@ import type { Role as RoleSchema, } from "@versia/client/schemas"; import { config, ProxiableUrl } from "@versia-server/config"; -import { db } from "@versia-server/kit/db"; -import { Roles, RoleToUsers } from "@versia-server/kit/tables"; import { and, desc, @@ -15,6 +13,8 @@ import { type SQL, } from "drizzle-orm"; import type { z } from "zod"; +import { db } from "../tables/db.ts"; +import { Roles, RoleToUsers } from "../tables/schema.ts"; import { BaseInterface } from "./base.ts"; type RoleType = InferSelectModel; diff --git a/packages/plugin-kit/db/timeline.ts b/packages/kit/db/timeline.ts similarity index 99% rename from packages/plugin-kit/db/timeline.ts rename to packages/kit/db/timeline.ts index e059163b..9b1908ef 100644 --- a/packages/plugin-kit/db/timeline.ts +++ b/packages/kit/db/timeline.ts @@ -1,6 +1,6 @@ import { config } from "@versia-server/config"; -import { Notes, Notifications, Users } from "@versia-server/kit/tables"; import { gt, type SQL } from "drizzle-orm"; +import { Notes, Notifications, Users } from "../tables/schema.ts"; import { Note } from "./note.ts"; import { Notification } from "./notification.ts"; import { User } from "./user.ts"; diff --git a/packages/plugin-kit/db/token.ts b/packages/kit/db/token.ts similarity index 96% rename from packages/plugin-kit/db/token.ts rename to packages/kit/db/token.ts index c54258fc..871e780b 100644 --- a/packages/plugin-kit/db/token.ts +++ b/packages/kit/db/token.ts @@ -1,6 +1,4 @@ import type { Token as TokenSchema } from "@versia/client/schemas"; -import { type Application, db, User } from "@versia-server/kit/db"; -import { Tokens } from "@versia-server/kit/tables"; import { desc, eq, @@ -10,7 +8,11 @@ import { type SQL, } from "drizzle-orm"; import type { z } from "zod"; +import { db } from "../tables/db.ts"; +import { Tokens } from "../tables/schema.ts"; +import type { Application } from "./application.ts"; import { BaseInterface } from "./base.ts"; +import { User } from "./user.ts"; type TokenType = InferSelectModel & { application: typeof Application.$type | null; diff --git a/packages/plugin-kit/db/user.ts b/packages/kit/db/user.ts similarity index 77% rename from packages/plugin-kit/db/user.ts rename to packages/kit/db/user.ts index f0d9bfa7..af301652 100644 --- a/packages/plugin-kit/db/user.ts +++ b/packages/kit/db/user.ts @@ -3,31 +3,12 @@ import type { Mention as MentionSchema, RolePermission, Source, - Status as StatusSchema, } from "@versia/client/schemas"; import { sign } from "@versia/sdk/crypto"; import * as VersiaEntities from "@versia/sdk/entities"; import { FederationRequester } from "@versia/sdk/http"; import type { ImageContentFormatSchema } from "@versia/sdk/schemas"; import { config, ProxiableUrl } from "@versia-server/config"; -import { - db, - Media, - Notification, - PushSubscription, - Reaction, -} from "@versia-server/kit/db"; -import { uuid } from "@versia-server/kit/regex"; -import { - EmojiToUser, - Likes, - Notes, - NoteToMentions, - Notifications, - Relationships, - Users, - UserToPinnedNotes, -} from "@versia-server/kit/tables"; import { federationDeliveryLogger, federationResolversLogger, @@ -52,15 +33,26 @@ import { htmlToText } from "html-to-text"; import type { z } from "zod"; import { getBestContentType } from "@/content_types"; import { randomString } from "@/math"; -import { searchManager } from "~/classes/search/search-manager"; import type { HttpVerb, KnownEntity } from "~/types/api.ts"; -import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts"; -import { PushJobType, pushQueue } from "../queues/push.ts"; +import { DeliveryJobType, deliveryQueue } from "../queues/delivery/queue.ts"; +import { PushJobType, pushQueue } from "../queues/push/queue.ts"; +import { uuid } from "../regex.ts"; +import { db } from "../tables/db.ts"; +import { + EmojiToUser, + Notes, + NoteToMentions, + Notifications, + Relationships, + Users, + UserToPinnedNotes, +} from "../tables/schema.ts"; import { BaseInterface } from "./base.ts"; import { Emoji } from "./emoji.ts"; import { Instance } from "./instance.ts"; -import { Like } from "./like.ts"; -import { Note } from "./note.ts"; +import { Media } from "./media.ts"; +import type { Note } from "./note.ts"; +import { PushSubscription } from "./pushsubscription.ts"; import { Relationship } from "./relationship.ts"; import { Role } from "./role.ts"; @@ -571,127 +563,6 @@ export class User extends BaseInterface { .filter((x) => x !== null); } - /** - * Reblog a note. - * - * If the note is already reblogged, it will return the existing reblog. Also creates a notification for the author of the note. - * @param note The note to reblog - * @param visibility The visibility of the reblog - * @param uri The URI of the reblog, if it is remote - * @returns The reblog object created or the existing reblog - */ - public async reblog( - note: Note, - visibility: z.infer, - uri?: URL, - ): Promise { - const existingReblog = await Note.fromSql( - and(eq(Notes.authorId, this.id), eq(Notes.reblogId, note.id)), - undefined, - this.id, - ); - - if (existingReblog) { - return existingReblog; - } - - const newReblog = await Note.insert({ - id: randomUUIDv7(), - authorId: this.id, - reblogId: note.id, - visibility, - sensitive: false, - updatedAt: new Date().toISOString(), - applicationId: null, - uri: uri?.href, - }); - - await note.recalculateReblogCount(); - - // Refetch the note *again* to get the proper value of .reblogged - const finalNewReblog = await Note.fromId(newReblog.id, this?.id); - - if (!finalNewReblog) { - throw new Error("Failed to reblog"); - } - - if (note.author.local) { - // Notify the user that their post has been reblogged - await note.author.notify("reblog", this, finalNewReblog); - } - - if (this.local) { - const federatedUsers = await this.federateToFollowers( - finalNewReblog.toVersiaShare(), - ); - - if ( - note.remote && - !federatedUsers.find((u) => u.id === note.author.id) - ) { - await this.federateToUser( - finalNewReblog.toVersiaShare(), - note.author, - ); - } - } - - return finalNewReblog; - } - - /** - * Unreblog a note. - * - * If the note is not reblogged, it will return without doing anything. Also removes any notifications for this reblog. - * @param note The note to unreblog - * @returns - */ - public async unreblog(note: Note): Promise { - const reblogToDelete = await Note.fromSql( - and(eq(Notes.authorId, this.id), eq(Notes.reblogId, note.id)), - undefined, - this.id, - ); - - if (!reblogToDelete) { - return; - } - - await reblogToDelete.delete(); - - await note.recalculateReblogCount(); - - if (note.author.local) { - // Remove any eventual notifications for this reblog - await db - .delete(Notifications) - .where( - and( - eq(Notifications.accountId, this.id), - eq(Notifications.type, "reblog"), - eq(Notifications.notifiedId, note.data.authorId), - eq(Notifications.noteId, note.id), - ), - ); - } - - if (this.local) { - const federatedUsers = await this.federateToFollowers( - reblogToDelete.toVersiaUnshare(), - ); - - if ( - note.remote && - !federatedUsers.find((u) => u.id === note.author.id) - ) { - await this.federateToUser( - reblogToDelete.toVersiaUnshare(), - note.author, - ); - } - } - } - public async recalculateFollowerCount(): Promise { const followerCount = await db.$count( Relationships, @@ -731,188 +602,6 @@ export class User extends BaseInterface { }); } - /** - * Like a note. - * - * If the note is already liked, it will return the existing like. Also creates a notification for the author of the note. - * @param note The note to like - * @param uri The URI of the like, if it is remote - * @returns The like object created or the existing like - */ - public async like(note: Note, uri?: URL): Promise { - // Check if the user has already liked the note - const existingLike = await Like.fromSql( - and(eq(Likes.likerId, this.id), eq(Likes.likedId, note.id)), - ); - - if (existingLike) { - return existingLike; - } - - const newLike = await Like.insert({ - id: randomUUIDv7(), - likerId: this.id, - likedId: note.id, - uri: uri?.href, - }); - - await note.recalculateLikeCount(); - - if (note.author.local) { - // Notify the user that their post has been favourited - await note.author.notify("favourite", this, note); - } - - if (this.local) { - const federatedUsers = await this.federateToFollowers( - newLike.toVersia(), - ); - - if ( - note.remote && - !federatedUsers.find((u) => u.id === note.author.id) - ) { - await this.federateToUser(newLike.toVersia(), note.author); - } - } - - return newLike; - } - - /** - * Unlike a note. - * - * If the note is not liked, it will return without doing anything. Also removes any notifications for this like. - * @param note The note to unlike - * @returns - */ - public async unlike(note: Note): Promise { - const likeToDelete = await Like.fromSql( - and(eq(Likes.likerId, this.id), eq(Likes.likedId, note.id)), - ); - - if (!likeToDelete) { - return; - } - - await likeToDelete.delete(); - - await note.recalculateLikeCount(); - - if (note.author.local) { - // Remove any eventual notifications for this like - await likeToDelete.clearRelatedNotifications(); - } - - if (this.local) { - const federatedUsers = await this.federateToFollowers( - likeToDelete.unlikeToVersia(this), - ); - - if ( - note.remote && - !federatedUsers.find((u) => u.id === note.author.id) - ) { - await this.federateToUser( - likeToDelete.unlikeToVersia(this), - note.author, - ); - } - } - } - - /** - * Add an emoji reaction to a note - * @param note - The note to react to - * @param emoji - The emoji to react with (Emoji object for custom emojis, or Unicode emoji) - * @returns The created reaction - */ - public async react(note: Note, emoji: Emoji | string): Promise { - const existingReaction = await Reaction.fromEmoji(emoji, this, note); - - if (existingReaction) { - return; // Reaction already exists, don't create duplicate - } - - // Create the reaction - const reaction = await Reaction.insert({ - id: randomUUIDv7(), - authorId: this.id, - noteId: note.id, - emojiText: emoji instanceof Emoji ? null : emoji, - emojiId: emoji instanceof Emoji ? emoji.id : null, - }); - - const finalNote = await Note.fromId(note.id, this.id); - - if (!finalNote) { - throw new Error("Failed to fetch note after reaction"); - } - - if (note.author.local) { - // Notify the user that their post has been reacted to - await note.author.notify("reaction", this, finalNote); - } - - if (this.local) { - const federatedUsers = await this.federateToFollowers( - reaction.toVersia(), - ); - - if ( - note.remote && - !federatedUsers.find((u) => u.id === note.author.id) - ) { - await this.federateToUser(reaction.toVersia(), note.author); - } - } - } - - /** - * Remove an emoji reaction from a note - * @param note - The note to remove reaction from - * @param emoji - The emoji to remove reaction for (Emoji object for custom emojis, or Unicode emoji) - */ - public async unreact(note: Note, emoji: Emoji | string): Promise { - const reactionToDelete = await Reaction.fromEmoji(emoji, this, note); - - if (!reactionToDelete) { - return; // Reaction doesn't exist, nothing to delete - } - - await reactionToDelete.delete(); - - if (note.author.local) { - // Remove any eventual notifications for this reaction - await db - .delete(Notifications) - .where( - and( - eq(Notifications.accountId, this.id), - eq(Notifications.type, "reaction"), - eq(Notifications.notifiedId, note.data.authorId), - eq(Notifications.noteId, note.id), - ), - ); - } - - if (this.local) { - const federatedUsers = await this.federateToFollowers( - reactionToDelete.toVersiaUnreact(), - ); - - if ( - note.remote && - !federatedUsers.find((u) => u.id === note.author.id) - ) { - await this.federateToUser( - reactionToDelete.toVersiaUnreact(), - note.author, - ); - } - } - } - public async notify( type: | "mention" @@ -924,13 +613,18 @@ export class User extends BaseInterface { relatedUser: User, note?: Note, ): Promise { - const notification = await Notification.insert({ - id: randomUUIDv7(), - accountId: relatedUser.id, - type, - notifiedId: this.id, - noteId: note?.id ?? null, - }); + const notification = ( + await db + .insert(Notifications) + .values({ + id: randomUUIDv7(), + accountId: relatedUser.id, + type, + notifiedId: this.id, + noteId: note?.id ?? null, + }) + .returning() + )[0]; // Also do push notifications if (config.notifications.push) { @@ -1046,10 +740,10 @@ export class User extends BaseInterface { ); } - const user = await User.federationRequester.fetchEntity( - uri, - VersiaEntities.User, - ); + const user = await new FederationRequester( + config.instance.keys.private, + config.http.base_url, + ).fetchEntity(uri, VersiaEntities.User); return User.fromVersia(user); } @@ -1266,9 +960,6 @@ export class User extends BaseInterface { } as z.infer, }); - // Add to search index - await searchManager.addUser(user); - return user; } @@ -1332,13 +1023,6 @@ export class User extends BaseInterface { return updated.data; } - public static get federationRequester(): FederationRequester { - return new FederationRequester( - config.instance.keys.private, - config.http.base_url, - ); - } - public get federationRequester(): Promise { return crypto.subtle .importKey( diff --git a/packages/plugin-kit/example.ts b/packages/kit/example.ts similarity index 100% rename from packages/plugin-kit/example.ts rename to packages/kit/example.ts diff --git a/packages/plugin-kit/hooks.ts b/packages/kit/hooks.ts similarity index 100% rename from packages/plugin-kit/hooks.ts rename to packages/kit/hooks.ts diff --git a/packages/plugin-kit/inbox-processor.ts b/packages/kit/inbox-processor.ts similarity index 96% rename from packages/plugin-kit/inbox-processor.ts rename to packages/kit/inbox-processor.ts index ddfe69d1..faced9f2 100644 --- a/packages/plugin-kit/inbox-processor.ts +++ b/packages/kit/inbox-processor.ts @@ -2,16 +2,6 @@ import { EntitySorter, type JSONObject } from "@versia/sdk"; import { verify } from "@versia/sdk/crypto"; import * as VersiaEntities from "@versia/sdk/entities"; import { config } from "@versia-server/config"; -import { ApiError } from "@versia-server/kit"; -import { - type Instance, - Like, - Note, - Reaction, - Relationship, - User, -} from "@versia-server/kit/db"; -import { Likes, Notes } from "@versia-server/kit/tables"; import { federationInboxLogger } from "@versia-server/logging"; import type { SocketAddress } from "bun"; import { Glob } from "bun"; @@ -19,6 +9,14 @@ import chalk from "chalk"; import { and, eq } from "drizzle-orm"; import { matches } from "ip-matching"; import { isValidationError } from "zod-validation-error"; +import { ApiError } from "./api-error.ts"; +import type { Instance } from "./db/instance.ts"; +import { Like } from "./db/like.ts"; +import { Note } from "./db/note.ts"; +import { Reaction } from "./db/reaction.ts"; +import { Relationship } from "./db/relationship.ts"; +import { User } from "./db/user.ts"; +import { Likes, Notes } from "./tables/schema.ts"; /** * Checks if the hostname is defederated using glob matching. @@ -439,7 +437,7 @@ export class InboxProcessor { throw new ApiError(404, "Shared Note not found"); } - await author.reblog(sharedNote, "public", new URL(share.data.uri)); + await sharedNote.reblog(author, "public", new URL(share.data.uri)); } /** @@ -515,7 +513,7 @@ export class InboxProcessor { throw new ApiError(404, "Like author not found"); } - await likeAuthor.unlike(liked); + await liked.unlike(likeAuthor); return; } @@ -547,7 +545,7 @@ export class InboxProcessor { ); } - await author.unreblog(reblogged); + await reblogged.unreblog(author); return; } default: { @@ -579,7 +577,7 @@ export class InboxProcessor { throw new ApiError(404, "Liked Note not found"); } - await author.like(likedNote, new URL(like.data.uri)); + await likedNote.like(author, new URL(like.data.uri)); } /** diff --git a/packages/plugin-kit/index.ts b/packages/kit/index.ts similarity index 100% rename from packages/plugin-kit/index.ts rename to packages/kit/index.ts diff --git a/packages/plugin-kit/json-schema.ts b/packages/kit/json-schema.ts similarity index 100% rename from packages/plugin-kit/json-schema.ts rename to packages/kit/json-schema.ts diff --git a/packages/plugin-kit/manifest.schema.json b/packages/kit/manifest.schema.json similarity index 100% rename from packages/plugin-kit/manifest.schema.json rename to packages/kit/manifest.schema.json diff --git a/packages/plugin-kit/markdown.ts b/packages/kit/markdown.ts similarity index 100% rename from packages/plugin-kit/markdown.ts rename to packages/kit/markdown.ts diff --git a/packages/plugin-kit/package.json b/packages/kit/package.json similarity index 57% rename from packages/plugin-kit/package.json rename to packages/kit/package.json index 489beb27..34e54d3d 100644 --- a/packages/plugin-kit/package.json +++ b/packages/kit/package.json @@ -9,6 +9,9 @@ "name": "CPlusPatch", "url": "https://cpluspatch.com" }, + "scripts": { + "build": "bun run build.ts" + }, "bugs": { "url": "https://github.com/versia-pub/server/issues" }, @@ -58,47 +61,75 @@ "@hackmd/markdown-it-task-lists": "catalog:", "bullmq": "catalog:", "web-push": "catalog:", - "ip-matching": "catalog:" + "ip-matching": "catalog:", + "sonic-channel": "catalog:" }, "files": [ "tables/migrations" ], "exports": { ".": { - "import": "./index.ts", - "default": "./index.ts" + "import": "./index.ts" }, "./db": { - "import": "./db/index.ts", - "default": "./db/index.ts" + "import": "./db/index.ts" }, "./tables": { - "import": "./tables/schema.ts", - "default": "./tables/schema.ts" + "import": "./tables/schema.ts" }, "./api": { - "import": "./api.ts", - "default": "./api.ts" + "import": "./api.ts" }, "./redis": { - "import": "./redis.ts", - "default": "./redis.ts" + "import": "./redis.ts" }, "./regex": { - "import": "./regex.ts", - "default": "./regex.ts" + "import": "./regex.ts" }, - "./queues/*": { - "import": "./queues/*.ts", - "default": "./queues/*.ts" + "./queues/delivery": { + "import": "./queues/delivery/queue.ts" + }, + "./queues/delivery/worker": { + "import": "./queues/delivery/worker.ts" + }, + "./queues/fetch": { + "import": "./queues/fetch/queue.ts" + }, + "./queues/fetch/worker": { + "import": "./queues/fetch/worker.ts" + }, + "./queues/inbox": { + "import": "./queues/inbox/queue.ts" + }, + "./queues/inbox/worker": { + "import": "./queues/inbox/worker.ts" + }, + "./queues/media": { + "import": "./queues/media/queue.ts" + }, + "./queues/media/worker": { + "import": "./queues/media/worker.ts" + }, + "./queues/push": { + "import": "./queues/push/queue.ts" + }, + "./queues/push/worker": { + "import": "./queues/push/worker.ts" + }, + "./queues/relationships": { + "import": "./queues/relationships/queue.ts" + }, + "./queues/relationships/worker": { + "import": "./queues/relationships/worker.ts" }, "./markdown": { - "import": "./markdown.ts", - "default": "./markdown.ts" + "import": "./markdown.ts" }, "./parsers": { - "import": "./parsers.ts", - "default": "./parsers.ts" + "import": "./parsers.ts" + }, + "./search": { + "import": "./search-manager.ts" } } } diff --git a/packages/plugin-kit/parsers.ts b/packages/kit/parsers.ts similarity index 100% rename from packages/plugin-kit/parsers.ts rename to packages/kit/parsers.ts diff --git a/packages/plugin-kit/plugin.ts b/packages/kit/plugin.ts similarity index 100% rename from packages/plugin-kit/plugin.ts rename to packages/kit/plugin.ts diff --git a/packages/kit/queues/delivery/queue.ts b/packages/kit/queues/delivery/queue.ts new file mode 100644 index 00000000..0709fa70 --- /dev/null +++ b/packages/kit/queues/delivery/queue.ts @@ -0,0 +1,20 @@ +import type { JSONObject } from "@versia/sdk"; +import { Queue } from "bullmq"; +import { connection } from "../../redis.ts"; + +export enum DeliveryJobType { + FederateEntity = "federateEntity", +} + +export type DeliveryJobData = { + entity: JSONObject; + recipientId: string; + senderId: string; +}; + +export const deliveryQueue = new Queue( + "delivery", + { + connection, + }, +); diff --git a/packages/plugin-kit/queues/delivery.ts b/packages/kit/queues/delivery/worker.ts similarity index 84% rename from packages/plugin-kit/queues/delivery.ts rename to packages/kit/queues/delivery/worker.ts index 368c4ff1..b56d475d 100644 --- a/packages/plugin-kit/queues/delivery.ts +++ b/packages/kit/queues/delivery/worker.ts @@ -1,27 +1,14 @@ -import type { JSONObject } from "@versia/sdk"; import * as VersiaEntities from "@versia/sdk/entities"; import { config } from "@versia-server/config"; -import { Queue, Worker } from "bullmq"; +import { Worker } from "bullmq"; import chalk from "chalk"; -import { User } from "../db/user.ts"; -import { connection } from "../redis.ts"; - -export enum DeliveryJobType { - FederateEntity = "federateEntity", -} - -export type DeliveryJobData = { - entity: JSONObject; - recipientId: string; - senderId: string; -}; - -export const deliveryQueue = new Queue( - "delivery", - { - connection, - }, -); +import { User } from "../../db/user.ts"; +import { connection } from "../../redis.ts"; +import { + type DeliveryJobData, + DeliveryJobType, + deliveryQueue, +} from "./queue.ts"; export const getDeliveryWorker = (): Worker< DeliveryJobData, diff --git a/packages/kit/queues/fetch/queue.ts b/packages/kit/queues/fetch/queue.ts new file mode 100644 index 00000000..a28c1698 --- /dev/null +++ b/packages/kit/queues/fetch/queue.ts @@ -0,0 +1,17 @@ +import { Queue } from "bullmq"; +import { connection } from "../../redis.ts"; + +export enum FetchJobType { + Instance = "instance", + User = "user", + Note = "user", +} + +export type FetchJobData = { + uri: string; + refetcher?: string; +}; + +export const fetchQueue = new Queue("fetch", { + connection, +}); diff --git a/packages/plugin-kit/queues/fetch.ts b/packages/kit/queues/fetch/worker.ts similarity index 78% rename from packages/plugin-kit/queues/fetch.ts rename to packages/kit/queues/fetch/worker.ts index 836e2fe1..90ddfbff 100644 --- a/packages/plugin-kit/queues/fetch.ts +++ b/packages/kit/queues/fetch/worker.ts @@ -1,24 +1,10 @@ import { config } from "@versia-server/config"; -import { Queue, Worker } from "bullmq"; +import { Worker } from "bullmq"; import { eq } from "drizzle-orm"; -import { Instance } from "../db/instance.ts"; -import { connection } from "../redis.ts"; -import { Instances } from "../tables/schema.ts"; - -export enum FetchJobType { - Instance = "instance", - User = "user", - Note = "user", -} - -export type FetchJobData = { - uri: string; - refetcher?: string; -}; - -export const fetchQueue = new Queue("fetch", { - connection, -}); +import { Instance } from "../../db/instance.ts"; +import { connection } from "../../redis.ts"; +import { Instances } from "../../tables/schema.ts"; +import { type FetchJobData, FetchJobType, fetchQueue } from "./queue.ts"; export const getFetchWorker = (): Worker => new Worker( diff --git a/packages/kit/queues/inbox/queue.ts b/packages/kit/queues/inbox/queue.ts new file mode 100644 index 00000000..741b1ec9 --- /dev/null +++ b/packages/kit/queues/inbox/queue.ts @@ -0,0 +1,31 @@ +import type { JSONObject } from "@versia/sdk"; +import { Queue } from "bullmq"; +import type { SocketAddress } from "bun"; +import { connection } from "../../redis.ts"; + +export enum InboxJobType { + ProcessEntity = "processEntity", +} + +export type InboxJobData = { + data: JSONObject; + headers: { + "versia-signature"?: string; + "versia-signed-at"?: number; + "versia-signed-by"?: string; + authorization?: string; + }; + request: { + url: string; + method: string; + body: string; + }; + ip: SocketAddress | null; +}; + +export const inboxQueue = new Queue( + "inbox", + { + connection, + }, +); diff --git a/packages/plugin-kit/queues/inbox.ts b/packages/kit/queues/inbox/worker.ts similarity index 88% rename from packages/plugin-kit/queues/inbox.ts rename to packages/kit/queues/inbox/worker.ts index be0c54be..95220d92 100644 --- a/packages/plugin-kit/queues/inbox.ts +++ b/packages/kit/queues/inbox/worker.ts @@ -1,39 +1,11 @@ -import type { JSONObject } from "@versia/sdk"; import { config } from "@versia-server/config"; -import { Queue, Worker } from "bullmq"; -import type { SocketAddress } from "bun"; -import { ApiError } from "../api-error.ts"; -import { Instance } from "../db/instance.ts"; -import { User } from "../db/user.ts"; -import { InboxProcessor } from "../inbox-processor.ts"; -import { connection } from "../redis.ts"; - -export enum InboxJobType { - ProcessEntity = "processEntity", -} - -export type InboxJobData = { - data: JSONObject; - headers: { - "versia-signature"?: string; - "versia-signed-at"?: number; - "versia-signed-by"?: string; - authorization?: string; - }; - request: { - url: string; - method: string; - body: string; - }; - ip: SocketAddress | null; -}; - -export const inboxQueue = new Queue( - "inbox", - { - connection, - }, -); +import { Worker } from "bullmq"; +import { ApiError } from "../../api-error.ts"; +import { Instance } from "../../db/instance.ts"; +import { User } from "../../db/user.ts"; +import { InboxProcessor } from "../../inbox-processor.ts"; +import { connection } from "../../redis.ts"; +import { type InboxJobData, InboxJobType, inboxQueue } from "./queue.ts"; export const getInboxWorker = (): Worker => new Worker( diff --git a/packages/kit/queues/media/queue.ts b/packages/kit/queues/media/queue.ts new file mode 100644 index 00000000..7501db04 --- /dev/null +++ b/packages/kit/queues/media/queue.ts @@ -0,0 +1,16 @@ +import { Queue } from "bullmq"; +import { connection } from "../../redis.ts"; + +export enum MediaJobType { + ConvertMedia = "convertMedia", + CalculateMetadata = "calculateMetadata", +} + +export type MediaJobData = { + attachmentId: string; + filename: string; +}; + +export const mediaQueue = new Queue("media", { + connection, +}); diff --git a/packages/plugin-kit/queues/media.ts b/packages/kit/queues/media/worker.ts similarity index 86% rename from packages/plugin-kit/queues/media.ts rename to packages/kit/queues/media/worker.ts index c1ad3e56..38a40d84 100644 --- a/packages/plugin-kit/queues/media.ts +++ b/packages/kit/queues/media/worker.ts @@ -1,23 +1,10 @@ import { config } from "@versia-server/config"; -import { Queue, Worker } from "bullmq"; -import { calculateBlurhash } from "../../../classes/media/preprocessors/blurhash.ts"; -import { convertImage } from "../../../classes/media/preprocessors/image-conversion.ts"; -import { Media } from "../db/media.ts"; -import { connection } from "../redis.ts"; - -export enum MediaJobType { - ConvertMedia = "convertMedia", - CalculateMetadata = "calculateMetadata", -} - -export type MediaJobData = { - attachmentId: string; - filename: string; -}; - -export const mediaQueue = new Queue("media", { - connection, -}); +import { Worker } from "bullmq"; +import { calculateBlurhash } from "../../../../classes/media/preprocessors/blurhash.ts"; +import { convertImage } from "../../../../classes/media/preprocessors/image-conversion.ts"; +import { Media } from "../../db/media.ts"; +import { connection } from "../../redis.ts"; +import { type MediaJobData, MediaJobType, mediaQueue } from "./queue.ts"; export const getMediaWorker = (): Worker => new Worker( diff --git a/packages/kit/queues/push/queue.ts b/packages/kit/queues/push/queue.ts new file mode 100644 index 00000000..0e71d0ca --- /dev/null +++ b/packages/kit/queues/push/queue.ts @@ -0,0 +1,18 @@ +import { Queue } from "bullmq"; +import { connection } from "../../redis.ts"; + +export enum PushJobType { + Notify = "notify", +} + +export type PushJobData = { + psId: string; + type: string; + relatedUserId: string; + noteId?: string; + notificationId: string; +}; + +export const pushQueue = new Queue("push", { + connection, +}); diff --git a/packages/plugin-kit/queues/push.ts b/packages/kit/queues/push/worker.ts similarity index 89% rename from packages/plugin-kit/queues/push.ts rename to packages/kit/queues/push/worker.ts index 986479dd..d3e5b871 100644 --- a/packages/plugin-kit/queues/push.ts +++ b/packages/kit/queues/push/worker.ts @@ -1,28 +1,13 @@ import { config } from "@versia-server/config"; -import { Queue, Worker } from "bullmq"; +import { Worker } from "bullmq"; import { sendNotification } from "web-push"; import { htmlToText } from "@/content_types.ts"; -import { Note } from "../db/note.ts"; -import { PushSubscription } from "../db/pushsubscription.ts"; -import { Token } from "../db/token.ts"; -import { User } from "../db/user.ts"; -import { connection } from "../redis.ts"; - -export enum PushJobType { - Notify = "notify", -} - -export type PushJobData = { - psId: string; - type: string; - relatedUserId: string; - noteId?: string; - notificationId: string; -}; - -export const pushQueue = new Queue("push", { - connection, -}); +import { Note } from "../../db/note.ts"; +import { PushSubscription } from "../../db/pushsubscription.ts"; +import { Token } from "../../db/token.ts"; +import { User } from "../../db/user.ts"; +import { connection } from "../../redis.ts"; +import { type PushJobData, type PushJobType, pushQueue } from "./queue.ts"; export const getPushWorker = (): Worker => new Worker( diff --git a/packages/kit/queues/relationships/queue.ts b/packages/kit/queues/relationships/queue.ts new file mode 100644 index 00000000..91aaf007 --- /dev/null +++ b/packages/kit/queues/relationships/queue.ts @@ -0,0 +1,19 @@ +import { Queue } from "bullmq"; +import { connection } from "../../redis.ts"; + +export enum RelationshipJobType { + Unmute = "unmute", +} + +export type RelationshipJobData = { + ownerId: string; + subjectId: string; +}; + +export const relationshipQueue = new Queue< + RelationshipJobData, + void, + RelationshipJobType +>("relationships", { + connection, +}); diff --git a/packages/plugin-kit/queues/relationships.ts b/packages/kit/queues/relationships/worker.ts similarity index 76% rename from packages/plugin-kit/queues/relationships.ts rename to packages/kit/queues/relationships/worker.ts index a8884096..04b49abf 100644 --- a/packages/plugin-kit/queues/relationships.ts +++ b/packages/kit/queues/relationships/worker.ts @@ -1,25 +1,13 @@ import { config } from "@versia-server/config"; -import { Queue, Worker } from "bullmq"; -import { Relationship } from "../db/relationship.ts"; -import { User } from "../db/user.ts"; -import { connection } from "../redis.ts"; - -export enum RelationshipJobType { - Unmute = "unmute", -} - -export type RelationshipJobData = { - ownerId: string; - subjectId: string; -}; - -export const relationshipQueue = new Queue< - RelationshipJobData, - void, - RelationshipJobType ->("relationships", { - connection, -}); +import { Worker } from "bullmq"; +import { Relationship } from "../../db/relationship.ts"; +import { User } from "../../db/user.ts"; +import { connection } from "../../redis.ts"; +import { + type RelationshipJobData, + RelationshipJobType, + relationshipQueue, +} from "./queue.ts"; export const getRelationshipWorker = (): Worker< RelationshipJobData, diff --git a/packages/plugin-kit/redis.ts b/packages/kit/redis.ts similarity index 100% rename from packages/plugin-kit/redis.ts rename to packages/kit/redis.ts diff --git a/packages/plugin-kit/regex.ts b/packages/kit/regex.ts similarity index 100% rename from packages/plugin-kit/regex.ts rename to packages/kit/regex.ts diff --git a/packages/plugin-kit/schema.ts b/packages/kit/schema.ts similarity index 100% rename from packages/plugin-kit/schema.ts rename to packages/kit/schema.ts diff --git a/packages/kit/search-manager.ts b/packages/kit/search-manager.ts new file mode 100644 index 00000000..1f0ee0d4 --- /dev/null +++ b/packages/kit/search-manager.ts @@ -0,0 +1,311 @@ +/** + * @file search-manager.ts + * @description Sonic search integration for indexing and searching accounts and statuses + */ + +import { config } from "@versia-server/config"; +import { sonicLogger } from "@versia-server/logging"; +import type { SQL, ValueOrArray } from "drizzle-orm"; +import { + Ingest as SonicChannelIngest, + Search as SonicChannelSearch, +} from "sonic-channel"; +import { Note } from "./db/note.ts"; +import { User } from "./db/user.ts"; +import { db } from "./tables/db.ts"; + +/** + * Enum for Sonic index types + */ +export enum SonicIndexType { + Accounts = "accounts", + Statuses = "statuses", +} + +/** + * Class for managing Sonic search operations + */ +export class SonicSearchManager { + private searchChannel: SonicChannelSearch; + private ingestChannel: SonicChannelIngest; + private connected = false; + + /** + * @param config Configuration for Sonic + */ + public constructor() { + if (!config.search.sonic) { + throw new Error("Sonic configuration is missing"); + } + + this.searchChannel = new SonicChannelSearch({ + host: config.search.sonic.host, + port: config.search.sonic.port, + auth: config.search.sonic.password, + }); + + this.ingestChannel = new SonicChannelIngest({ + host: config.search.sonic.host, + port: config.search.sonic.port, + auth: config.search.sonic.password, + }); + } + + /** + * Connect to Sonic + */ + public async connect(silent = false): Promise { + if (!config.search.enabled) { + !silent && sonicLogger.info`Sonic search is disabled`; + return; + } + + if (this.connected) { + return; + } + + !silent && sonicLogger.info`Connecting to Sonic...`; + + // Connect to Sonic + await new Promise((resolve, reject) => { + this.searchChannel.connect({ + connected: (): void => { + !silent && + sonicLogger.info`Connected to Sonic Search Channel`; + resolve(true); + }, + disconnected: (): void => + sonicLogger.error`Disconnected from Sonic Search Channel. You might be using an incorrect password.`, + timeout: (): void => + sonicLogger.error`Sonic Search Channel connection timed out`, + retrying: (): void => + sonicLogger.warn`Retrying connection to Sonic Search Channel`, + error: (error): void => { + sonicLogger.error`Failed to connect to Sonic Search Channel: ${error}`; + reject(error); + }, + }); + }); + + await new Promise((resolve, reject) => { + this.ingestChannel.connect({ + connected: (): void => { + !silent && + sonicLogger.info`Connected to Sonic Ingest Channel`; + resolve(true); + }, + disconnected: (): void => + sonicLogger.error`Disconnected from Sonic Ingest Channel`, + timeout: (): void => + sonicLogger.error`Sonic Ingest Channel connection timed out`, + retrying: (): void => + sonicLogger.warn`Retrying connection to Sonic Ingest Channel`, + error: (error): void => { + sonicLogger.error`Failed to connect to Sonic Ingest Channel: ${error}`; + reject(error); + }, + }); + }); + + try { + await Promise.all([ + this.searchChannel.ping(), + this.ingestChannel.ping(), + ]); + this.connected = true; + !silent && sonicLogger.info`Connected to Sonic`; + } catch (error) { + sonicLogger.fatal`Error while connecting to Sonic: ${error}`; + throw error; + } + } + + /** + * Add a user to Sonic + * @param user User to add + */ + public async addUser(user: User): Promise { + if (!config.search.enabled) { + return; + } + + try { + await this.ingestChannel.push( + SonicIndexType.Accounts, + "users", + user.id, + `${user.data.username} ${user.data.displayName} ${user.data.note}`, + ); + } catch (error) { + sonicLogger.error`Failed to add user to Sonic: ${error}`; + } + } + + /** + * Get a batch of accounts from the database + * @param n Batch number + * @param batchSize Size of the batch + */ + private static getNthDatabaseAccountBatch( + n: number, + batchSize = 1000, + ): Promise[]> { + return db.query.Users.findMany({ + offset: n * batchSize, + limit: batchSize, + columns: { + id: true, + username: true, + displayName: true, + note: true, + createdAt: true, + }, + orderBy: (user, { asc }): ValueOrArray => asc(user.createdAt), + }); + } + + /** + * Get a batch of statuses from the database + * @param n Batch number + * @param batchSize Size of the batch + */ + private static getNthDatabaseStatusBatch( + n: number, + batchSize = 1000, + ): Promise[]> { + return db.query.Notes.findMany({ + offset: n * batchSize, + limit: batchSize, + columns: { + id: true, + content: true, + createdAt: true, + }, + orderBy: (status, { asc }): ValueOrArray => + asc(status.createdAt), + }); + } + + /** + * Rebuild search indexes + * @param indexes Indexes to rebuild + * @param batchSize Size of each batch + * @param progressCallback Callback for progress updates + */ + public async rebuildSearchIndexes( + indexes: SonicIndexType[], + batchSize = 100, + progressCallback?: (progress: number) => void, + ): Promise { + for (const index of indexes) { + if (index === SonicIndexType.Accounts) { + await this.rebuildAccountsIndex(batchSize, progressCallback); + } else if (index === SonicIndexType.Statuses) { + await this.rebuildStatusesIndex(batchSize, progressCallback); + } + } + } + + /** + * Rebuild accounts index + * @param batchSize Size of each batch + * @param progressCallback Callback for progress updates + */ + private async rebuildAccountsIndex( + batchSize: number, + progressCallback?: (progress: number) => void, + ): Promise { + const accountCount = await User.getCount(); + const batchCount = Math.ceil(accountCount / batchSize); + + for (let i = 0; i < batchCount; i++) { + const accounts = + await SonicSearchManager.getNthDatabaseAccountBatch( + i, + batchSize, + ); + await Promise.all( + accounts.map((account) => + this.ingestChannel.push( + SonicIndexType.Accounts, + "users", + account.id as string, + `${account.username} ${account.displayName} ${account.note}`, + ), + ), + ); + progressCallback?.((i + 1) / batchCount); + } + } + + /** + * Rebuild statuses index + * @param batchSize Size of each batch + * @param progressCallback Callback for progress updates + */ + private async rebuildStatusesIndex( + batchSize: number, + progressCallback?: (progress: number) => void, + ): Promise { + const statusCount = await Note.getCount(); + const batchCount = Math.ceil(statusCount / batchSize); + + for (let i = 0; i < batchCount; i++) { + const statuses = await SonicSearchManager.getNthDatabaseStatusBatch( + i, + batchSize, + ); + await Promise.all( + statuses.map((status) => + this.ingestChannel.push( + SonicIndexType.Statuses, + "notes", + status.id as string, + status.content as string, + ), + ), + ); + progressCallback?.((i + 1) / batchCount); + } + } + + /** + * Search for accounts + * @param query Search query + * @param limit Maximum number of results + * @param offset Offset for pagination + */ + public searchAccounts( + query: string, + limit = 10, + offset = 0, + ): Promise { + return this.searchChannel.query( + SonicIndexType.Accounts, + "users", + query, + { limit, offset }, + ); + } + + /** + * Search for statuses + * @param query Search query + * @param limit Maximum number of results + * @param offset Offset for pagination + */ + public searchStatuses( + query: string, + limit = 10, + offset = 0, + ): Promise { + return this.searchChannel.query( + SonicIndexType.Statuses, + "notes", + query, + { limit, offset }, + ); + } +} + +export const searchManager = new SonicSearchManager(); diff --git a/packages/plugin-kit/tables/db.ts b/packages/kit/tables/db.ts similarity index 96% rename from packages/plugin-kit/tables/db.ts rename to packages/kit/tables/db.ts index bed83b5d..90a23c8d 100644 --- a/packages/plugin-kit/tables/db.ts +++ b/packages/kit/tables/db.ts @@ -1,3 +1,4 @@ +import { join } from "node:path"; import { config } from "@versia-server/config"; import { databaseLogger } from "@versia-server/logging"; import { SQL } from "bun"; @@ -67,7 +68,7 @@ export const setupDatabase = async (info = true): Promise => { try { await migrate(db, { - migrationsFolder: "./packages/plugin-kit/tables/migrations", + migrationsFolder: join(import.meta.dir, "migrations"), }); } catch (e) { databaseLogger.fatal`Failed to migrate database. Please check your configuration.`; diff --git a/packages/plugin-kit/tables/migrations/0000_illegal_living_lightning.sql b/packages/kit/tables/migrations/0000_illegal_living_lightning.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0000_illegal_living_lightning.sql rename to packages/kit/tables/migrations/0000_illegal_living_lightning.sql diff --git a/packages/plugin-kit/tables/migrations/0001_salty_night_thrasher.sql b/packages/kit/tables/migrations/0001_salty_night_thrasher.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0001_salty_night_thrasher.sql rename to packages/kit/tables/migrations/0001_salty_night_thrasher.sql diff --git a/packages/plugin-kit/tables/migrations/0002_stiff_ares.sql b/packages/kit/tables/migrations/0002_stiff_ares.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0002_stiff_ares.sql rename to packages/kit/tables/migrations/0002_stiff_ares.sql diff --git a/packages/plugin-kit/tables/migrations/0003_spicy_arachne.sql b/packages/kit/tables/migrations/0003_spicy_arachne.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0003_spicy_arachne.sql rename to packages/kit/tables/migrations/0003_spicy_arachne.sql diff --git a/packages/plugin-kit/tables/migrations/0004_burly_lockjaw.sql b/packages/kit/tables/migrations/0004_burly_lockjaw.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0004_burly_lockjaw.sql rename to packages/kit/tables/migrations/0004_burly_lockjaw.sql diff --git a/packages/plugin-kit/tables/migrations/0005_sleepy_puma.sql b/packages/kit/tables/migrations/0005_sleepy_puma.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0005_sleepy_puma.sql rename to packages/kit/tables/migrations/0005_sleepy_puma.sql diff --git a/packages/plugin-kit/tables/migrations/0006_messy_network.sql b/packages/kit/tables/migrations/0006_messy_network.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0006_messy_network.sql rename to packages/kit/tables/migrations/0006_messy_network.sql diff --git a/packages/plugin-kit/tables/migrations/0007_naive_sleeper.sql b/packages/kit/tables/migrations/0007_naive_sleeper.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0007_naive_sleeper.sql rename to packages/kit/tables/migrations/0007_naive_sleeper.sql diff --git a/packages/plugin-kit/tables/migrations/0008_flawless_brother_voodoo.sql b/packages/kit/tables/migrations/0008_flawless_brother_voodoo.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0008_flawless_brother_voodoo.sql rename to packages/kit/tables/migrations/0008_flawless_brother_voodoo.sql diff --git a/packages/plugin-kit/tables/migrations/0009_easy_slyde.sql b/packages/kit/tables/migrations/0009_easy_slyde.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0009_easy_slyde.sql rename to packages/kit/tables/migrations/0009_easy_slyde.sql diff --git a/packages/plugin-kit/tables/migrations/0010_daffy_frightful_four.sql b/packages/kit/tables/migrations/0010_daffy_frightful_four.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0010_daffy_frightful_four.sql rename to packages/kit/tables/migrations/0010_daffy_frightful_four.sql diff --git a/packages/plugin-kit/tables/migrations/0011_special_the_fury.sql b/packages/kit/tables/migrations/0011_special_the_fury.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0011_special_the_fury.sql rename to packages/kit/tables/migrations/0011_special_the_fury.sql diff --git a/packages/plugin-kit/tables/migrations/0012_certain_thor_girl.sql b/packages/kit/tables/migrations/0012_certain_thor_girl.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0012_certain_thor_girl.sql rename to packages/kit/tables/migrations/0012_certain_thor_girl.sql diff --git a/packages/plugin-kit/tables/migrations/0013_wandering_celestials.sql b/packages/kit/tables/migrations/0013_wandering_celestials.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0013_wandering_celestials.sql rename to packages/kit/tables/migrations/0013_wandering_celestials.sql diff --git a/packages/plugin-kit/tables/migrations/0014_wonderful_sandman.sql b/packages/kit/tables/migrations/0014_wonderful_sandman.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0014_wonderful_sandman.sql rename to packages/kit/tables/migrations/0014_wonderful_sandman.sql diff --git a/packages/plugin-kit/tables/migrations/0015_easy_mojo.sql b/packages/kit/tables/migrations/0015_easy_mojo.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0015_easy_mojo.sql rename to packages/kit/tables/migrations/0015_easy_mojo.sql diff --git a/packages/plugin-kit/tables/migrations/0016_keen_mindworm.sql b/packages/kit/tables/migrations/0016_keen_mindworm.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0016_keen_mindworm.sql rename to packages/kit/tables/migrations/0016_keen_mindworm.sql diff --git a/packages/plugin-kit/tables/migrations/0017_dusty_black_knight.sql b/packages/kit/tables/migrations/0017_dusty_black_knight.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0017_dusty_black_knight.sql rename to packages/kit/tables/migrations/0017_dusty_black_knight.sql diff --git a/packages/plugin-kit/tables/migrations/0018_rapid_hairball.sql b/packages/kit/tables/migrations/0018_rapid_hairball.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0018_rapid_hairball.sql rename to packages/kit/tables/migrations/0018_rapid_hairball.sql diff --git a/packages/plugin-kit/tables/migrations/0019_mushy_lorna_dane.sql b/packages/kit/tables/migrations/0019_mushy_lorna_dane.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0019_mushy_lorna_dane.sql rename to packages/kit/tables/migrations/0019_mushy_lorna_dane.sql diff --git a/packages/plugin-kit/tables/migrations/0020_giant_the_stranger.sql b/packages/kit/tables/migrations/0020_giant_the_stranger.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0020_giant_the_stranger.sql rename to packages/kit/tables/migrations/0020_giant_the_stranger.sql diff --git a/packages/plugin-kit/tables/migrations/0021_wise_stephen_strange.sql b/packages/kit/tables/migrations/0021_wise_stephen_strange.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0021_wise_stephen_strange.sql rename to packages/kit/tables/migrations/0021_wise_stephen_strange.sql diff --git a/packages/plugin-kit/tables/migrations/0022_curly_the_call.sql b/packages/kit/tables/migrations/0022_curly_the_call.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0022_curly_the_call.sql rename to packages/kit/tables/migrations/0022_curly_the_call.sql diff --git a/packages/plugin-kit/tables/migrations/0023_lazy_wolfsbane.sql b/packages/kit/tables/migrations/0023_lazy_wolfsbane.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0023_lazy_wolfsbane.sql rename to packages/kit/tables/migrations/0023_lazy_wolfsbane.sql diff --git a/packages/plugin-kit/tables/migrations/0024_lush_aaron_stack.sql b/packages/kit/tables/migrations/0024_lush_aaron_stack.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0024_lush_aaron_stack.sql rename to packages/kit/tables/migrations/0024_lush_aaron_stack.sql diff --git a/packages/plugin-kit/tables/migrations/0025_violet_susan_delgado.sql b/packages/kit/tables/migrations/0025_violet_susan_delgado.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0025_violet_susan_delgado.sql rename to packages/kit/tables/migrations/0025_violet_susan_delgado.sql diff --git a/packages/plugin-kit/tables/migrations/0026_neat_stranger.sql b/packages/kit/tables/migrations/0026_neat_stranger.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0026_neat_stranger.sql rename to packages/kit/tables/migrations/0026_neat_stranger.sql diff --git a/packages/plugin-kit/tables/migrations/0027_peaceful_whistler.sql b/packages/kit/tables/migrations/0027_peaceful_whistler.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0027_peaceful_whistler.sql rename to packages/kit/tables/migrations/0027_peaceful_whistler.sql diff --git a/packages/plugin-kit/tables/migrations/0028_unique_fat_cobra.sql b/packages/kit/tables/migrations/0028_unique_fat_cobra.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0028_unique_fat_cobra.sql rename to packages/kit/tables/migrations/0028_unique_fat_cobra.sql diff --git a/packages/plugin-kit/tables/migrations/0029_shiny_korvac.sql b/packages/kit/tables/migrations/0029_shiny_korvac.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0029_shiny_korvac.sql rename to packages/kit/tables/migrations/0029_shiny_korvac.sql diff --git a/packages/plugin-kit/tables/migrations/0030_curvy_vulture.sql b/packages/kit/tables/migrations/0030_curvy_vulture.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0030_curvy_vulture.sql rename to packages/kit/tables/migrations/0030_curvy_vulture.sql diff --git a/packages/plugin-kit/tables/migrations/0031_mature_demogoblin.sql b/packages/kit/tables/migrations/0031_mature_demogoblin.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0031_mature_demogoblin.sql rename to packages/kit/tables/migrations/0031_mature_demogoblin.sql diff --git a/packages/plugin-kit/tables/migrations/0032_ambiguous_sue_storm.sql b/packages/kit/tables/migrations/0032_ambiguous_sue_storm.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0032_ambiguous_sue_storm.sql rename to packages/kit/tables/migrations/0032_ambiguous_sue_storm.sql diff --git a/packages/plugin-kit/tables/migrations/0033_panoramic_sister_grimm.sql b/packages/kit/tables/migrations/0033_panoramic_sister_grimm.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0033_panoramic_sister_grimm.sql rename to packages/kit/tables/migrations/0033_panoramic_sister_grimm.sql diff --git a/packages/plugin-kit/tables/migrations/0034_jittery_proemial_gods.sql b/packages/kit/tables/migrations/0034_jittery_proemial_gods.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0034_jittery_proemial_gods.sql rename to packages/kit/tables/migrations/0034_jittery_proemial_gods.sql diff --git a/packages/plugin-kit/tables/migrations/0035_pretty_whiplash.sql b/packages/kit/tables/migrations/0035_pretty_whiplash.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0035_pretty_whiplash.sql rename to packages/kit/tables/migrations/0035_pretty_whiplash.sql diff --git a/packages/plugin-kit/tables/migrations/0036_cuddly_ironclad.sql b/packages/kit/tables/migrations/0036_cuddly_ironclad.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0036_cuddly_ironclad.sql rename to packages/kit/tables/migrations/0036_cuddly_ironclad.sql diff --git a/packages/plugin-kit/tables/migrations/0037_condemned_talisman.sql b/packages/kit/tables/migrations/0037_condemned_talisman.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0037_condemned_talisman.sql rename to packages/kit/tables/migrations/0037_condemned_talisman.sql diff --git a/packages/plugin-kit/tables/migrations/0038_friendly_supernaut.sql b/packages/kit/tables/migrations/0038_friendly_supernaut.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0038_friendly_supernaut.sql rename to packages/kit/tables/migrations/0038_friendly_supernaut.sql diff --git a/packages/plugin-kit/tables/migrations/0039_special_serpent_society.sql b/packages/kit/tables/migrations/0039_special_serpent_society.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0039_special_serpent_society.sql rename to packages/kit/tables/migrations/0039_special_serpent_society.sql diff --git a/packages/plugin-kit/tables/migrations/0040_good_nocturne.sql b/packages/kit/tables/migrations/0040_good_nocturne.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0040_good_nocturne.sql rename to packages/kit/tables/migrations/0040_good_nocturne.sql diff --git a/packages/plugin-kit/tables/migrations/0041_bright_doctor_spectrum.sql b/packages/kit/tables/migrations/0041_bright_doctor_spectrum.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0041_bright_doctor_spectrum.sql rename to packages/kit/tables/migrations/0041_bright_doctor_spectrum.sql diff --git a/packages/plugin-kit/tables/migrations/0042_swift_the_phantom.sql b/packages/kit/tables/migrations/0042_swift_the_phantom.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0042_swift_the_phantom.sql rename to packages/kit/tables/migrations/0042_swift_the_phantom.sql diff --git a/packages/plugin-kit/tables/migrations/0043_mute_jigsaw.sql b/packages/kit/tables/migrations/0043_mute_jigsaw.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0043_mute_jigsaw.sql rename to packages/kit/tables/migrations/0043_mute_jigsaw.sql diff --git a/packages/plugin-kit/tables/migrations/0044_quiet_jasper_sitwell.sql b/packages/kit/tables/migrations/0044_quiet_jasper_sitwell.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0044_quiet_jasper_sitwell.sql rename to packages/kit/tables/migrations/0044_quiet_jasper_sitwell.sql diff --git a/packages/plugin-kit/tables/migrations/0045_polite_mikhail_rasputin.sql b/packages/kit/tables/migrations/0045_polite_mikhail_rasputin.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0045_polite_mikhail_rasputin.sql rename to packages/kit/tables/migrations/0045_polite_mikhail_rasputin.sql diff --git a/packages/plugin-kit/tables/migrations/0046_wooden_electro.sql b/packages/kit/tables/migrations/0046_wooden_electro.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0046_wooden_electro.sql rename to packages/kit/tables/migrations/0046_wooden_electro.sql diff --git a/packages/plugin-kit/tables/migrations/0047_black_vermin.sql b/packages/kit/tables/migrations/0047_black_vermin.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0047_black_vermin.sql rename to packages/kit/tables/migrations/0047_black_vermin.sql diff --git a/packages/plugin-kit/tables/migrations/0048_chilly_vector.sql b/packages/kit/tables/migrations/0048_chilly_vector.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0048_chilly_vector.sql rename to packages/kit/tables/migrations/0048_chilly_vector.sql diff --git a/packages/plugin-kit/tables/migrations/0049_graceful_iron_man.sql b/packages/kit/tables/migrations/0049_graceful_iron_man.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0049_graceful_iron_man.sql rename to packages/kit/tables/migrations/0049_graceful_iron_man.sql diff --git a/packages/plugin-kit/tables/migrations/0050_thick_lester.sql b/packages/kit/tables/migrations/0050_thick_lester.sql similarity index 100% rename from packages/plugin-kit/tables/migrations/0050_thick_lester.sql rename to packages/kit/tables/migrations/0050_thick_lester.sql diff --git a/packages/plugin-kit/tables/migrations/meta/0000_snapshot.json b/packages/kit/tables/migrations/meta/0000_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0000_snapshot.json rename to packages/kit/tables/migrations/meta/0000_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0001_snapshot.json b/packages/kit/tables/migrations/meta/0001_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0001_snapshot.json rename to packages/kit/tables/migrations/meta/0001_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0002_snapshot.json b/packages/kit/tables/migrations/meta/0002_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0002_snapshot.json rename to packages/kit/tables/migrations/meta/0002_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0003_snapshot.json b/packages/kit/tables/migrations/meta/0003_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0003_snapshot.json rename to packages/kit/tables/migrations/meta/0003_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0004_snapshot.json b/packages/kit/tables/migrations/meta/0004_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0004_snapshot.json rename to packages/kit/tables/migrations/meta/0004_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0005_snapshot.json b/packages/kit/tables/migrations/meta/0005_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0005_snapshot.json rename to packages/kit/tables/migrations/meta/0005_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0006_snapshot.json b/packages/kit/tables/migrations/meta/0006_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0006_snapshot.json rename to packages/kit/tables/migrations/meta/0006_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0007_snapshot.json b/packages/kit/tables/migrations/meta/0007_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0007_snapshot.json rename to packages/kit/tables/migrations/meta/0007_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0008_snapshot.json b/packages/kit/tables/migrations/meta/0008_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0008_snapshot.json rename to packages/kit/tables/migrations/meta/0008_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0009_snapshot.json b/packages/kit/tables/migrations/meta/0009_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0009_snapshot.json rename to packages/kit/tables/migrations/meta/0009_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0010_snapshot.json b/packages/kit/tables/migrations/meta/0010_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0010_snapshot.json rename to packages/kit/tables/migrations/meta/0010_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0011_snapshot.json b/packages/kit/tables/migrations/meta/0011_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0011_snapshot.json rename to packages/kit/tables/migrations/meta/0011_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0012_snapshot.json b/packages/kit/tables/migrations/meta/0012_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0012_snapshot.json rename to packages/kit/tables/migrations/meta/0012_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0013_snapshot.json b/packages/kit/tables/migrations/meta/0013_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0013_snapshot.json rename to packages/kit/tables/migrations/meta/0013_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0014_snapshot.json b/packages/kit/tables/migrations/meta/0014_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0014_snapshot.json rename to packages/kit/tables/migrations/meta/0014_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0015_snapshot.json b/packages/kit/tables/migrations/meta/0015_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0015_snapshot.json rename to packages/kit/tables/migrations/meta/0015_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0016_snapshot.json b/packages/kit/tables/migrations/meta/0016_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0016_snapshot.json rename to packages/kit/tables/migrations/meta/0016_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0017_snapshot.json b/packages/kit/tables/migrations/meta/0017_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0017_snapshot.json rename to packages/kit/tables/migrations/meta/0017_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0018_snapshot.json b/packages/kit/tables/migrations/meta/0018_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0018_snapshot.json rename to packages/kit/tables/migrations/meta/0018_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0019_snapshot.json b/packages/kit/tables/migrations/meta/0019_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0019_snapshot.json rename to packages/kit/tables/migrations/meta/0019_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0020_snapshot.json b/packages/kit/tables/migrations/meta/0020_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0020_snapshot.json rename to packages/kit/tables/migrations/meta/0020_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0021_snapshot.json b/packages/kit/tables/migrations/meta/0021_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0021_snapshot.json rename to packages/kit/tables/migrations/meta/0021_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0022_snapshot.json b/packages/kit/tables/migrations/meta/0022_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0022_snapshot.json rename to packages/kit/tables/migrations/meta/0022_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0023_snapshot.json b/packages/kit/tables/migrations/meta/0023_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0023_snapshot.json rename to packages/kit/tables/migrations/meta/0023_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0024_snapshot.json b/packages/kit/tables/migrations/meta/0024_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0024_snapshot.json rename to packages/kit/tables/migrations/meta/0024_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0025_snapshot.json b/packages/kit/tables/migrations/meta/0025_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0025_snapshot.json rename to packages/kit/tables/migrations/meta/0025_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0026_snapshot.json b/packages/kit/tables/migrations/meta/0026_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0026_snapshot.json rename to packages/kit/tables/migrations/meta/0026_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0027_snapshot.json b/packages/kit/tables/migrations/meta/0027_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0027_snapshot.json rename to packages/kit/tables/migrations/meta/0027_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0028_snapshot.json b/packages/kit/tables/migrations/meta/0028_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0028_snapshot.json rename to packages/kit/tables/migrations/meta/0028_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0029_snapshot.json b/packages/kit/tables/migrations/meta/0029_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0029_snapshot.json rename to packages/kit/tables/migrations/meta/0029_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0030_snapshot.json b/packages/kit/tables/migrations/meta/0030_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0030_snapshot.json rename to packages/kit/tables/migrations/meta/0030_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0031_snapshot.json b/packages/kit/tables/migrations/meta/0031_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0031_snapshot.json rename to packages/kit/tables/migrations/meta/0031_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0032_snapshot.json b/packages/kit/tables/migrations/meta/0032_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0032_snapshot.json rename to packages/kit/tables/migrations/meta/0032_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0033_snapshot.json b/packages/kit/tables/migrations/meta/0033_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0033_snapshot.json rename to packages/kit/tables/migrations/meta/0033_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0034_snapshot.json b/packages/kit/tables/migrations/meta/0034_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0034_snapshot.json rename to packages/kit/tables/migrations/meta/0034_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0035_snapshot.json b/packages/kit/tables/migrations/meta/0035_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0035_snapshot.json rename to packages/kit/tables/migrations/meta/0035_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0036_snapshot.json b/packages/kit/tables/migrations/meta/0036_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0036_snapshot.json rename to packages/kit/tables/migrations/meta/0036_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0037_snapshot.json b/packages/kit/tables/migrations/meta/0037_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0037_snapshot.json rename to packages/kit/tables/migrations/meta/0037_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0038_snapshot.json b/packages/kit/tables/migrations/meta/0038_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0038_snapshot.json rename to packages/kit/tables/migrations/meta/0038_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0039_snapshot.json b/packages/kit/tables/migrations/meta/0039_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0039_snapshot.json rename to packages/kit/tables/migrations/meta/0039_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0040_snapshot.json b/packages/kit/tables/migrations/meta/0040_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0040_snapshot.json rename to packages/kit/tables/migrations/meta/0040_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0041_snapshot.json b/packages/kit/tables/migrations/meta/0041_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0041_snapshot.json rename to packages/kit/tables/migrations/meta/0041_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0042_snapshot.json b/packages/kit/tables/migrations/meta/0042_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0042_snapshot.json rename to packages/kit/tables/migrations/meta/0042_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0043_snapshot.json b/packages/kit/tables/migrations/meta/0043_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0043_snapshot.json rename to packages/kit/tables/migrations/meta/0043_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0044_snapshot.json b/packages/kit/tables/migrations/meta/0044_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0044_snapshot.json rename to packages/kit/tables/migrations/meta/0044_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0045_snapshot.json b/packages/kit/tables/migrations/meta/0045_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0045_snapshot.json rename to packages/kit/tables/migrations/meta/0045_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0046_snapshot.json b/packages/kit/tables/migrations/meta/0046_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0046_snapshot.json rename to packages/kit/tables/migrations/meta/0046_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0047_snapshot.json b/packages/kit/tables/migrations/meta/0047_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0047_snapshot.json rename to packages/kit/tables/migrations/meta/0047_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0048_snapshot.json b/packages/kit/tables/migrations/meta/0048_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0048_snapshot.json rename to packages/kit/tables/migrations/meta/0048_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0049_snapshot.json b/packages/kit/tables/migrations/meta/0049_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0049_snapshot.json rename to packages/kit/tables/migrations/meta/0049_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/0050_snapshot.json b/packages/kit/tables/migrations/meta/0050_snapshot.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/0050_snapshot.json rename to packages/kit/tables/migrations/meta/0050_snapshot.json diff --git a/packages/plugin-kit/tables/migrations/meta/_journal.json b/packages/kit/tables/migrations/meta/_journal.json similarity index 100% rename from packages/plugin-kit/tables/migrations/meta/_journal.json rename to packages/kit/tables/migrations/meta/_journal.json diff --git a/packages/plugin-kit/tables/schema.ts b/packages/kit/tables/schema.ts similarity index 100% rename from packages/plugin-kit/tables/schema.ts rename to packages/kit/tables/schema.ts diff --git a/packages/plugin-kit/timelines/streaming.test.ts b/packages/kit/timelines/streaming.test.ts similarity index 99% rename from packages/plugin-kit/timelines/streaming.test.ts rename to packages/kit/timelines/streaming.test.ts index 7b9d58b0..1e56ff67 100644 --- a/packages/plugin-kit/timelines/streaming.test.ts +++ b/packages/kit/timelines/streaming.test.ts @@ -1,7 +1,8 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { randomUUIDv7 } from "bun"; -import { setupDatabase, User } from "../db/index.ts"; +import { User } from "../db/user.ts"; import { connection } from "../redis.ts"; +import { setupDatabase } from "../tables/db.ts"; import { type EventTypes, StreamingTimeline, diff --git a/packages/plugin-kit/timelines/streaming.ts b/packages/kit/timelines/streaming.ts similarity index 100% rename from packages/plugin-kit/timelines/streaming.ts rename to packages/kit/timelines/streaming.ts diff --git a/packages/logging/build.ts b/packages/logging/build.ts new file mode 100644 index 00000000..908e5d50 --- /dev/null +++ b/packages/logging/build.ts @@ -0,0 +1,19 @@ +import { $, build } from "bun"; +import manifest from "./package.json" with { type: "json" }; + +console.log("Building..."); + +await $`rm -rf dist && mkdir dist`; + +await build({ + entrypoints: Object.values(manifest.exports).map((entry) => entry.import), + outdir: "dist", + target: "bun", + splitting: true, + minify: true, + external: [ + ...Object.keys(manifest.dependencies).filter((dep) => + dep.startsWith("@versia"), + ), + ], +}); diff --git a/packages/logging/package.json b/packages/logging/package.json index 703a3154..4409422b 100644 --- a/packages/logging/package.json +++ b/packages/logging/package.json @@ -4,10 +4,12 @@ "type": "module", "version": "0.0.1", "private": true, + "scripts": { + "build": "bun run build.ts" + }, "exports": { ".": { - "import": "./index.ts", - "default": "./index.ts" + "import": "./index.ts" } }, "dependencies": { diff --git a/packages/sdk/build.ts b/packages/sdk/build.ts new file mode 100644 index 00000000..cdff0968 --- /dev/null +++ b/packages/sdk/build.ts @@ -0,0 +1,29 @@ +import { $, build } from "bun"; +import manifest from "./package.json" with { type: "json" }; + +console.log("Building..."); + +await $`rm -rf dist && mkdir dist`; + +await build({ + entrypoints: Object.values(manifest.exports).map((entry) => entry.import), + outdir: "dist", + target: "bun", + splitting: true, + minify: true, + external: [ + ...Object.keys(manifest.dependencies).filter((dep) => + dep.startsWith("@versia"), + ), + "acorn", + ], +}); + +console.log("Copying files..."); + +await $`mkdir -p dist/node_modules`; + +// Copy acorn to dist +await $`cp -rL ../../node_modules/acorn dist/node_modules/acorn`; + +console.log("Build complete!"); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 172416d7..beb44f86 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -7,6 +7,9 @@ "name": "Jesse Wierzbinski (CPlusPatch)", "url": "https://cpluspatch.com" }, + "scripts": { + "build": "bun run build.ts" + }, "readme": "README.md", "repository": { "type": "git", @@ -41,24 +44,19 @@ }, "exports": { ".": { - "import": "./inbox-processor.ts", - "default": "./inbox-processor.ts" + "import": "./inbox-processor.ts" }, "./http": { - "import": "./http.ts", - "default": "./http.ts" + "import": "./http.ts" }, "./crypto": { - "import": "./crypto.ts", - "default": "./crypto.ts" + "import": "./crypto.ts" }, "./entities": { - "import": "./entities/index.ts", - "default": "./entities/index.ts" + "import": "./entities/index.ts" }, "./schemas": { - "import": "./schemas/index.ts", - "default": "./schemas/index.ts" + "import": "./schemas/index.ts" } }, "funding": { diff --git a/packages/tests/index.ts b/packages/tests/index.ts index 9eb663ba..96468b82 100644 --- a/packages/tests/index.ts +++ b/packages/tests/index.ts @@ -2,13 +2,13 @@ import { mock } from "bun:test"; import { Client as VersiaClient } from "@versia/client"; import { config } from "@versia-server/config"; import { db, Note, setupDatabase, Token, User } from "@versia-server/kit/db"; +import { searchManager } from "@versia-server/kit/search"; import { Notes, Users } from "@versia-server/kit/tables"; import { solveChallenge } from "altcha-lib"; import { env, randomUUIDv7 } from "bun"; import { asc, type InferSelectModel, inArray, like } from "drizzle-orm"; import { generateChallenge } from "@/challenges"; import { randomString } from "@/math"; -import { searchManager } from "~/classes/search/search-manager"; import { appFactory } from "~/packages/api/app"; env.DISABLE_RATE_LIMIT = "true"; diff --git a/packages/tests/package.json b/packages/tests/package.json index b49f0747..f149b8b2 100644 --- a/packages/tests/package.json +++ b/packages/tests/package.json @@ -6,8 +6,7 @@ "private": true, "exports": { ".": { - "import": "./index.ts", - "default": "./index.ts" + "import": "./index.ts" } }, "dependencies": { diff --git a/packages/tests/setup.ts b/packages/tests/setup.ts index 733232fd..a578afb8 100644 --- a/packages/tests/setup.ts +++ b/packages/tests/setup.ts @@ -5,4 +5,4 @@ await setupDatabase(); await deleteOldTestUsers(); // Start workers -await import("~/packages/worker/index.ts"); +await import("../../worker.ts"); diff --git a/packages/worker/build.ts b/packages/worker/build.ts index 4f436ab8..908e5d50 100644 --- a/packages/worker/build.ts +++ b/packages/worker/build.ts @@ -1,36 +1,19 @@ import { $, build } from "bun"; +import manifest from "./package.json" with { type: "json" }; console.log("Building..."); await $`rm -rf dist && mkdir dist`; await build({ - entrypoints: [ - "packages/worker/index.ts", - // HACK: Include to avoid cyclical import errors - "packages/config/index.ts", - "cli/index.ts", - ], + entrypoints: Object.values(manifest.exports).map((entry) => entry.import), outdir: "dist", target: "bun", splitting: true, - minify: false, + minify: true, + external: [ + ...Object.keys(manifest.dependencies).filter((dep) => + dep.startsWith("@versia"), + ), + ], }); - -console.log("Copying files..."); - -// Fix Bun build mistake -await $`sed -i 's/ProxiableUrl, exportedConfig/exportedConfig/g' dist/packages/config/*.js`; - -// Copy Drizzle stuff -await $`mkdir -p dist/packages/plugin-kit/tables`; -await $`cp -rL packages/plugin-kit/tables/migrations dist/packages/plugin-kit/tables`; - -// Copy Sharp to dist -await $`mkdir -p dist/node_modules/@img`; -await $`cp -rL node_modules/@img/sharp-libvips-linux* dist/node_modules/@img`; -await $`cp -rL node_modules/@img/sharp-linux* dist/node_modules/@img`; - -await $`cp -rL node_modules/detect-libc dist/node_modules/`; - -console.log("Build complete!"); diff --git a/packages/worker/package.json b/packages/worker/package.json index 5fd4d9bc..26af6f52 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -35,7 +35,15 @@ "private": true, "scripts": { "dev": "bun run --hot index.ts", - "build": "bun run build-worker.ts" + "build": "bun run build.ts" + }, + "exports": { + ".": { + "import": "./workers.ts" + }, + "./setup": { + "import": "./setup.ts" + } }, "dependencies": { "@versia-server/config": "workspace:*", diff --git a/packages/worker/setup.ts b/packages/worker/setup.ts index 595d0121..3d684288 100644 --- a/packages/worker/setup.ts +++ b/packages/worker/setup.ts @@ -1,9 +1,9 @@ import { config } from "@versia-server/config"; import { Note, setupDatabase } from "@versia-server/kit/db"; import { connection } from "@versia-server/kit/redis"; +import { searchManager } from "@versia-server/kit/search"; import { serverLogger } from "@versia-server/logging"; import chalk from "chalk"; -import { searchManager } from "../../classes/search/search-manager.ts"; const timeAtStart = performance.now(); diff --git a/packages/worker/workers.ts b/packages/worker/workers.ts index 819f3a6c..d7691877 100644 --- a/packages/worker/workers.ts +++ b/packages/worker/workers.ts @@ -1,9 +1,9 @@ -import { getDeliveryWorker } from "@versia-server/kit/queues/delivery"; -import { getFetchWorker } from "@versia-server/kit/queues/fetch"; -import { getInboxWorker } from "@versia-server/kit/queues/inbox"; -import { getMediaWorker } from "@versia-server/kit/queues/media"; -import { getPushWorker } from "@versia-server/kit/queues/push"; -import { getRelationshipWorker } from "@versia-server/kit/queues/relationships"; +import { getDeliveryWorker } from "@versia-server/kit/queues/delivery/worker"; +import { getFetchWorker } from "@versia-server/kit/queues/fetch/worker"; +import { getInboxWorker } from "@versia-server/kit/queues/inbox/worker"; +import { getMediaWorker } from "@versia-server/kit/queues/media/worker"; +import { getPushWorker } from "@versia-server/kit/queues/push/worker"; +import { getRelationshipWorker } from "@versia-server/kit/queues/relationships/worker"; export const workers = { fetch: getFetchWorker, diff --git a/types/api.ts b/types/api.ts index 56a1bfba..637f6d19 100644 --- a/types/api.ts +++ b/types/api.ts @@ -1,5 +1,5 @@ import type * as VersiaEntities from "@versia/sdk/entities"; -import type { ConfigSchema } from "@versia-server/config/schema"; +import type { ConfigSchema } from "@versia-server/config"; import type { Application, Token, User } from "@versia-server/kit/db"; import type { SocketAddress } from "bun"; import type { Hono } from "hono"; diff --git a/utils/server.ts b/utils/server.ts index 39aa4058..831c5122 100644 --- a/utils/server.ts +++ b/utils/server.ts @@ -1,4 +1,4 @@ -import type { ConfigSchema } from "@versia-server/config/schema"; +import type { ConfigSchema } from "@versia-server/config"; import { debugResponse } from "@versia-server/kit/api"; import { type Server, serve } from "bun"; import type { Hono } from "hono"; diff --git a/packages/worker/index.ts b/worker.ts similarity index 82% rename from packages/worker/index.ts rename to worker.ts index 5170c038..e3cc7169 100644 --- a/packages/worker/index.ts +++ b/worker.ts @@ -1,13 +1,13 @@ import process from "node:process"; import { serverLogger } from "@versia-server/logging"; +import { workers } from "@versia-server/worker"; import chalk from "chalk"; -import { workers } from "./workers.ts"; process.on("SIGINT", () => { process.exit(); }); -await import("./setup.ts"); +await import("@versia-server/worker/setup"); for (const [worker, fn] of Object.entries(workers)) { serverLogger.info`Starting ${worker} Worker...`;