mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
refactor: ♻️ Rewrite build system to fit the monorepo architecture
This commit is contained in:
parent
7de4b573e3
commit
90b6399407
217 changed files with 2143 additions and 1858 deletions
171
packages/kit/api-error.ts
Normal file
171
packages/kit/api-error.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import type { JSONObject } from "hono/utils/types";
|
||||
import type { DescribeRouteOptions } from "hono-openapi";
|
||||
import { resolver } from "hono-openapi/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* API Error
|
||||
*
|
||||
* Custom error class used to throw errors in the API. Includes a status code, a message and an optional description.
|
||||
* @extends Error
|
||||
*/
|
||||
export class ApiError extends Error {
|
||||
/**
|
||||
* @param {ContentfulStatusCode} status - The status code of the error
|
||||
* @param {string} message - The message of the error
|
||||
* @param {string | JSONObject} [details] - The description of the error
|
||||
*/
|
||||
public constructor(
|
||||
public status: ContentfulStatusCode,
|
||||
public override message: string,
|
||||
public details?: string | JSONObject,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
}
|
||||
|
||||
public static zodSchema = z.object({
|
||||
error: z.string(),
|
||||
details: z
|
||||
.string()
|
||||
.or(z.record(z.string(), z.string().or(z.number())))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
public get schema(): NonNullable<
|
||||
DescribeRouteOptions["responses"]
|
||||
>[number] {
|
||||
return {
|
||||
description: this.message,
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: resolver(ApiError.zodSchema),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static missingAuthentication(): ApiError {
|
||||
return new ApiError(
|
||||
401,
|
||||
"Missing authentication",
|
||||
"The Authorization header is missing or could not be parsed.",
|
||||
);
|
||||
}
|
||||
|
||||
public static forbidden(): ApiError {
|
||||
return new ApiError(
|
||||
403,
|
||||
"Missing permissions",
|
||||
"You do not have permission to access or modify this resource.",
|
||||
);
|
||||
}
|
||||
|
||||
public static notFound(): ApiError {
|
||||
return new ApiError(
|
||||
404,
|
||||
"Not found",
|
||||
"The requested resource could not be found.",
|
||||
);
|
||||
}
|
||||
|
||||
public static noteNotFound(): ApiError {
|
||||
return new ApiError(
|
||||
404,
|
||||
"Note not found",
|
||||
"The requested note could not be found.",
|
||||
);
|
||||
}
|
||||
|
||||
public static accountNotFound(): ApiError {
|
||||
return new ApiError(
|
||||
404,
|
||||
"Account not found",
|
||||
"The requested account could not be found.",
|
||||
);
|
||||
}
|
||||
|
||||
public static roleNotFound(): ApiError {
|
||||
return new ApiError(
|
||||
404,
|
||||
"Role not found",
|
||||
"The requested role could not be found.",
|
||||
);
|
||||
}
|
||||
|
||||
public static instanceNotFound(): ApiError {
|
||||
return new ApiError(
|
||||
404,
|
||||
"Instance not found",
|
||||
"The requested instance could not be found.",
|
||||
);
|
||||
}
|
||||
|
||||
public static likeNotFound(): ApiError {
|
||||
return new ApiError(
|
||||
404,
|
||||
"Like not found",
|
||||
"The requested like could not be found.",
|
||||
);
|
||||
}
|
||||
|
||||
public static pushSubscriptionNotFound(): ApiError {
|
||||
return new ApiError(
|
||||
404,
|
||||
"Push subscription not found",
|
||||
"No push subscription associated with this access token",
|
||||
);
|
||||
}
|
||||
|
||||
public static tokenNotFound(): ApiError {
|
||||
return new ApiError(
|
||||
404,
|
||||
"Token not found",
|
||||
"The requested token could not be found.",
|
||||
);
|
||||
}
|
||||
|
||||
public static mediaNotFound(): ApiError {
|
||||
return new ApiError(
|
||||
404,
|
||||
"Media not found",
|
||||
"The requested media could not be found.",
|
||||
);
|
||||
}
|
||||
|
||||
public static applicationNotFound(): ApiError {
|
||||
return new ApiError(
|
||||
404,
|
||||
"Application not found",
|
||||
"The requested application could not be found.",
|
||||
);
|
||||
}
|
||||
|
||||
public static emojiNotFound(): ApiError {
|
||||
return new ApiError(
|
||||
404,
|
||||
"Emoji not found",
|
||||
"The requested emoji could not be found.",
|
||||
);
|
||||
}
|
||||
|
||||
public static notificationNotFound(): ApiError {
|
||||
return new ApiError(
|
||||
404,
|
||||
"Notification not found",
|
||||
"The requested notification could not be found.",
|
||||
);
|
||||
}
|
||||
|
||||
public static validationFailed(): ApiError {
|
||||
return new ApiError(422, "Invalid values in request");
|
||||
}
|
||||
|
||||
public static internalServerError(): ApiError {
|
||||
return new ApiError(
|
||||
500,
|
||||
"Internal server error. This is likely a bug.",
|
||||
);
|
||||
}
|
||||
}
|
||||
433
packages/kit/api.ts
Normal file
433
packages/kit/api.ts
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
import type { Hook } from "@hono/zod-validator";
|
||||
import type { RolePermission } from "@versia/client/schemas";
|
||||
import { config } from "@versia-server/config";
|
||||
import { serverLogger } from "@versia-server/logging";
|
||||
import { extractParams, verifySolution } from "altcha-lib";
|
||||
import chalk from "chalk";
|
||||
import { eq, type SQL } from "drizzle-orm";
|
||||
import type { Context, Hono, MiddlewareHandler, ValidationTargets } from "hono";
|
||||
import { every } from "hono/combine";
|
||||
import { createMiddleware } from "hono/factory";
|
||||
import { validator } from "hono-openapi/zod";
|
||||
import { type ParsedQs, parse } from "qs";
|
||||
import { type ZodAny, type ZodError, z } from "zod";
|
||||
import { fromZodError } from "zod-validation-error";
|
||||
import type { AuthData, HonoEnv } from "~/types/api";
|
||||
import { ApiError } from "./api-error.ts";
|
||||
import { Application } from "./db/application.ts";
|
||||
import { Emoji } from "./db/emoji.ts";
|
||||
import { Note } from "./db/note.ts";
|
||||
import { Token } from "./db/token.ts";
|
||||
import { User } from "./db/user.ts";
|
||||
import { db } from "./tables/db.ts";
|
||||
import { Challenges } from "./tables/schema.ts";
|
||||
|
||||
export const apiRoute = (fn: (app: Hono<HonoEnv>) => void): typeof fn => fn;
|
||||
|
||||
export const handleZodError: Hook<
|
||||
z.infer<ZodAny>,
|
||||
HonoEnv,
|
||||
string,
|
||||
keyof ValidationTargets
|
||||
> = (result, context): Response | undefined => {
|
||||
if (!result.success) {
|
||||
return context.json(
|
||||
{
|
||||
error: fromZodError(result.error as ZodError).message,
|
||||
},
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const checkPermissions = (
|
||||
auth: AuthData | null,
|
||||
required: RolePermission[],
|
||||
): void => {
|
||||
const userPerms = auth?.user
|
||||
? auth.user.getAllPermissions()
|
||||
: config.permissions.anonymous;
|
||||
|
||||
if (!required.every((perm) => userPerms.includes(perm))) {
|
||||
const missingPerms = required.filter(
|
||||
(perm) => !userPerms.includes(perm),
|
||||
);
|
||||
throw new ApiError(
|
||||
403,
|
||||
"Missing permissions",
|
||||
`Missing: ${missingPerms.join(", ")}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const checkRouteNeedsAuth = (
|
||||
auth: AuthData | null,
|
||||
required: boolean,
|
||||
): AuthData => {
|
||||
if (auth?.user && auth?.token) {
|
||||
return {
|
||||
user: auth.user,
|
||||
token: auth.token,
|
||||
application: auth.application,
|
||||
};
|
||||
}
|
||||
if (required) {
|
||||
throw new ApiError(401, "This route requires authentication");
|
||||
}
|
||||
|
||||
return {
|
||||
user: null,
|
||||
token: null,
|
||||
application: null,
|
||||
};
|
||||
};
|
||||
|
||||
export const checkRouteNeedsChallenge = async (
|
||||
required: boolean,
|
||||
context: Context,
|
||||
): Promise<void> => {
|
||||
if (!(required && config.validation.challenges)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const challengeSolution = context.req.header("X-Challenge-Solution");
|
||||
|
||||
if (!challengeSolution) {
|
||||
throw new ApiError(
|
||||
401,
|
||||
"Challenge required",
|
||||
"This route requires a challenge solution to be sent to it via the X-Challenge-Solution header. Please check the documentation for more information.",
|
||||
);
|
||||
}
|
||||
|
||||
const { challenge_id } = extractParams(challengeSolution);
|
||||
|
||||
if (!challenge_id) {
|
||||
throw new ApiError(401, "The challenge solution provided is invalid.");
|
||||
}
|
||||
|
||||
const challenge = await db.query.Challenges.findFirst({
|
||||
where: (c): SQL | undefined => eq(c.id, challenge_id),
|
||||
});
|
||||
|
||||
if (!challenge) {
|
||||
throw new ApiError(401, "The challenge solution provided is invalid.");
|
||||
}
|
||||
|
||||
if (new Date(challenge.expiresAt) < new Date()) {
|
||||
throw new ApiError(401, "The challenge provided has expired.");
|
||||
}
|
||||
|
||||
const isValid = await verifySolution(
|
||||
challengeSolution,
|
||||
config.validation.challenges.key,
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
throw new ApiError(
|
||||
401,
|
||||
"The challenge solution provided is incorrect.",
|
||||
);
|
||||
}
|
||||
|
||||
// Expire the challenge
|
||||
await db
|
||||
.update(Challenges)
|
||||
.set({ expiresAt: new Date().toISOString() })
|
||||
.where(eq(Challenges.id, challenge_id));
|
||||
};
|
||||
|
||||
export type HonoEnvWithAuth = HonoEnv & {
|
||||
Variables: {
|
||||
auth: AuthData & {
|
||||
user: NonNullable<AuthData["user"]>;
|
||||
token: NonNullable<AuthData["token"]>;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const auth = <AuthRequired extends boolean>(options: {
|
||||
auth: AuthRequired;
|
||||
permissions?: RolePermission[];
|
||||
challenge?: boolean;
|
||||
scopes?: string[];
|
||||
// If authRequired is true, HonoEnv.Variables.auth.user will never be null
|
||||
}): MiddlewareHandler<
|
||||
AuthRequired extends true ? HonoEnvWithAuth : HonoEnv
|
||||
> => {
|
||||
return createMiddleware(async (context, next) => {
|
||||
const header = context.req.header("Authorization");
|
||||
const tokenString = header?.split(" ")[1];
|
||||
|
||||
const token = tokenString
|
||||
? await Token.fromAccessToken(tokenString)
|
||||
: null;
|
||||
|
||||
const auth: AuthData = {
|
||||
token,
|
||||
application: token?.data.application
|
||||
? new Application(token?.data.application)
|
||||
: null,
|
||||
user: (await token?.getUser()) ?? null,
|
||||
};
|
||||
|
||||
// Authentication check
|
||||
const authCheck = checkRouteNeedsAuth(auth, options.auth);
|
||||
|
||||
context.set("auth", authCheck);
|
||||
|
||||
// Permissions check
|
||||
if (options.permissions) {
|
||||
checkPermissions(auth, options.permissions);
|
||||
}
|
||||
|
||||
// Challenge check
|
||||
if (options.challenge && config.validation.challenges) {
|
||||
await checkRouteNeedsChallenge(options.challenge, context);
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
};
|
||||
|
||||
type WithIdParam = {
|
||||
in: { param: { id: string } };
|
||||
out: { param: { id: string } };
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware to check if a note exists and is viewable by the user.
|
||||
*
|
||||
* Useful in /api/v1/statuses/:id/* routes
|
||||
* @returns MiddlewareHandler
|
||||
*/
|
||||
export const withNoteParam = every(
|
||||
validator("param", z.object({ id: z.string().uuid() }), handleZodError),
|
||||
createMiddleware<
|
||||
HonoEnv & {
|
||||
Variables: {
|
||||
note: Note;
|
||||
};
|
||||
},
|
||||
string,
|
||||
WithIdParam
|
||||
>(async (context, next) => {
|
||||
const { id } = context.req.valid("param");
|
||||
const { user } = context.get("auth");
|
||||
|
||||
const note = await Note.fromId(id, user?.id);
|
||||
|
||||
if (!(note && (await note.isViewableByUser(user)))) {
|
||||
throw ApiError.noteNotFound();
|
||||
}
|
||||
|
||||
context.set("note", note);
|
||||
|
||||
await next();
|
||||
}),
|
||||
) as MiddlewareHandler<
|
||||
HonoEnv & {
|
||||
Variables: {
|
||||
note: Note;
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Middleware to check if a user exists
|
||||
*
|
||||
* Useful in /api/v1/accounts/:id/* routes
|
||||
* @returns MiddlewareHandler
|
||||
*/
|
||||
export const withUserParam = every(
|
||||
validator("param", z.object({ id: z.string().uuid() }), handleZodError),
|
||||
createMiddleware<
|
||||
HonoEnv & {
|
||||
Variables: {
|
||||
user: User;
|
||||
};
|
||||
},
|
||||
string,
|
||||
WithIdParam
|
||||
>(async (context, next) => {
|
||||
const { id } = context.req.valid("param");
|
||||
const user = await User.fromId(id);
|
||||
|
||||
if (!user) {
|
||||
throw new ApiError(404, "User not found");
|
||||
}
|
||||
|
||||
context.set("user", user);
|
||||
|
||||
await next();
|
||||
}),
|
||||
) as MiddlewareHandler<
|
||||
HonoEnv & {
|
||||
Variables: {
|
||||
user: User;
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
/**
|
||||
* Middleware to check if an emoji exists and is viewable by the user
|
||||
*
|
||||
* Useful in /api/v1/emojis/:id/* routes
|
||||
* @returns
|
||||
*/
|
||||
export const withEmojiParam = every(
|
||||
validator("param", z.object({ id: z.string().uuid() }), handleZodError),
|
||||
createMiddleware<
|
||||
HonoEnv & {
|
||||
Variables: {
|
||||
emoji: Emoji;
|
||||
};
|
||||
},
|
||||
string,
|
||||
WithIdParam
|
||||
>(async (context, next) => {
|
||||
const { id } = context.req.valid("param");
|
||||
|
||||
const emoji = await Emoji.fromId(id);
|
||||
|
||||
if (!emoji) {
|
||||
throw ApiError.emojiNotFound();
|
||||
}
|
||||
|
||||
context.set("emoji", emoji);
|
||||
|
||||
await next();
|
||||
}),
|
||||
) as MiddlewareHandler<
|
||||
HonoEnv & {
|
||||
Variables: {
|
||||
emoji: Emoji;
|
||||
};
|
||||
}
|
||||
>;
|
||||
|
||||
// Helper function to parse form data
|
||||
async function parseFormData(context: Context): Promise<{
|
||||
parsed: ParsedQs;
|
||||
files: Map<string, File>;
|
||||
}> {
|
||||
const formData = await context.req.formData();
|
||||
const urlparams = new URLSearchParams();
|
||||
const files = new Map<string, File>();
|
||||
for (const [key, value] of [
|
||||
...(formData.entries() as IterableIterator<[string, string | File]>),
|
||||
]) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const val of value) {
|
||||
urlparams.append(key, val);
|
||||
}
|
||||
} else if (value instanceof File) {
|
||||
if (!files.has(key)) {
|
||||
files.set(key, value);
|
||||
}
|
||||
} else {
|
||||
urlparams.append(key, String(value));
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = parse(urlparams.toString(), {
|
||||
parseArrays: true,
|
||||
interpretNumericEntities: true,
|
||||
});
|
||||
|
||||
return {
|
||||
parsed,
|
||||
files,
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to parse urlencoded data
|
||||
async function parseUrlEncoded(context: Context): Promise<ParsedQs> {
|
||||
const parsed = parse(await context.req.text(), {
|
||||
parseArrays: true,
|
||||
interpretNumericEntities: true,
|
||||
});
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export const qsQuery = (): MiddlewareHandler<HonoEnv> => {
|
||||
return createMiddleware<HonoEnv>(async (context, next) => {
|
||||
const parsed = parse(new URL(context.req.url).searchParams.toString(), {
|
||||
parseArrays: true,
|
||||
interpretNumericEntities: true,
|
||||
});
|
||||
|
||||
// @ts-expect-error Very bad hack
|
||||
context.req.query = (): typeof parsed => parsed;
|
||||
|
||||
// @ts-expect-error I'm so sorry for this
|
||||
context.req.queries = (): typeof parsed => parsed;
|
||||
await next();
|
||||
});
|
||||
};
|
||||
|
||||
export const setContextFormDataToObject = (
|
||||
context: Context,
|
||||
setTo: object,
|
||||
): Context => {
|
||||
context.req.bodyCache.json = setTo;
|
||||
context.req.parseBody = (): Promise<unknown> =>
|
||||
Promise.resolve(context.req.bodyCache.json);
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Monkeypatching
|
||||
context.req.json = (): Promise<any> =>
|
||||
Promise.resolve(context.req.bodyCache.json);
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
/*
|
||||
* Middleware to magically unfuck forms
|
||||
* Add it to random Hono routes and hope it works
|
||||
* @returns
|
||||
*/
|
||||
export const jsonOrForm = (): MiddlewareHandler<HonoEnv> => {
|
||||
return createMiddleware(async (context, next) => {
|
||||
const contentType = context.req.header("content-type");
|
||||
|
||||
if (contentType?.includes("application/json")) {
|
||||
setContextFormDataToObject(context, await context.req.json());
|
||||
} else if (contentType?.includes("application/x-www-form-urlencoded")) {
|
||||
const parsed = await parseUrlEncoded(context);
|
||||
|
||||
setContextFormDataToObject(context, parsed);
|
||||
context.req.raw.headers.set("Content-Type", "application/json");
|
||||
} else if (contentType?.includes("multipart/form-data")) {
|
||||
const { parsed, files } = await parseFormData(context);
|
||||
|
||||
setContextFormDataToObject(context, {
|
||||
...parsed,
|
||||
...Object.fromEntries(files),
|
||||
});
|
||||
context.req.raw.headers.set("Content-Type", "application/json");
|
||||
} else if (!contentType) {
|
||||
setContextFormDataToObject(context, {});
|
||||
context.req.raw.headers.set("Content-Type", "application/json");
|
||||
}
|
||||
|
||||
await next();
|
||||
});
|
||||
};
|
||||
|
||||
export const debugResponse = async (res: Response): Promise<void> => {
|
||||
const body = await res.clone().text();
|
||||
|
||||
const status = `${chalk.bold("Status")}: ${chalk.green(res.status)}`;
|
||||
|
||||
const headers = `${chalk.bold("Headers")}:\n${Array.from(
|
||||
res.headers.entries(),
|
||||
)
|
||||
.map(([key, value]) => ` - ${chalk.cyan(key)}: ${chalk.white(value)}`)
|
||||
.join("\n")}`;
|
||||
|
||||
const bodyLog = `${chalk.bold("Body")}: ${chalk.gray(body)}`;
|
||||
|
||||
serverLogger.debug`${status}\n${headers}\n${bodyLog}`;
|
||||
};
|
||||
41
packages/kit/build.ts
Normal file
41
packages/kit/build.ts
Normal file
|
|
@ -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!");
|
||||
165
packages/kit/db/application.ts
Normal file
165
packages/kit/db/application.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import type {
|
||||
Application as ApplicationSchema,
|
||||
CredentialApplication,
|
||||
} from "@versia/client/schemas";
|
||||
import {
|
||||
desc,
|
||||
eq,
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
inArray,
|
||||
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<typeof Applications>;
|
||||
|
||||
export class Application extends BaseInterface<typeof Applications> {
|
||||
public static $type: ApplicationType;
|
||||
|
||||
public async reload(): Promise<void> {
|
||||
const reloaded = await Application.fromId(this.data.id);
|
||||
|
||||
if (!reloaded) {
|
||||
throw new Error("Failed to reload application");
|
||||
}
|
||||
|
||||
this.data = reloaded.data;
|
||||
}
|
||||
|
||||
public static async fromId(id: string | null): Promise<Application | null> {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Application.fromSql(eq(Applications.id, id));
|
||||
}
|
||||
|
||||
public static async fromIds(ids: string[]): Promise<Application[]> {
|
||||
return await Application.manyFromSql(inArray(Applications.id, ids));
|
||||
}
|
||||
|
||||
public static async fromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Applications.id),
|
||||
): Promise<Application | null> {
|
||||
const found = await db.query.Applications.findFirst({
|
||||
where: sql,
|
||||
orderBy,
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
return new Application(found);
|
||||
}
|
||||
|
||||
public static async manyFromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Applications.id),
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
extra?: Parameters<typeof db.query.Applications.findMany>[0],
|
||||
): Promise<Application[]> {
|
||||
const found = await db.query.Applications.findMany({
|
||||
where: sql,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
with: extra?.with,
|
||||
});
|
||||
|
||||
return found.map((s) => new Application(s));
|
||||
}
|
||||
|
||||
public static async getFromToken(
|
||||
token: string,
|
||||
): Promise<Application | null> {
|
||||
const result = await Token.fromAccessToken(token);
|
||||
|
||||
return result?.data.application
|
||||
? new Application(result.data.application)
|
||||
: null;
|
||||
}
|
||||
|
||||
public static fromClientId(clientId: string): Promise<Application | null> {
|
||||
return Application.fromSql(eq(Applications.clientId, clientId));
|
||||
}
|
||||
|
||||
public async update(
|
||||
newApplication: Partial<ApplicationType>,
|
||||
): Promise<ApplicationType> {
|
||||
await db
|
||||
.update(Applications)
|
||||
.set(newApplication)
|
||||
.where(eq(Applications.id, this.id));
|
||||
|
||||
const updated = await Application.fromId(this.data.id);
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Failed to update application");
|
||||
}
|
||||
|
||||
this.data = updated.data;
|
||||
return updated.data;
|
||||
}
|
||||
|
||||
public save(): Promise<ApplicationType> {
|
||||
return this.update(this.data);
|
||||
}
|
||||
|
||||
public async delete(ids?: string[]): Promise<void> {
|
||||
if (Array.isArray(ids)) {
|
||||
await db.delete(Applications).where(inArray(Applications.id, ids));
|
||||
} else {
|
||||
await db.delete(Applications).where(eq(Applications.id, this.id));
|
||||
}
|
||||
}
|
||||
|
||||
public static async insert(
|
||||
data: InferInsertModel<typeof Applications>,
|
||||
): Promise<Application> {
|
||||
const inserted = (
|
||||
await db.insert(Applications).values(data).returning()
|
||||
)[0];
|
||||
|
||||
const application = await Application.fromId(inserted.id);
|
||||
|
||||
if (!application) {
|
||||
throw new Error("Failed to insert application");
|
||||
}
|
||||
|
||||
return application;
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
public toApi(): z.infer<typeof ApplicationSchema> {
|
||||
return {
|
||||
name: this.data.name,
|
||||
website: this.data.website,
|
||||
scopes: this.data.scopes.split(" "),
|
||||
redirect_uri: this.data.redirectUri,
|
||||
redirect_uris: this.data.redirectUri.split("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
public toApiCredential(): z.infer<typeof CredentialApplication> {
|
||||
return {
|
||||
name: this.data.name,
|
||||
website: this.data.website,
|
||||
client_id: this.data.clientId,
|
||||
client_secret: this.data.secret,
|
||||
client_secret_expires_at: "0",
|
||||
scopes: this.data.scopes.split(" "),
|
||||
redirect_uri: this.data.redirectUri,
|
||||
redirect_uris: this.data.redirectUri.split("\n"),
|
||||
};
|
||||
}
|
||||
}
|
||||
54
packages/kit/db/base.ts
Normal file
54
packages/kit/db/base.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import type { InferModelFromColumns, InferSelectModel } from "drizzle-orm";
|
||||
import type { PgTableWithColumns } from "drizzle-orm/pg-core";
|
||||
|
||||
/**
|
||||
* BaseInterface is an abstract class that provides a common interface for all models.
|
||||
* It includes methods for saving, deleting, updating, and reloading data.
|
||||
*
|
||||
* @template Table - The type of the table with columns.
|
||||
* @template Columns - The type of the columns inferred from the table.
|
||||
*/
|
||||
export abstract class BaseInterface<
|
||||
// biome-ignore lint/suspicious/noExplicitAny: This is just an extended interface
|
||||
Table extends PgTableWithColumns<any>,
|
||||
Columns = InferModelFromColumns<Table["_"]["columns"]>,
|
||||
> {
|
||||
/**
|
||||
* Constructs a new instance of the BaseInterface.
|
||||
*
|
||||
* @param data - The data for the model.
|
||||
*/
|
||||
public constructor(public data: Columns) {}
|
||||
|
||||
/**
|
||||
* Saves the current state of the model to the database.
|
||||
*
|
||||
* @returns A promise that resolves with the saved model.
|
||||
*/
|
||||
public abstract save(): Promise<Columns>;
|
||||
|
||||
/**
|
||||
* Deletes the model from the database.
|
||||
*
|
||||
* @param ids - The ids of the models to delete. If not provided, the current model will be deleted.
|
||||
* @returns A promise that resolves when the deletion is complete.
|
||||
*/
|
||||
public abstract delete(ids?: string[]): Promise<void>;
|
||||
|
||||
/**
|
||||
* Updates the model with new data.
|
||||
*
|
||||
* @param newData - The new data for the model.
|
||||
* @returns A promise that resolves with the updated model.
|
||||
*/
|
||||
public abstract update(
|
||||
newData: Partial<InferSelectModel<Table>>,
|
||||
): Promise<Columns>;
|
||||
|
||||
/**
|
||||
* Reloads the model from the database.
|
||||
*
|
||||
* @returns A promise that resolves when the reloading is complete.
|
||||
*/
|
||||
public abstract reload(): Promise<void>;
|
||||
}
|
||||
240
packages/kit/db/emoji.ts
Normal file
240
packages/kit/db/emoji.ts
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import {
|
||||
type CustomEmoji,
|
||||
emojiWithColonsRegex,
|
||||
emojiWithIdentifiersRegex,
|
||||
} from "@versia/client/schemas";
|
||||
import * as VersiaEntities from "@versia/sdk/entities";
|
||||
import type { ImageContentFormatSchema } from "@versia/sdk/schemas";
|
||||
import { randomUUIDv7 } from "bun";
|
||||
import {
|
||||
and,
|
||||
desc,
|
||||
eq,
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
inArray,
|
||||
isNull,
|
||||
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<typeof Emojis> & {
|
||||
media: InferSelectModel<typeof Medias>;
|
||||
instance: InferSelectModel<typeof Instances> | null;
|
||||
};
|
||||
|
||||
export class Emoji extends BaseInterface<typeof Emojis, EmojiType> {
|
||||
public static $type: EmojiType;
|
||||
public media: Media;
|
||||
|
||||
public constructor(data: EmojiType) {
|
||||
super(data);
|
||||
this.media = new Media(data.media);
|
||||
}
|
||||
|
||||
public async reload(): Promise<void> {
|
||||
const reloaded = await Emoji.fromId(this.data.id);
|
||||
|
||||
if (!reloaded) {
|
||||
throw new Error("Failed to reload emoji");
|
||||
}
|
||||
|
||||
this.data = reloaded.data;
|
||||
}
|
||||
|
||||
public static async fromId(id: string | null): Promise<Emoji | null> {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Emoji.fromSql(eq(Emojis.id, id));
|
||||
}
|
||||
|
||||
public static async fromIds(ids: string[]): Promise<Emoji[]> {
|
||||
return await Emoji.manyFromSql(inArray(Emojis.id, ids));
|
||||
}
|
||||
|
||||
public static async fromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Emojis.id),
|
||||
): Promise<Emoji | null> {
|
||||
const found = await db.query.Emojis.findFirst({
|
||||
where: sql,
|
||||
orderBy,
|
||||
with: {
|
||||
instance: true,
|
||||
media: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
return new Emoji(found);
|
||||
}
|
||||
|
||||
public static async manyFromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Emojis.id),
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
extra?: Parameters<typeof db.query.Emojis.findMany>[0],
|
||||
): Promise<Emoji[]> {
|
||||
const found = await db.query.Emojis.findMany({
|
||||
where: sql,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
with: { ...extra?.with, instance: true, media: true },
|
||||
});
|
||||
|
||||
return found.map((s) => new Emoji(s));
|
||||
}
|
||||
|
||||
public async update(newEmoji: Partial<EmojiType>): Promise<EmojiType> {
|
||||
await db.update(Emojis).set(newEmoji).where(eq(Emojis.id, this.id));
|
||||
|
||||
const updated = await Emoji.fromId(this.data.id);
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Failed to update emoji");
|
||||
}
|
||||
|
||||
this.data = updated.data;
|
||||
return updated.data;
|
||||
}
|
||||
|
||||
public save(): Promise<EmojiType> {
|
||||
return this.update(this.data);
|
||||
}
|
||||
|
||||
public async delete(ids?: string[]): Promise<void> {
|
||||
if (Array.isArray(ids)) {
|
||||
await db.delete(Emojis).where(inArray(Emojis.id, ids));
|
||||
} else {
|
||||
await db.delete(Emojis).where(eq(Emojis.id, this.id));
|
||||
}
|
||||
}
|
||||
|
||||
public static async insert(
|
||||
data: InferInsertModel<typeof Emojis>,
|
||||
): Promise<Emoji> {
|
||||
const inserted = (await db.insert(Emojis).values(data).returning())[0];
|
||||
|
||||
const emoji = await Emoji.fromId(inserted.id);
|
||||
|
||||
if (!emoji) {
|
||||
throw new Error("Failed to insert emoji");
|
||||
}
|
||||
|
||||
return emoji;
|
||||
}
|
||||
|
||||
public static async fetchFromRemote(
|
||||
emojiToFetch: {
|
||||
name: string;
|
||||
url: z.infer<typeof ImageContentFormatSchema>;
|
||||
},
|
||||
instance: Instance,
|
||||
): Promise<Emoji> {
|
||||
const existingEmoji = await Emoji.fromSql(
|
||||
and(
|
||||
eq(Emojis.shortcode, emojiToFetch.name),
|
||||
eq(Emojis.instanceId, instance.id),
|
||||
),
|
||||
);
|
||||
|
||||
if (existingEmoji) {
|
||||
return existingEmoji;
|
||||
}
|
||||
|
||||
return await Emoji.fromVersia(emojiToFetch, instance);
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse emojis from text
|
||||
*
|
||||
* @param text The text to parse
|
||||
* @returns An array of emojis
|
||||
*/
|
||||
public static parseFromText(text: string): Promise<Emoji[]> {
|
||||
const matches = text.match(emojiWithColonsRegex);
|
||||
if (!matches || matches.length === 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
return Emoji.manyFromSql(
|
||||
and(
|
||||
inArray(
|
||||
Emojis.shortcode,
|
||||
matches.map((match) => match.replace(/:/g, "")),
|
||||
),
|
||||
isNull(Emojis.instanceId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public toApi(): z.infer<typeof CustomEmoji> {
|
||||
return {
|
||||
id: this.id,
|
||||
shortcode: this.data.shortcode,
|
||||
static_url: this.media.getUrl().proxied,
|
||||
url: this.media.getUrl().proxied,
|
||||
visible_in_picker: this.data.visibleInPicker,
|
||||
category: this.data.category,
|
||||
global: this.data.ownerId === null,
|
||||
description:
|
||||
this.media.data.content[this.media.getPreferredMimeType()]
|
||||
.description ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
public toVersia(): {
|
||||
name: string;
|
||||
url: z.infer<typeof ImageContentFormatSchema>;
|
||||
} {
|
||||
return {
|
||||
name: `:${this.data.shortcode}:`,
|
||||
url: this.media.toVersia().data as z.infer<
|
||||
typeof ImageContentFormatSchema
|
||||
>,
|
||||
};
|
||||
}
|
||||
|
||||
public static async fromVersia(
|
||||
emoji: {
|
||||
name: string;
|
||||
url: z.infer<typeof ImageContentFormatSchema>;
|
||||
},
|
||||
instance: Instance,
|
||||
): Promise<Emoji> {
|
||||
// Extracts the shortcode from the emoji name (e.g. :shortcode: -> shortcode)
|
||||
const shortcode = [...emoji.name.matchAll(emojiWithIdentifiersRegex)][0]
|
||||
.groups.shortcode;
|
||||
|
||||
if (!shortcode) {
|
||||
throw new Error("Could not extract shortcode from emoji name");
|
||||
}
|
||||
|
||||
const media = await Media.fromVersia(
|
||||
new VersiaEntities.ImageContentFormat(emoji.url),
|
||||
);
|
||||
|
||||
return Emoji.insert({
|
||||
id: randomUUIDv7(),
|
||||
shortcode,
|
||||
mediaId: media.id,
|
||||
visibleInPicker: true,
|
||||
instanceId: instance.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
15
packages/kit/db/index.ts
Normal file
15
packages/kit/db/index.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export { db, setupDatabase } from "../tables/db.ts";
|
||||
export { Application } from "./application.ts";
|
||||
export { Emoji } from "./emoji.ts";
|
||||
export { Instance } from "./instance.ts";
|
||||
export { Like } from "./like.ts";
|
||||
export { Media } from "./media.ts";
|
||||
export { Note } from "./note.ts";
|
||||
export { Notification } from "./notification.ts";
|
||||
export { PushSubscription } from "./pushsubscription.ts";
|
||||
export { Reaction } from "./reaction.ts";
|
||||
export { Relationship } from "./relationship.ts";
|
||||
export { Role } from "./role.ts";
|
||||
export { Timeline } from "./timeline.ts";
|
||||
export { Token } from "./token.ts";
|
||||
export { User } from "./user.ts";
|
||||
369
packages/kit/db/instance.ts
Normal file
369
packages/kit/db/instance.ts
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
import * as VersiaEntities from "@versia/sdk/entities";
|
||||
import { FederationRequester } from "@versia/sdk/http";
|
||||
import { config } from "@versia-server/config";
|
||||
import {
|
||||
federationMessagingLogger,
|
||||
federationResolversLogger,
|
||||
} from "@versia-server/logging";
|
||||
import { randomUUIDv7 } from "bun";
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
desc,
|
||||
eq,
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
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 type { User } from "./user.ts";
|
||||
|
||||
type InstanceType = InferSelectModel<typeof Instances>;
|
||||
|
||||
export class Instance extends BaseInterface<typeof Instances> {
|
||||
public static $type: InstanceType;
|
||||
|
||||
public async reload(): Promise<void> {
|
||||
const reloaded = await Instance.fromId(this.data.id);
|
||||
|
||||
if (!reloaded) {
|
||||
throw new Error("Failed to reload instance");
|
||||
}
|
||||
|
||||
this.data = reloaded.data;
|
||||
}
|
||||
|
||||
public static async fromId(id: string | null): Promise<Instance | null> {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Instance.fromSql(eq(Instances.id, id));
|
||||
}
|
||||
|
||||
public static async fromIds(ids: string[]): Promise<Instance[]> {
|
||||
return await Instance.manyFromSql(inArray(Instances.id, ids));
|
||||
}
|
||||
|
||||
public static async fromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Instances.id),
|
||||
): Promise<Instance | null> {
|
||||
const found = await db.query.Instances.findFirst({
|
||||
where: sql,
|
||||
orderBy,
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
return new Instance(found);
|
||||
}
|
||||
|
||||
public static async manyFromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Instances.id),
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
extra?: Parameters<typeof db.query.Instances.findMany>[0],
|
||||
): Promise<Instance[]> {
|
||||
const found = await db.query.Instances.findMany({
|
||||
where: sql,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
with: extra?.with,
|
||||
});
|
||||
|
||||
return found.map((s) => new Instance(s));
|
||||
}
|
||||
|
||||
public async update(
|
||||
newInstance: Partial<InstanceType>,
|
||||
): Promise<InstanceType> {
|
||||
await db
|
||||
.update(Instances)
|
||||
.set(newInstance)
|
||||
.where(eq(Instances.id, this.id));
|
||||
|
||||
const updated = await Instance.fromId(this.data.id);
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Failed to update instance");
|
||||
}
|
||||
|
||||
this.data = updated.data;
|
||||
return updated.data;
|
||||
}
|
||||
|
||||
public save(): Promise<InstanceType> {
|
||||
return this.update(this.data);
|
||||
}
|
||||
|
||||
public async delete(ids?: string[]): Promise<void> {
|
||||
if (Array.isArray(ids)) {
|
||||
await db.delete(Instances).where(inArray(Instances.id, ids));
|
||||
} else {
|
||||
await db.delete(Instances).where(eq(Instances.id, this.id));
|
||||
}
|
||||
}
|
||||
|
||||
public static async fromUser(user: User): Promise<Instance | null> {
|
||||
if (!user.data.instanceId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Instance.fromId(user.data.instanceId);
|
||||
}
|
||||
|
||||
public static async insert(
|
||||
data: InferInsertModel<typeof Instances>,
|
||||
): Promise<Instance> {
|
||||
const inserted = (
|
||||
await db.insert(Instances).values(data).returning()
|
||||
)[0];
|
||||
|
||||
const instance = await Instance.fromId(inserted.id);
|
||||
|
||||
if (!instance) {
|
||||
throw new Error("Failed to insert instance");
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
public static async fetchMetadata(url: URL): Promise<{
|
||||
metadata: VersiaEntities.InstanceMetadata;
|
||||
protocol: "versia" | "activitypub";
|
||||
}> {
|
||||
const origin = new URL(url).origin;
|
||||
const wellKnownUrl = new URL("/.well-known/versia", origin);
|
||||
|
||||
try {
|
||||
const metadata = await new FederationRequester(
|
||||
config.instance.keys.private,
|
||||
config.http.base_url,
|
||||
).fetchEntity(wellKnownUrl, VersiaEntities.InstanceMetadata);
|
||||
|
||||
return { metadata, protocol: "versia" };
|
||||
} catch {
|
||||
// If the server doesn't have a Versia well-known endpoint, it's not a Versia instance
|
||||
// Try to resolve ActivityPub metadata instead
|
||||
const data = await Instance.fetchActivityPubMetadata(url);
|
||||
|
||||
if (!data) {
|
||||
throw new ApiError(
|
||||
404,
|
||||
`Instance at ${origin} is not reachable or does not exist`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
metadata: data,
|
||||
protocol: "activitypub",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static async fetchActivityPubMetadata(
|
||||
url: URL,
|
||||
): Promise<VersiaEntities.InstanceMetadata | null> {
|
||||
const origin = new URL(url).origin;
|
||||
const wellKnownUrl = new URL("/.well-known/nodeinfo", origin);
|
||||
|
||||
// Go to endpoint, then follow the links to the actual metadata
|
||||
try {
|
||||
const { json, ok, status } = await fetch(wellKnownUrl, {
|
||||
// @ts-expect-error Bun extension
|
||||
proxy: config.http.proxy_address,
|
||||
});
|
||||
|
||||
if (!ok) {
|
||||
federationResolversLogger.error`Failed to fetch ActivityPub metadata for instance ${chalk.bold(
|
||||
origin,
|
||||
)} - HTTP ${status}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
const wellKnown = (await json()) as {
|
||||
links: { rel: string; href: string }[];
|
||||
};
|
||||
|
||||
if (!wellKnown.links) {
|
||||
federationResolversLogger.error`Failed to fetch ActivityPub metadata for instance ${chalk.bold(
|
||||
origin,
|
||||
)} - No links found`;
|
||||
return null;
|
||||
}
|
||||
|
||||
const metadataUrl = wellKnown.links.find(
|
||||
(link: { rel: string }) =>
|
||||
link.rel ===
|
||||
"http://nodeinfo.diaspora.software/ns/schema/2.0",
|
||||
);
|
||||
|
||||
if (!metadataUrl) {
|
||||
federationResolversLogger.error`Failed to fetch ActivityPub metadata for instance ${chalk.bold(
|
||||
origin,
|
||||
)} - No metadata URL found`;
|
||||
return null;
|
||||
}
|
||||
|
||||
const {
|
||||
json: json2,
|
||||
ok: ok2,
|
||||
status: status2,
|
||||
} = await fetch(metadataUrl.href, {
|
||||
// @ts-expect-error Bun extension
|
||||
proxy: config.http.proxy_address,
|
||||
});
|
||||
|
||||
if (!ok2) {
|
||||
federationResolversLogger.error`Failed to fetch ActivityPub metadata for instance ${chalk.bold(
|
||||
origin,
|
||||
)} - HTTP ${status2}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
const metadata = (await json2()) as {
|
||||
metadata: {
|
||||
nodeName?: string;
|
||||
title?: string;
|
||||
nodeDescription?: string;
|
||||
description?: string;
|
||||
};
|
||||
software: { version: string };
|
||||
};
|
||||
|
||||
return new VersiaEntities.InstanceMetadata({
|
||||
name:
|
||||
metadata.metadata.nodeName || metadata.metadata.title || "",
|
||||
description:
|
||||
metadata.metadata.nodeDescription ||
|
||||
metadata.metadata.description,
|
||||
type: "InstanceMetadata",
|
||||
software: {
|
||||
name: "Unknown ActivityPub software",
|
||||
version: metadata.software.version,
|
||||
},
|
||||
created_at: new Date().toISOString(),
|
||||
public_key: {
|
||||
key: "",
|
||||
algorithm: "ed25519",
|
||||
},
|
||||
host: new URL(url).host,
|
||||
compatibility: {
|
||||
extensions: [],
|
||||
versions: [],
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
federationResolversLogger.error`Failed to fetch ActivityPub metadata for instance ${chalk.bold(
|
||||
origin,
|
||||
)} - Error! ${error}`;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static resolveFromHost(host: string): Promise<Instance> {
|
||||
if (host.startsWith("http")) {
|
||||
const url = new URL(host);
|
||||
|
||||
return Instance.resolve(url);
|
||||
}
|
||||
|
||||
const url = new URL(`https://${host}`);
|
||||
|
||||
return Instance.resolve(url);
|
||||
}
|
||||
|
||||
public static async resolve(url: URL): Promise<Instance> {
|
||||
const host = url.host;
|
||||
|
||||
const existingInstance = await Instance.fromSql(
|
||||
eq(Instances.baseUrl, host),
|
||||
);
|
||||
|
||||
if (existingInstance) {
|
||||
return existingInstance;
|
||||
}
|
||||
|
||||
const output = await Instance.fetchMetadata(url);
|
||||
|
||||
const { metadata, protocol } = output;
|
||||
|
||||
return Instance.insert({
|
||||
id: randomUUIDv7(),
|
||||
baseUrl: host,
|
||||
name: metadata.data.name,
|
||||
version: metadata.data.software.version,
|
||||
logo: metadata.data.logo,
|
||||
protocol,
|
||||
publicKey: metadata.data.public_key,
|
||||
inbox: metadata.data.shared_inbox ?? null,
|
||||
extensions: metadata.data.extensions ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
public async updateFromRemote(): Promise<Instance> {
|
||||
const output = await Instance.fetchMetadata(
|
||||
new URL(`https://${this.data.baseUrl}`),
|
||||
);
|
||||
|
||||
if (!output) {
|
||||
federationResolversLogger.error`Failed to update instance ${chalk.bold(
|
||||
this.data.baseUrl,
|
||||
)}`;
|
||||
throw new Error("Failed to update instance");
|
||||
}
|
||||
|
||||
const { metadata, protocol } = output;
|
||||
|
||||
await this.update({
|
||||
name: metadata.data.name,
|
||||
version: metadata.data.software.version,
|
||||
logo: metadata.data.logo,
|
||||
protocol,
|
||||
publicKey: metadata.data.public_key,
|
||||
inbox: metadata.data.shared_inbox ?? null,
|
||||
extensions: metadata.data.extensions ?? null,
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public async sendMessage(content: string): Promise<void> {
|
||||
if (
|
||||
!this.data.extensions?.["pub.versia:instance_messaging"]?.endpoint
|
||||
) {
|
||||
federationMessagingLogger.info`Instance ${chalk.gray(
|
||||
this.data.baseUrl,
|
||||
)} does not support Instance Messaging, skipping message`;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint = new URL(
|
||||
this.data.extensions["pub.versia:instance_messaging"].endpoint,
|
||||
);
|
||||
|
||||
await fetch(endpoint.href, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
body: content,
|
||||
});
|
||||
}
|
||||
|
||||
public static getCount(): Promise<number> {
|
||||
return db.$count(Instances);
|
||||
}
|
||||
}
|
||||
182
packages/kit/db/like.ts
Normal file
182
packages/kit/db/like.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import * as VersiaEntities from "@versia/sdk/entities";
|
||||
import { config } from "@versia-server/config";
|
||||
import {
|
||||
and,
|
||||
desc,
|
||||
eq,
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
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";
|
||||
|
||||
type LikeType = InferSelectModel<typeof Likes> & {
|
||||
liker: InferSelectModel<typeof Users>;
|
||||
liked: InferSelectModel<typeof Notes>;
|
||||
};
|
||||
|
||||
export class Like extends BaseInterface<typeof Likes, LikeType> {
|
||||
public static $type: LikeType;
|
||||
|
||||
public async reload(): Promise<void> {
|
||||
const reloaded = await Like.fromId(this.data.id);
|
||||
|
||||
if (!reloaded) {
|
||||
throw new Error("Failed to reload like");
|
||||
}
|
||||
|
||||
this.data = reloaded.data;
|
||||
}
|
||||
|
||||
public static async fromId(id: string | null): Promise<Like | null> {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Like.fromSql(eq(Likes.id, id));
|
||||
}
|
||||
|
||||
public static async fromIds(ids: string[]): Promise<Like[]> {
|
||||
return await Like.manyFromSql(inArray(Likes.id, ids));
|
||||
}
|
||||
|
||||
public static async fromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Likes.id),
|
||||
): Promise<Like | null> {
|
||||
const found = await db.query.Likes.findFirst({
|
||||
where: sql,
|
||||
orderBy,
|
||||
with: {
|
||||
liked: true,
|
||||
liker: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
return new Like(found);
|
||||
}
|
||||
|
||||
public static async manyFromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Likes.id),
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
extra?: Parameters<typeof db.query.Likes.findMany>[0],
|
||||
): Promise<Like[]> {
|
||||
const found = await db.query.Likes.findMany({
|
||||
where: sql,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
with: {
|
||||
liked: true,
|
||||
liker: true,
|
||||
...extra?.with,
|
||||
},
|
||||
});
|
||||
|
||||
return found.map((s) => new Like(s));
|
||||
}
|
||||
|
||||
public async update(newRole: Partial<LikeType>): Promise<LikeType> {
|
||||
await db.update(Likes).set(newRole).where(eq(Likes.id, this.id));
|
||||
|
||||
const updated = await Like.fromId(this.data.id);
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Failed to update like");
|
||||
}
|
||||
|
||||
return updated.data;
|
||||
}
|
||||
|
||||
public save(): Promise<LikeType> {
|
||||
return this.update(this.data);
|
||||
}
|
||||
|
||||
public async delete(): Promise<void> {
|
||||
await db.delete(Likes).where(eq(Likes.id, this.id));
|
||||
}
|
||||
|
||||
public static async insert(
|
||||
data: InferInsertModel<typeof Likes>,
|
||||
): Promise<Like> {
|
||||
const inserted = (await db.insert(Likes).values(data).returning())[0];
|
||||
|
||||
const like = await Like.fromId(inserted.id);
|
||||
|
||||
if (!like) {
|
||||
throw new Error("Failed to insert like");
|
||||
}
|
||||
|
||||
return like;
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
public async clearRelatedNotifications(): Promise<void> {
|
||||
await db
|
||||
.delete(Notifications)
|
||||
.where(
|
||||
and(
|
||||
eq(Notifications.accountId, this.id),
|
||||
eq(Notifications.type, "favourite"),
|
||||
eq(Notifications.notifiedId, this.data.liked.authorId),
|
||||
eq(Notifications.noteId, this.data.liked.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public getUri(): URL {
|
||||
return new URL(`/likes/${this.data.id}`, config.http.base_url);
|
||||
}
|
||||
|
||||
public toVersia(): VersiaEntities.Like {
|
||||
return new VersiaEntities.Like({
|
||||
id: this.data.id,
|
||||
author: User.getUri(
|
||||
this.data.liker.id,
|
||||
this.data.liker.uri ? new URL(this.data.liker.uri) : null,
|
||||
).href,
|
||||
type: "pub.versia:likes/Like",
|
||||
created_at: new Date(this.data.createdAt).toISOString(),
|
||||
liked: this.data.liked.uri
|
||||
? new URL(this.data.liked.uri).href
|
||||
: new URL(`/notes/${this.data.liked.id}`, config.http.base_url)
|
||||
.href,
|
||||
uri: this.getUri().href,
|
||||
});
|
||||
}
|
||||
|
||||
public unlikeToVersia(unliker?: User): VersiaEntities.Delete {
|
||||
return new VersiaEntities.Delete({
|
||||
type: "Delete",
|
||||
id: crypto.randomUUID(),
|
||||
created_at: new Date().toISOString(),
|
||||
author: User.getUri(
|
||||
unliker?.id ?? this.data.liker.id,
|
||||
unliker?.data.uri
|
||||
? new URL(unliker.data.uri)
|
||||
: this.data.liker.uri
|
||||
? new URL(this.data.liker.uri)
|
||||
: null,
|
||||
).href,
|
||||
deleted_type: "pub.versia:likes/Like",
|
||||
deleted: this.getUri().href,
|
||||
});
|
||||
}
|
||||
}
|
||||
554
packages/kit/db/media.ts
Normal file
554
packages/kit/db/media.ts
Normal file
|
|
@ -0,0 +1,554 @@
|
|||
import { join } from "node:path";
|
||||
import type { Attachment as AttachmentSchema } from "@versia/client/schemas";
|
||||
import * as VersiaEntities from "@versia/sdk/entities";
|
||||
import type {
|
||||
ContentFormatSchema,
|
||||
ImageContentFormatSchema,
|
||||
} from "@versia/sdk/schemas";
|
||||
import { config, MediaBackendType, ProxiableUrl } from "@versia-server/config";
|
||||
import { randomUUIDv7, S3Client, SHA256, write } from "bun";
|
||||
import {
|
||||
desc,
|
||||
eq,
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
inArray,
|
||||
type SQL,
|
||||
} from "drizzle-orm";
|
||||
import sharp from "sharp";
|
||||
import type { z } from "zod";
|
||||
import { mimeLookup } from "@/content_types.ts";
|
||||
import { getMediaHash } from "../../../classes/media/media-hasher.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<typeof Medias>;
|
||||
|
||||
export class Media extends BaseInterface<typeof Medias> {
|
||||
public static $type: MediaType;
|
||||
|
||||
public async reload(): Promise<void> {
|
||||
const reloaded = await Media.fromId(this.data.id);
|
||||
|
||||
if (!reloaded) {
|
||||
throw new Error("Failed to reload attachment");
|
||||
}
|
||||
|
||||
this.data = reloaded.data;
|
||||
}
|
||||
|
||||
public static async fromId(id: string | null): Promise<Media | null> {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Media.fromSql(eq(Medias.id, id));
|
||||
}
|
||||
|
||||
public static async fromIds(ids: string[]): Promise<Media[]> {
|
||||
return await Media.manyFromSql(inArray(Medias.id, ids));
|
||||
}
|
||||
|
||||
public static async fromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Medias.id),
|
||||
): Promise<Media | null> {
|
||||
const found = await db.query.Medias.findFirst({
|
||||
where: sql,
|
||||
orderBy,
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
return new Media(found);
|
||||
}
|
||||
|
||||
public static async manyFromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Medias.id),
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
extra?: Parameters<typeof db.query.Medias.findMany>[0],
|
||||
): Promise<Media[]> {
|
||||
const found = await db.query.Medias.findMany({
|
||||
where: sql,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
with: extra?.with,
|
||||
});
|
||||
|
||||
return found.map((s) => new Media(s));
|
||||
}
|
||||
|
||||
public async update(newAttachment: Partial<MediaType>): Promise<MediaType> {
|
||||
await db
|
||||
.update(Medias)
|
||||
.set(newAttachment)
|
||||
.where(eq(Medias.id, this.id));
|
||||
|
||||
const updated = await Media.fromId(this.data.id);
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Failed to update attachment");
|
||||
}
|
||||
|
||||
this.data = updated.data;
|
||||
return updated.data;
|
||||
}
|
||||
|
||||
public save(): Promise<MediaType> {
|
||||
return this.update(this.data);
|
||||
}
|
||||
|
||||
public async delete(ids?: string[]): Promise<void> {
|
||||
if (Array.isArray(ids)) {
|
||||
await db.delete(Medias).where(inArray(Medias.id, ids));
|
||||
} else {
|
||||
await db.delete(Medias).where(eq(Medias.id, this.id));
|
||||
}
|
||||
|
||||
// TODO: Also delete the file from the media manager
|
||||
}
|
||||
|
||||
public static async insert(
|
||||
data: InferInsertModel<typeof Medias>,
|
||||
): Promise<Media> {
|
||||
const inserted = (await db.insert(Medias).values(data).returning())[0];
|
||||
|
||||
const attachment = await Media.fromId(inserted.id);
|
||||
|
||||
if (!attachment) {
|
||||
throw new Error("Failed to insert attachment");
|
||||
}
|
||||
|
||||
return attachment;
|
||||
}
|
||||
|
||||
private static async upload(file: File): Promise<{
|
||||
path: string;
|
||||
}> {
|
||||
const fileName = file.name ?? randomUUIDv7();
|
||||
const hash = await getMediaHash(file);
|
||||
|
||||
switch (config.media.backend) {
|
||||
case MediaBackendType.Local: {
|
||||
const path = join(config.media.uploads_path, hash, fileName);
|
||||
|
||||
await write(path, file);
|
||||
|
||||
return { path: join(hash, fileName) };
|
||||
}
|
||||
|
||||
case MediaBackendType.S3: {
|
||||
const path = join(hash, fileName);
|
||||
|
||||
if (!config.s3) {
|
||||
throw new ApiError(500, "S3 configuration missing");
|
||||
}
|
||||
|
||||
const client = new S3Client({
|
||||
endpoint: config.s3.endpoint.origin,
|
||||
region: config.s3.region,
|
||||
bucket: config.s3.bucket_name,
|
||||
accessKeyId: config.s3.access_key,
|
||||
secretAccessKey: config.s3.secret_access_key,
|
||||
virtualHostedStyle: !config.s3.path_style,
|
||||
});
|
||||
|
||||
await client.write(path, file);
|
||||
const finalPath = config.s3.path
|
||||
? join(config.s3.path, path)
|
||||
: path;
|
||||
|
||||
return { path: finalPath };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static async fromFile(
|
||||
file: File,
|
||||
options?: {
|
||||
description?: string;
|
||||
thumbnail?: File;
|
||||
},
|
||||
): Promise<Media> {
|
||||
Media.checkFile(file);
|
||||
|
||||
const { path } = await Media.upload(file);
|
||||
|
||||
const url = Media.getUrl(path);
|
||||
|
||||
let thumbnailUrl: URL | null = null;
|
||||
|
||||
if (options?.thumbnail) {
|
||||
const { path } = await Media.upload(options.thumbnail);
|
||||
|
||||
thumbnailUrl = Media.getUrl(path);
|
||||
}
|
||||
|
||||
const content = await Media.fileToContentFormat(file, url, {
|
||||
description: options?.description,
|
||||
});
|
||||
const thumbnailContent =
|
||||
thumbnailUrl && options?.thumbnail
|
||||
? await Media.fileToContentFormat(
|
||||
options.thumbnail,
|
||||
thumbnailUrl,
|
||||
{
|
||||
description: options?.description,
|
||||
},
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const newAttachment = await Media.insert({
|
||||
id: randomUUIDv7(),
|
||||
content,
|
||||
thumbnail: thumbnailContent as z.infer<
|
||||
typeof ImageContentFormatSchema
|
||||
>,
|
||||
});
|
||||
|
||||
if (config.media.conversion.convert_images) {
|
||||
await mediaQueue.add(MediaJobType.ConvertMedia, {
|
||||
attachmentId: newAttachment.id,
|
||||
filename: file.name,
|
||||
});
|
||||
}
|
||||
|
||||
await mediaQueue.add(MediaJobType.CalculateMetadata, {
|
||||
attachmentId: newAttachment.id,
|
||||
filename: file.name,
|
||||
});
|
||||
|
||||
return newAttachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and adds a new media attachment from a URL
|
||||
* @param uri
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
public static async fromUrl(
|
||||
uri: URL,
|
||||
options?: {
|
||||
description?: string;
|
||||
},
|
||||
): Promise<Media> {
|
||||
const mimeType = await mimeLookup(uri);
|
||||
|
||||
const content: z.infer<typeof ContentFormatSchema> = {
|
||||
[mimeType]: {
|
||||
content: uri.toString(),
|
||||
remote: true,
|
||||
description: options?.description,
|
||||
},
|
||||
};
|
||||
|
||||
const newAttachment = await Media.insert({
|
||||
id: randomUUIDv7(),
|
||||
content,
|
||||
});
|
||||
|
||||
await mediaQueue.add(MediaJobType.CalculateMetadata, {
|
||||
attachmentId: newAttachment.id,
|
||||
// CalculateMetadata doesn't use the filename, but the type is annoying
|
||||
// and requires it anyway
|
||||
filename: "blank",
|
||||
});
|
||||
|
||||
return newAttachment;
|
||||
}
|
||||
|
||||
private static checkFile(file: File): void {
|
||||
if (file.size > config.validation.media.max_bytes) {
|
||||
throw new ApiError(
|
||||
413,
|
||||
`File too large, max size is ${config.validation.media.max_bytes} bytes`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
config.validation.media.allowed_mime_types.length > 0 &&
|
||||
!config.validation.media.allowed_mime_types.includes(file.type)
|
||||
) {
|
||||
throw new ApiError(
|
||||
415,
|
||||
`File type ${file.type} is not allowed`,
|
||||
`Allowed types: ${config.validation.media.allowed_mime_types.join(
|
||||
", ",
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async updateFromFile(file: File): Promise<void> {
|
||||
Media.checkFile(file);
|
||||
|
||||
const { path } = await Media.upload(file);
|
||||
|
||||
const url = Media.getUrl(path);
|
||||
|
||||
const content = await Media.fileToContentFormat(file, url, {
|
||||
description:
|
||||
this.data.content[Object.keys(this.data.content)[0]]
|
||||
.description || undefined,
|
||||
});
|
||||
|
||||
await this.update({
|
||||
content,
|
||||
});
|
||||
|
||||
await mediaQueue.add(MediaJobType.CalculateMetadata, {
|
||||
attachmentId: this.id,
|
||||
filename: file.name,
|
||||
});
|
||||
}
|
||||
|
||||
public async updateFromUrl(uri: URL): Promise<void> {
|
||||
const mimeType = await mimeLookup(uri);
|
||||
|
||||
const content: z.infer<typeof ContentFormatSchema> = {
|
||||
[mimeType]: {
|
||||
content: uri.toString(),
|
||||
remote: true,
|
||||
description:
|
||||
this.data.content[Object.keys(this.data.content)[0]]
|
||||
.description || undefined,
|
||||
},
|
||||
};
|
||||
|
||||
await this.update({
|
||||
content,
|
||||
});
|
||||
|
||||
await mediaQueue.add(MediaJobType.CalculateMetadata, {
|
||||
attachmentId: this.id,
|
||||
filename: "blank",
|
||||
});
|
||||
}
|
||||
|
||||
public async updateThumbnail(file: File): Promise<void> {
|
||||
Media.checkFile(file);
|
||||
|
||||
const { path } = await Media.upload(file);
|
||||
|
||||
const url = Media.getUrl(path);
|
||||
|
||||
const content = await Media.fileToContentFormat(file, url);
|
||||
|
||||
await this.update({
|
||||
thumbnail: content as z.infer<typeof ImageContentFormatSchema>,
|
||||
});
|
||||
}
|
||||
|
||||
public async updateMetadata(
|
||||
metadata: Partial<
|
||||
Omit<
|
||||
z.infer<typeof ContentFormatSchema>[keyof z.infer<
|
||||
typeof ContentFormatSchema
|
||||
>],
|
||||
"content"
|
||||
>
|
||||
>,
|
||||
): Promise<void> {
|
||||
const content = this.data.content;
|
||||
|
||||
for (const type of Object.keys(content)) {
|
||||
content[type] = {
|
||||
...content[type],
|
||||
...metadata,
|
||||
};
|
||||
}
|
||||
|
||||
await this.update({
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
public static getUrl(name: string): URL {
|
||||
if (config.media.backend === MediaBackendType.Local) {
|
||||
return new URL(`/media/${name}`, config.http.base_url);
|
||||
}
|
||||
if (config.media.backend === MediaBackendType.S3) {
|
||||
return new URL(`/${name}`, config.s3?.public_url);
|
||||
}
|
||||
|
||||
throw new Error("Unknown media backend");
|
||||
}
|
||||
|
||||
public getUrl(): ProxiableUrl {
|
||||
const type = this.getPreferredMimeType();
|
||||
|
||||
return new ProxiableUrl(this.data.content[type]?.content ?? "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets favourite MIME type for the attachment
|
||||
* Uses a hardcoded list of preferred types, for images
|
||||
*
|
||||
* @returns {string} Preferred MIME type
|
||||
*/
|
||||
public getPreferredMimeType(): string {
|
||||
return Media.getPreferredMimeType(Object.keys(this.data.content));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets favourite MIME type from a list
|
||||
* Uses a hardcoded list of preferred types, for images
|
||||
*
|
||||
* @returns {string} Preferred MIME type
|
||||
*/
|
||||
public static getPreferredMimeType(types: string[]): string {
|
||||
const ranking = [
|
||||
"image/svg+xml",
|
||||
"image/avif",
|
||||
"image/jxl",
|
||||
"image/webp",
|
||||
"image/heif",
|
||||
"image/heif-sequence",
|
||||
"image/heic",
|
||||
"image/heic-sequence",
|
||||
"image/apng",
|
||||
"image/gif",
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/bmp",
|
||||
];
|
||||
|
||||
return ranking.find((type) => types.includes(type)) ?? types[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps MIME type to Mastodon attachment type
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
public getMastodonType(): z.infer<typeof AttachmentSchema.shape.type> {
|
||||
const type = this.getPreferredMimeType();
|
||||
|
||||
if (type.startsWith("image/")) {
|
||||
return "image";
|
||||
}
|
||||
if (type.startsWith("video/")) {
|
||||
return "video";
|
||||
}
|
||||
if (type.startsWith("audio/")) {
|
||||
return "audio";
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts metadata from a file and outputs as ContentFormat
|
||||
*
|
||||
* Does not calculate thumbhash (do this in a worker)
|
||||
* @param file
|
||||
* @param uri Uploaded file URI
|
||||
* @param options Extra metadata, such as description
|
||||
* @returns
|
||||
*/
|
||||
public static async fileToContentFormat(
|
||||
file: File,
|
||||
uri: URL,
|
||||
options?: Partial<{
|
||||
description: string;
|
||||
}>,
|
||||
): Promise<z.infer<typeof ContentFormatSchema>> {
|
||||
const buffer = await file.arrayBuffer();
|
||||
const isImage = file.type.startsWith("image/");
|
||||
const { width, height } = isImage ? await sharp(buffer).metadata() : {};
|
||||
const hash = new SHA256().update(file).digest("hex");
|
||||
|
||||
// Missing: fps, duration
|
||||
// Thumbhash should be added in a worker after the file is uploaded
|
||||
return {
|
||||
[file.type]: {
|
||||
content: uri.toString(),
|
||||
remote: true,
|
||||
hash: {
|
||||
sha256: hash,
|
||||
},
|
||||
width,
|
||||
height,
|
||||
description: options?.description,
|
||||
size: file.size,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public toApiMeta(): z.infer<typeof AttachmentSchema.shape.meta> {
|
||||
const type = this.getPreferredMimeType();
|
||||
const data = this.data.content[type];
|
||||
const size =
|
||||
data.width && data.height
|
||||
? `${data.width}x${data.height}`
|
||||
: undefined;
|
||||
const aspect =
|
||||
data.width && data.height ? data.width / data.height : undefined;
|
||||
|
||||
return {
|
||||
width: data.width || undefined,
|
||||
height: data.height || undefined,
|
||||
fps: data.fps || undefined,
|
||||
size,
|
||||
// Idk whether size or length is the right value
|
||||
duration: data.duration || undefined,
|
||||
// Versia doesn't have a concept of length in ContentFormat
|
||||
length: undefined,
|
||||
aspect,
|
||||
original: {
|
||||
width: data.width || undefined,
|
||||
height: data.height || undefined,
|
||||
size,
|
||||
aspect,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public toApi(): z.infer<typeof AttachmentSchema> {
|
||||
const type = this.getPreferredMimeType();
|
||||
const data = this.data.content[type];
|
||||
|
||||
// Thumbnail should only have a single MIME type
|
||||
const thumbnailData =
|
||||
this.data.thumbnail?.[Object.keys(this.data.thumbnail)[0]];
|
||||
|
||||
return {
|
||||
id: this.data.id,
|
||||
type: this.getMastodonType(),
|
||||
url: this.getUrl().proxied,
|
||||
remote_url: null,
|
||||
preview_url: thumbnailData?.content
|
||||
? new ProxiableUrl(thumbnailData.content).proxied
|
||||
: null,
|
||||
meta: this.toApiMeta(),
|
||||
description: data.description || null,
|
||||
blurhash: this.data.blurhash,
|
||||
};
|
||||
}
|
||||
|
||||
public toVersia(): VersiaEntities.ContentFormat {
|
||||
return new VersiaEntities.ContentFormat(this.data.content);
|
||||
}
|
||||
|
||||
public static fromVersia(
|
||||
contentFormat: VersiaEntities.ContentFormat,
|
||||
): Promise<Media> {
|
||||
return Media.insert({
|
||||
id: randomUUIDv7(),
|
||||
content: contentFormat.data,
|
||||
originalContent: contentFormat.data,
|
||||
});
|
||||
}
|
||||
}
|
||||
1502
packages/kit/db/note.ts
Normal file
1502
packages/kit/db/note.ts
Normal file
File diff suppressed because it is too large
Load diff
198
packages/kit/db/notification.ts
Normal file
198
packages/kit/db/notification.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
import type { Notification as NotificationSchema } from "@versia/client/schemas";
|
||||
import {
|
||||
desc,
|
||||
eq,
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
inArray,
|
||||
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 { Note } from "./note.ts";
|
||||
import {
|
||||
transformOutputToUserWithRelations,
|
||||
User,
|
||||
userRelations,
|
||||
} from "./user.ts";
|
||||
|
||||
export type NotificationType = InferSelectModel<typeof Notifications> & {
|
||||
status: typeof Note.$type | null;
|
||||
account: typeof User.$type;
|
||||
};
|
||||
|
||||
export class Notification extends BaseInterface<
|
||||
typeof Notifications,
|
||||
NotificationType
|
||||
> {
|
||||
public async reload(): Promise<void> {
|
||||
const reloaded = await Notification.fromId(this.data.id);
|
||||
|
||||
if (!reloaded) {
|
||||
throw new Error("Failed to reload notification");
|
||||
}
|
||||
|
||||
this.data = reloaded.data;
|
||||
}
|
||||
|
||||
public static async fromId(
|
||||
id: string | null,
|
||||
userId?: string,
|
||||
): Promise<Notification | null> {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Notification.fromSql(
|
||||
eq(Notifications.id, id),
|
||||
undefined,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
public static async fromIds(
|
||||
ids: string[],
|
||||
userId?: string,
|
||||
): Promise<Notification[]> {
|
||||
return await Notification.manyFromSql(
|
||||
inArray(Notifications.id, ids),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
public static async fromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Notifications.id),
|
||||
userId?: string,
|
||||
): Promise<Notification | null> {
|
||||
const found = await db.query.Notifications.findFirst({
|
||||
where: sql,
|
||||
orderBy,
|
||||
with: {
|
||||
account: {
|
||||
with: {
|
||||
...userRelations,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
return new Notification({
|
||||
...found,
|
||||
account: transformOutputToUserWithRelations(found.account),
|
||||
status: (await Note.fromId(found.noteId, userId))?.data ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
public static async manyFromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Notifications.id),
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
extra?: Parameters<typeof db.query.Notifications.findMany>[0],
|
||||
userId?: string,
|
||||
): Promise<Notification[]> {
|
||||
const found = await db.query.Notifications.findMany({
|
||||
where: sql,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
with: {
|
||||
...extra?.with,
|
||||
account: {
|
||||
with: {
|
||||
...userRelations,
|
||||
},
|
||||
},
|
||||
},
|
||||
extras: extra?.extras,
|
||||
});
|
||||
|
||||
return (
|
||||
await Promise.all(
|
||||
found.map(async (notif) => ({
|
||||
...notif,
|
||||
account: transformOutputToUserWithRelations(notif.account),
|
||||
status:
|
||||
(await Note.fromId(notif.noteId, userId))?.data ?? null,
|
||||
})),
|
||||
)
|
||||
).map((s) => new Notification(s));
|
||||
}
|
||||
|
||||
public async update(
|
||||
newAttachment: Partial<NotificationType>,
|
||||
): Promise<NotificationType> {
|
||||
await db
|
||||
.update(Notifications)
|
||||
.set(newAttachment)
|
||||
.where(eq(Notifications.id, this.id));
|
||||
|
||||
const updated = await Notification.fromId(this.data.id);
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Failed to update notification");
|
||||
}
|
||||
|
||||
this.data = updated.data;
|
||||
return updated.data;
|
||||
}
|
||||
|
||||
public save(): Promise<NotificationType> {
|
||||
return this.update(this.data);
|
||||
}
|
||||
|
||||
public async delete(ids?: string[]): Promise<void> {
|
||||
if (Array.isArray(ids)) {
|
||||
await db
|
||||
.delete(Notifications)
|
||||
.where(inArray(Notifications.id, ids));
|
||||
} else {
|
||||
await db.delete(Notifications).where(eq(Notifications.id, this.id));
|
||||
}
|
||||
}
|
||||
|
||||
public static async insert(
|
||||
data: InferInsertModel<typeof Notifications>,
|
||||
): Promise<Notification> {
|
||||
const inserted = (
|
||||
await db.insert(Notifications).values(data).returning()
|
||||
)[0];
|
||||
|
||||
const notification = await Notification.fromId(inserted.id);
|
||||
|
||||
if (!notification) {
|
||||
throw new Error("Failed to insert notification");
|
||||
}
|
||||
|
||||
return notification;
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
public async toApi(): Promise<z.infer<typeof NotificationSchema>> {
|
||||
const account = new User(this.data.account);
|
||||
|
||||
return {
|
||||
account: account.toApi(),
|
||||
created_at: new Date(this.data.createdAt).toISOString(),
|
||||
id: this.data.id,
|
||||
type: this.data.type,
|
||||
status: this.data.status
|
||||
? await new Note(this.data.status).toApi(account)
|
||||
: undefined,
|
||||
group_key: `ungrouped-${this.data.id}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
192
packages/kit/db/pushsubscription.ts
Normal file
192
packages/kit/db/pushsubscription.ts
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import type { WebPushSubscription as WebPushSubscriptionSchema } from "@versia/client/schemas";
|
||||
import {
|
||||
desc,
|
||||
eq,
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
inArray,
|
||||
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<typeof PushSubscriptions>;
|
||||
|
||||
export class PushSubscription extends BaseInterface<
|
||||
typeof PushSubscriptions,
|
||||
PushSubscriptionType
|
||||
> {
|
||||
public static $type: PushSubscriptionType;
|
||||
|
||||
public async reload(): Promise<void> {
|
||||
const reloaded = await PushSubscription.fromId(this.data.id);
|
||||
|
||||
if (!reloaded) {
|
||||
throw new Error("Failed to reload subscription");
|
||||
}
|
||||
|
||||
this.data = reloaded.data;
|
||||
}
|
||||
|
||||
public static async fromId(
|
||||
id: string | null,
|
||||
): Promise<PushSubscription | null> {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await PushSubscription.fromSql(eq(PushSubscriptions.id, id));
|
||||
}
|
||||
|
||||
public static async fromIds(ids: string[]): Promise<PushSubscription[]> {
|
||||
return await PushSubscription.manyFromSql(
|
||||
inArray(PushSubscriptions.id, ids),
|
||||
);
|
||||
}
|
||||
|
||||
public static async fromToken(
|
||||
token: Token,
|
||||
): Promise<PushSubscription | null> {
|
||||
return await PushSubscription.fromSql(
|
||||
eq(PushSubscriptions.tokenId, token.id),
|
||||
);
|
||||
}
|
||||
|
||||
public static async manyFromUser(
|
||||
user: User,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
): Promise<PushSubscription[]> {
|
||||
const found = await db
|
||||
.select()
|
||||
.from(PushSubscriptions)
|
||||
.leftJoin(Tokens, eq(Tokens.id, PushSubscriptions.tokenId))
|
||||
.where(eq(Tokens.userId, user.id))
|
||||
.limit(limit ?? 9e10)
|
||||
.offset(offset ?? 0);
|
||||
|
||||
return found.map((s) => new PushSubscription(s.PushSubscriptions));
|
||||
}
|
||||
|
||||
public static async fromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(PushSubscriptions.id),
|
||||
): Promise<PushSubscription | null> {
|
||||
const found = await db.query.PushSubscriptions.findFirst({
|
||||
where: sql,
|
||||
orderBy,
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
return new PushSubscription(found);
|
||||
}
|
||||
|
||||
public static async manyFromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(PushSubscriptions.id),
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
extra?: Parameters<typeof db.query.PushSubscriptions.findMany>[0],
|
||||
): Promise<PushSubscription[]> {
|
||||
const found = await db.query.PushSubscriptions.findMany({
|
||||
where: sql,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
with: extra?.with,
|
||||
});
|
||||
|
||||
return found.map((s) => new PushSubscription(s));
|
||||
}
|
||||
|
||||
public async update(
|
||||
newSubscription: Partial<PushSubscriptionType>,
|
||||
): Promise<PushSubscriptionType> {
|
||||
await db
|
||||
.update(PushSubscriptions)
|
||||
.set(newSubscription)
|
||||
.where(eq(PushSubscriptions.id, this.id));
|
||||
|
||||
const updated = await PushSubscription.fromId(this.data.id);
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Failed to update subscription");
|
||||
}
|
||||
|
||||
this.data = updated.data;
|
||||
return updated.data;
|
||||
}
|
||||
|
||||
public save(): Promise<PushSubscriptionType> {
|
||||
return this.update(this.data);
|
||||
}
|
||||
|
||||
public static async clearAllOfToken(token: Token): Promise<void> {
|
||||
await db
|
||||
.delete(PushSubscriptions)
|
||||
.where(eq(PushSubscriptions.tokenId, token.id));
|
||||
}
|
||||
|
||||
public async delete(ids?: string[]): Promise<void> {
|
||||
if (Array.isArray(ids)) {
|
||||
await db
|
||||
.delete(PushSubscriptions)
|
||||
.where(inArray(PushSubscriptions.id, ids));
|
||||
} else {
|
||||
await db
|
||||
.delete(PushSubscriptions)
|
||||
.where(eq(PushSubscriptions.id, this.id));
|
||||
}
|
||||
}
|
||||
|
||||
public static async insert(
|
||||
data: InferInsertModel<typeof PushSubscriptions>,
|
||||
): Promise<PushSubscription> {
|
||||
const inserted = (
|
||||
await db.insert(PushSubscriptions).values(data).returning()
|
||||
)[0];
|
||||
|
||||
const subscription = await PushSubscription.fromId(inserted.id);
|
||||
|
||||
if (!subscription) {
|
||||
throw new Error("Failed to insert subscription");
|
||||
}
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
public getAlerts(): z.infer<typeof WebPushSubscriptionSchema.shape.alerts> {
|
||||
return {
|
||||
mention: this.data.alerts.mention ?? false,
|
||||
favourite: this.data.alerts.favourite ?? false,
|
||||
reblog: this.data.alerts.reblog ?? false,
|
||||
follow: this.data.alerts.follow ?? false,
|
||||
poll: this.data.alerts.poll ?? false,
|
||||
follow_request: this.data.alerts.follow_request ?? false,
|
||||
status: this.data.alerts.status ?? false,
|
||||
update: this.data.alerts.update ?? false,
|
||||
"admin.sign_up": this.data.alerts["admin.sign_up"] ?? false,
|
||||
"admin.report": this.data.alerts["admin.report"] ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
public toApi(): z.infer<typeof WebPushSubscriptionSchema> {
|
||||
return {
|
||||
id: this.data.id,
|
||||
alerts: this.getAlerts(),
|
||||
endpoint: this.data.endpoint,
|
||||
// FIXME: Add real key
|
||||
server_key: "",
|
||||
};
|
||||
}
|
||||
}
|
||||
289
packages/kit/db/reaction.ts
Normal file
289
packages/kit/db/reaction.ts
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
import * as VersiaEntities from "@versia/sdk/entities";
|
||||
import { config } from "@versia-server/config";
|
||||
import { randomUUIDv7 } from "bun";
|
||||
import {
|
||||
and,
|
||||
desc,
|
||||
eq,
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
inArray,
|
||||
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<typeof Reactions> & {
|
||||
emoji: typeof Emoji.$type | null;
|
||||
author: InferSelectModel<typeof Users>;
|
||||
note: InferSelectModel<typeof Notes>;
|
||||
};
|
||||
|
||||
export class Reaction extends BaseInterface<typeof Reactions, ReactionType> {
|
||||
public static $type: ReactionType;
|
||||
|
||||
public async reload(): Promise<void> {
|
||||
const reloaded = await Reaction.fromId(this.data.id);
|
||||
|
||||
if (!reloaded) {
|
||||
throw new Error("Failed to reload reaction");
|
||||
}
|
||||
|
||||
this.data = reloaded.data;
|
||||
}
|
||||
|
||||
public static async fromId(id: string | null): Promise<Reaction | null> {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Reaction.fromSql(eq(Reactions.id, id));
|
||||
}
|
||||
|
||||
public static async fromIds(ids: string[]): Promise<Reaction[]> {
|
||||
return await Reaction.manyFromSql(inArray(Reactions.id, ids));
|
||||
}
|
||||
|
||||
public static async fromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Reactions.id),
|
||||
): Promise<Reaction | null> {
|
||||
const found = await db.query.Reactions.findFirst({
|
||||
where: sql,
|
||||
with: {
|
||||
emoji: {
|
||||
with: {
|
||||
instance: true,
|
||||
media: true,
|
||||
},
|
||||
},
|
||||
author: true,
|
||||
note: true,
|
||||
},
|
||||
orderBy,
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
return new Reaction(found);
|
||||
}
|
||||
|
||||
public static async manyFromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Reactions.id),
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
extra?: Parameters<typeof db.query.Reactions.findMany>[0],
|
||||
): Promise<Reaction[]> {
|
||||
const found = await db.query.Reactions.findMany({
|
||||
where: sql,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
with: {
|
||||
...extra?.with,
|
||||
emoji: {
|
||||
with: {
|
||||
instance: true,
|
||||
media: true,
|
||||
},
|
||||
},
|
||||
author: true,
|
||||
note: true,
|
||||
},
|
||||
});
|
||||
|
||||
return found.map((s) => new Reaction(s));
|
||||
}
|
||||
|
||||
public async update(
|
||||
newReaction: Partial<ReactionType>,
|
||||
): Promise<ReactionType> {
|
||||
await db
|
||||
.update(Reactions)
|
||||
.set(newReaction)
|
||||
.where(eq(Reactions.id, this.id));
|
||||
|
||||
const updated = await Reaction.fromId(this.data.id);
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Failed to update reaction");
|
||||
}
|
||||
|
||||
this.data = updated.data;
|
||||
return updated.data;
|
||||
}
|
||||
|
||||
public save(): Promise<ReactionType> {
|
||||
return this.update(this.data);
|
||||
}
|
||||
|
||||
public async delete(ids?: string[]): Promise<void> {
|
||||
if (Array.isArray(ids)) {
|
||||
await db.delete(Reactions).where(inArray(Reactions.id, ids));
|
||||
} else {
|
||||
await db.delete(Reactions).where(eq(Reactions.id, this.id));
|
||||
}
|
||||
}
|
||||
|
||||
public static async insert(
|
||||
data: InferInsertModel<typeof Reactions>,
|
||||
): Promise<Reaction> {
|
||||
// Needs one of emojiId or emojiText, but not both
|
||||
if (!(data.emojiId || data.emojiText)) {
|
||||
throw new Error("EmojiID or emojiText is required");
|
||||
}
|
||||
|
||||
if (data.emojiId && data.emojiText) {
|
||||
throw new Error("Cannot have both emojiId and emojiText");
|
||||
}
|
||||
|
||||
const inserted = (
|
||||
await db.insert(Reactions).values(data).returning()
|
||||
)[0];
|
||||
|
||||
const reaction = await Reaction.fromId(inserted.id);
|
||||
|
||||
if (!reaction) {
|
||||
throw new Error("Failed to insert reaction");
|
||||
}
|
||||
|
||||
return reaction;
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
public static fromEmoji(
|
||||
emoji: Emoji | string,
|
||||
author: User,
|
||||
note: Note,
|
||||
): Promise<Reaction | null> {
|
||||
if (emoji instanceof Emoji) {
|
||||
return Reaction.fromSql(
|
||||
and(
|
||||
eq(Reactions.authorId, author.id),
|
||||
eq(Reactions.noteId, note.id),
|
||||
isNull(Reactions.emojiText),
|
||||
eq(Reactions.emojiId, emoji.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Reaction.fromSql(
|
||||
and(
|
||||
eq(Reactions.authorId, author.id),
|
||||
eq(Reactions.noteId, note.id),
|
||||
eq(Reactions.emojiText, emoji),
|
||||
isNull(Reactions.emojiId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public getUri(baseUrl: URL): URL {
|
||||
return this.data.uri
|
||||
? new URL(this.data.uri)
|
||||
: new URL(
|
||||
`/notes/${this.data.noteId}/reactions/${this.id}`,
|
||||
baseUrl,
|
||||
);
|
||||
}
|
||||
|
||||
public get local(): boolean {
|
||||
return this.data.author.instanceId === null;
|
||||
}
|
||||
|
||||
public hasCustomEmoji(): boolean {
|
||||
return !!this.data.emoji || !this.data.emojiText;
|
||||
}
|
||||
|
||||
public toVersia(): VersiaEntities.Reaction {
|
||||
if (!this.local) {
|
||||
throw new Error("Cannot convert a non-local reaction to Versia");
|
||||
}
|
||||
|
||||
return new VersiaEntities.Reaction({
|
||||
uri: this.getUri(config.http.base_url).href,
|
||||
type: "pub.versia:reactions/Reaction",
|
||||
author: User.getUri(
|
||||
this.data.authorId,
|
||||
this.data.author.uri ? new URL(this.data.author.uri) : null,
|
||||
).href,
|
||||
created_at: new Date(this.data.createdAt).toISOString(),
|
||||
id: this.id,
|
||||
object: this.data.note.uri
|
||||
? new URL(this.data.note.uri).href
|
||||
: new URL(`/notes/${this.data.noteId}`, config.http.base_url)
|
||||
.href,
|
||||
content: this.hasCustomEmoji()
|
||||
? `:${this.data.emoji?.shortcode}:`
|
||||
: this.data.emojiText || "",
|
||||
extensions: this.hasCustomEmoji()
|
||||
? {
|
||||
"pub.versia:custom_emojis": {
|
||||
emojis: [
|
||||
new Emoji(
|
||||
this.data.emoji as typeof Emoji.$type,
|
||||
).toVersia(),
|
||||
],
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
public toVersiaUnreact(): VersiaEntities.Delete {
|
||||
return new VersiaEntities.Delete({
|
||||
type: "Delete",
|
||||
id: crypto.randomUUID(),
|
||||
created_at: new Date().toISOString(),
|
||||
author: User.getUri(
|
||||
this.data.authorId,
|
||||
this.data.author.uri ? new URL(this.data.author.uri) : null,
|
||||
).href,
|
||||
deleted_type: "pub.versia:reactions/Reaction",
|
||||
deleted: this.getUri(config.http.base_url).href,
|
||||
});
|
||||
}
|
||||
|
||||
public static async fromVersia(
|
||||
reactionToConvert: VersiaEntities.Reaction,
|
||||
author: User,
|
||||
note: Note,
|
||||
): Promise<Reaction> {
|
||||
if (author.local) {
|
||||
throw new Error("Cannot process a reaction from a local user");
|
||||
}
|
||||
|
||||
const emojiEntity =
|
||||
reactionToConvert.data.extensions?.["pub.versia:custom_emojis"]
|
||||
?.emojis[0];
|
||||
const emoji = emojiEntity
|
||||
? await Emoji.fetchFromRemote(
|
||||
emojiEntity,
|
||||
new Instance(
|
||||
author.data.instance as NonNullable<
|
||||
(typeof User.$type)["instance"]
|
||||
>,
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
return Reaction.insert({
|
||||
id: randomUUIDv7(),
|
||||
uri: reactionToConvert.data.uri,
|
||||
authorId: author.id,
|
||||
noteId: note.id,
|
||||
emojiId: emoji ? emoji.id : null,
|
||||
emojiText: emoji ? null : reactionToConvert.data.content,
|
||||
});
|
||||
}
|
||||
}
|
||||
353
packages/kit/db/relationship.ts
Normal file
353
packages/kit/db/relationship.ts
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
import type { Relationship as RelationshipSchema } from "@versia/client/schemas";
|
||||
import { randomUUIDv7 } from "bun";
|
||||
import {
|
||||
and,
|
||||
desc,
|
||||
eq,
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
inArray,
|
||||
type SQL,
|
||||
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";
|
||||
|
||||
type RelationshipType = InferSelectModel<typeof Relationships>;
|
||||
|
||||
type RelationshipWithOpposite = RelationshipType & {
|
||||
followedBy: boolean;
|
||||
blockedBy: boolean;
|
||||
requestedBy: boolean;
|
||||
};
|
||||
|
||||
export class Relationship extends BaseInterface<
|
||||
typeof Relationships,
|
||||
RelationshipWithOpposite
|
||||
> {
|
||||
public static schema = z.object({
|
||||
id: z.string(),
|
||||
blocked_by: z.boolean(),
|
||||
blocking: z.boolean(),
|
||||
domain_blocking: z.boolean(),
|
||||
endorsed: z.boolean(),
|
||||
followed_by: z.boolean(),
|
||||
following: z.boolean(),
|
||||
muting_notifications: z.boolean(),
|
||||
muting: z.boolean(),
|
||||
note: z.string().nullable(),
|
||||
notifying: z.boolean(),
|
||||
requested_by: z.boolean(),
|
||||
requested: z.boolean(),
|
||||
showing_reblogs: z.boolean(),
|
||||
});
|
||||
|
||||
public static $type: RelationshipWithOpposite;
|
||||
|
||||
public async reload(): Promise<void> {
|
||||
const reloaded = await Relationship.fromId(this.data.id);
|
||||
|
||||
if (!reloaded) {
|
||||
throw new Error("Failed to reload relationship");
|
||||
}
|
||||
|
||||
this.data = reloaded.data;
|
||||
}
|
||||
|
||||
public static async fromId(
|
||||
id: string | null,
|
||||
): Promise<Relationship | null> {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Relationship.fromSql(eq(Relationships.id, id));
|
||||
}
|
||||
|
||||
public static async fromIds(ids: string[]): Promise<Relationship[]> {
|
||||
return await Relationship.manyFromSql(inArray(Relationships.id, ids));
|
||||
}
|
||||
|
||||
public static async fromOwnerAndSubject(
|
||||
owner: User,
|
||||
subject: User,
|
||||
): Promise<Relationship> {
|
||||
const found = await Relationship.fromSql(
|
||||
and(
|
||||
eq(Relationships.ownerId, owner.id),
|
||||
eq(Relationships.subjectId, subject.id),
|
||||
),
|
||||
);
|
||||
|
||||
if (!found) {
|
||||
// Create a new relationship if one doesn't exist
|
||||
return await Relationship.insert({
|
||||
id: randomUUIDv7(),
|
||||
ownerId: owner.id,
|
||||
subjectId: subject.id,
|
||||
languages: [],
|
||||
following: false,
|
||||
showingReblogs: false,
|
||||
notifying: false,
|
||||
blocking: false,
|
||||
muting: false,
|
||||
mutingNotifications: false,
|
||||
requested: false,
|
||||
domainBlocking: false,
|
||||
endorsed: false,
|
||||
note: "",
|
||||
});
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
public static async fromOwnerAndSubjects(
|
||||
owner: User,
|
||||
subjectIds: string[],
|
||||
): Promise<Relationship[]> {
|
||||
const found = await Relationship.manyFromSql(
|
||||
and(
|
||||
eq(Relationships.ownerId, owner.id),
|
||||
inArray(Relationships.subjectId, subjectIds),
|
||||
),
|
||||
);
|
||||
|
||||
const missingSubjectsIds = subjectIds.filter(
|
||||
(id) => !found.find((rel) => rel.data.subjectId === id),
|
||||
);
|
||||
|
||||
for (const subjectId of missingSubjectsIds) {
|
||||
await Relationship.insert({
|
||||
id: randomUUIDv7(),
|
||||
ownerId: owner.id,
|
||||
subjectId,
|
||||
languages: [],
|
||||
following: false,
|
||||
showingReblogs: false,
|
||||
notifying: false,
|
||||
blocking: false,
|
||||
muting: false,
|
||||
mutingNotifications: false,
|
||||
requested: false,
|
||||
domainBlocking: false,
|
||||
endorsed: false,
|
||||
note: "",
|
||||
});
|
||||
}
|
||||
|
||||
return await Relationship.manyFromSql(
|
||||
and(
|
||||
eq(Relationships.ownerId, owner.id),
|
||||
inArray(Relationships.subjectId, subjectIds),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public static async fromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Relationships.id),
|
||||
): Promise<Relationship | null> {
|
||||
const found = await db.query.Relationships.findFirst({
|
||||
where: sql,
|
||||
orderBy,
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const opposite = await Relationship.getOpposite(found);
|
||||
|
||||
return new Relationship({
|
||||
...found,
|
||||
followedBy: opposite.following,
|
||||
blockedBy: opposite.blocking,
|
||||
requestedBy: opposite.requested,
|
||||
});
|
||||
}
|
||||
|
||||
public static async manyFromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Relationships.id),
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
extra?: Parameters<typeof db.query.Relationships.findMany>[0],
|
||||
): Promise<Relationship[]> {
|
||||
const found = await db.query.Relationships.findMany({
|
||||
where: sql,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
with: extra?.with,
|
||||
});
|
||||
|
||||
const opposites = await Promise.all(
|
||||
found.map((rel) => Relationship.getOpposite(rel)),
|
||||
);
|
||||
|
||||
return found.map((s, i) => {
|
||||
return new Relationship({
|
||||
...s,
|
||||
followedBy: opposites[i].following,
|
||||
blockedBy: opposites[i].blocking,
|
||||
requestedBy: opposites[i].requested,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static async getOpposite(oppositeTo: {
|
||||
subjectId: string;
|
||||
ownerId: string;
|
||||
}): Promise<RelationshipType> {
|
||||
let output = await db.query.Relationships.findFirst({
|
||||
where: (rel): SQL | undefined =>
|
||||
and(
|
||||
eq(rel.ownerId, oppositeTo.subjectId),
|
||||
eq(rel.subjectId, oppositeTo.ownerId),
|
||||
),
|
||||
});
|
||||
|
||||
// If the opposite relationship doesn't exist, create it
|
||||
if (!output) {
|
||||
output = (
|
||||
await db
|
||||
.insert(Relationships)
|
||||
.values({
|
||||
id: randomUUIDv7(),
|
||||
ownerId: oppositeTo.subjectId,
|
||||
subjectId: oppositeTo.ownerId,
|
||||
languages: [],
|
||||
following: false,
|
||||
showingReblogs: false,
|
||||
notifying: false,
|
||||
blocking: false,
|
||||
domainBlocking: false,
|
||||
endorsed: false,
|
||||
note: "",
|
||||
muting: false,
|
||||
mutingNotifications: false,
|
||||
requested: false,
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public async update(
|
||||
newRelationship: Partial<RelationshipType>,
|
||||
): Promise<RelationshipWithOpposite> {
|
||||
await db
|
||||
.update(Relationships)
|
||||
.set(newRelationship)
|
||||
.where(eq(Relationships.id, this.id));
|
||||
|
||||
// If a user follows another user, update followerCount and followingCount
|
||||
if (newRelationship.following && !this.data.following) {
|
||||
await db
|
||||
.update(Users)
|
||||
.set({
|
||||
followingCount: sql`${Users.followingCount} + 1`,
|
||||
})
|
||||
.where(eq(Users.id, this.data.ownerId));
|
||||
|
||||
await db
|
||||
.update(Users)
|
||||
.set({
|
||||
followerCount: sql`${Users.followerCount} + 1`,
|
||||
})
|
||||
.where(eq(Users.id, this.data.subjectId));
|
||||
}
|
||||
|
||||
// If a user unfollows another user, update followerCount and followingCount
|
||||
if (!newRelationship.following && this.data.following) {
|
||||
await db
|
||||
.update(Users)
|
||||
.set({
|
||||
followingCount: sql`${Users.followingCount} - 1`,
|
||||
})
|
||||
.where(eq(Users.id, this.data.ownerId));
|
||||
|
||||
await db
|
||||
.update(Users)
|
||||
.set({
|
||||
followerCount: sql`${Users.followerCount} - 1`,
|
||||
})
|
||||
.where(eq(Users.id, this.data.subjectId));
|
||||
}
|
||||
|
||||
const updated = await Relationship.fromId(this.data.id);
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Failed to update relationship");
|
||||
}
|
||||
|
||||
this.data = updated.data;
|
||||
return updated.data;
|
||||
}
|
||||
|
||||
public save(): Promise<RelationshipWithOpposite> {
|
||||
return this.update(this.data);
|
||||
}
|
||||
|
||||
public async delete(ids?: string[]): Promise<void> {
|
||||
if (Array.isArray(ids)) {
|
||||
await db
|
||||
.delete(Relationships)
|
||||
.where(inArray(Relationships.id, ids));
|
||||
} else {
|
||||
await db.delete(Relationships).where(eq(Relationships.id, this.id));
|
||||
}
|
||||
}
|
||||
|
||||
public static async insert(
|
||||
data: InferInsertModel<typeof Relationships>,
|
||||
): Promise<Relationship> {
|
||||
const inserted = (
|
||||
await db.insert(Relationships).values(data).returning()
|
||||
)[0];
|
||||
|
||||
const relationship = await Relationship.fromId(inserted.id);
|
||||
|
||||
if (!relationship) {
|
||||
throw new Error("Failed to insert relationship");
|
||||
}
|
||||
|
||||
// Create opposite relationship if necessary
|
||||
await Relationship.getOpposite({
|
||||
subjectId: relationship.data.subjectId,
|
||||
ownerId: relationship.data.ownerId,
|
||||
});
|
||||
|
||||
return relationship;
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
public toApi(): z.infer<typeof RelationshipSchema> {
|
||||
return {
|
||||
id: this.data.subjectId,
|
||||
blocked_by: this.data.blockedBy,
|
||||
blocking: this.data.blocking,
|
||||
domain_blocking: this.data.domainBlocking,
|
||||
endorsed: this.data.endorsed,
|
||||
followed_by: this.data.followedBy,
|
||||
following: this.data.following,
|
||||
muting_notifications: this.data.mutingNotifications,
|
||||
muting: this.data.muting,
|
||||
note: this.data.note,
|
||||
notifying: this.data.notifying,
|
||||
requested_by: this.data.requestedBy,
|
||||
requested: this.data.requested,
|
||||
showing_reblogs: this.data.showingReblogs,
|
||||
languages: this.data.languages ?? [],
|
||||
};
|
||||
}
|
||||
}
|
||||
225
packages/kit/db/role.ts
Normal file
225
packages/kit/db/role.ts
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
import type {
|
||||
RolePermission,
|
||||
Role as RoleSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { config, ProxiableUrl } from "@versia-server/config";
|
||||
import {
|
||||
and,
|
||||
desc,
|
||||
eq,
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
inArray,
|
||||
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<typeof Roles>;
|
||||
|
||||
export class Role extends BaseInterface<typeof Roles> {
|
||||
public static $type: RoleType;
|
||||
public static defaultRole = new Role({
|
||||
id: "default",
|
||||
name: "Default",
|
||||
permissions: config.permissions.default,
|
||||
priority: 0,
|
||||
description: "Default role for all users",
|
||||
visible: false,
|
||||
icon: null,
|
||||
});
|
||||
public static adminRole = new Role({
|
||||
id: "admin",
|
||||
name: "Admin",
|
||||
permissions: config.permissions.admin,
|
||||
priority: 2 ** 31 - 1,
|
||||
description: "Default role for all administrators",
|
||||
visible: false,
|
||||
icon: null,
|
||||
});
|
||||
|
||||
public async reload(): Promise<void> {
|
||||
const reloaded = await Role.fromId(this.data.id);
|
||||
|
||||
if (!reloaded) {
|
||||
throw new Error("Failed to reload role");
|
||||
}
|
||||
|
||||
this.data = reloaded.data;
|
||||
}
|
||||
|
||||
public static async fromId(id: string | null): Promise<Role | null> {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Role.fromSql(eq(Roles.id, id));
|
||||
}
|
||||
|
||||
public static async fromIds(ids: string[]): Promise<Role[]> {
|
||||
return await Role.manyFromSql(inArray(Roles.id, ids));
|
||||
}
|
||||
|
||||
public static async fromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Roles.id),
|
||||
): Promise<Role | null> {
|
||||
const found = await db.query.Roles.findFirst({
|
||||
where: sql,
|
||||
orderBy,
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
return new Role(found);
|
||||
}
|
||||
|
||||
public static async getAll(): Promise<Role[]> {
|
||||
return (await Role.manyFromSql(undefined)).concat(
|
||||
Role.defaultRole,
|
||||
Role.adminRole,
|
||||
);
|
||||
}
|
||||
|
||||
public static async getUserRoles(
|
||||
userId: string,
|
||||
isAdmin: boolean,
|
||||
): Promise<Role[]> {
|
||||
return (
|
||||
await db.query.RoleToUsers.findMany({
|
||||
where: (role): SQL | undefined => eq(role.userId, userId),
|
||||
with: {
|
||||
role: true,
|
||||
user: {
|
||||
columns: {
|
||||
isAdmin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
.map((r) => new Role(r.role))
|
||||
.concat(
|
||||
new Role({
|
||||
id: "default",
|
||||
name: "Default",
|
||||
permissions: config.permissions.default,
|
||||
priority: 0,
|
||||
description: "Default role for all users",
|
||||
visible: false,
|
||||
icon: null,
|
||||
}),
|
||||
)
|
||||
.concat(
|
||||
isAdmin
|
||||
? [
|
||||
new Role({
|
||||
id: "admin",
|
||||
name: "Admin",
|
||||
permissions: config.permissions.admin,
|
||||
priority: 2 ** 31 - 1,
|
||||
description:
|
||||
"Default role for all administrators",
|
||||
visible: false,
|
||||
icon: null,
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
public static async manyFromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Roles.id),
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
extra?: Parameters<typeof db.query.Roles.findMany>[0],
|
||||
): Promise<Role[]> {
|
||||
const found = await db.query.Roles.findMany({
|
||||
where: sql,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
with: extra?.with,
|
||||
});
|
||||
|
||||
return found.map((s) => new Role(s));
|
||||
}
|
||||
|
||||
public async update(newRole: Partial<RoleType>): Promise<RoleType> {
|
||||
await db.update(Roles).set(newRole).where(eq(Roles.id, this.id));
|
||||
|
||||
const updated = await Role.fromId(this.data.id);
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Failed to update role");
|
||||
}
|
||||
|
||||
return updated.data;
|
||||
}
|
||||
|
||||
public save(): Promise<RoleType> {
|
||||
return this.update(this.data);
|
||||
}
|
||||
|
||||
public async delete(ids?: string[]): Promise<void> {
|
||||
if (Array.isArray(ids)) {
|
||||
await db.delete(Roles).where(inArray(Roles.id, ids));
|
||||
} else {
|
||||
await db.delete(Roles).where(eq(Roles.id, this.id));
|
||||
}
|
||||
}
|
||||
|
||||
public static async insert(
|
||||
data: InferInsertModel<typeof Roles>,
|
||||
): Promise<Role> {
|
||||
const inserted = (await db.insert(Roles).values(data).returning())[0];
|
||||
|
||||
const role = await Role.fromId(inserted.id);
|
||||
|
||||
if (!role) {
|
||||
throw new Error("Failed to insert role");
|
||||
}
|
||||
|
||||
return role;
|
||||
}
|
||||
|
||||
public async linkUser(userId: string): Promise<void> {
|
||||
await db.insert(RoleToUsers).values({
|
||||
userId,
|
||||
roleId: this.id,
|
||||
});
|
||||
}
|
||||
|
||||
public async unlinkUser(userId: string): Promise<void> {
|
||||
await db
|
||||
.delete(RoleToUsers)
|
||||
.where(
|
||||
and(
|
||||
eq(RoleToUsers.roleId, this.id),
|
||||
eq(RoleToUsers.userId, userId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
public toApi(): z.infer<typeof RoleSchema> {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.data.name,
|
||||
permissions: this.data.permissions as unknown as RolePermission[],
|
||||
priority: this.data.priority,
|
||||
description: this.data.description ?? undefined,
|
||||
visible: this.data.visible,
|
||||
icon: this.data.icon
|
||||
? new ProxiableUrl(this.data.icon).proxied
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
241
packages/kit/db/timeline.ts
Normal file
241
packages/kit/db/timeline.ts
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
import { config } from "@versia-server/config";
|
||||
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";
|
||||
|
||||
enum TimelineType {
|
||||
Note = "Note",
|
||||
User = "User",
|
||||
Notification = "Notification",
|
||||
}
|
||||
|
||||
export class Timeline<Type extends Note | User | Notification> {
|
||||
public constructor(private type: TimelineType) {}
|
||||
|
||||
public static getNoteTimeline(
|
||||
sql: SQL<unknown> | undefined,
|
||||
limit: number,
|
||||
url: URL,
|
||||
userId?: string,
|
||||
): Promise<{ link: string; objects: Note[] }> {
|
||||
return new Timeline<Note>(TimelineType.Note).fetchTimeline(
|
||||
sql,
|
||||
limit,
|
||||
url,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
public static getUserTimeline(
|
||||
sql: SQL<unknown> | undefined,
|
||||
limit: number,
|
||||
url: URL,
|
||||
): Promise<{ link: string; objects: User[] }> {
|
||||
return new Timeline<User>(TimelineType.User).fetchTimeline(
|
||||
sql,
|
||||
limit,
|
||||
url,
|
||||
);
|
||||
}
|
||||
|
||||
public static getNotificationTimeline(
|
||||
sql: SQL<unknown> | undefined,
|
||||
limit: number,
|
||||
url: URL,
|
||||
userId?: string,
|
||||
): Promise<{ link: string; objects: Notification[] }> {
|
||||
return new Timeline<Notification>(
|
||||
TimelineType.Notification,
|
||||
).fetchTimeline(sql, limit, url, userId);
|
||||
}
|
||||
|
||||
private async fetchObjects(
|
||||
sql: SQL<unknown> | undefined,
|
||||
limit: number,
|
||||
userId?: string,
|
||||
): Promise<Type[]> {
|
||||
switch (this.type) {
|
||||
case TimelineType.Note:
|
||||
return (await Note.manyFromSql(
|
||||
sql,
|
||||
undefined,
|
||||
limit,
|
||||
undefined,
|
||||
userId,
|
||||
)) as Type[];
|
||||
case TimelineType.User:
|
||||
return (await User.manyFromSql(
|
||||
sql,
|
||||
undefined,
|
||||
limit,
|
||||
)) as Type[];
|
||||
case TimelineType.Notification:
|
||||
return (await Notification.manyFromSql(
|
||||
sql,
|
||||
undefined,
|
||||
limit,
|
||||
undefined,
|
||||
undefined,
|
||||
userId,
|
||||
)) as Type[];
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchLinkHeader(
|
||||
objects: Type[],
|
||||
url: URL,
|
||||
limit: number,
|
||||
): Promise<string> {
|
||||
const linkHeader: string[] = [];
|
||||
const urlWithoutQuery = new URL(url.pathname, config.http.base_url);
|
||||
|
||||
if (objects.length > 0) {
|
||||
switch (this.type) {
|
||||
case TimelineType.Note:
|
||||
linkHeader.push(
|
||||
...(await Timeline.fetchNoteLinkHeader(
|
||||
objects as Note[],
|
||||
urlWithoutQuery,
|
||||
limit,
|
||||
)),
|
||||
);
|
||||
break;
|
||||
case TimelineType.User:
|
||||
linkHeader.push(
|
||||
...(await Timeline.fetchUserLinkHeader(
|
||||
objects as User[],
|
||||
urlWithoutQuery,
|
||||
limit,
|
||||
)),
|
||||
);
|
||||
break;
|
||||
case TimelineType.Notification:
|
||||
linkHeader.push(
|
||||
...(await Timeline.fetchNotificationLinkHeader(
|
||||
objects as Notification[],
|
||||
urlWithoutQuery,
|
||||
limit,
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return linkHeader.join(", ");
|
||||
}
|
||||
|
||||
private static async fetchNoteLinkHeader(
|
||||
notes: Note[],
|
||||
urlWithoutQuery: URL,
|
||||
limit: number,
|
||||
): Promise<string[]> {
|
||||
const linkHeader: string[] = [];
|
||||
|
||||
const objectBefore = await Note.fromSql(gt(Notes.id, notes[0].data.id));
|
||||
if (objectBefore) {
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?limit=${limit ?? 20}&min_id=${notes[0].data.id}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
if (notes.length >= (limit ?? 20)) {
|
||||
const objectAfter = await Note.fromSql(
|
||||
gt(Notes.id, notes.at(-1)?.data.id ?? ""),
|
||||
);
|
||||
if (objectAfter) {
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?limit=${limit ?? 20}&max_id=${notes.at(-1)?.data.id}>; rel="next"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return linkHeader;
|
||||
}
|
||||
|
||||
private static async fetchUserLinkHeader(
|
||||
users: User[],
|
||||
urlWithoutQuery: URL,
|
||||
limit: number,
|
||||
): Promise<string[]> {
|
||||
const linkHeader: string[] = [];
|
||||
|
||||
const objectBefore = await User.fromSql(gt(Users.id, users[0].id));
|
||||
if (objectBefore) {
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?limit=${limit ?? 20}&min_id=${users[0].id}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
if (users.length >= (limit ?? 20)) {
|
||||
const objectAfter = await User.fromSql(
|
||||
gt(Users.id, users.at(-1)?.id ?? ""),
|
||||
);
|
||||
if (objectAfter) {
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?limit=${limit ?? 20}&max_id=${users.at(-1)?.id}>; rel="next"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return linkHeader;
|
||||
}
|
||||
|
||||
private static async fetchNotificationLinkHeader(
|
||||
notifications: Notification[],
|
||||
urlWithoutQuery: URL,
|
||||
limit: number,
|
||||
): Promise<string[]> {
|
||||
const linkHeader: string[] = [];
|
||||
|
||||
const objectBefore = await Notification.fromSql(
|
||||
gt(Notifications.id, notifications[0].data.id),
|
||||
);
|
||||
if (objectBefore) {
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?limit=${limit ?? 20}&min_id=${notifications[0].data.id}>; rel="prev"`,
|
||||
);
|
||||
}
|
||||
|
||||
if (notifications.length >= (limit ?? 20)) {
|
||||
const objectAfter = await Notification.fromSql(
|
||||
gt(Notifications.id, notifications.at(-1)?.data.id ?? ""),
|
||||
);
|
||||
if (objectAfter) {
|
||||
linkHeader.push(
|
||||
`<${urlWithoutQuery}?limit=${limit ?? 20}&max_id=${notifications.at(-1)?.data.id}>; rel="next"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return linkHeader;
|
||||
}
|
||||
|
||||
private async fetchTimeline(
|
||||
sql: SQL<unknown> | undefined,
|
||||
limit: number,
|
||||
url: URL,
|
||||
userId?: string,
|
||||
): Promise<{ link: string; objects: Type[] }> {
|
||||
const objects = await this.fetchObjects(sql, limit, userId);
|
||||
const link = await this.fetchLinkHeader(objects, url, limit);
|
||||
|
||||
switch (this.type) {
|
||||
case TimelineType.Note:
|
||||
return {
|
||||
link,
|
||||
objects,
|
||||
};
|
||||
case TimelineType.User:
|
||||
return {
|
||||
link,
|
||||
objects,
|
||||
};
|
||||
case TimelineType.Notification:
|
||||
return {
|
||||
link,
|
||||
objects,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
168
packages/kit/db/token.ts
Normal file
168
packages/kit/db/token.ts
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
import type { Token as TokenSchema } from "@versia/client/schemas";
|
||||
import {
|
||||
desc,
|
||||
eq,
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
inArray,
|
||||
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<typeof Tokens> & {
|
||||
application: typeof Application.$type | null;
|
||||
};
|
||||
|
||||
export class Token extends BaseInterface<typeof Tokens, TokenType> {
|
||||
public static $type: TokenType;
|
||||
|
||||
public async reload(): Promise<void> {
|
||||
const reloaded = await Token.fromId(this.data.id);
|
||||
|
||||
if (!reloaded) {
|
||||
throw new Error("Failed to reload token");
|
||||
}
|
||||
|
||||
this.data = reloaded.data;
|
||||
}
|
||||
|
||||
public static async fromId(id: string | null): Promise<Token | null> {
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await Token.fromSql(eq(Tokens.id, id));
|
||||
}
|
||||
|
||||
public static async fromIds(ids: string[]): Promise<Token[]> {
|
||||
return await Token.manyFromSql(inArray(Tokens.id, ids));
|
||||
}
|
||||
|
||||
public static async fromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Tokens.id),
|
||||
): Promise<Token | null> {
|
||||
const found = await db.query.Tokens.findFirst({
|
||||
where: sql,
|
||||
orderBy,
|
||||
with: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
return null;
|
||||
}
|
||||
return new Token(found);
|
||||
}
|
||||
|
||||
public static async manyFromSql(
|
||||
sql: SQL<unknown> | undefined,
|
||||
orderBy: SQL<unknown> | undefined = desc(Tokens.id),
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
extra?: Parameters<typeof db.query.Tokens.findMany>[0],
|
||||
): Promise<Token[]> {
|
||||
const found = await db.query.Tokens.findMany({
|
||||
where: sql,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
with: {
|
||||
application: true,
|
||||
...extra?.with,
|
||||
},
|
||||
});
|
||||
|
||||
return found.map((s) => new Token(s));
|
||||
}
|
||||
|
||||
public async update(newAttachment: Partial<TokenType>): Promise<TokenType> {
|
||||
await db
|
||||
.update(Tokens)
|
||||
.set(newAttachment)
|
||||
.where(eq(Tokens.id, this.id));
|
||||
|
||||
const updated = await Token.fromId(this.data.id);
|
||||
|
||||
if (!updated) {
|
||||
throw new Error("Failed to update token");
|
||||
}
|
||||
|
||||
this.data = updated.data;
|
||||
return updated.data;
|
||||
}
|
||||
|
||||
public save(): Promise<TokenType> {
|
||||
return this.update(this.data);
|
||||
}
|
||||
|
||||
public async delete(ids?: string[]): Promise<void> {
|
||||
if (Array.isArray(ids)) {
|
||||
await db.delete(Tokens).where(inArray(Tokens.id, ids));
|
||||
} else {
|
||||
await db.delete(Tokens).where(eq(Tokens.id, this.id));
|
||||
}
|
||||
}
|
||||
|
||||
public static async insert(
|
||||
data: InferInsertModel<typeof Tokens>,
|
||||
): Promise<Token> {
|
||||
const inserted = (await db.insert(Tokens).values(data).returning())[0];
|
||||
|
||||
const token = await Token.fromId(inserted.id);
|
||||
|
||||
if (!token) {
|
||||
throw new Error("Failed to insert token");
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
public static async insertMany(
|
||||
data: InferInsertModel<typeof Tokens>[],
|
||||
): Promise<Token[]> {
|
||||
const inserted = await db.insert(Tokens).values(data).returning();
|
||||
|
||||
return await Token.fromIds(inserted.map((i) => i.id));
|
||||
}
|
||||
|
||||
public get id(): string {
|
||||
return this.data.id;
|
||||
}
|
||||
|
||||
public static async fromAccessToken(
|
||||
accessToken: string,
|
||||
): Promise<Token | null> {
|
||||
return await Token.fromSql(eq(Tokens.accessToken, accessToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the associated user from this token
|
||||
*
|
||||
* @returns The user associated with this token
|
||||
*/
|
||||
public async getUser(): Promise<User | null> {
|
||||
if (!this.data.userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await User.fromId(this.data.userId);
|
||||
}
|
||||
|
||||
public toApi(): z.infer<typeof TokenSchema> {
|
||||
return {
|
||||
access_token: this.data.accessToken,
|
||||
token_type: "Bearer",
|
||||
scope: this.data.scope,
|
||||
created_at: Math.floor(
|
||||
new Date(this.data.createdAt).getTime() / 1000,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
1253
packages/kit/db/user.ts
Normal file
1253
packages/kit/db/user.ts
Normal file
File diff suppressed because it is too large
Load diff
16
packages/kit/example.ts
Normal file
16
packages/kit/example.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { z } from "zod";
|
||||
import { Hooks } from "./hooks.ts";
|
||||
import { Plugin } from "./plugin.ts";
|
||||
|
||||
const myPlugin = new Plugin(
|
||||
z.object({
|
||||
apiKey: z.string(),
|
||||
}),
|
||||
);
|
||||
|
||||
myPlugin.registerHandler(Hooks.Response, (req) => {
|
||||
console.info("Request received:", req);
|
||||
return req;
|
||||
});
|
||||
|
||||
export default myPlugin;
|
||||
9
packages/kit/hooks.ts
Normal file
9
packages/kit/hooks.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export enum Hooks {
|
||||
Request = "request",
|
||||
Response = "response",
|
||||
}
|
||||
|
||||
export type ServerHooks = {
|
||||
[Hooks.Request]: (request: Request) => Request;
|
||||
[Hooks.Response]: (response: Response) => Response;
|
||||
};
|
||||
599
packages/kit/inbox-processor.ts
Normal file
599
packages/kit/inbox-processor.ts
Normal file
|
|
@ -0,0 +1,599 @@
|
|||
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 { federationInboxLogger } from "@versia-server/logging";
|
||||
import type { SocketAddress } from "bun";
|
||||
import { Glob } from "bun";
|
||||
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.
|
||||
* @param {string} hostname - The hostname to check. Can contain glob patterns.
|
||||
* @returns {boolean} - True if defederated, false otherwise.
|
||||
*/
|
||||
function isDefederated(hostname: string): boolean {
|
||||
const pattern = new Glob(hostname);
|
||||
|
||||
return (
|
||||
config.federation.blocked.find(
|
||||
(blocked) => pattern.match(blocked.toString()) !== null,
|
||||
) !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes incoming federation inbox messages.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const processor = new InboxProcessor(context, body, sender, headers);
|
||||
*
|
||||
* await processor.process();
|
||||
* ```
|
||||
*/
|
||||
export class InboxProcessor {
|
||||
/**
|
||||
* Creates a new InboxProcessor instance.
|
||||
*
|
||||
* @param request Request object.
|
||||
* @param body Entity JSON body.
|
||||
* @param sender Sender of the request's instance and key (from Versia-Signed-By header). Null if request is from a bridge.
|
||||
* @param headers Various request headers.
|
||||
* @param logger LogTape logger instance.
|
||||
* @param requestIp Request IP address. Grabs it from the Hono context if not provided.
|
||||
*/
|
||||
public constructor(
|
||||
private request: Request,
|
||||
private body: JSONObject,
|
||||
private sender: {
|
||||
instance: Instance;
|
||||
key: CryptoKey;
|
||||
} | null,
|
||||
private authorizationHeader?: string,
|
||||
private requestIp: SocketAddress | null = null,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Verifies the request signature.
|
||||
*
|
||||
* @returns {Promise<boolean>} - Whether the signature is valid.
|
||||
*/
|
||||
private isSignatureValid(): Promise<boolean> {
|
||||
if (!this.sender) {
|
||||
throw new Error("Sender is not defined");
|
||||
}
|
||||
|
||||
return verify(this.sender.key, this.request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if signature checks can be skipped.
|
||||
* Useful for requests from federation bridges.
|
||||
*
|
||||
* @returns {boolean} - Whether to skip signature checks.
|
||||
*/
|
||||
private shouldCheckSignature(): boolean {
|
||||
if (config.federation.bridge) {
|
||||
const token = this.authorizationHeader?.split("Bearer ")[1];
|
||||
|
||||
if (token) {
|
||||
return this.isRequestFromBridge(token);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a request is from a federation bridge.
|
||||
*
|
||||
* @param token - Authorization token to check.
|
||||
* @returns {boolean} - Whether the request is from a federation bridge.
|
||||
*/
|
||||
private isRequestFromBridge(token: string): boolean {
|
||||
if (!config.federation.bridge) {
|
||||
throw new ApiError(
|
||||
500,
|
||||
"Bridge is not configured.",
|
||||
"Please remove the Authorization header.",
|
||||
);
|
||||
}
|
||||
|
||||
if (token !== config.federation.bridge.token) {
|
||||
throw new ApiError(
|
||||
401,
|
||||
"Invalid token.",
|
||||
"Please use the correct token, or remove the Authorization header.",
|
||||
);
|
||||
}
|
||||
|
||||
if (config.federation.bridge.allowed_ips.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!this.requestIp) {
|
||||
throw new ApiError(
|
||||
500,
|
||||
"The request IP address could not be determined.",
|
||||
"This may be due to an incorrectly configured reverse proxy.",
|
||||
);
|
||||
}
|
||||
|
||||
for (const ip of config.federation.bridge.allowed_ips) {
|
||||
if (matches(ip, this.requestIp.address)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ApiError(
|
||||
403,
|
||||
"The request is not from a trusted bridge IP address.",
|
||||
"Remove the Authorization header if you are not trying to access this API as a bridge.",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs request processing.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
* @throws {ApiError} - If there is an error processing the request.
|
||||
*/
|
||||
public async process(): Promise<void> {
|
||||
!this.sender &&
|
||||
federationInboxLogger.debug`Processing request from potential bridge`;
|
||||
|
||||
if (this.sender && isDefederated(this.sender.instance.data.baseUrl)) {
|
||||
// Return 201 to avoid
|
||||
// 1. Leaking defederated instance information
|
||||
// 2. Preventing the sender from thinking the message was not delivered and retrying
|
||||
return;
|
||||
}
|
||||
|
||||
federationInboxLogger.debug`Instance ${chalk.gray(
|
||||
this.sender?.instance.data.baseUrl,
|
||||
)} is not defederated`;
|
||||
|
||||
const shouldCheckSignature = this.shouldCheckSignature();
|
||||
|
||||
shouldCheckSignature
|
||||
? federationInboxLogger.debug`Checking signature`
|
||||
: federationInboxLogger.debug`Skipping signature check`;
|
||||
|
||||
if (shouldCheckSignature) {
|
||||
const isValid = await this.isSignatureValid();
|
||||
|
||||
if (!isValid) {
|
||||
throw new ApiError(401, "Signature is not valid");
|
||||
}
|
||||
}
|
||||
|
||||
shouldCheckSignature && federationInboxLogger.debug`Signature is valid`;
|
||||
|
||||
try {
|
||||
await new EntitySorter(this.body)
|
||||
.on(VersiaEntities.Note, (n) => InboxProcessor.processNote(n))
|
||||
.on(VersiaEntities.Follow, (f) =>
|
||||
InboxProcessor.processFollowRequest(f),
|
||||
)
|
||||
.on(VersiaEntities.FollowAccept, (f) =>
|
||||
InboxProcessor.processFollowAccept(f),
|
||||
)
|
||||
.on(VersiaEntities.FollowReject, (f) =>
|
||||
InboxProcessor.processFollowReject(f),
|
||||
)
|
||||
.on(VersiaEntities.Like, (l) =>
|
||||
InboxProcessor.processLikeRequest(l),
|
||||
)
|
||||
.on(VersiaEntities.Delete, (d) =>
|
||||
InboxProcessor.processDelete(d),
|
||||
)
|
||||
.on(VersiaEntities.User, (u) => InboxProcessor.processUser(u))
|
||||
.on(VersiaEntities.Share, (s) => InboxProcessor.processShare(s))
|
||||
.on(VersiaEntities.Reaction, (r) =>
|
||||
InboxProcessor.processReaction(r),
|
||||
)
|
||||
.sort(() => {
|
||||
throw new ApiError(400, "Unknown entity type");
|
||||
});
|
||||
} catch (e) {
|
||||
return this.handleError(e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Reaction entity processing
|
||||
*
|
||||
* @param {VersiaEntities.Reaction} reaction - The Reaction entity to process.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private static async processReaction(
|
||||
reaction: VersiaEntities.Reaction,
|
||||
): Promise<void> {
|
||||
const author = await User.resolve(new URL(reaction.data.author));
|
||||
const note = await Note.resolve(new URL(reaction.data.object));
|
||||
|
||||
if (!author) {
|
||||
throw new ApiError(404, "Author not found");
|
||||
}
|
||||
|
||||
if (!note) {
|
||||
throw new ApiError(404, "Note not found");
|
||||
}
|
||||
|
||||
await Reaction.fromVersia(reaction, author, note);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Note entity processing
|
||||
*
|
||||
* @param {VersiaNote} note - The Note entity to process.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private static async processNote(note: VersiaEntities.Note): Promise<void> {
|
||||
// If note has a blocked word
|
||||
if (
|
||||
Object.values(note.content?.data ?? {})
|
||||
.flatMap((c) => c.content)
|
||||
.some((content) =>
|
||||
config.validation.filters.note_content.some((filter) =>
|
||||
filter.test(content),
|
||||
),
|
||||
)
|
||||
) {
|
||||
// Drop silently
|
||||
return;
|
||||
}
|
||||
|
||||
await Note.fromVersia(note);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles User entity processing.
|
||||
*
|
||||
* @param {VersiaUser} user - The User entity to process.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private static async processUser(user: VersiaEntities.User): Promise<void> {
|
||||
if (
|
||||
config.validation.filters.username.some((filter) =>
|
||||
filter.test(user.data.username),
|
||||
) ||
|
||||
(user.data.display_name &&
|
||||
config.validation.filters.displayname.some((filter) =>
|
||||
filter.test(user.data.display_name ?? ""),
|
||||
))
|
||||
) {
|
||||
// Drop silently
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
Object.values(user.bio?.data ?? {})
|
||||
.flatMap((c) => c.content)
|
||||
.some((content) =>
|
||||
config.validation.filters.bio.some((filter) =>
|
||||
filter.test(content),
|
||||
),
|
||||
)
|
||||
) {
|
||||
// Drop silently
|
||||
return;
|
||||
}
|
||||
|
||||
await User.fromVersia(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Follow entity processing.
|
||||
*
|
||||
* @param {VersiaFollow} follow - The Follow entity to process.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private static async processFollowRequest(
|
||||
follow: VersiaEntities.Follow,
|
||||
): Promise<void> {
|
||||
const author = await User.resolve(new URL(follow.data.author));
|
||||
const followee = await User.resolve(new URL(follow.data.followee));
|
||||
|
||||
if (!author) {
|
||||
throw new ApiError(404, "Author not found");
|
||||
}
|
||||
|
||||
if (!followee) {
|
||||
throw new ApiError(404, "Followee not found");
|
||||
}
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
author,
|
||||
followee,
|
||||
);
|
||||
|
||||
if (foundRelationship.data.following) {
|
||||
return;
|
||||
}
|
||||
|
||||
await foundRelationship.update({
|
||||
// If followee is not "locked" (doesn't manually approves follow requests), set following to true
|
||||
following: !followee.data.isLocked,
|
||||
requested: followee.data.isLocked,
|
||||
showingReblogs: true,
|
||||
notifying: true,
|
||||
languages: [],
|
||||
});
|
||||
|
||||
await followee.notify(
|
||||
followee.data.isLocked ? "follow_request" : "follow",
|
||||
author,
|
||||
);
|
||||
|
||||
if (!followee.data.isLocked) {
|
||||
await followee.acceptFollowRequest(author);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles FollowAccept entity processing
|
||||
*
|
||||
* @param {VersiaFollowAccept} followAccept - The FollowAccept entity to process.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private static async processFollowAccept(
|
||||
followAccept: VersiaEntities.FollowAccept,
|
||||
): Promise<void> {
|
||||
const author = await User.resolve(new URL(followAccept.data.author));
|
||||
const follower = await User.resolve(
|
||||
new URL(followAccept.data.follower),
|
||||
);
|
||||
|
||||
if (!author) {
|
||||
throw new ApiError(404, "Author not found");
|
||||
}
|
||||
|
||||
if (!follower) {
|
||||
throw new ApiError(404, "Follower not found");
|
||||
}
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
follower,
|
||||
author,
|
||||
);
|
||||
|
||||
if (!foundRelationship.data.requested) {
|
||||
return;
|
||||
}
|
||||
|
||||
await foundRelationship.update({
|
||||
requested: false,
|
||||
following: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles FollowReject entity processing
|
||||
*
|
||||
* @param {VersiaFollowReject} followReject - The FollowReject entity to process.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private static async processFollowReject(
|
||||
followReject: VersiaEntities.FollowReject,
|
||||
): Promise<void> {
|
||||
const author = await User.resolve(new URL(followReject.data.author));
|
||||
const follower = await User.resolve(
|
||||
new URL(followReject.data.follower),
|
||||
);
|
||||
|
||||
if (!author) {
|
||||
throw new ApiError(404, "Author not found");
|
||||
}
|
||||
|
||||
if (!follower) {
|
||||
throw new ApiError(404, "Follower not found");
|
||||
}
|
||||
|
||||
const foundRelationship = await Relationship.fromOwnerAndSubject(
|
||||
follower,
|
||||
author,
|
||||
);
|
||||
|
||||
if (!foundRelationship.data.requested) {
|
||||
return;
|
||||
}
|
||||
|
||||
await foundRelationship.update({
|
||||
requested: false,
|
||||
following: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Share entity processing.
|
||||
*
|
||||
* @param {VersiaShare} share - The Share entity to process.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private static async processShare(
|
||||
share: VersiaEntities.Share,
|
||||
): Promise<void> {
|
||||
const author = await User.resolve(new URL(share.data.author));
|
||||
const sharedNote = await Note.resolve(new URL(share.data.shared));
|
||||
|
||||
if (!author) {
|
||||
throw new ApiError(404, "Author not found");
|
||||
}
|
||||
|
||||
if (!sharedNote) {
|
||||
throw new ApiError(404, "Shared Note not found");
|
||||
}
|
||||
|
||||
await sharedNote.reblog(author, "public", new URL(share.data.uri));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Delete entity processing.
|
||||
*
|
||||
* @param {VersiaDelete} delete_ - The Delete entity to process.
|
||||
* @returns {Promise<void>}
|
||||
*/ // JS doesn't allow the use of `delete` as a variable name
|
||||
public static async processDelete(
|
||||
delete_: VersiaEntities.Delete,
|
||||
): Promise<void> {
|
||||
const toDelete = delete_.data.deleted;
|
||||
|
||||
const author = delete_.data.author
|
||||
? await User.resolve(new URL(delete_.data.author))
|
||||
: null;
|
||||
|
||||
switch (delete_.data.deleted_type) {
|
||||
case "Note": {
|
||||
const note = await Note.fromSql(
|
||||
eq(Notes.uri, toDelete),
|
||||
author ? eq(Notes.authorId, author.id) : undefined,
|
||||
);
|
||||
|
||||
if (!note) {
|
||||
throw new ApiError(
|
||||
404,
|
||||
"Note to delete not found or not owned by sender",
|
||||
);
|
||||
}
|
||||
|
||||
await note.delete();
|
||||
return;
|
||||
}
|
||||
case "User": {
|
||||
const userToDelete = await User.resolve(new URL(toDelete));
|
||||
|
||||
if (!userToDelete) {
|
||||
throw new ApiError(404, "User to delete not found");
|
||||
}
|
||||
|
||||
if (!author || userToDelete.id === author.id) {
|
||||
await userToDelete.delete();
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ApiError(400, "Cannot delete other users than self");
|
||||
}
|
||||
case "pub.versia:likes/Like": {
|
||||
const like = await Like.fromSql(
|
||||
eq(Likes.uri, toDelete),
|
||||
author ? eq(Likes.likerId, author.id) : undefined,
|
||||
);
|
||||
|
||||
if (!like) {
|
||||
throw new ApiError(
|
||||
404,
|
||||
"Like not found or not owned by sender",
|
||||
);
|
||||
}
|
||||
|
||||
const likeAuthor = await User.fromId(like.data.likerId);
|
||||
const liked = await Note.fromId(like.data.likedId);
|
||||
|
||||
if (!liked) {
|
||||
throw new ApiError(
|
||||
404,
|
||||
"Liked Note not found or not owned by sender",
|
||||
);
|
||||
}
|
||||
|
||||
if (!likeAuthor) {
|
||||
throw new ApiError(404, "Like author not found");
|
||||
}
|
||||
|
||||
await liked.unlike(likeAuthor);
|
||||
|
||||
return;
|
||||
}
|
||||
case "pub.versia:shares/Share": {
|
||||
if (!author) {
|
||||
throw new ApiError(404, "Author not found");
|
||||
}
|
||||
|
||||
const reblog = await Note.fromSql(
|
||||
and(eq(Notes.uri, toDelete), eq(Notes.authorId, author.id)),
|
||||
);
|
||||
|
||||
if (!reblog) {
|
||||
throw new ApiError(
|
||||
404,
|
||||
"Share not found or not owned by sender",
|
||||
);
|
||||
}
|
||||
|
||||
const reblogged = await Note.fromId(
|
||||
reblog.data.reblogId,
|
||||
author.id,
|
||||
);
|
||||
|
||||
if (!reblogged) {
|
||||
throw new ApiError(
|
||||
404,
|
||||
"Share not found or not owned by sender",
|
||||
);
|
||||
}
|
||||
|
||||
await reblogged.unreblog(author);
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
throw new ApiError(
|
||||
400,
|
||||
`Deletion of object ${toDelete} not implemented`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Like entity processing.
|
||||
*
|
||||
* @param {VersiaLikeExtension} like - The Like entity to process.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private static async processLikeRequest(
|
||||
like: VersiaEntities.Like,
|
||||
): Promise<void> {
|
||||
const author = await User.resolve(new URL(like.data.author));
|
||||
const likedNote = await Note.resolve(new URL(like.data.liked));
|
||||
|
||||
if (!author) {
|
||||
throw new ApiError(404, "Author not found");
|
||||
}
|
||||
|
||||
if (!likedNote) {
|
||||
throw new ApiError(404, "Liked Note not found");
|
||||
}
|
||||
|
||||
await likedNote.like(author, new URL(like.data.uri));
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes Errors into the appropriate HTTP response.
|
||||
*
|
||||
* @param {Error} e - The error object.
|
||||
* @returns {void}
|
||||
* @throws {ApiError} - The error response.
|
||||
*/
|
||||
private handleError(e: Error): void {
|
||||
if (isValidationError(e)) {
|
||||
throw new ApiError(400, "Failed to process request", e.message);
|
||||
}
|
||||
|
||||
federationInboxLogger.error`${e}`;
|
||||
|
||||
throw new ApiError(500, "Failed to process request", e.message);
|
||||
}
|
||||
}
|
||||
4
packages/kit/index.ts
Normal file
4
packages/kit/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export { ApiError } from "./api-error.ts";
|
||||
export { Hooks } from "./hooks.ts";
|
||||
export { Plugin } from "./plugin.ts";
|
||||
export { type Manifest, manifestSchema } from "./schema.ts";
|
||||
6
packages/kit/json-schema.ts
Normal file
6
packages/kit/json-schema.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
import { manifestSchema } from "./schema.ts";
|
||||
|
||||
const jsonSchema = zodToJsonSchema(manifestSchema);
|
||||
|
||||
console.write(`${JSON.stringify(jsonSchema, null, 4)}\n`);
|
||||
84
packages/kit/manifest.schema.json
Normal file
84
packages/kit/manifest.schema.json
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"maxLength": 100
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 4096
|
||||
},
|
||||
"authors": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 100
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
}
|
||||
},
|
||||
"required": ["name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"git",
|
||||
"svn",
|
||||
"mercurial",
|
||||
"bzr",
|
||||
"darcs",
|
||||
"mtn",
|
||||
"cvs",
|
||||
"fossil",
|
||||
"bazaar",
|
||||
"arch",
|
||||
"tla",
|
||||
"archie",
|
||||
"monotone",
|
||||
"perforce",
|
||||
"sourcevault",
|
||||
"plastic",
|
||||
"clearcase",
|
||||
"accurev",
|
||||
"surroundscm",
|
||||
"bitkeeper",
|
||||
"other"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["name", "version", "description"],
|
||||
"additionalProperties": false,
|
||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||
}
|
||||
35
packages/kit/markdown.ts
Normal file
35
packages/kit/markdown.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import markdownItTaskLists from "@hackmd/markdown-it-task-lists";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import markdownItContainer from "markdown-it-container";
|
||||
import markdownItTocDoneRight from "markdown-it-toc-done-right";
|
||||
|
||||
const createMarkdownIt = (): MarkdownIt => {
|
||||
const renderer = MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
});
|
||||
|
||||
renderer.use(markdownItTocDoneRight, {
|
||||
containerClass: "toc",
|
||||
level: [1, 2, 3, 4],
|
||||
listType: "ul",
|
||||
listClass: "toc-list",
|
||||
itemClass: "toc-item",
|
||||
linkClass: "toc-link",
|
||||
});
|
||||
|
||||
renderer.use(markdownItTaskLists);
|
||||
|
||||
renderer.use(markdownItContainer);
|
||||
|
||||
return renderer;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts markdown text to HTML using MarkdownIt.
|
||||
* @param content
|
||||
* @returns
|
||||
*/
|
||||
export const markdownToHtml = async (content: string): Promise<string> => {
|
||||
return (await createMarkdownIt()).render(content);
|
||||
};
|
||||
135
packages/kit/package.json
Normal file
135
packages/kit/package.json
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
{
|
||||
"name": "@versia-server/kit",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"version": "0.0.0",
|
||||
"description": "Framework for building Versia Server plugins",
|
||||
"author": {
|
||||
"email": "contact@cpluspatch.com",
|
||||
"name": "CPlusPatch",
|
||||
"url": "https://cpluspatch.com"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "bun run build.ts"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/versia-pub/server/issues"
|
||||
},
|
||||
"icon": "https://github.com/versia-pub/server",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"keywords": [
|
||||
"federated",
|
||||
"activitypub",
|
||||
"bun"
|
||||
],
|
||||
"maintainers": [
|
||||
{
|
||||
"email": "contact@cpluspatch.com",
|
||||
"name": "CPlusPatch",
|
||||
"url": "https://cpluspatch.com"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/versia-pub/server.git"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:",
|
||||
"hono": "catalog:",
|
||||
"mitt": "catalog:",
|
||||
"zod": "catalog:",
|
||||
"zod-to-json-schema": "catalog:",
|
||||
"zod-validation-error": "catalog:",
|
||||
"chalk": "catalog:",
|
||||
"@versia/client": "workspace:*",
|
||||
"@versia-server/config": "workspace:*",
|
||||
"@versia-server/logging": "workspace:*",
|
||||
"@versia/sdk": "workspace:*",
|
||||
"html-to-text": "catalog:",
|
||||
"sharp": "catalog:",
|
||||
"magic-regexp": "catalog:",
|
||||
"altcha-lib": "catalog:",
|
||||
"hono-openapi": "catalog:",
|
||||
"qs": "catalog:",
|
||||
"@hono/zod-validator": "catalog:",
|
||||
"ioredis": "catalog:",
|
||||
"linkify-html": "catalog:",
|
||||
"markdown-it": "catalog:",
|
||||
"markdown-it-toc-done-right": "catalog:",
|
||||
"markdown-it-container": "catalog:",
|
||||
"@hackmd/markdown-it-task-lists": "catalog:",
|
||||
"bullmq": "catalog:",
|
||||
"web-push": "catalog:",
|
||||
"ip-matching": "catalog:",
|
||||
"sonic-channel": "catalog:"
|
||||
},
|
||||
"files": [
|
||||
"tables/migrations"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.ts"
|
||||
},
|
||||
"./db": {
|
||||
"import": "./db/index.ts"
|
||||
},
|
||||
"./tables": {
|
||||
"import": "./tables/schema.ts"
|
||||
},
|
||||
"./api": {
|
||||
"import": "./api.ts"
|
||||
},
|
||||
"./redis": {
|
||||
"import": "./redis.ts"
|
||||
},
|
||||
"./regex": {
|
||||
"import": "./regex.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"
|
||||
},
|
||||
"./parsers": {
|
||||
"import": "./parsers.ts"
|
||||
},
|
||||
"./search": {
|
||||
"import": "./search-manager.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
169
packages/kit/parsers.ts
Normal file
169
packages/kit/parsers.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
import type * as VersiaEntities from "@versia/sdk/entities";
|
||||
import { FederationRequester } from "@versia/sdk/http";
|
||||
import { config } from "@versia-server/config";
|
||||
import { and, eq, inArray, isNull, or } from "drizzle-orm";
|
||||
import linkifyHtml from "linkify-html";
|
||||
import {
|
||||
anyOf,
|
||||
charIn,
|
||||
createRegExp,
|
||||
digit,
|
||||
exactly,
|
||||
global,
|
||||
letter,
|
||||
} from "magic-regexp";
|
||||
import { sanitizeHtml, sanitizeHtmlInline } from "@/sanitization";
|
||||
import { User } from "./db/user.ts";
|
||||
import { markdownToHtml } from "./markdown.ts";
|
||||
import { mention } from "./regex.ts";
|
||||
import { db } from "./tables/db.ts";
|
||||
import { Instances, Users } from "./tables/schema.ts";
|
||||
|
||||
/**
|
||||
* Get people mentioned in the content (match @username or @username@domain.com mentions)
|
||||
* @param text The text to parse mentions from.
|
||||
* @returns An array of users mentioned in the text.
|
||||
*/
|
||||
export const parseMentionsFromText = async (text: string): Promise<User[]> => {
|
||||
const mentionedPeople = [...text.matchAll(mention)];
|
||||
if (mentionedPeople.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const baseUrlHost = config.http.base_url.host;
|
||||
const isLocal = (host?: string): boolean => host === baseUrlHost || !host;
|
||||
|
||||
// Find local and matching users
|
||||
const foundUsers = await db
|
||||
.select({
|
||||
id: Users.id,
|
||||
username: Users.username,
|
||||
baseUrl: Instances.baseUrl,
|
||||
})
|
||||
.from(Users)
|
||||
.leftJoin(Instances, eq(Users.instanceId, Instances.id))
|
||||
.where(
|
||||
or(
|
||||
...mentionedPeople.map((person) =>
|
||||
and(
|
||||
eq(Users.username, person[1] ?? ""),
|
||||
isLocal(person[2])
|
||||
? isNull(Users.instanceId)
|
||||
: eq(Instances.baseUrl, person[2] ?? ""),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Separate found and unresolved users
|
||||
const finalList = await User.manyFromSql(
|
||||
inArray(
|
||||
Users.id,
|
||||
foundUsers.map((u) => u.id),
|
||||
),
|
||||
);
|
||||
|
||||
// Every remote user that isn't in database
|
||||
const notFoundRemoteUsers = mentionedPeople.filter(
|
||||
(p) =>
|
||||
!(
|
||||
foundUsers.some(
|
||||
(user) => user.username === p[1] && user.baseUrl === p[2],
|
||||
) || isLocal(p[2])
|
||||
),
|
||||
);
|
||||
|
||||
// Resolve remote mentions not in database
|
||||
for (const person of notFoundRemoteUsers) {
|
||||
const url = await FederationRequester.resolveWebFinger(
|
||||
person[1] ?? "",
|
||||
person[2] ?? "",
|
||||
);
|
||||
|
||||
if (url) {
|
||||
const user = await User.resolve(url);
|
||||
|
||||
if (user) {
|
||||
finalList.push(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalList;
|
||||
};
|
||||
|
||||
export const linkifyUserMentions = (text: string, mentions: User[]): string => {
|
||||
return mentions.reduce((finalText, mention) => {
|
||||
const { username, instance } = mention.data;
|
||||
const { uri } = mention;
|
||||
const baseHost = config.http.base_url.host;
|
||||
const linkTemplate = (displayText: string): string =>
|
||||
`<a class="u-url mention" rel="nofollow noopener noreferrer" target="_blank" href="${uri}">${displayText}</a>`;
|
||||
|
||||
if (mention.remote) {
|
||||
return finalText.replaceAll(
|
||||
`@${username}@${instance?.baseUrl}`,
|
||||
linkTemplate(`@${username}@${instance?.baseUrl}`),
|
||||
);
|
||||
}
|
||||
|
||||
return finalText.replace(
|
||||
createRegExp(
|
||||
exactly(
|
||||
exactly(`@${username}`)
|
||||
.notBefore(anyOf(letter, digit, charIn("@")))
|
||||
.notAfter(anyOf(letter, digit, charIn("@"))),
|
||||
).or(exactly(`@${username}@${baseHost}`)),
|
||||
[global],
|
||||
),
|
||||
linkTemplate(`@${username}@${baseHost}`),
|
||||
);
|
||||
}, text);
|
||||
};
|
||||
|
||||
export const versiaTextToHtml = async (
|
||||
content: VersiaEntities.TextContentFormat,
|
||||
mentions: User[] = [],
|
||||
inline = false,
|
||||
): Promise<string> => {
|
||||
const sanitizer = inline ? sanitizeHtmlInline : sanitizeHtml;
|
||||
let htmlContent = "";
|
||||
|
||||
if (content.data["text/html"]) {
|
||||
htmlContent = await sanitizer(content.data["text/html"].content);
|
||||
} else if (content.data["text/markdown"]) {
|
||||
htmlContent = await sanitizer(
|
||||
await markdownToHtml(content.data["text/markdown"].content),
|
||||
);
|
||||
} else if (content.data["text/plain"]?.content) {
|
||||
htmlContent = (await sanitizer(content.data["text/plain"].content))
|
||||
.split("\n")
|
||||
.map((line) => `<p>${line}</p>`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
htmlContent = linkifyUserMentions(htmlContent, mentions);
|
||||
|
||||
return linkifyHtml(htmlContent, {
|
||||
defaultProtocol: "https",
|
||||
validate: { email: (): false => false },
|
||||
target: "_blank",
|
||||
rel: "nofollow noopener noreferrer",
|
||||
});
|
||||
};
|
||||
|
||||
export const parseUserAddress = (
|
||||
address: string,
|
||||
): {
|
||||
username: string;
|
||||
domain?: string;
|
||||
} => {
|
||||
let output = address;
|
||||
// Remove leading @ if it exists
|
||||
if (output.startsWith("@")) {
|
||||
output = output.slice(1);
|
||||
}
|
||||
|
||||
const [username, domain] = output.split("@");
|
||||
return { username, domain };
|
||||
};
|
||||
89
packages/kit/plugin.ts
Normal file
89
packages/kit/plugin.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import type { Hono, MiddlewareHandler } from "hono";
|
||||
import { createMiddleware } from "hono/factory";
|
||||
import type { z } from "zod";
|
||||
import { fromZodError, type ZodError } from "zod-validation-error";
|
||||
import type { HonoEnv } from "~/types/api";
|
||||
import type { ServerHooks } from "./hooks.ts";
|
||||
|
||||
export type HonoPluginEnv<ConfigType extends z.ZodTypeAny> = HonoEnv & {
|
||||
Variables: {
|
||||
pluginConfig: z.infer<ConfigType>;
|
||||
};
|
||||
};
|
||||
|
||||
export class Plugin<ConfigSchema extends z.ZodTypeAny> {
|
||||
private handlers: Partial<ServerHooks> = {};
|
||||
private store: z.infer<ConfigSchema> | null = null;
|
||||
private routes: {
|
||||
path: string;
|
||||
fn: (app: Hono<HonoPluginEnv<ConfigSchema>>) => void;
|
||||
}[] = [];
|
||||
|
||||
public constructor(private configSchema: ConfigSchema) {}
|
||||
|
||||
public get middleware(): MiddlewareHandler<HonoPluginEnv<ConfigSchema>> {
|
||||
// Middleware that adds the plugin's configuration to the request object
|
||||
return createMiddleware<HonoPluginEnv<ConfigSchema>>(
|
||||
async (context, next) => {
|
||||
context.set("pluginConfig", this.getConfig());
|
||||
await next();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public registerRoute(
|
||||
path: string,
|
||||
fn: (app: Hono<HonoPluginEnv<ConfigSchema>>) => void,
|
||||
): void {
|
||||
this.routes.push({
|
||||
path,
|
||||
fn,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the plugin's configuration from the Versia Server configuration file.
|
||||
* This will be called when the plugin is loaded.
|
||||
* @param config Values the user has set in the configuration file.
|
||||
*/
|
||||
protected async _loadConfig(config: z.input<ConfigSchema>): Promise<void> {
|
||||
try {
|
||||
this.store = await this.configSchema.parseAsync(config);
|
||||
} catch (error) {
|
||||
throw fromZodError(error as ZodError);
|
||||
}
|
||||
}
|
||||
|
||||
protected _addToApp(app: Hono<HonoEnv>): void {
|
||||
for (const route of this.routes) {
|
||||
app.use(route.path, this.middleware);
|
||||
route.fn(app as unknown as Hono<HonoPluginEnv<ConfigSchema>>);
|
||||
}
|
||||
}
|
||||
|
||||
public registerHandler<HookName extends keyof ServerHooks>(
|
||||
hook: HookName,
|
||||
handler: ServerHooks[HookName],
|
||||
): void {
|
||||
this.handlers[hook] = handler;
|
||||
}
|
||||
|
||||
public static [Symbol.hasInstance](instance: unknown): boolean {
|
||||
return (
|
||||
typeof instance === "object" &&
|
||||
instance !== null &&
|
||||
"registerHandler" in instance
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the internal configuration object.
|
||||
*/
|
||||
private getConfig(): z.infer<ConfigSchema> {
|
||||
if (!this.store) {
|
||||
throw new Error("Configuration has not been loaded yet.");
|
||||
}
|
||||
|
||||
return this.store;
|
||||
}
|
||||
}
|
||||
20
packages/kit/queues/delivery/queue.ts
Normal file
20
packages/kit/queues/delivery/queue.ts
Normal file
|
|
@ -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<DeliveryJobData, void, DeliveryJobType>(
|
||||
"delivery",
|
||||
{
|
||||
connection,
|
||||
},
|
||||
);
|
||||
84
packages/kit/queues/delivery/worker.ts
Normal file
84
packages/kit/queues/delivery/worker.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import * as VersiaEntities from "@versia/sdk/entities";
|
||||
import { config } from "@versia-server/config";
|
||||
import { Worker } from "bullmq";
|
||||
import chalk from "chalk";
|
||||
import { User } from "../../db/user.ts";
|
||||
import { connection } from "../../redis.ts";
|
||||
import {
|
||||
type DeliveryJobData,
|
||||
DeliveryJobType,
|
||||
deliveryQueue,
|
||||
} from "./queue.ts";
|
||||
|
||||
export const getDeliveryWorker = (): Worker<
|
||||
DeliveryJobData,
|
||||
void,
|
||||
DeliveryJobType
|
||||
> =>
|
||||
new Worker<DeliveryJobData, void, DeliveryJobType>(
|
||||
deliveryQueue.name,
|
||||
async (job) => {
|
||||
switch (job.name) {
|
||||
case DeliveryJobType.FederateEntity: {
|
||||
const { entity, recipientId, senderId } = job.data;
|
||||
|
||||
const sender = await User.fromId(senderId);
|
||||
|
||||
if (!sender) {
|
||||
throw new Error(
|
||||
`Could not resolve sender ID ${chalk.gray(
|
||||
senderId,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const recipient = await User.fromId(recipientId);
|
||||
|
||||
if (!recipient) {
|
||||
throw new Error(
|
||||
`Could not resolve recipient ID ${chalk.gray(
|
||||
recipientId,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
await job.log(
|
||||
`Federating entity [${
|
||||
entity.id
|
||||
}] from @${sender.getAcct()} to @${recipient.getAcct()}`,
|
||||
);
|
||||
|
||||
const type = entity.type;
|
||||
const entityCtor = Object.values(VersiaEntities).find(
|
||||
(ctor) => ctor.name === type,
|
||||
) as typeof VersiaEntities.Entity | undefined;
|
||||
|
||||
if (!entityCtor) {
|
||||
throw new Error(
|
||||
`Could not resolve entity type ${chalk.gray(
|
||||
type,
|
||||
)} for entity [${entity.id}]`,
|
||||
);
|
||||
}
|
||||
|
||||
await sender.federateToUser(
|
||||
await entityCtor.fromJSON(entity),
|
||||
recipient,
|
||||
);
|
||||
|
||||
await job.log(
|
||||
`✔ Finished federating entity [${entity.id}]`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
connection,
|
||||
removeOnComplete: {
|
||||
age: config.queues.delivery?.remove_after_complete_seconds,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: config.queues.delivery?.remove_after_failure_seconds,
|
||||
},
|
||||
},
|
||||
);
|
||||
17
packages/kit/queues/fetch/queue.ts
Normal file
17
packages/kit/queues/fetch/queue.ts
Normal file
|
|
@ -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<FetchJobData, void, FetchJobType>("fetch", {
|
||||
connection,
|
||||
});
|
||||
57
packages/kit/queues/fetch/worker.ts
Normal file
57
packages/kit/queues/fetch/worker.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { config } from "@versia-server/config";
|
||||
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";
|
||||
import { type FetchJobData, FetchJobType, fetchQueue } from "./queue.ts";
|
||||
|
||||
export const getFetchWorker = (): Worker<FetchJobData, void, FetchJobType> =>
|
||||
new Worker<FetchJobData, void, FetchJobType>(
|
||||
fetchQueue.name,
|
||||
async (job) => {
|
||||
switch (job.name) {
|
||||
case FetchJobType.Instance: {
|
||||
const { uri } = job.data;
|
||||
|
||||
await job.log(`Fetching instance metadata from [${uri}]`);
|
||||
|
||||
// Check if exists
|
||||
const host = new URL(uri).host;
|
||||
|
||||
const existingInstance = await Instance.fromSql(
|
||||
eq(Instances.baseUrl, host),
|
||||
);
|
||||
|
||||
if (existingInstance) {
|
||||
await job.log(
|
||||
"Instance is known, refetching remote data.",
|
||||
);
|
||||
|
||||
await existingInstance.updateFromRemote();
|
||||
|
||||
await job.log(
|
||||
`Instance [${uri}] successfully refetched`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await Instance.resolve(new URL(uri));
|
||||
|
||||
await job.log(
|
||||
`✔ Finished fetching instance metadata from [${uri}]`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
connection,
|
||||
removeOnComplete: {
|
||||
age: config.queues.fetch?.remove_after_complete_seconds,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: config.queues.fetch?.remove_after_failure_seconds,
|
||||
},
|
||||
},
|
||||
);
|
||||
31
packages/kit/queues/inbox/queue.ts
Normal file
31
packages/kit/queues/inbox/queue.ts
Normal file
|
|
@ -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<InboxJobData, Response, InboxJobType>(
|
||||
"inbox",
|
||||
{
|
||||
connection,
|
||||
},
|
||||
);
|
||||
190
packages/kit/queues/inbox/worker.ts
Normal file
190
packages/kit/queues/inbox/worker.ts
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import { config } from "@versia-server/config";
|
||||
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<InboxJobData, void, InboxJobType> =>
|
||||
new Worker<InboxJobData, void, InboxJobType>(
|
||||
inboxQueue.name,
|
||||
async (job) => {
|
||||
switch (job.name) {
|
||||
case InboxJobType.ProcessEntity: {
|
||||
const { data, headers, request, ip } = job.data;
|
||||
|
||||
await job.log(`Processing entity [${data.id}]`);
|
||||
|
||||
const req = new Request(request.url, {
|
||||
method: request.method,
|
||||
headers: new Headers(
|
||||
Object.entries(headers)
|
||||
.map(([k, v]) => [k, String(v)])
|
||||
.concat([
|
||||
["content-type", "application/json"],
|
||||
]) as [string, string][],
|
||||
),
|
||||
body: request.body,
|
||||
});
|
||||
|
||||
if (headers.authorization) {
|
||||
try {
|
||||
const processor = new InboxProcessor(
|
||||
req,
|
||||
data,
|
||||
null,
|
||||
headers.authorization,
|
||||
ip,
|
||||
);
|
||||
|
||||
await job.log(
|
||||
`Entity [${data.id}] is potentially from a bridge`,
|
||||
);
|
||||
|
||||
await processor.process();
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
// Error occurred
|
||||
await job.log(
|
||||
`Error during processing: ${e.message}`,
|
||||
);
|
||||
|
||||
await job.log(
|
||||
`Failed processing entity [${data.id}]`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
await job.log(
|
||||
`✔ Finished processing entity [${data.id}]`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { "versia-signed-by": signedBy } = headers as {
|
||||
"versia-signed-by": string;
|
||||
};
|
||||
|
||||
const sender = await User.resolve(new URL(signedBy));
|
||||
|
||||
if (!(sender || signedBy.startsWith("instance "))) {
|
||||
await job.log(
|
||||
`Could not resolve sender URI [${signedBy}]`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender?.local) {
|
||||
throw new Error(
|
||||
"Cannot process federation requests from local users",
|
||||
);
|
||||
}
|
||||
|
||||
const remoteInstance = sender
|
||||
? await Instance.fromUser(sender)
|
||||
: await Instance.resolveFromHost(
|
||||
signedBy.split(" ")[1],
|
||||
);
|
||||
|
||||
if (!remoteInstance) {
|
||||
await job.log("Could not resolve the remote instance.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await job.log(
|
||||
`Entity [${data.id}] is from remote instance [${remoteInstance.data.baseUrl}]`,
|
||||
);
|
||||
|
||||
if (!remoteInstance.data.publicKey?.key) {
|
||||
throw new Error(
|
||||
`Instance ${remoteInstance.data.baseUrl} has no public key stored in database`,
|
||||
);
|
||||
}
|
||||
|
||||
const key = await crypto.subtle.importKey(
|
||||
"spki",
|
||||
Buffer.from(
|
||||
sender?.data.publicKey ??
|
||||
remoteInstance.data.publicKey.key,
|
||||
"base64",
|
||||
),
|
||||
"Ed25519",
|
||||
false,
|
||||
["verify"],
|
||||
);
|
||||
|
||||
try {
|
||||
const processor = new InboxProcessor(
|
||||
req,
|
||||
data,
|
||||
{
|
||||
instance: remoteInstance,
|
||||
key,
|
||||
},
|
||||
undefined,
|
||||
ip,
|
||||
);
|
||||
|
||||
await processor.process();
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
// Error occurred
|
||||
await job.log(
|
||||
`Error during processing: ${e.message}`,
|
||||
);
|
||||
|
||||
await job.log(
|
||||
`Failed processing entity [${data.id}]`,
|
||||
);
|
||||
|
||||
await job.log(
|
||||
`Sending error message to instance [${remoteInstance.data.baseUrl}]`,
|
||||
);
|
||||
|
||||
await remoteInstance.sendMessage(
|
||||
`Failed processing entity [${
|
||||
data.uri
|
||||
}] delivered to inbox. Returned error:\n\n${JSON.stringify(
|
||||
e.message,
|
||||
null,
|
||||
4,
|
||||
)}`,
|
||||
);
|
||||
|
||||
await job.log("Message sent");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
await job.log(`Finished processing entity [${data.id}]`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error(`Unknown job type: ${job.name}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
connection,
|
||||
removeOnComplete: {
|
||||
age: config.queues.inbox?.remove_after_complete_seconds,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: config.queues.inbox?.remove_after_failure_seconds,
|
||||
},
|
||||
},
|
||||
);
|
||||
16
packages/kit/queues/media/queue.ts
Normal file
16
packages/kit/queues/media/queue.ts
Normal file
|
|
@ -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<MediaJobData, void, MediaJobType>("media", {
|
||||
connection,
|
||||
});
|
||||
112
packages/kit/queues/media/worker.ts
Normal file
112
packages/kit/queues/media/worker.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import { config } from "@versia-server/config";
|
||||
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<MediaJobData, void, MediaJobType> =>
|
||||
new Worker<MediaJobData, void, MediaJobType>(
|
||||
mediaQueue.name,
|
||||
async (job) => {
|
||||
switch (job.name) {
|
||||
case MediaJobType.ConvertMedia: {
|
||||
const { attachmentId, filename } = job.data;
|
||||
|
||||
await job.log(`Fetching attachment ID [${attachmentId}]`);
|
||||
|
||||
const attachment = await Media.fromId(attachmentId);
|
||||
|
||||
if (!attachment) {
|
||||
throw new Error(
|
||||
`Attachment not found: [${attachmentId}]`,
|
||||
);
|
||||
}
|
||||
|
||||
await job.log(`Processing attachment [${attachmentId}]`);
|
||||
await job.log(
|
||||
`Fetching file from [${attachment.getUrl()}]`,
|
||||
);
|
||||
|
||||
// Download the file and process it.
|
||||
const blob = await (
|
||||
await fetch(attachment.getUrl())
|
||||
).blob();
|
||||
|
||||
const file = new File([blob], filename);
|
||||
|
||||
await job.log(`Converting attachment [${attachmentId}]`);
|
||||
|
||||
const processedFile = await convertImage(
|
||||
file,
|
||||
config.media.conversion.convert_to,
|
||||
{
|
||||
convertVectors:
|
||||
config.media.conversion.convert_vectors,
|
||||
},
|
||||
);
|
||||
|
||||
await job.log(`Uploading attachment [${attachmentId}]`);
|
||||
|
||||
await attachment.updateFromFile(processedFile);
|
||||
|
||||
await job.log(
|
||||
`✔ Finished processing attachment [${attachmentId}]`,
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
case MediaJobType.CalculateMetadata: {
|
||||
// Calculate blurhash
|
||||
const { attachmentId } = job.data;
|
||||
|
||||
await job.log(`Fetching attachment ID [${attachmentId}]`);
|
||||
|
||||
const attachment = await Media.fromId(attachmentId);
|
||||
|
||||
if (!attachment) {
|
||||
throw new Error(
|
||||
`Attachment not found: [${attachmentId}]`,
|
||||
);
|
||||
}
|
||||
|
||||
await job.log(`Processing attachment [${attachmentId}]`);
|
||||
await job.log(
|
||||
`Fetching file from [${attachment.getUrl()}]`,
|
||||
);
|
||||
|
||||
// Download the file and process it.
|
||||
const blob = await (
|
||||
await fetch(attachment.getUrl())
|
||||
).blob();
|
||||
|
||||
// Filename is not important for blurhash
|
||||
const file = new File([blob], "");
|
||||
|
||||
await job.log(`Generating blurhash for [${attachmentId}]`);
|
||||
|
||||
const blurhash = await calculateBlurhash(file);
|
||||
|
||||
await attachment.update({
|
||||
blurhash,
|
||||
});
|
||||
|
||||
await job.log(
|
||||
`✔ Finished processing attachment [${attachmentId}]`,
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
connection,
|
||||
removeOnComplete: {
|
||||
age: config.queues.media?.remove_after_complete_seconds,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: config.queues.media?.remove_after_failure_seconds,
|
||||
},
|
||||
},
|
||||
);
|
||||
18
packages/kit/queues/push/queue.ts
Normal file
18
packages/kit/queues/push/queue.ts
Normal file
|
|
@ -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<PushJobData, void, PushJobType>("push", {
|
||||
connection,
|
||||
});
|
||||
153
packages/kit/queues/push/worker.ts
Normal file
153
packages/kit/queues/push/worker.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { config } from "@versia-server/config";
|
||||
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";
|
||||
import { type PushJobData, type PushJobType, pushQueue } from "./queue.ts";
|
||||
|
||||
export const getPushWorker = (): Worker<PushJobData, void, PushJobType> =>
|
||||
new Worker<PushJobData, void, PushJobType>(
|
||||
pushQueue.name,
|
||||
async (job) => {
|
||||
const {
|
||||
data: { psId, relatedUserId, type, noteId, notificationId },
|
||||
} = job;
|
||||
|
||||
if (!config.notifications.push) {
|
||||
await job.log("Push notifications are disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!(
|
||||
config.notifications.push.vapid_keys.private ||
|
||||
config.notifications.push.vapid_keys.public
|
||||
)
|
||||
) {
|
||||
await job.log("Push notifications are not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
await job.log(
|
||||
`Sending push notification for note [${notificationId}]`,
|
||||
);
|
||||
|
||||
const ps = await PushSubscription.fromId(psId);
|
||||
|
||||
if (!ps) {
|
||||
throw new Error(
|
||||
`Could not resolve push subscription ID ${psId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const token = await Token.fromId(ps.data.tokenId);
|
||||
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
`Could not resolve token ID ${ps.data.tokenId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const relatedUser = await User.fromId(relatedUserId);
|
||||
|
||||
if (!relatedUser) {
|
||||
throw new Error(
|
||||
`Could not resolve related user ID ${relatedUserId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const note = noteId ? await Note.fromId(noteId) : null;
|
||||
|
||||
const truncate = (str: string, len: number): string => {
|
||||
if (str.length <= len) {
|
||||
return str;
|
||||
}
|
||||
|
||||
return `${str.slice(0, len)}...`;
|
||||
};
|
||||
|
||||
const name = truncate(
|
||||
relatedUser.data.displayName || relatedUser.data.username,
|
||||
50,
|
||||
);
|
||||
|
||||
let title = name;
|
||||
|
||||
switch (type) {
|
||||
case "mention":
|
||||
title = `${name} mentioned you`;
|
||||
break;
|
||||
case "reply":
|
||||
title = `${name} replied to you`;
|
||||
break;
|
||||
case "favourite":
|
||||
title = `${name} liked your note`;
|
||||
break;
|
||||
case "reblog":
|
||||
title = `${name} reblogged your note`;
|
||||
break;
|
||||
case "follow":
|
||||
title = `${name} followed you`;
|
||||
break;
|
||||
case "follow_request":
|
||||
title = `${name} requested to follow you`;
|
||||
break;
|
||||
case "poll":
|
||||
title = "Poll ended";
|
||||
break;
|
||||
}
|
||||
|
||||
const body = note
|
||||
? htmlToText(note.data.spoilerText || note.data.content)
|
||||
: htmlToText(relatedUser.data.note);
|
||||
|
||||
await sendNotification(
|
||||
{
|
||||
endpoint: ps.data.endpoint,
|
||||
keys: {
|
||||
auth: ps.data.authSecret,
|
||||
p256dh: ps.data.publicKey,
|
||||
},
|
||||
},
|
||||
JSON.stringify({
|
||||
access_token: token.data.accessToken,
|
||||
// FIXME
|
||||
preferred_locale: "en-US",
|
||||
notification_id: notificationId,
|
||||
notification_type: type,
|
||||
icon: relatedUser.getAvatarUrl(),
|
||||
title,
|
||||
body: truncate(body, 140),
|
||||
}),
|
||||
{
|
||||
vapidDetails: {
|
||||
subject:
|
||||
config.notifications.push.subject ||
|
||||
config.http.base_url.origin,
|
||||
privateKey:
|
||||
config.notifications.push.vapid_keys.private ?? "",
|
||||
publicKey:
|
||||
config.notifications.push.vapid_keys.public ?? "",
|
||||
},
|
||||
contentEncoding: "aesgcm",
|
||||
},
|
||||
);
|
||||
|
||||
await job.log(
|
||||
`✔ Finished delivering push notification for note [${notificationId}]`,
|
||||
);
|
||||
},
|
||||
{
|
||||
connection,
|
||||
removeOnComplete: {
|
||||
age: config.queues.push?.remove_after_complete_seconds,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: config.queues.push?.remove_after_failure_seconds,
|
||||
},
|
||||
},
|
||||
);
|
||||
19
packages/kit/queues/relationships/queue.ts
Normal file
19
packages/kit/queues/relationships/queue.ts
Normal file
|
|
@ -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,
|
||||
});
|
||||
55
packages/kit/queues/relationships/worker.ts
Normal file
55
packages/kit/queues/relationships/worker.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { config } from "@versia-server/config";
|
||||
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,
|
||||
void,
|
||||
RelationshipJobType
|
||||
> =>
|
||||
new Worker<RelationshipJobData, void, RelationshipJobType>(
|
||||
relationshipQueue.name,
|
||||
async (job) => {
|
||||
switch (job.name) {
|
||||
case RelationshipJobType.Unmute: {
|
||||
const { ownerId, subjectId } = job.data;
|
||||
|
||||
const owner = await User.fromId(ownerId);
|
||||
const subject = await User.fromId(subjectId);
|
||||
|
||||
if (!(owner && subject)) {
|
||||
await job.log("Users not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const foundRelationship =
|
||||
await Relationship.fromOwnerAndSubject(owner, subject);
|
||||
|
||||
if (foundRelationship.data.muting) {
|
||||
await foundRelationship.update({
|
||||
muting: false,
|
||||
mutingNotifications: false,
|
||||
});
|
||||
}
|
||||
|
||||
await job.log(`✔ Finished unmuting [${subjectId}]`);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
connection,
|
||||
removeOnComplete: {
|
||||
age: config.queues.fetch?.remove_after_complete_seconds,
|
||||
},
|
||||
removeOnFail: {
|
||||
age: config.queues.fetch?.remove_after_failure_seconds,
|
||||
},
|
||||
},
|
||||
);
|
||||
10
packages/kit/redis.ts
Normal file
10
packages/kit/redis.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { config } from "@versia-server/config";
|
||||
import IORedis from "ioredis";
|
||||
|
||||
export const connection = new IORedis({
|
||||
host: config.redis.queue.host,
|
||||
port: config.redis.queue.port,
|
||||
password: config.redis.queue.password,
|
||||
db: config.redis.queue.database,
|
||||
maxRetriesPerRequest: null,
|
||||
});
|
||||
49
packages/kit/regex.ts
Normal file
49
packages/kit/regex.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import {
|
||||
anyOf,
|
||||
caseInsensitive,
|
||||
charIn,
|
||||
createRegExp,
|
||||
digit,
|
||||
exactly,
|
||||
global,
|
||||
letter,
|
||||
maybe,
|
||||
oneOrMore,
|
||||
} from "magic-regexp";
|
||||
|
||||
export const uuid = createRegExp(
|
||||
anyOf(digit, charIn("ABCDEF")).times(8),
|
||||
exactly("-"),
|
||||
anyOf(digit, charIn("ABCDEF")).times(4),
|
||||
exactly("-"),
|
||||
exactly("7"),
|
||||
anyOf(digit, charIn("ABCDEF")).times(3),
|
||||
exactly("-"),
|
||||
anyOf("8", "9", "A", "B").times(1),
|
||||
anyOf(digit, charIn("ABCDEF")).times(3),
|
||||
exactly("-"),
|
||||
anyOf(digit, charIn("ABCDEF")).times(12),
|
||||
[caseInsensitive],
|
||||
);
|
||||
|
||||
export const mention = createRegExp(
|
||||
exactly("@"),
|
||||
oneOrMore(anyOf(letter.lowercase, digit, charIn("-_"))).groupedAs(
|
||||
"username",
|
||||
),
|
||||
maybe(
|
||||
exactly("@"),
|
||||
oneOrMore(anyOf(letter, digit, charIn("_-.:"))).groupedAs("domain"),
|
||||
),
|
||||
[global],
|
||||
);
|
||||
|
||||
export const webfingerMention = createRegExp(
|
||||
exactly("acct:"),
|
||||
oneOrMore(anyOf(letter, digit, charIn("-_"))).groupedAs("username"),
|
||||
maybe(
|
||||
exactly("@"),
|
||||
oneOrMore(anyOf(letter, digit, charIn("_-.:"))).groupedAs("domain"),
|
||||
),
|
||||
[],
|
||||
);
|
||||
101
packages/kit/schema.ts
Normal file
101
packages/kit/schema.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const manifestSchema = z.object({
|
||||
// biome-ignore lint/style/useNamingConvention: JSON schema requires this to be $schema
|
||||
$schema: z.string().optional(),
|
||||
name: z.string().min(3).max(100),
|
||||
version: z
|
||||
.string()
|
||||
.regex(
|
||||
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/gm,
|
||||
"Version must be valid SemVer string",
|
||||
),
|
||||
description: z.string().min(1).max(4096),
|
||||
authors: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().min(1).max(100),
|
||||
email: z.string().email().optional(),
|
||||
url: z.string().url().optional(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
repository: z
|
||||
.object({
|
||||
type: z
|
||||
.enum([
|
||||
"git",
|
||||
"svn",
|
||||
"mercurial",
|
||||
"bzr",
|
||||
"darcs",
|
||||
"mtn",
|
||||
"cvs",
|
||||
"fossil",
|
||||
"bazaar",
|
||||
"arch",
|
||||
"tla",
|
||||
"archie",
|
||||
"monotone",
|
||||
"perforce",
|
||||
"sourcevault",
|
||||
"plastic",
|
||||
"clearcase",
|
||||
"accurev",
|
||||
"surroundscm",
|
||||
"bitkeeper",
|
||||
"other",
|
||||
])
|
||||
.optional(),
|
||||
url: z.string().url().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type Manifest = {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
authors?:
|
||||
| {
|
||||
name: string;
|
||||
email?: string | undefined;
|
||||
url?: string | undefined;
|
||||
}[]
|
||||
| undefined;
|
||||
repository?:
|
||||
| {
|
||||
type?:
|
||||
| "git"
|
||||
| "svn"
|
||||
| "mercurial"
|
||||
| "bzr"
|
||||
| "darcs"
|
||||
| "mtn"
|
||||
| "cvs"
|
||||
| "fossil"
|
||||
| "bazaar"
|
||||
| "arch"
|
||||
| "tla"
|
||||
| "archie"
|
||||
| "monotone"
|
||||
| "perforce"
|
||||
| "sourcevault"
|
||||
| "plastic"
|
||||
| "clearcase"
|
||||
| "accurev"
|
||||
| "surroundscm"
|
||||
| "bitkeeper"
|
||||
| "other"
|
||||
| undefined;
|
||||
url?: string | undefined;
|
||||
}
|
||||
| undefined;
|
||||
};
|
||||
|
||||
// This is a type guard to ensure that the schema and the type are in sync
|
||||
function assert<_T extends never>() {
|
||||
// ...
|
||||
}
|
||||
type TypeEqualityGuard<A, B> = Exclude<A, B> | Exclude<B, A>;
|
||||
assert<TypeEqualityGuard<Manifest, z.infer<typeof manifestSchema>>>();
|
||||
311
packages/kit/search-manager.ts
Normal file
311
packages/kit/search-manager.ts
Normal file
|
|
@ -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<void> {
|
||||
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<boolean>((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<boolean>((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<void> {
|
||||
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<Record<string, string | null | Date>[]> {
|
||||
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<SQL> => 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<Record<string, string | Date>[]> {
|
||||
return db.query.Notes.findMany({
|
||||
offset: n * batchSize,
|
||||
limit: batchSize,
|
||||
columns: {
|
||||
id: true,
|
||||
content: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: (status, { asc }): ValueOrArray<SQL> =>
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string[]> {
|
||||
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<string[]> {
|
||||
return this.searchChannel.query(
|
||||
SonicIndexType.Statuses,
|
||||
"notes",
|
||||
query,
|
||||
{ limit, offset },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const searchManager = new SonicSearchManager();
|
||||
80
packages/kit/tables/db.ts
Normal file
80
packages/kit/tables/db.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { join } from "node:path";
|
||||
import { config } from "@versia-server/config";
|
||||
import { databaseLogger } from "@versia-server/logging";
|
||||
import { SQL } from "bun";
|
||||
import chalk from "chalk";
|
||||
import { type BunSQLDatabase, drizzle } from "drizzle-orm/bun-sql";
|
||||
import { withReplicas } from "drizzle-orm/pg-core";
|
||||
import { migrate } from "drizzle-orm/postgres-js/migrator";
|
||||
import * as schema from "./schema.ts";
|
||||
|
||||
const primaryDb = new SQL({
|
||||
host: config.postgres.host,
|
||||
port: config.postgres.port,
|
||||
user: config.postgres.username,
|
||||
password: config.postgres.password,
|
||||
database: config.postgres.database,
|
||||
});
|
||||
|
||||
const replicas = config.postgres.replicas.map(
|
||||
(replica) =>
|
||||
new SQL({
|
||||
host: replica.host,
|
||||
port: replica.port,
|
||||
user: replica.username,
|
||||
password: replica.password,
|
||||
database: replica.database,
|
||||
}),
|
||||
);
|
||||
|
||||
export const db =
|
||||
(replicas.length ?? 0) > 0
|
||||
? withReplicas(
|
||||
drizzle(primaryDb, { schema }),
|
||||
replicas.map((r) => drizzle(r, { schema })) as [
|
||||
// biome-ignore lint/style/useNamingConvention: Required by drizzle-orm
|
||||
BunSQLDatabase<typeof schema> & { $client: SQL },
|
||||
// biome-ignore lint/style/useNamingConvention: Required by drizzle-orm
|
||||
...(BunSQLDatabase<typeof schema> & { $client: SQL })[],
|
||||
],
|
||||
)
|
||||
: drizzle(primaryDb, { schema });
|
||||
|
||||
export const setupDatabase = async (info = true): Promise<void> => {
|
||||
for (const dbPool of [primaryDb, ...replicas]) {
|
||||
try {
|
||||
await dbPool.connect();
|
||||
} catch (e) {
|
||||
if (
|
||||
(e as Error).message ===
|
||||
"Client has already been connected. You cannot reuse a client."
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
databaseLogger.fatal`Failed to connect to database ${chalk.bold(
|
||||
// Index of the database in the array
|
||||
replicas.indexOf(dbPool) === -1
|
||||
? "primary"
|
||||
: `replica-${replicas.indexOf(dbPool)}`,
|
||||
)}. Please check your configuration.`;
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate the database
|
||||
info && databaseLogger.info`Migrating database...`;
|
||||
|
||||
try {
|
||||
await migrate(db, {
|
||||
migrationsFolder: join(import.meta.dir, "migrations"),
|
||||
});
|
||||
} catch (e) {
|
||||
databaseLogger.fatal`Failed to migrate database. Please check your configuration.`;
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
info && databaseLogger.info`Database migrated`;
|
||||
};
|
||||
459
packages/kit/tables/migrations/0000_illegal_living_lightning.sql
Normal file
459
packages/kit/tables/migrations/0000_illegal_living_lightning.sql
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
-- Current sql file was generated after introspecting the database
|
||||
-- If you want to run this migration please uncomment this code before executing migrations
|
||||
CREATE TABLE IF NOT EXISTS "_prisma_migrations" (
|
||||
"id" varchar(36) PRIMARY KEY NOT NULL,
|
||||
"checksum" varchar(64) NOT NULL,
|
||||
"finished_at" timestamp with time zone,
|
||||
"migration_name" varchar(255) NOT NULL,
|
||||
"logs" text,
|
||||
"rolled_back_at" timestamp with time zone,
|
||||
"started_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"applied_steps_count" integer DEFAULT 0 NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "Emoji" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"shortcode" text NOT NULL,
|
||||
"url" text NOT NULL,
|
||||
"visible_in_picker" boolean NOT NULL,
|
||||
"instanceId" uuid,
|
||||
"alt" text,
|
||||
"content_type" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "Like" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"likerId" uuid NOT NULL,
|
||||
"likedId" uuid NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "LysandObject" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"remote_id" text NOT NULL,
|
||||
"type" text NOT NULL,
|
||||
"uri" text NOT NULL,
|
||||
"created_at" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"authorId" uuid,
|
||||
"extra_data" jsonb NOT NULL,
|
||||
"extensions" jsonb NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "Relationship" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"ownerId" uuid NOT NULL,
|
||||
"subjectId" uuid NOT NULL,
|
||||
"following" boolean NOT NULL,
|
||||
"showingReblogs" boolean NOT NULL,
|
||||
"notifying" boolean NOT NULL,
|
||||
"followedBy" boolean NOT NULL,
|
||||
"blocking" boolean NOT NULL,
|
||||
"blockedBy" boolean NOT NULL,
|
||||
"muting" boolean NOT NULL,
|
||||
"mutingNotifications" boolean NOT NULL,
|
||||
"requested" boolean NOT NULL,
|
||||
"domainBlocking" boolean NOT NULL,
|
||||
"endorsed" boolean NOT NULL,
|
||||
"languages" text[],
|
||||
"note" text NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "Application" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"website" text,
|
||||
"vapid_key" text,
|
||||
"client_id" text NOT NULL,
|
||||
"secret" text NOT NULL,
|
||||
"scopes" text NOT NULL,
|
||||
"redirect_uris" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "Token" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"token_type" text NOT NULL,
|
||||
"scope" text NOT NULL,
|
||||
"access_token" text NOT NULL,
|
||||
"code" text NOT NULL,
|
||||
"created_at" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"userId" uuid,
|
||||
"applicationId" uuid
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "_EmojiToUser" (
|
||||
"A" uuid NOT NULL,
|
||||
"B" uuid NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "_EmojiToStatus" (
|
||||
"A" uuid NOT NULL,
|
||||
"B" uuid NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "_StatusToUser" (
|
||||
"A" uuid NOT NULL,
|
||||
"B" uuid NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "_UserPinnedNotes" (
|
||||
"A" uuid NOT NULL,
|
||||
"B" uuid NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "Attachment" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"url" text NOT NULL,
|
||||
"remote_url" text,
|
||||
"thumbnail_url" text,
|
||||
"mime_type" text NOT NULL,
|
||||
"description" text,
|
||||
"blurhash" text,
|
||||
"sha256" text,
|
||||
"fps" integer,
|
||||
"duration" integer,
|
||||
"width" integer,
|
||||
"height" integer,
|
||||
"size" integer,
|
||||
"statusId" uuid
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "Notification" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"type" text NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"notifiedId" uuid NOT NULL,
|
||||
"accountId" uuid NOT NULL,
|
||||
"statusId" uuid
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "Status" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"uri" text,
|
||||
"authorId" uuid NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"reblogId" uuid,
|
||||
"content" text DEFAULT '' NOT NULL,
|
||||
"contentType" text DEFAULT 'text/plain' NOT NULL,
|
||||
"visibility" text NOT NULL,
|
||||
"inReplyToPostId" uuid,
|
||||
"quotingPostId" uuid,
|
||||
"instanceId" uuid,
|
||||
"sensitive" boolean NOT NULL,
|
||||
"spoilerText" text DEFAULT '' NOT NULL,
|
||||
"applicationId" uuid,
|
||||
"contentSource" text DEFAULT '' NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "Instance" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"base_url" text NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"version" text NOT NULL,
|
||||
"logo" jsonb NOT NULL,
|
||||
"disableAutomoderation" boolean DEFAULT false NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "OpenIdAccount" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"userId" uuid,
|
||||
"serverId" text NOT NULL,
|
||||
"issuerId" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "User" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"uri" text,
|
||||
"username" text NOT NULL,
|
||||
"displayName" text NOT NULL,
|
||||
"password" text,
|
||||
"email" text,
|
||||
"note" text DEFAULT '' NOT NULL,
|
||||
"isAdmin" boolean DEFAULT false NOT NULL,
|
||||
"endpoints" jsonb,
|
||||
"source" jsonb NOT NULL,
|
||||
"avatar" text NOT NULL,
|
||||
"header" text NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"updatedAt" timestamp(3) NOT NULL,
|
||||
"isBot" boolean DEFAULT false NOT NULL,
|
||||
"isLocked" boolean DEFAULT false NOT NULL,
|
||||
"isDiscoverable" boolean DEFAULT false NOT NULL,
|
||||
"sanctions" text[],
|
||||
"publicKey" text NOT NULL,
|
||||
"privateKey" text,
|
||||
"instanceId" uuid,
|
||||
"disableAutomoderation" boolean DEFAULT false NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "OpenIdLoginFlow" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"codeVerifier" text NOT NULL,
|
||||
"applicationId" uuid,
|
||||
"issuerId" text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "Flag" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"flagType" text DEFAULT 'other' NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
"flaggeStatusId" uuid,
|
||||
"flaggedUserId" uuid
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "ModNote" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"notedStatusId" uuid,
|
||||
"notedUserId" uuid,
|
||||
"modId" uuid NOT NULL,
|
||||
"note" text NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "ModTag" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"taggedStatusId" uuid,
|
||||
"taggedUserId" uuid,
|
||||
"modId" uuid NOT NULL,
|
||||
"tag" text NOT NULL,
|
||||
"createdAt" timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "LysandObject_remote_id_key" ON "LysandObject" ("remote_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "LysandObject_uri_key" ON "LysandObject" ("uri");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Application_client_id_key" ON "Application" ("client_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "_EmojiToUser_AB_unique" ON "_EmojiToUser" ("A","B");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "_EmojiToUser_B_index" ON "_EmojiToUser" ("B");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "_EmojiToStatus_AB_unique" ON "_EmojiToStatus" ("A","B");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "_EmojiToStatus_B_index" ON "_EmojiToStatus" ("B");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "_StatusToUser_AB_unique" ON "_StatusToUser" ("A","B");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "_StatusToUser_B_index" ON "_StatusToUser" ("B");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "_UserPinnedNotes_AB_unique" ON "_UserPinnedNotes" ("A","B");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "_UserPinnedNotes_B_index" ON "_UserPinnedNotes" ("B");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Status_uri_key" ON "Status" ("uri");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "User_uri_key" ON "User" ("uri");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "User_username_key" ON "User" ("username");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "User_email_key" ON "User" ("email");--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Emoji" ADD CONSTRAINT "Emoji_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "public"."Instance"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Like" ADD CONSTRAINT "Like_likerId_fkey" FOREIGN KEY ("likerId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Like" ADD CONSTRAINT "Like_likedId_fkey" FOREIGN KEY ("likedId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "LysandObject" ADD CONSTRAINT "LysandObject_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "public"."LysandObject"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Relationship" ADD CONSTRAINT "Relationship_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Relationship" ADD CONSTRAINT "Relationship_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Token" ADD CONSTRAINT "Token_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Token" ADD CONSTRAINT "Token_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "public"."Application"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_EmojiToUser" ADD CONSTRAINT "_EmojiToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Emoji"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_EmojiToUser" ADD CONSTRAINT "_EmojiToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_EmojiToStatus" ADD CONSTRAINT "_EmojiToStatus_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Emoji"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_EmojiToStatus" ADD CONSTRAINT "_EmojiToStatus_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_StatusToUser" ADD CONSTRAINT "_StatusToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_StatusToUser" ADD CONSTRAINT "_StatusToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_UserPinnedNotes" ADD CONSTRAINT "_UserPinnedNotes_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_UserPinnedNotes" ADD CONSTRAINT "_UserPinnedNotes_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Attachment" ADD CONSTRAINT "Attachment_statusId_fkey" FOREIGN KEY ("statusId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_notifiedId_fkey" FOREIGN KEY ("notifiedId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_statusId_fkey" FOREIGN KEY ("statusId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Status" ADD CONSTRAINT "Status_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Status" ADD CONSTRAINT "Status_reblogId_fkey" FOREIGN KEY ("reblogId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Status" ADD CONSTRAINT "Status_inReplyToPostId_fkey" FOREIGN KEY ("inReplyToPostId") REFERENCES "public"."Status"("id") ON DELETE set null ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Status" ADD CONSTRAINT "Status_quotingPostId_fkey" FOREIGN KEY ("quotingPostId") REFERENCES "public"."Status"("id") ON DELETE set null ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Status" ADD CONSTRAINT "Status_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "public"."Instance"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Status" ADD CONSTRAINT "Status_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "public"."Application"("id") ON DELETE set null ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "OpenIdAccount" ADD CONSTRAINT "OpenIdAccount_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE set null ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "User" ADD CONSTRAINT "User_instanceId_fkey" FOREIGN KEY ("instanceId") REFERENCES "public"."Instance"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "OpenIdLoginFlow" ADD CONSTRAINT "OpenIdLoginFlow_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "public"."Application"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Flag" ADD CONSTRAINT "Flag_flaggeStatusId_fkey" FOREIGN KEY ("flaggeStatusId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Flag" ADD CONSTRAINT "Flag_flaggedUserId_fkey" FOREIGN KEY ("flaggedUserId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_notedStatusId_fkey" FOREIGN KEY ("notedStatusId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_notedUserId_fkey" FOREIGN KEY ("notedUserId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_modId_fkey" FOREIGN KEY ("modId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModTag" ADD CONSTRAINT "ModTag_taggedStatusId_fkey" FOREIGN KEY ("taggedStatusId") REFERENCES "public"."Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModTag" ADD CONSTRAINT "ModTag_taggedUserId_fkey" FOREIGN KEY ("taggedUserId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModTag" ADD CONSTRAINT "ModTag_modId_fkey" FOREIGN KEY ("modId") REFERENCES "public"."User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
292
packages/kit/tables/migrations/0001_salty_night_thrasher.sql
Normal file
292
packages/kit/tables/migrations/0001_salty_night_thrasher.sql
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
DROP TABLE "_prisma_migrations";--> statement-breakpoint
|
||||
ALTER TABLE "Emoji" DROP CONSTRAINT "Emoji_instanceId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Like" DROP CONSTRAINT "Like_likerId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Like" DROP CONSTRAINT "Like_likedId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "LysandObject" DROP CONSTRAINT "LysandObject_authorId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Relationship" DROP CONSTRAINT "Relationship_ownerId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Relationship" DROP CONSTRAINT "Relationship_subjectId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Token" DROP CONSTRAINT "Token_userId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Token" DROP CONSTRAINT "Token_applicationId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "_EmojiToUser" DROP CONSTRAINT "_EmojiToUser_A_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "_EmojiToUser" DROP CONSTRAINT "_EmojiToUser_B_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "_EmojiToStatus" DROP CONSTRAINT "_EmojiToStatus_A_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "_EmojiToStatus" DROP CONSTRAINT "_EmojiToStatus_B_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "_StatusToUser" DROP CONSTRAINT "_StatusToUser_A_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "_StatusToUser" DROP CONSTRAINT "_StatusToUser_B_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "_UserPinnedNotes" DROP CONSTRAINT "_UserPinnedNotes_A_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "_UserPinnedNotes" DROP CONSTRAINT "_UserPinnedNotes_B_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Attachment" DROP CONSTRAINT "Attachment_statusId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Notification" DROP CONSTRAINT "Notification_notifiedId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Notification" DROP CONSTRAINT "Notification_accountId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Notification" DROP CONSTRAINT "Notification_statusId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Status" DROP CONSTRAINT "Status_authorId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Status" DROP CONSTRAINT "Status_instanceId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Status" DROP CONSTRAINT "Status_applicationId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Status" DROP CONSTRAINT "Status_reblogId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "OpenIdAccount" DROP CONSTRAINT "OpenIdAccount_userId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "User" DROP CONSTRAINT "User_instanceId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "OpenIdLoginFlow" DROP CONSTRAINT "OpenIdLoginFlow_applicationId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Flag" DROP CONSTRAINT "Flag_flaggeStatusId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Flag" DROP CONSTRAINT "Flag_flaggedUserId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "ModNote" DROP CONSTRAINT "ModNote_notedStatusId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "ModNote" DROP CONSTRAINT "ModNote_notedUserId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "ModNote" DROP CONSTRAINT "ModNote_modId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "ModTag" DROP CONSTRAINT "ModTag_taggedStatusId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "ModTag" DROP CONSTRAINT "ModTag_taggedUserId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "ModTag" DROP CONSTRAINT "ModTag_modId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Like" ALTER COLUMN "createdAt" SET DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "LysandObject" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "Relationship" ALTER COLUMN "createdAt" SET DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "Token" ALTER COLUMN "created_at" SET DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "Notification" ALTER COLUMN "createdAt" SET DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "Status" ALTER COLUMN "createdAt" SET DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "User" ALTER COLUMN "createdAt" SET DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "User" ALTER COLUMN "updatedAt" SET DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "User" ALTER COLUMN "sanctions" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Flag" ALTER COLUMN "createdAt" SET DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "ModNote" ALTER COLUMN "createdAt" SET DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "ModTag" ALTER COLUMN "createdAt" SET DEFAULT now();--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Emoji" ADD CONSTRAINT "Emoji_instanceId_Instance_id_fk" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Like" ADD CONSTRAINT "Like_likerId_User_id_fk" FOREIGN KEY ("likerId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Like" ADD CONSTRAINT "Like_likedId_Status_id_fk" FOREIGN KEY ("likedId") REFERENCES "Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "LysandObject" ADD CONSTRAINT "LysandObject_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "LysandObject"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Relationship" ADD CONSTRAINT "Relationship_ownerId_User_id_fk" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Relationship" ADD CONSTRAINT "Relationship_subjectId_User_id_fk" FOREIGN KEY ("subjectId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Token" ADD CONSTRAINT "Token_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Token" ADD CONSTRAINT "Token_applicationId_Application_id_fk" FOREIGN KEY ("applicationId") REFERENCES "Application"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_EmojiToUser" ADD CONSTRAINT "_EmojiToUser_A_Emoji_id_fk" FOREIGN KEY ("A") REFERENCES "Emoji"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_EmojiToUser" ADD CONSTRAINT "_EmojiToUser_B_User_id_fk" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_EmojiToStatus" ADD CONSTRAINT "_EmojiToStatus_A_Emoji_id_fk" FOREIGN KEY ("A") REFERENCES "Emoji"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_EmojiToStatus" ADD CONSTRAINT "_EmojiToStatus_B_Status_id_fk" FOREIGN KEY ("B") REFERENCES "Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_StatusToUser" ADD CONSTRAINT "_StatusToUser_A_Status_id_fk" FOREIGN KEY ("A") REFERENCES "Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_StatusToUser" ADD CONSTRAINT "_StatusToUser_B_User_id_fk" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_UserPinnedNotes" ADD CONSTRAINT "_UserPinnedNotes_A_Status_id_fk" FOREIGN KEY ("A") REFERENCES "Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_UserPinnedNotes" ADD CONSTRAINT "_UserPinnedNotes_B_User_id_fk" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Attachment" ADD CONSTRAINT "Attachment_statusId_Status_id_fk" FOREIGN KEY ("statusId") REFERENCES "Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_notifiedId_User_id_fk" FOREIGN KEY ("notifiedId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_accountId_User_id_fk" FOREIGN KEY ("accountId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_statusId_Status_id_fk" FOREIGN KEY ("statusId") REFERENCES "Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Status" ADD CONSTRAINT "Status_authorId_User_id_fk" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Status" ADD CONSTRAINT "Status_instanceId_Instance_id_fk" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Status" ADD CONSTRAINT "Status_applicationId_Application_id_fk" FOREIGN KEY ("applicationId") REFERENCES "Application"("id") ON DELETE set null ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Status" ADD CONSTRAINT "Status_reblogId_fkey" FOREIGN KEY ("reblogId") REFERENCES "Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "OpenIdAccount" ADD CONSTRAINT "OpenIdAccount_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE set null ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "User" ADD CONSTRAINT "User_instanceId_Instance_id_fk" FOREIGN KEY ("instanceId") REFERENCES "Instance"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "OpenIdLoginFlow" ADD CONSTRAINT "OpenIdLoginFlow_applicationId_Application_id_fk" FOREIGN KEY ("applicationId") REFERENCES "Application"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Flag" ADD CONSTRAINT "Flag_flaggeStatusId_Status_id_fk" FOREIGN KEY ("flaggeStatusId") REFERENCES "Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Flag" ADD CONSTRAINT "Flag_flaggedUserId_User_id_fk" FOREIGN KEY ("flaggedUserId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_notedStatusId_Status_id_fk" FOREIGN KEY ("notedStatusId") REFERENCES "Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_notedUserId_User_id_fk" FOREIGN KEY ("notedUserId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModNote" ADD CONSTRAINT "ModNote_modId_User_id_fk" FOREIGN KEY ("modId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModTag" ADD CONSTRAINT "ModTag_taggedStatusId_Status_id_fk" FOREIGN KEY ("taggedStatusId") REFERENCES "Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModTag" ADD CONSTRAINT "ModTag_taggedUserId_User_id_fk" FOREIGN KEY ("taggedUserId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModTag" ADD CONSTRAINT "ModTag_modId_User_id_fk" FOREIGN KEY ("modId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
20
packages/kit/tables/migrations/0002_stiff_ares.sql
Normal file
20
packages/kit/tables/migrations/0002_stiff_ares.sql
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
ALTER TABLE "_StatusToUser" RENAME TO "StatusToMentions";--> statement-breakpoint
|
||||
ALTER TABLE "StatusToMentions" DROP CONSTRAINT "_StatusToUser_A_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "StatusToMentions" DROP CONSTRAINT "_StatusToUser_B_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "_StatusToUser_AB_unique";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "_StatusToUser_B_index";--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "StatusToMentions_A_B_index" ON "StatusToMentions" ("A","B");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "StatusToMentions_B_index" ON "StatusToMentions" ("B");--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "StatusToMentions" ADD CONSTRAINT "StatusToMentions_A_Status_id_fk" FOREIGN KEY ("A") REFERENCES "Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "StatusToMentions" ADD CONSTRAINT "StatusToMentions_B_User_id_fk" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
21
packages/kit/tables/migrations/0003_spicy_arachne.sql
Normal file
21
packages/kit/tables/migrations/0003_spicy_arachne.sql
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
ALTER TABLE "StatusToMentions" RENAME COLUMN "A" TO "statusId";--> statement-breakpoint
|
||||
ALTER TABLE "StatusToMentions" RENAME COLUMN "B" TO "userId";--> statement-breakpoint
|
||||
ALTER TABLE "StatusToMentions" DROP CONSTRAINT "StatusToMentions_A_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "StatusToMentions" DROP CONSTRAINT "StatusToMentions_B_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "StatusToMentions_A_B_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "StatusToMentions_B_index";--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "StatusToMentions_statusId_userId_index" ON "StatusToMentions" ("statusId","userId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "StatusToMentions_userId_index" ON "StatusToMentions" ("userId");--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "StatusToMentions" ADD CONSTRAINT "StatusToMentions_statusId_Status_id_fk" FOREIGN KEY ("statusId") REFERENCES "Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "StatusToMentions" ADD CONSTRAINT "StatusToMentions_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
57
packages/kit/tables/migrations/0004_burly_lockjaw.sql
Normal file
57
packages/kit/tables/migrations/0004_burly_lockjaw.sql
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
ALTER TABLE "_EmojiToStatus" RENAME TO "EmojiToStatus";--> statement-breakpoint
|
||||
ALTER TABLE "_UserPinnedNotes" RENAME TO "UserToPinnedNotes";--> statement-breakpoint
|
||||
ALTER TABLE "EmojiToStatus" RENAME COLUMN "A" TO "emojiId";--> statement-breakpoint
|
||||
ALTER TABLE "EmojiToStatus" RENAME COLUMN "B" TO "statusId";--> statement-breakpoint
|
||||
ALTER TABLE "UserToPinnedNotes" RENAME COLUMN "A" TO "userId";--> statement-breakpoint
|
||||
ALTER TABLE "UserToPinnedNotes" RENAME COLUMN "B" TO "statusId";--> statement-breakpoint
|
||||
ALTER TABLE "LysandObject" DROP CONSTRAINT "LysandObject_authorId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "EmojiToStatus" DROP CONSTRAINT "_EmojiToStatus_A_Emoji_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "EmojiToStatus" DROP CONSTRAINT "_EmojiToStatus_B_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "UserToPinnedNotes" DROP CONSTRAINT "_UserPinnedNotes_A_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "UserToPinnedNotes" DROP CONSTRAINT "_UserPinnedNotes_B_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "LysandObject_remote_id_key";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "LysandObject_uri_key";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "_EmojiToStatus_AB_unique";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "_EmojiToStatus_B_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "_UserPinnedNotes_AB_unique";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "_UserPinnedNotes_B_index";--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "LysandObject_remote_id_index" ON "LysandObject" ("remote_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "LysandObject_uri_index" ON "LysandObject" ("uri");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "EmojiToStatus_emojiId_statusId_index" ON "EmojiToStatus" ("emojiId","statusId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "EmojiToStatus_statusId_index" ON "EmojiToStatus" ("statusId");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "UserToPinnedNotes_userId_statusId_index" ON "UserToPinnedNotes" ("userId","statusId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "UserToPinnedNotes_statusId_index" ON "UserToPinnedNotes" ("statusId");--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "LysandObject" ADD CONSTRAINT "LysandObject_authorId_LysandObject_id_fk" FOREIGN KEY ("authorId") REFERENCES "LysandObject"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "EmojiToStatus" ADD CONSTRAINT "EmojiToStatus_emojiId_Emoji_id_fk" FOREIGN KEY ("emojiId") REFERENCES "Emoji"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "EmojiToStatus" ADD CONSTRAINT "EmojiToStatus_statusId_Status_id_fk" FOREIGN KEY ("statusId") REFERENCES "Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "UserToPinnedNotes" ADD CONSTRAINT "UserToPinnedNotes_userId_Status_id_fk" FOREIGN KEY ("userId") REFERENCES "Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "UserToPinnedNotes" ADD CONSTRAINT "UserToPinnedNotes_statusId_User_id_fk" FOREIGN KEY ("statusId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
40
packages/kit/tables/migrations/0005_sleepy_puma.sql
Normal file
40
packages/kit/tables/migrations/0005_sleepy_puma.sql
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
ALTER TABLE "Instance" RENAME COLUMN "disableAutomoderation" TO "disable_automoderation";--> statement-breakpoint
|
||||
ALTER TABLE "Relationship" RENAME COLUMN "showingReblogs" TO "showing_reblogs";--> statement-breakpoint
|
||||
ALTER TABLE "Relationship" RENAME COLUMN "followedBy" TO "followed_by";--> statement-breakpoint
|
||||
ALTER TABLE "Relationship" RENAME COLUMN "blockedBy" TO "blocked_by";--> statement-breakpoint
|
||||
ALTER TABLE "Relationship" RENAME COLUMN "mutingNotifications" TO "muting_notifications";--> statement-breakpoint
|
||||
ALTER TABLE "Relationship" RENAME COLUMN "domainBlocking" TO "domain_blocking";--> statement-breakpoint
|
||||
ALTER TABLE "Relationship" RENAME COLUMN "createdAt" TO "created_at";--> statement-breakpoint
|
||||
ALTER TABLE "Relationship" RENAME COLUMN "updatedAt" TO "updated_at";--> statement-breakpoint
|
||||
ALTER TABLE "Status" RENAME COLUMN "contentType" TO "content_type";--> statement-breakpoint
|
||||
ALTER TABLE "Status" RENAME COLUMN "spoilerText" TO "spoiler_text";--> statement-breakpoint
|
||||
ALTER TABLE "Status" RENAME COLUMN "contentSource" TO "content_source";--> statement-breakpoint
|
||||
ALTER TABLE "Status" DROP CONSTRAINT "Status_reblogId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Status" DROP CONSTRAINT "Status_inReplyToPostId_fkey";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Status" DROP CONSTRAINT "Status_quotingPostId_fkey";
|
||||
--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "Application_client_id_key";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "Status_uri_key";--> statement-breakpoint
|
||||
ALTER TABLE "Relationship" ALTER COLUMN "updated_at" SET DEFAULT now();--> statement-breakpoint
|
||||
ALTER TABLE "Status" ALTER COLUMN "updatedAt" SET DEFAULT now();--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Application_client_id_index" ON "Application" ("client_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Status_uri_index" ON "Status" ("uri");--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Status" ADD CONSTRAINT "Status_reblogId_Status_id_fk" FOREIGN KEY ("reblogId") REFERENCES "Status"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Status" ADD CONSTRAINT "Status_inReplyToPostId_Status_id_fk" FOREIGN KEY ("inReplyToPostId") REFERENCES "Status"("id") ON DELETE set null ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Status" ADD CONSTRAINT "Status_quotingPostId_Status_id_fk" FOREIGN KEY ("quotingPostId") REFERENCES "Status"("id") ON DELETE set null ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
46
packages/kit/tables/migrations/0006_messy_network.sql
Normal file
46
packages/kit/tables/migrations/0006_messy_network.sql
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
ALTER TABLE "_EmojiToUser" RENAME TO "EmojiToUser";--> statement-breakpoint
|
||||
ALTER TABLE "Flag" RENAME COLUMN "flagType" TO "flag_type";--> statement-breakpoint
|
||||
ALTER TABLE "Flag" RENAME COLUMN "createdAt" TO "created_at";--> statement-breakpoint
|
||||
ALTER TABLE "ModNote" RENAME COLUMN "createdAt" TO "created_at";--> statement-breakpoint
|
||||
ALTER TABLE "ModTag" RENAME COLUMN "createdAt" TO "created_at";--> statement-breakpoint
|
||||
ALTER TABLE "OpenIdAccount" RENAME COLUMN "serverId" TO "server_id";--> statement-breakpoint
|
||||
ALTER TABLE "OpenIdAccount" RENAME COLUMN "issuerId" TO "issuer_id";--> statement-breakpoint
|
||||
ALTER TABLE "OpenIdLoginFlow" RENAME COLUMN "codeVerifier" TO "code_verifier";--> statement-breakpoint
|
||||
ALTER TABLE "OpenIdLoginFlow" RENAME COLUMN "issuerId" TO "issuer_id";--> statement-breakpoint
|
||||
ALTER TABLE "User" RENAME COLUMN "displayName" TO "display_name";--> statement-breakpoint
|
||||
ALTER TABLE "User" RENAME COLUMN "isAdmin" TO "is_admin";--> statement-breakpoint
|
||||
ALTER TABLE "User" RENAME COLUMN "createdAt" TO "created_at";--> statement-breakpoint
|
||||
ALTER TABLE "User" RENAME COLUMN "updatedAt" TO "updated_at";--> statement-breakpoint
|
||||
ALTER TABLE "User" RENAME COLUMN "isBot" TO "is_bot";--> statement-breakpoint
|
||||
ALTER TABLE "User" RENAME COLUMN "isLocked" TO "is_locked";--> statement-breakpoint
|
||||
ALTER TABLE "User" RENAME COLUMN "isDiscoverable" TO "is_discoverable";--> statement-breakpoint
|
||||
ALTER TABLE "User" RENAME COLUMN "publicKey" TO "public_key";--> statement-breakpoint
|
||||
ALTER TABLE "User" RENAME COLUMN "privateKey" TO "private_key";--> statement-breakpoint
|
||||
ALTER TABLE "User" RENAME COLUMN "disableAutomoderation" TO "disable_automoderation";--> statement-breakpoint
|
||||
ALTER TABLE "EmojiToUser" RENAME COLUMN "A" TO "emojiId";--> statement-breakpoint
|
||||
ALTER TABLE "EmojiToUser" RENAME COLUMN "B" TO "userId";--> statement-breakpoint
|
||||
ALTER TABLE "EmojiToUser" DROP CONSTRAINT "_EmojiToUser_A_Emoji_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "EmojiToUser" DROP CONSTRAINT "_EmojiToUser_B_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "User_uri_key";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "User_username_key";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "User_email_key";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "_EmojiToUser_AB_unique";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "_EmojiToUser_B_index";--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "User_uri_index" ON "User" ("uri");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "User_username_index" ON "User" ("username");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "User_email_index" ON "User" ("email");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "EmojiToUser_emojiId_userId_index" ON "EmojiToUser" ("emojiId","userId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "EmojiToUser_userId_index" ON "EmojiToUser" ("userId");--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "EmojiToUser" ADD CONSTRAINT "EmojiToUser_emojiId_Emoji_id_fk" FOREIGN KEY ("emojiId") REFERENCES "Emoji"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "EmojiToUser" ADD CONSTRAINT "EmojiToUser_userId_User_id_fk" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
3
packages/kit/tables/migrations/0007_naive_sleeper.sql
Normal file
3
packages/kit/tables/migrations/0007_naive_sleeper.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE "Status" DROP CONSTRAINT "Status_instanceId_Instance_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Status" DROP COLUMN IF EXISTS "instanceId";
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "Notification" ADD COLUMN "dismissed" boolean DEFAULT false NOT NULL;
|
||||
331
packages/kit/tables/migrations/0009_easy_slyde.sql
Normal file
331
packages/kit/tables/migrations/0009_easy_slyde.sql
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
ALTER TABLE "Application" RENAME TO "Applications";--> statement-breakpoint
|
||||
ALTER TABLE "Attachment" RENAME TO "Attachments";--> statement-breakpoint
|
||||
ALTER TABLE "Emoji" RENAME TO "Emojis";--> statement-breakpoint
|
||||
ALTER TABLE "EmojiToStatus" RENAME TO "EmojiToNote";--> statement-breakpoint
|
||||
ALTER TABLE "Flag" RENAME TO "Flags";--> statement-breakpoint
|
||||
ALTER TABLE "Instance" RENAME TO "Instances";--> statement-breakpoint
|
||||
ALTER TABLE "Like" RENAME TO "Likes";--> statement-breakpoint
|
||||
ALTER TABLE "ModNote" RENAME TO "ModNotes";--> statement-breakpoint
|
||||
ALTER TABLE "ModTag" RENAME TO "ModTags";--> statement-breakpoint
|
||||
ALTER TABLE "Notification" RENAME TO "Notifications";--> statement-breakpoint
|
||||
ALTER TABLE "OpenIdAccount" RENAME TO "OpenIdAccounts";--> statement-breakpoint
|
||||
ALTER TABLE "OpenIdLoginFlow" RENAME TO "OpenIdLoginFlows";--> statement-breakpoint
|
||||
ALTER TABLE "Relationship" RENAME TO "Relationships";--> statement-breakpoint
|
||||
ALTER TABLE "Status" RENAME TO "Notes";--> statement-breakpoint
|
||||
ALTER TABLE "StatusToMentions" RENAME TO "NoteToMentions";--> statement-breakpoint
|
||||
ALTER TABLE "Token" RENAME TO "Tokens";--> statement-breakpoint
|
||||
ALTER TABLE "User" RENAME TO "Users";--> statement-breakpoint
|
||||
ALTER TABLE "UserToPinnedNotes" RENAME COLUMN "statusId" TO "noteId";--> statement-breakpoint
|
||||
ALTER TABLE "Attachments" RENAME COLUMN "statusId" TO "noteId";--> statement-breakpoint
|
||||
ALTER TABLE "EmojiToNote" RENAME COLUMN "statusId" TO "noteId";--> statement-breakpoint
|
||||
ALTER TABLE "Flags" RENAME COLUMN "flaggeStatusId" TO "noteId";--> statement-breakpoint
|
||||
ALTER TABLE "Flags" RENAME COLUMN "flaggedUserId" TO "userId";--> statement-breakpoint
|
||||
ALTER TABLE "ModNotes" RENAME COLUMN "notedStatusId" TO "noteId";--> statement-breakpoint
|
||||
ALTER TABLE "ModNotes" RENAME COLUMN "notedUserId" TO "userId";--> statement-breakpoint
|
||||
ALTER TABLE "ModTags" RENAME COLUMN "taggedStatusId" TO "statusId";--> statement-breakpoint
|
||||
ALTER TABLE "ModTags" RENAME COLUMN "taggedUserId" TO "userId";--> statement-breakpoint
|
||||
ALTER TABLE "Notifications" RENAME COLUMN "statusId" TO "noteId";--> statement-breakpoint
|
||||
ALTER TABLE "Notes" RENAME COLUMN "inReplyToPostId" TO "replyId";--> statement-breakpoint
|
||||
ALTER TABLE "Notes" RENAME COLUMN "quotingPostId" TO "quoteId";--> statement-breakpoint
|
||||
ALTER TABLE "NoteToMentions" RENAME COLUMN "statusId" TO "noteId";--> statement-breakpoint
|
||||
ALTER TABLE "EmojiToUser" DROP CONSTRAINT "EmojiToUser_emojiId_Emoji_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "EmojiToUser" DROP CONSTRAINT "EmojiToUser_userId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "UserToPinnedNotes" DROP CONSTRAINT "UserToPinnedNotes_userId_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "UserToPinnedNotes" DROP CONSTRAINT "UserToPinnedNotes_statusId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Attachments" DROP CONSTRAINT "Attachment_statusId_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Emojis" DROP CONSTRAINT "Emoji_instanceId_Instance_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "EmojiToNote" DROP CONSTRAINT "EmojiToStatus_emojiId_Emoji_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "EmojiToNote" DROP CONSTRAINT "EmojiToStatus_statusId_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Flags" DROP CONSTRAINT "Flag_flaggeStatusId_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Flags" DROP CONSTRAINT "Flag_flaggedUserId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Likes" DROP CONSTRAINT "Like_likerId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Likes" DROP CONSTRAINT "Like_likedId_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "ModNotes" DROP CONSTRAINT "ModNote_notedStatusId_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "ModNotes" DROP CONSTRAINT "ModNote_notedUserId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "ModNotes" DROP CONSTRAINT "ModNote_modId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "ModTags" DROP CONSTRAINT "ModTag_taggedStatusId_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "ModTags" DROP CONSTRAINT "ModTag_taggedUserId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "ModTags" DROP CONSTRAINT "ModTag_modId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Notifications" DROP CONSTRAINT "Notification_notifiedId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Notifications" DROP CONSTRAINT "Notification_accountId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Notifications" DROP CONSTRAINT "Notification_statusId_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "OpenIdAccounts" DROP CONSTRAINT "OpenIdAccount_userId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "OpenIdLoginFlows" DROP CONSTRAINT "OpenIdLoginFlow_applicationId_Application_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Relationships" DROP CONSTRAINT "Relationship_ownerId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Relationships" DROP CONSTRAINT "Relationship_subjectId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Notes" DROP CONSTRAINT "Status_authorId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Notes" DROP CONSTRAINT "Status_applicationId_Application_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Notes" DROP CONSTRAINT "Status_reblogId_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Notes" DROP CONSTRAINT "Status_inReplyToPostId_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Notes" DROP CONSTRAINT "Status_quotingPostId_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "NoteToMentions" DROP CONSTRAINT "StatusToMentions_statusId_Status_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "NoteToMentions" DROP CONSTRAINT "StatusToMentions_userId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Tokens" DROP CONSTRAINT "Token_userId_User_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Tokens" DROP CONSTRAINT "Token_applicationId_Application_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Users" DROP CONSTRAINT "User_instanceId_Instance_id_fk";
|
||||
--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "UserToPinnedNotes_userId_statusId_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "UserToPinnedNotes_statusId_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "Application_client_id_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "EmojiToStatus_emojiId_statusId_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "EmojiToStatus_statusId_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "Status_uri_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "StatusToMentions_statusId_userId_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "StatusToMentions_userId_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "User_uri_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "User_username_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "User_email_index";--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "UserToPinnedNotes_userId_noteId_index" ON "UserToPinnedNotes" ("userId","noteId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "UserToPinnedNotes_noteId_index" ON "UserToPinnedNotes" ("noteId");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Applications_client_id_index" ON "Applications" ("client_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "EmojiToNote_emojiId_noteId_index" ON "EmojiToNote" ("emojiId","noteId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "EmojiToNote_noteId_index" ON "EmojiToNote" ("noteId");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Notes_uri_index" ON "Notes" ("uri");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "NoteToMentions_noteId_userId_index" ON "NoteToMentions" ("noteId","userId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "NoteToMentions_userId_index" ON "NoteToMentions" ("userId");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Users_uri_index" ON "Users" ("uri");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Users_username_index" ON "Users" ("username");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Users_email_index" ON "Users" ("email");--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "EmojiToUser" ADD CONSTRAINT "EmojiToUser_emojiId_Emojis_id_fk" FOREIGN KEY ("emojiId") REFERENCES "Emojis"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "EmojiToUser" ADD CONSTRAINT "EmojiToUser_userId_Users_id_fk" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "UserToPinnedNotes" ADD CONSTRAINT "UserToPinnedNotes_userId_Notes_id_fk" FOREIGN KEY ("userId") REFERENCES "Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "UserToPinnedNotes" ADD CONSTRAINT "UserToPinnedNotes_noteId_Users_id_fk" FOREIGN KEY ("noteId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Attachments" ADD CONSTRAINT "Attachments_noteId_Notes_id_fk" FOREIGN KEY ("noteId") REFERENCES "Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Emojis" ADD CONSTRAINT "Emojis_instanceId_Instances_id_fk" FOREIGN KEY ("instanceId") REFERENCES "Instances"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "EmojiToNote" ADD CONSTRAINT "EmojiToNote_emojiId_Emojis_id_fk" FOREIGN KEY ("emojiId") REFERENCES "Emojis"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "EmojiToNote" ADD CONSTRAINT "EmojiToNote_noteId_Notes_id_fk" FOREIGN KEY ("noteId") REFERENCES "Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Flags" ADD CONSTRAINT "Flags_noteId_Notes_id_fk" FOREIGN KEY ("noteId") REFERENCES "Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Flags" ADD CONSTRAINT "Flags_userId_Users_id_fk" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Likes" ADD CONSTRAINT "Likes_likerId_Users_id_fk" FOREIGN KEY ("likerId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Likes" ADD CONSTRAINT "Likes_likedId_Notes_id_fk" FOREIGN KEY ("likedId") REFERENCES "Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModNotes" ADD CONSTRAINT "ModNotes_noteId_Notes_id_fk" FOREIGN KEY ("noteId") REFERENCES "Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModNotes" ADD CONSTRAINT "ModNotes_userId_Users_id_fk" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModNotes" ADD CONSTRAINT "ModNotes_modId_Users_id_fk" FOREIGN KEY ("modId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModTags" ADD CONSTRAINT "ModTags_statusId_Notes_id_fk" FOREIGN KEY ("statusId") REFERENCES "Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModTags" ADD CONSTRAINT "ModTags_userId_Users_id_fk" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModTags" ADD CONSTRAINT "ModTags_modId_Users_id_fk" FOREIGN KEY ("modId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Notifications" ADD CONSTRAINT "Notifications_notifiedId_Users_id_fk" FOREIGN KEY ("notifiedId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Notifications" ADD CONSTRAINT "Notifications_accountId_Users_id_fk" FOREIGN KEY ("accountId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Notifications" ADD CONSTRAINT "Notifications_noteId_Notes_id_fk" FOREIGN KEY ("noteId") REFERENCES "Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "OpenIdAccounts" ADD CONSTRAINT "OpenIdAccounts_userId_Users_id_fk" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE set null ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "OpenIdLoginFlows" ADD CONSTRAINT "OpenIdLoginFlows_applicationId_Applications_id_fk" FOREIGN KEY ("applicationId") REFERENCES "Applications"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Relationships" ADD CONSTRAINT "Relationships_ownerId_Users_id_fk" FOREIGN KEY ("ownerId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Relationships" ADD CONSTRAINT "Relationships_subjectId_Users_id_fk" FOREIGN KEY ("subjectId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Notes" ADD CONSTRAINT "Notes_authorId_Users_id_fk" FOREIGN KEY ("authorId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Notes" ADD CONSTRAINT "Notes_applicationId_Applications_id_fk" FOREIGN KEY ("applicationId") REFERENCES "Applications"("id") ON DELETE set null ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Notes" ADD CONSTRAINT "Notes_reblogId_Notes_id_fk" FOREIGN KEY ("reblogId") REFERENCES "Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Notes" ADD CONSTRAINT "Notes_replyId_Notes_id_fk" FOREIGN KEY ("replyId") REFERENCES "Notes"("id") ON DELETE set null ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Notes" ADD CONSTRAINT "Notes_quoteId_Notes_id_fk" FOREIGN KEY ("quoteId") REFERENCES "Notes"("id") ON DELETE set null ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "NoteToMentions" ADD CONSTRAINT "NoteToMentions_noteId_Notes_id_fk" FOREIGN KEY ("noteId") REFERENCES "Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "NoteToMentions" ADD CONSTRAINT "NoteToMentions_userId_Users_id_fk" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Tokens" ADD CONSTRAINT "Tokens_userId_Users_id_fk" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Tokens" ADD CONSTRAINT "Tokens_applicationId_Applications_id_fk" FOREIGN KEY ("applicationId") REFERENCES "Applications"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Users" ADD CONSTRAINT "Users_instanceId_Instances_id_fk" FOREIGN KEY ("instanceId") REFERENCES "Instances"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
ALTER TABLE "ModTags" RENAME COLUMN "statusId" TO "noteId";--> statement-breakpoint
|
||||
ALTER TABLE "ModTags" DROP CONSTRAINT "ModTags_statusId_Notes_id_fk";
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "ModTags" ADD CONSTRAINT "ModTags_noteId_Notes_id_fk" FOREIGN KEY ("noteId") REFERENCES "Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
15
packages/kit/tables/migrations/0011_special_the_fury.sql
Normal file
15
packages/kit/tables/migrations/0011_special_the_fury.sql
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
ALTER TABLE "UserToPinnedNotes" DROP CONSTRAINT "UserToPinnedNotes_userId_Notes_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "UserToPinnedNotes" DROP CONSTRAINT "UserToPinnedNotes_noteId_Users_id_fk";
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "UserToPinnedNotes" ADD CONSTRAINT "UserToPinnedNotes_userId_Users_id_fk" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "UserToPinnedNotes" ADD CONSTRAINT "UserToPinnedNotes_noteId_Notes_id_fk" FOREIGN KEY ("noteId") REFERENCES "Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
19
packages/kit/tables/migrations/0012_certain_thor_girl.sql
Normal file
19
packages/kit/tables/migrations/0012_certain_thor_girl.sql
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
CREATE TABLE IF NOT EXISTS "Markers" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"noteId" uuid,
|
||||
"userId" uuid,
|
||||
"timeline" text NOT NULL,
|
||||
"created_at" timestamp(3) DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Markers" ADD CONSTRAINT "Markers_noteId_Notes_id_fk" FOREIGN KEY ("noteId") REFERENCES "Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Markers" ADD CONSTRAINT "Markers_userId_Users_id_fk" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
ALTER TABLE "Markers" ALTER COLUMN "userId" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Markers" ADD COLUMN "notificationId" uuid;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Markers" ADD CONSTRAINT "Markers_notificationId_Notifications_id_fk" FOREIGN KEY ("notificationId") REFERENCES "Notifications"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
28
packages/kit/tables/migrations/0014_wonderful_sandman.sql
Normal file
28
packages/kit/tables/migrations/0014_wonderful_sandman.sql
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
CREATE TABLE IF NOT EXISTS "FilterKeywords" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"filterId" uuid NOT NULL,
|
||||
"keyword" text NOT NULL,
|
||||
"whole_word" boolean NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "Filters" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"userId" uuid NOT NULL,
|
||||
"context" text[],
|
||||
"title" text NOT NULL,
|
||||
"filter_action" text NOT NULL,
|
||||
"expires_at" timestamp(3),
|
||||
"created_at" timestamp(3) DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "FilterKeywords" ADD CONSTRAINT "FilterKeywords_filterId_Filters_id_fk" FOREIGN KEY ("filterId") REFERENCES "Filters"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Filters" ADD CONSTRAINT "Filters_userId_Users_id_fk" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
1
packages/kit/tables/migrations/0015_easy_mojo.sql
Normal file
1
packages/kit/tables/migrations/0015_easy_mojo.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "Tokens" ALTER COLUMN "userId" SET NOT NULL;
|
||||
3
packages/kit/tables/migrations/0016_keen_mindworm.sql
Normal file
3
packages/kit/tables/migrations/0016_keen_mindworm.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE "Applications" RENAME COLUMN "redirect_uris" TO "redirect_uri";--> statement-breakpoint
|
||||
ALTER TABLE "Tokens" ADD COLUMN "client_id" text NOT NULL DEFAULT '';--> statement-breakpoint
|
||||
ALTER TABLE "Tokens" ADD COLUMN "redirect_uri" text NOT NULL DEFAULT '';
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "Tokens" ALTER COLUMN "code" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Tokens" ADD COLUMN "expires_at" timestamp(3);
|
||||
2
packages/kit/tables/migrations/0018_rapid_hairball.sql
Normal file
2
packages/kit/tables/migrations/0018_rapid_hairball.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "Tokens" ALTER COLUMN "client_id" SET DEFAULT '';
|
||||
ALTER TABLE "Tokens" ALTER COLUMN "redirect_uri" SET DEFAULT '';
|
||||
1
packages/kit/tables/migrations/0019_mushy_lorna_dane.sql
Normal file
1
packages/kit/tables/migrations/0019_mushy_lorna_dane.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "Tokens" ADD COLUMN "id_token" text;
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "Users" ADD COLUMN "fields" jsonb DEFAULT '[]' NOT NULL;
|
||||
10
packages/kit/tables/migrations/0021_wise_stephen_strange.sql
Normal file
10
packages/kit/tables/migrations/0021_wise_stephen_strange.sql
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
ALTER TABLE "Notes" DROP CONSTRAINT "Notes_replyId_Notes_id_fk";
|
||||
--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "Notes_uri_index";--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Notes" ADD CONSTRAINT "Notes_replyId_Notes_id_fk" FOREIGN KEY ("replyId") REFERENCES "Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Notes" ADD CONSTRAINT "Notes_uri_unique" UNIQUE("uri");
|
||||
7
packages/kit/tables/migrations/0022_curly_the_call.sql
Normal file
7
packages/kit/tables/migrations/0022_curly_the_call.sql
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
ALTER TABLE "Emojis" ADD COLUMN "ownerId" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "Emojis" ADD COLUMN "category" text;--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Emojis" ADD CONSTRAINT "Emojis_ownerId_Users_id_fk" FOREIGN KEY ("ownerId") REFERENCES "public"."Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
2
packages/kit/tables/migrations/0023_lazy_wolfsbane.sql
Normal file
2
packages/kit/tables/migrations/0023_lazy_wolfsbane.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "Users" ADD COLUMN "email_verification_token" text;--> statement-breakpoint
|
||||
ALTER TABLE "Users" ADD COLUMN "password_reset_token" text;
|
||||
55
packages/kit/tables/migrations/0024_lush_aaron_stack.sql
Normal file
55
packages/kit/tables/migrations/0024_lush_aaron_stack.sql
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
CREATE TABLE IF NOT EXISTS "RoleToUsers" (
|
||||
"roleId" uuid NOT NULL,
|
||||
"userId" uuid NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "Roles" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"permissions" text[] NOT NULL,
|
||||
"priority" integer DEFAULT 0 NOT NULL,
|
||||
"description" text,
|
||||
"visible" boolean DEFAULT false NOT NULL,
|
||||
"icon" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "Applications_client_id_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "EmojiToNote_emojiId_noteId_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "EmojiToNote_noteId_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "EmojiToUser_emojiId_userId_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "EmojiToUser_userId_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "LysandObject_remote_id_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "LysandObject_uri_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "NoteToMentions_noteId_userId_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "NoteToMentions_userId_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "UserToPinnedNotes_userId_noteId_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "UserToPinnedNotes_noteId_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "Users_uri_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "Users_username_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "Users_email_index";--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "RoleToUsers" ADD CONSTRAINT "RoleToUsers_roleId_Roles_id_fk" FOREIGN KEY ("roleId") REFERENCES "public"."Roles"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "RoleToUsers" ADD CONSTRAINT "RoleToUsers_userId_Users_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Applications_client_id_index" ON "Applications" USING btree ("client_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "EmojiToNote_emojiId_noteId_index" ON "EmojiToNote" USING btree ("emojiId","noteId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "EmojiToNote_noteId_index" ON "EmojiToNote" USING btree ("noteId");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "EmojiToUser_emojiId_userId_index" ON "EmojiToUser" USING btree ("emojiId","userId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "EmojiToUser_userId_index" ON "EmojiToUser" USING btree ("userId");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "LysandObject_remote_id_index" ON "LysandObject" USING btree ("remote_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "LysandObject_uri_index" ON "LysandObject" USING btree ("uri");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "NoteToMentions_noteId_userId_index" ON "NoteToMentions" USING btree ("noteId","userId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "NoteToMentions_userId_index" ON "NoteToMentions" USING btree ("userId");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "UserToPinnedNotes_userId_noteId_index" ON "UserToPinnedNotes" USING btree ("userId","noteId");--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "UserToPinnedNotes_noteId_index" ON "UserToPinnedNotes" USING btree ("noteId");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Users_uri_index" ON "Users" USING btree ("uri");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Users_username_index" ON "Users" USING btree ("username");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "Users_email_index" ON "Users" USING btree ("email");
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "Relationships" ADD COLUMN "requested_by" boolean DEFAULT false NOT NULL;
|
||||
6
packages/kit/tables/migrations/0026_neat_stranger.sql
Normal file
6
packages/kit/tables/migrations/0026_neat_stranger.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
CREATE TABLE IF NOT EXISTS "CaptchaChallenges" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"challenge" jsonb NOT NULL,
|
||||
"expires_at" timestamp(3) DEFAULT NOW() + INTERVAL '5 minutes',
|
||||
"created_at" timestamp(3) DEFAULT now() NOT NULL
|
||||
);
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "CaptchaChallenges" RENAME TO "Challenges";
|
||||
2
packages/kit/tables/migrations/0028_unique_fat_cobra.sql
Normal file
2
packages/kit/tables/migrations/0028_unique_fat_cobra.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "Challenges" ALTER COLUMN "expires_at" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Instances" ADD COLUMN "protocol" text DEFAULT 'lysand' NOT NULL;
|
||||
1
packages/kit/tables/migrations/0029_shiny_korvac.sql
Normal file
1
packages/kit/tables/migrations/0029_shiny_korvac.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "Instances" ALTER COLUMN "logo" DROP NOT NULL;
|
||||
2
packages/kit/tables/migrations/0030_curvy_vulture.sql
Normal file
2
packages/kit/tables/migrations/0030_curvy_vulture.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
DROP INDEX IF EXISTS "Users_username_index";--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "Users_username_index" ON "Users" USING btree ("username");
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE "Relationships" DROP COLUMN IF EXISTS "followed_by";--> statement-breakpoint
|
||||
ALTER TABLE "Relationships" DROP COLUMN IF EXISTS "blocked_by";--> statement-breakpoint
|
||||
ALTER TABLE "Relationships" DROP COLUMN IF EXISTS "requested_by";
|
||||
14
packages/kit/tables/migrations/0032_ambiguous_sue_storm.sql
Normal file
14
packages/kit/tables/migrations/0032_ambiguous_sue_storm.sql
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
ALTER TABLE "LysandObject" RENAME TO "VersiaObject";--> statement-breakpoint
|
||||
ALTER TABLE "VersiaObject" DROP CONSTRAINT "LysandObject_authorId_LysandObject_id_fk";
|
||||
--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "LysandObject_remote_id_index";--> statement-breakpoint
|
||||
DROP INDEX IF EXISTS "LysandObject_uri_index";--> statement-breakpoint
|
||||
ALTER TABLE "Instances" ALTER COLUMN "protocol" SET DEFAULT 'versia';--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "VersiaObject" ADD CONSTRAINT "VersiaObject_authorId_VersiaObject_id_fk" FOREIGN KEY ("authorId") REFERENCES "public"."VersiaObject"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "VersiaObject_remote_id_index" ON "VersiaObject" USING btree ("remote_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "VersiaObject_uri_index" ON "VersiaObject" USING btree ("uri");
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "Filters" ALTER COLUMN "context" SET NOT NULL;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "Likes" ADD COLUMN "uri" text;--> statement-breakpoint
|
||||
ALTER TABLE "Likes" ADD CONSTRAINT "Likes_uri_unique" UNIQUE("uri");
|
||||
1
packages/kit/tables/migrations/0035_pretty_whiplash.sql
Normal file
1
packages/kit/tables/migrations/0035_pretty_whiplash.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "Instances" ADD COLUMN "public_key" jsonb;
|
||||
2
packages/kit/tables/migrations/0036_cuddly_ironclad.sql
Normal file
2
packages/kit/tables/migrations/0036_cuddly_ironclad.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "Instances" ADD COLUMN "inbox" text;--> statement-breakpoint
|
||||
ALTER TABLE "Instances" ADD COLUMN "extensions" jsonb;
|
||||
24
packages/kit/tables/migrations/0037_condemned_talisman.sql
Normal file
24
packages/kit/tables/migrations/0037_condemned_talisman.sql
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
CREATE TABLE IF NOT EXISTS "Reaction" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"emojiId" uuid NOT NULL,
|
||||
"noteId" uuid NOT NULL,
|
||||
"authorId" uuid NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_emojiId_Emojis_id_fk" FOREIGN KEY ("emojiId") REFERENCES "public"."Emojis"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_noteId_Notes_id_fk" FOREIGN KEY ("noteId") REFERENCES "public"."Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_authorId_Users_id_fk" FOREIGN KEY ("authorId") REFERENCES "public"."Users"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
ALTER TABLE "Reaction" ALTER COLUMN "emojiId" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Reaction" ADD COLUMN "uri" text;--> statement-breakpoint
|
||||
ALTER TABLE "Reaction" ADD COLUMN "emoji_text" text;--> statement-breakpoint
|
||||
ALTER TABLE "Reaction" ADD COLUMN "created_at" timestamp(3) DEFAULT now() NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Reaction" ADD COLUMN "update_at" timestamp(3) DEFAULT now() NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Reaction" ADD CONSTRAINT "Reaction_uri_unique" UNIQUE("uri");
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
ALTER TABLE "VersiaObject" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint
|
||||
DROP TABLE "VersiaObject" CASCADE;--> statement-breakpoint
|
||||
ALTER TABLE "Likes" RENAME COLUMN "createdAt" TO "created_at";--> statement-breakpoint
|
||||
ALTER TABLE "Notes" RENAME COLUMN "createdAt" TO "created_at";--> statement-breakpoint
|
||||
ALTER TABLE "Notes" RENAME COLUMN "updatedAt" TO "updated_at";--> statement-breakpoint
|
||||
ALTER TABLE "Notifications" RENAME COLUMN "createdAt" TO "created_at";--> statement-breakpoint
|
||||
ALTER TABLE "Reaction" RENAME COLUMN "update_at" TO "updated_at";--> statement-breakpoint
|
||||
ALTER TABLE "OpenIdAccounts" DROP CONSTRAINT "OpenIdAccounts_userId_Users_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "OpenIdAccounts" ADD CONSTRAINT "OpenIdAccounts_userId_Users_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."Users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "Users" ADD CONSTRAINT "Users_uri_unique" UNIQUE("uri");
|
||||
14
packages/kit/tables/migrations/0040_good_nocturne.sql
Normal file
14
packages/kit/tables/migrations/0040_good_nocturne.sql
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
CREATE TABLE "PushSubscriptions" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"endpoint" text NOT NULL,
|
||||
"public_key" text NOT NULL,
|
||||
"auth_secret" text NOT NULL,
|
||||
"alerts" jsonb NOT NULL,
|
||||
"policy" text NOT NULL,
|
||||
"created_at" timestamp(3) DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp(3) DEFAULT now() NOT NULL,
|
||||
"tokenId" uuid NOT NULL,
|
||||
CONSTRAINT "PushSubscriptions_tokenId_unique" UNIQUE("tokenId")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "PushSubscriptions" ADD CONSTRAINT "PushSubscriptions_tokenId_Tokens_id_fk" FOREIGN KEY ("tokenId") REFERENCES "public"."Tokens"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
ALTER TABLE "Attachments" RENAME TO "Medias";--> statement-breakpoint
|
||||
ALTER TABLE "Medias" DROP CONSTRAINT "Attachments_noteId_Notes_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Medias" ADD CONSTRAINT "Medias_noteId_Notes_id_fk" FOREIGN KEY ("noteId") REFERENCES "public"."Notes"("id") ON DELETE cascade ON UPDATE cascade;
|
||||
26
packages/kit/tables/migrations/0042_swift_the_phantom.sql
Normal file
26
packages/kit/tables/migrations/0042_swift_the_phantom.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
CREATE TABLE "MediasToNote" (
|
||||
"mediaId" uuid NOT NULL,
|
||||
"noteId" uuid NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Medias" DROP CONSTRAINT "Medias_noteId_Notes_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "Medias" ADD COLUMN "content" jsonb NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Medias" ADD COLUMN "original_content" jsonb;--> statement-breakpoint
|
||||
ALTER TABLE "Medias" ADD COLUMN "thumbnail" jsonb;--> statement-breakpoint
|
||||
ALTER TABLE "MediasToNote" ADD CONSTRAINT "MediasToNote_mediaId_Medias_id_fk" FOREIGN KEY ("mediaId") REFERENCES "public"."Medias"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "MediasToNote" ADD CONSTRAINT "MediasToNote_noteId_Notes_id_fk" FOREIGN KEY ("noteId") REFERENCES "public"."Notes"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
CREATE INDEX "MediasToNote_mediaId_index" ON "MediasToNote" USING btree ("mediaId");--> statement-breakpoint
|
||||
CREATE INDEX "MediasToNote_noteId_index" ON "MediasToNote" USING btree ("noteId");--> statement-breakpoint
|
||||
ALTER TABLE "Medias" DROP COLUMN "url";--> statement-breakpoint
|
||||
ALTER TABLE "Medias" DROP COLUMN "remote_url";--> statement-breakpoint
|
||||
ALTER TABLE "Medias" DROP COLUMN "thumbnail_url";--> statement-breakpoint
|
||||
ALTER TABLE "Medias" DROP COLUMN "mime_type";--> statement-breakpoint
|
||||
ALTER TABLE "Medias" DROP COLUMN "description";--> statement-breakpoint
|
||||
ALTER TABLE "Medias" DROP COLUMN "sha256";--> statement-breakpoint
|
||||
ALTER TABLE "Medias" DROP COLUMN "fps";--> statement-breakpoint
|
||||
ALTER TABLE "Medias" DROP COLUMN "duration";--> statement-breakpoint
|
||||
ALTER TABLE "Medias" DROP COLUMN "width";--> statement-breakpoint
|
||||
ALTER TABLE "Medias" DROP COLUMN "height";--> statement-breakpoint
|
||||
ALTER TABLE "Medias" DROP COLUMN "size";--> statement-breakpoint
|
||||
ALTER TABLE "Medias" DROP COLUMN "noteId";
|
||||
5
packages/kit/tables/migrations/0043_mute_jigsaw.sql
Normal file
5
packages/kit/tables/migrations/0043_mute_jigsaw.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
ALTER TABLE "Emojis" ADD COLUMN "mediaId" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "Emojis" ADD CONSTRAINT "Emojis_mediaId_Medias_id_fk" FOREIGN KEY ("mediaId") REFERENCES "public"."Medias"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "Emojis" DROP COLUMN "url";--> statement-breakpoint
|
||||
ALTER TABLE "Emojis" DROP COLUMN "alt";--> statement-breakpoint
|
||||
ALTER TABLE "Emojis" DROP COLUMN "content_type";
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "Emojis" ALTER COLUMN "mediaId" SET NOT NULL;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
ALTER TABLE "Users" ADD COLUMN "avatarId" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "Users" ADD COLUMN "headerId" uuid;--> statement-breakpoint
|
||||
ALTER TABLE "Users" ADD CONSTRAINT "Users_avatarId_Medias_id_fk" FOREIGN KEY ("avatarId") REFERENCES "public"."Medias"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "Users" ADD CONSTRAINT "Users_headerId_Medias_id_fk" FOREIGN KEY ("headerId") REFERENCES "public"."Medias"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "Users" DROP COLUMN "avatar";--> statement-breakpoint
|
||||
ALTER TABLE "Users" DROP COLUMN "header";
|
||||
2
packages/kit/tables/migrations/0046_wooden_electro.sql
Normal file
2
packages/kit/tables/migrations/0046_wooden_electro.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "Users" ADD COLUMN "is_hiding_collections" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Users" ADD COLUMN "is_indexable" boolean DEFAULT false NOT NULL;
|
||||
1
packages/kit/tables/migrations/0047_black_vermin.sql
Normal file
1
packages/kit/tables/migrations/0047_black_vermin.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "Users" ALTER COLUMN "is_indexable" SET DEFAULT true;
|
||||
22
packages/kit/tables/migrations/0048_chilly_vector.sql
Normal file
22
packages/kit/tables/migrations/0048_chilly_vector.sql
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
ALTER TABLE "Applications" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Challenges" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Emojis" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "FilterKeywords" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Filters" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Flags" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Instances" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Likes" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Markers" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Medias" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "ModNotes" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "ModTags" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Notes" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Notifications" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "OpenIdAccounts" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "OpenIdLoginFlows" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "PushSubscriptions" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Reaction" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Relationships" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Roles" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Tokens" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "Users" ALTER COLUMN "id" DROP DEFAULT;
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE "Notes" ALTER COLUMN "sensitive" SET DEFAULT false;--> statement-breakpoint
|
||||
ALTER TABLE "Users" ALTER COLUMN "display_name" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Users" ALTER COLUMN "source" DROP NOT NULL;
|
||||
6
packages/kit/tables/migrations/0050_thick_lester.sql
Normal file
6
packages/kit/tables/migrations/0050_thick_lester.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
ALTER TABLE "Notes" ADD COLUMN "reblog_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Notes" ADD COLUMN "like_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Notes" ADD COLUMN "reply_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Users" ADD COLUMN "follower_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Users" ADD COLUMN "following_count" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "Users" ADD COLUMN "status_count" integer DEFAULT 0 NOT NULL;
|
||||
1843
packages/kit/tables/migrations/meta/0000_snapshot.json
Normal file
1843
packages/kit/tables/migrations/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
1747
packages/kit/tables/migrations/meta/0001_snapshot.json
Normal file
1747
packages/kit/tables/migrations/meta/0001_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
1747
packages/kit/tables/migrations/meta/0002_snapshot.json
Normal file
1747
packages/kit/tables/migrations/meta/0002_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue