mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 16:38:19 +01:00
Begin moving project to use Drizzle instead of prisma
This commit is contained in:
parent
b107bed935
commit
f7abe06a60
|
|
@ -1,10 +1,9 @@
|
|||
import type { Application } from "@prisma/client";
|
||||
import { client } from "~database/datasource";
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import { db } from "~drizzle/db";
|
||||
import type { application } from "~drizzle/schema";
|
||||
import type { APIApplication } from "~types/entities/application";
|
||||
|
||||
/**
|
||||
* Represents an application that can authenticate with the API.
|
||||
*/
|
||||
export type Application = InferSelectModel<typeof application>;
|
||||
|
||||
/**
|
||||
* Retrieves the application associated with the given access token.
|
||||
|
|
@ -14,16 +13,14 @@ import type { APIApplication } from "~types/entities/application";
|
|||
export const getFromToken = async (
|
||||
token: string,
|
||||
): Promise<Application | null> => {
|
||||
const dbToken = await client.token.findFirst({
|
||||
where: {
|
||||
access_token: token,
|
||||
},
|
||||
include: {
|
||||
const result = await db.query.token.findFirst({
|
||||
where: (tokens, { eq }) => eq(tokens.accessToken, token),
|
||||
with: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
|
||||
return dbToken?.application || null;
|
||||
return result?.application || null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -34,6 +31,6 @@ export const applicationToAPI = (app: Application): APIApplication => {
|
|||
return {
|
||||
name: app.name,
|
||||
website: app.website,
|
||||
vapid_key: app.vapid_key,
|
||||
vapid_key: app.vapidKey,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,21 +1,24 @@
|
|||
import type { Attachment } from "@prisma/client";
|
||||
import type { Config } from "config-manager";
|
||||
import { MediaBackendType } from "media-manager";
|
||||
import type { APIAsyncAttachment } from "~types/entities/async_attachment";
|
||||
import type { APIAttachment } from "~types/entities/attachment";
|
||||
import type * as Lysand from "lysand-types";
|
||||
import { client } from "~database/datasource";
|
||||
import { db } from "~drizzle/db";
|
||||
import { attachment } from "~drizzle/schema";
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
|
||||
export type Attachment = InferSelectModel<typeof attachment>;
|
||||
|
||||
export const attachmentToAPI = (
|
||||
attachment: Attachment,
|
||||
): APIAsyncAttachment | APIAttachment => {
|
||||
let type = "unknown";
|
||||
|
||||
if (attachment.mime_type.startsWith("image/")) {
|
||||
if (attachment.mimeType.startsWith("image/")) {
|
||||
type = "image";
|
||||
} else if (attachment.mime_type.startsWith("video/")) {
|
||||
} else if (attachment.mimeType.startsWith("video/")) {
|
||||
type = "video";
|
||||
} else if (attachment.mime_type.startsWith("audio/")) {
|
||||
} else if (attachment.mimeType.startsWith("audio/")) {
|
||||
type = "audio";
|
||||
}
|
||||
|
||||
|
|
@ -23,8 +26,8 @@ export const attachmentToAPI = (
|
|||
id: attachment.id,
|
||||
type: type as "image" | "video" | "audio" | "unknown",
|
||||
url: attachment.url,
|
||||
remote_url: attachment.remote_url,
|
||||
preview_url: attachment.thumbnail_url,
|
||||
remote_url: attachment.remoteUrl,
|
||||
preview_url: attachment.thumbnailUrl,
|
||||
text_url: null,
|
||||
meta: {
|
||||
width: attachment.width || undefined,
|
||||
|
|
@ -63,7 +66,7 @@ export const attachmentToLysand = (
|
|||
attachment: Attachment,
|
||||
): Lysand.ContentFormat => {
|
||||
return {
|
||||
[attachment.mime_type]: {
|
||||
[attachment.mimeType]: {
|
||||
content: attachment.url,
|
||||
blurhash: attachment.blurhash ?? undefined,
|
||||
description: attachment.description ?? undefined,
|
||||
|
|
@ -82,13 +85,15 @@ export const attachmentToLysand = (
|
|||
};
|
||||
|
||||
export const attachmentFromLysand = async (
|
||||
attachment: Lysand.ContentFormat,
|
||||
): Promise<Attachment> => {
|
||||
const key = Object.keys(attachment)[0];
|
||||
const value = attachment[key];
|
||||
attachmentToConvert: Lysand.ContentFormat,
|
||||
): Promise<InferSelectModel<typeof attachment>> => {
|
||||
const key = Object.keys(attachmentToConvert)[0];
|
||||
const value = attachmentToConvert[key];
|
||||
|
||||
return await client.attachment.create({
|
||||
data: {
|
||||
const result = await db
|
||||
.insert(attachment)
|
||||
.values({
|
||||
mimeType: key,
|
||||
url: value.content,
|
||||
description: value.description || undefined,
|
||||
duration: value.duration || undefined,
|
||||
|
|
@ -97,10 +102,11 @@ export const attachmentFromLysand = async (
|
|||
size: value.size || undefined,
|
||||
width: value.width || undefined,
|
||||
sha256: value.hash?.sha256 || undefined,
|
||||
mime_type: key,
|
||||
blurhash: value.blurhash || undefined,
|
||||
},
|
||||
});
|
||||
})
|
||||
.returning();
|
||||
|
||||
return result[0];
|
||||
};
|
||||
|
||||
export const getUrl = (name: string, config: Config) => {
|
||||
|
|
|
|||
|
|
@ -1,33 +1,36 @@
|
|||
import type { Emoji } from "@prisma/client";
|
||||
import { client } from "~database/datasource";
|
||||
import type { APIEmoji } from "~types/entities/emoji";
|
||||
import type * as Lysand from "lysand-types";
|
||||
import { addInstanceIfNotExists } from "./Instance";
|
||||
import { db } from "~drizzle/db";
|
||||
import { emoji, instance } from "~drizzle/schema";
|
||||
import { and, eq, type InferSelectModel } from "drizzle-orm";
|
||||
|
||||
/**
|
||||
* Represents an emoji entity in the database.
|
||||
*/
|
||||
export type EmojiWithInstance = InferSelectModel<typeof emoji> & {
|
||||
instance: InferSelectModel<typeof instance> | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used for parsing emojis from local text
|
||||
* @param text The text to parse
|
||||
* @returns An array of emojis
|
||||
*/
|
||||
export const parseEmojis = async (text: string): Promise<Emoji[]> => {
|
||||
export const parseEmojis = async (text: string) => {
|
||||
const regex = /:[a-zA-Z0-9_]+:/g;
|
||||
const matches = text.match(regex);
|
||||
if (!matches) return [];
|
||||
return await client.emoji.findMany({
|
||||
where: {
|
||||
shortcode: {
|
||||
in: matches.map((match) => match.replace(/:/g, "")),
|
||||
},
|
||||
instanceId: null,
|
||||
},
|
||||
include: {
|
||||
const emojis = await db.query.emoji.findMany({
|
||||
where: (emoji, { eq, or }) =>
|
||||
or(
|
||||
...matches
|
||||
.map((match) => match.replace(/:/g, ""))
|
||||
.map((match) => eq(emoji.shortcode, match)),
|
||||
),
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
});
|
||||
|
||||
return emojis;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -36,62 +39,72 @@ export const parseEmojis = async (text: string): Promise<Emoji[]> => {
|
|||
* @param host Host to fetch the emoji from if remote
|
||||
* @returns The emoji
|
||||
*/
|
||||
export const fetchEmoji = async (emoji: Lysand.Emoji, host?: string) => {
|
||||
const existingEmoji = await client.emoji.findFirst({
|
||||
where: {
|
||||
shortcode: emoji.name,
|
||||
instance: host
|
||||
? {
|
||||
base_url: host,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
});
|
||||
export const fetchEmoji = async (
|
||||
emojiToFetch: Lysand.Emoji,
|
||||
host?: string,
|
||||
): Promise<EmojiWithInstance> => {
|
||||
const existingEmoji = await db
|
||||
.select()
|
||||
.from(emoji)
|
||||
.innerJoin(instance, eq(emoji.instanceId, instance.id))
|
||||
.where(
|
||||
and(
|
||||
eq(emoji.shortcode, emojiToFetch.name),
|
||||
host ? eq(instance.baseUrl, host) : undefined,
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (existingEmoji) return existingEmoji;
|
||||
if (existingEmoji[0])
|
||||
return {
|
||||
...existingEmoji[0].Emoji,
|
||||
instance: existingEmoji[0].Instance,
|
||||
};
|
||||
|
||||
const instance = host ? await addInstanceIfNotExists(host) : null;
|
||||
const foundInstance = host ? await addInstanceIfNotExists(host) : null;
|
||||
|
||||
return await client.emoji.create({
|
||||
data: {
|
||||
shortcode: emoji.name,
|
||||
url: Object.entries(emoji.url)[0][1].content,
|
||||
alt:
|
||||
emoji.alt ||
|
||||
Object.entries(emoji.url)[0][1].description ||
|
||||
undefined,
|
||||
content_type: Object.keys(emoji.url)[0],
|
||||
visible_in_picker: true,
|
||||
instance: host
|
||||
? {
|
||||
connect: {
|
||||
id: instance?.id,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
const result = (
|
||||
await db
|
||||
.insert(emoji)
|
||||
.values({
|
||||
shortcode: emojiToFetch.name,
|
||||
url: Object.entries(emojiToFetch.url)[0][1].content,
|
||||
alt:
|
||||
emojiToFetch.alt ||
|
||||
Object.entries(emojiToFetch.url)[0][1].description ||
|
||||
undefined,
|
||||
contentType: Object.keys(emojiToFetch.url)[0],
|
||||
visibleInPicker: true,
|
||||
instanceId: foundInstance?.id,
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
|
||||
return {
|
||||
...result,
|
||||
instance: foundInstance,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the emoji to an APIEmoji object.
|
||||
* @returns The APIEmoji object.
|
||||
*/
|
||||
export const emojiToAPI = (emoji: Emoji): APIEmoji => {
|
||||
export const emojiToAPI = (emoji: EmojiWithInstance): APIEmoji => {
|
||||
return {
|
||||
shortcode: emoji.shortcode,
|
||||
static_url: emoji.url, // TODO: Add static version
|
||||
url: emoji.url,
|
||||
visible_in_picker: emoji.visible_in_picker,
|
||||
visible_in_picker: emoji.visibleInPicker,
|
||||
category: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const emojiToLysand = (emoji: Emoji): Lysand.Emoji => {
|
||||
export const emojiToLysand = (emoji: EmojiWithInstance): Lysand.Emoji => {
|
||||
return {
|
||||
name: emoji.shortcode,
|
||||
url: {
|
||||
[emoji.content_type]: {
|
||||
[emoji.contentType]: {
|
||||
content: emoji.url,
|
||||
description: emoji.alt || undefined,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import type { User } from "@prisma/client";
|
||||
import type * as Lysand from "lysand-types";
|
||||
import { config } from "config-manager";
|
||||
import { getUserUri } from "./User";
|
||||
import { getUserUri, type User } from "./User";
|
||||
|
||||
export const objectToInboxRequest = async (
|
||||
object: Lysand.Entity,
|
||||
author: User,
|
||||
userToSendTo: User,
|
||||
): Promise<Request> => {
|
||||
if (!userToSendTo.instanceId || !userToSendTo.endpoints.inbox) {
|
||||
if (!userToSendTo.instanceId || !userToSendTo.endpoints?.inbox) {
|
||||
throw new Error("UserToSendTo has no inbox or is a local user");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Instance } from "@prisma/client";
|
||||
import { client } from "~database/datasource";
|
||||
import { db } from "~drizzle/db";
|
||||
import type * as Lysand from "lysand-types";
|
||||
import { instance } from "~drizzle/schema";
|
||||
|
||||
/**
|
||||
* Represents an instance in the database.
|
||||
|
|
@ -11,16 +11,12 @@ import type * as Lysand from "lysand-types";
|
|||
* @param url
|
||||
* @returns Either the database instance if it already exists, or a newly created instance.
|
||||
*/
|
||||
export const addInstanceIfNotExists = async (
|
||||
url: string,
|
||||
): Promise<Instance> => {
|
||||
export const addInstanceIfNotExists = async (url: string) => {
|
||||
const origin = new URL(url).origin;
|
||||
const host = new URL(url).host;
|
||||
|
||||
const found = await client.instance.findFirst({
|
||||
where: {
|
||||
base_url: host,
|
||||
},
|
||||
const found = await db.query.instance.findFirst({
|
||||
where: (instance, { eq }) => eq(instance.baseUrl, host),
|
||||
});
|
||||
|
||||
if (found) return found;
|
||||
|
|
@ -40,12 +36,15 @@ export const addInstanceIfNotExists = async (
|
|||
throw new Error("Invalid instance metadata (missing name or version)");
|
||||
}
|
||||
|
||||
return await client.instance.create({
|
||||
data: {
|
||||
base_url: host,
|
||||
name: metadata.name,
|
||||
version: metadata.version,
|
||||
logo: metadata.logo,
|
||||
},
|
||||
});
|
||||
return (
|
||||
await db
|
||||
.insert(instance)
|
||||
.values({
|
||||
baseUrl: host,
|
||||
name: metadata.name,
|
||||
version: metadata.version,
|
||||
logo: metadata.logo,
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import type { Like } from "@prisma/client";
|
||||
import { config } from "config-manager";
|
||||
import { client } from "~database/datasource";
|
||||
import type { StatusWithRelations } from "./Status";
|
||||
import type { UserWithRelations } from "./User";
|
||||
import type * as Lysand from "lysand-types";
|
||||
import { and, eq, type InferSelectModel } from "drizzle-orm";
|
||||
import { notification, like } from "~drizzle/schema";
|
||||
import { db } from "~drizzle/db";
|
||||
|
||||
export type Like = InferSelectModel<typeof like>;
|
||||
|
||||
/**
|
||||
* Represents a Like entity in the database.
|
||||
|
|
@ -33,22 +36,18 @@ export const createLike = async (
|
|||
user: UserWithRelations,
|
||||
status: StatusWithRelations,
|
||||
) => {
|
||||
await client.like.create({
|
||||
data: {
|
||||
likedId: status.id,
|
||||
likerId: user.id,
|
||||
},
|
||||
await db.insert(like).values({
|
||||
likedId: status.id,
|
||||
likerId: user.id,
|
||||
});
|
||||
|
||||
if (status.author.instanceId === user.instanceId) {
|
||||
// Notify the user that their post has been favourited
|
||||
await client.notification.create({
|
||||
data: {
|
||||
accountId: user.id,
|
||||
type: "favourite",
|
||||
notifiedId: status.authorId,
|
||||
statusId: status.id,
|
||||
},
|
||||
await db.insert(notification).values({
|
||||
accountId: user.id,
|
||||
type: "favourite",
|
||||
notifiedId: status.authorId,
|
||||
statusId: status.id,
|
||||
});
|
||||
} else {
|
||||
// TODO: Add database jobs for federating this
|
||||
|
|
@ -64,22 +63,21 @@ export const deleteLike = async (
|
|||
user: UserWithRelations,
|
||||
status: StatusWithRelations,
|
||||
) => {
|
||||
await client.like.deleteMany({
|
||||
where: {
|
||||
likedId: status.id,
|
||||
likerId: user.id,
|
||||
},
|
||||
});
|
||||
await db
|
||||
.delete(like)
|
||||
.where(and(eq(like.likedId, status.id), eq(like.likerId, user.id)));
|
||||
|
||||
// Notify the user that their post has been favourited
|
||||
await client.notification.deleteMany({
|
||||
where: {
|
||||
accountId: user.id,
|
||||
type: "favourite",
|
||||
notifiedId: status.authorId,
|
||||
statusId: status.id,
|
||||
},
|
||||
});
|
||||
await db
|
||||
.delete(notification)
|
||||
.where(
|
||||
and(
|
||||
eq(notification.accountId, user.id),
|
||||
eq(notification.type, "favourite"),
|
||||
eq(notification.notifiedId, status.authorId),
|
||||
eq(notification.statusId, status.id),
|
||||
),
|
||||
);
|
||||
|
||||
if (user.instanceId === null && status.author.instanceId !== null) {
|
||||
// User is local, federate the delete
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import type { Notification } from "@prisma/client";
|
||||
import type { APINotification } from "~types/entities/notification";
|
||||
import { type StatusWithRelations, statusToAPI } from "./Status";
|
||||
import { type UserWithRelations, userToAPI } from "./User";
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import type { notification } from "~drizzle/schema";
|
||||
|
||||
export type Notification = InferSelectModel<typeof notification>;
|
||||
|
||||
export type NotificationWithRelations = Notification & {
|
||||
status: StatusWithRelations | null;
|
||||
|
|
|
|||
|
|
@ -1,15 +1,22 @@
|
|||
import type { LysandObject } from "@prisma/client";
|
||||
import { client } from "~database/datasource";
|
||||
import type { LysandObjectType } from "~types/lysand/Object";
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import { db } from "~drizzle/db";
|
||||
import { lysandObject } from "~drizzle/schema";
|
||||
import { findFirstUser } from "./User";
|
||||
import type * as Lysand from "lysand-types";
|
||||
|
||||
export type LysandObject = InferSelectModel<typeof lysandObject>;
|
||||
|
||||
/**
|
||||
* Represents a Lysand object in the database.
|
||||
*/
|
||||
|
||||
export const createFromObject = async (object: LysandObjectType) => {
|
||||
const foundObject = await client.lysandObject.findFirst({
|
||||
where: { remote_id: object.id },
|
||||
include: {
|
||||
export const createFromObject = async (
|
||||
object: Lysand.Entity,
|
||||
authorUri: string,
|
||||
) => {
|
||||
const foundObject = await db.query.lysandObject.findFirst({
|
||||
where: (o, { eq }) => eq(o.remoteId, object.id),
|
||||
with: {
|
||||
author: true,
|
||||
},
|
||||
});
|
||||
|
|
@ -18,45 +25,43 @@ export const createFromObject = async (object: LysandObjectType) => {
|
|||
return foundObject;
|
||||
}
|
||||
|
||||
const author = await client.lysandObject.findFirst({
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
where: { uri: (object as any).author },
|
||||
const author = await findFirstUser({
|
||||
where: (user, { eq }) => eq(user.uri, authorUri),
|
||||
});
|
||||
|
||||
return await client.lysandObject.create({
|
||||
data: {
|
||||
authorId: author?.id,
|
||||
created_at: new Date(object.created_at).toISOString(),
|
||||
extensions: object.extensions || {},
|
||||
remote_id: object.id,
|
||||
type: object.type,
|
||||
uri: object.uri,
|
||||
// Rest of data (remove id, author, created_at, extensions, type, uri)
|
||||
extra_data: Object.fromEntries(
|
||||
Object.entries(object).filter(
|
||||
([key]) =>
|
||||
![
|
||||
"id",
|
||||
"author",
|
||||
"created_at",
|
||||
"extensions",
|
||||
"type",
|
||||
"uri",
|
||||
].includes(key),
|
||||
),
|
||||
return await db.insert(lysandObject).values({
|
||||
authorId: author?.id,
|
||||
createdAt: new Date(object.created_at).toISOString(),
|
||||
extensions: object.extensions,
|
||||
remoteId: object.id,
|
||||
type: object.type,
|
||||
uri: object.uri,
|
||||
// Rest of data (remove id, author, created_at, extensions, type, uri)
|
||||
extraData: Object.fromEntries(
|
||||
Object.entries(object).filter(
|
||||
([key]) =>
|
||||
![
|
||||
"id",
|
||||
"author",
|
||||
"created_at",
|
||||
"extensions",
|
||||
"type",
|
||||
"uri",
|
||||
].includes(key),
|
||||
),
|
||||
},
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
export const toLysand = (lyObject: LysandObject): LysandObjectType => {
|
||||
export const toLysand = (lyObject: LysandObject): Lysand.Entity => {
|
||||
return {
|
||||
id: lyObject.remote_id || lyObject.id,
|
||||
created_at: new Date(lyObject.created_at).toISOString(),
|
||||
id: lyObject.remoteId || lyObject.id,
|
||||
created_at: new Date(lyObject.createdAt).toISOString(),
|
||||
type: lyObject.type,
|
||||
uri: lyObject.uri,
|
||||
...lyObject.extra_data,
|
||||
extensions: lyObject.extensions,
|
||||
...(lyObject.extraData as object),
|
||||
// @ts-expect-error Assume stored JSON is valid
|
||||
extensions: lyObject.extensions as object,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import type { User } from "@prisma/client";
|
||||
import { config } from "config-manager";
|
||||
// import { Worker } from "bullmq";
|
||||
import { type StatusWithRelations, statusToLysand } from "./Status";
|
||||
|
|
@ -118,70 +117,6 @@ import { type StatusWithRelations, statusToLysand } from "./Status";
|
|||
}
|
||||
); */
|
||||
|
||||
/**
|
||||
* Convert a string into an ArrayBuffer
|
||||
* from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String
|
||||
*/
|
||||
export const str2ab = (str: string) => {
|
||||
const buf = new ArrayBuffer(str.length);
|
||||
const bufView = new Uint8Array(buf);
|
||||
for (let i = 0, strLen = str.length; i < strLen; i++) {
|
||||
bufView[i] = str.charCodeAt(i);
|
||||
}
|
||||
return buf;
|
||||
};
|
||||
|
||||
export const federateStatusTo = async (
|
||||
status: StatusWithRelations,
|
||||
sender: User,
|
||||
user: User,
|
||||
) => {
|
||||
const privateKey = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
str2ab(atob(user.privateKey ?? "")),
|
||||
"Ed25519",
|
||||
false,
|
||||
["sign"],
|
||||
);
|
||||
|
||||
const digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode("request_body"),
|
||||
);
|
||||
|
||||
const userInbox = new URL(user.endpoints.inbox);
|
||||
|
||||
const date = new Date();
|
||||
|
||||
const signature = await crypto.subtle.sign(
|
||||
"Ed25519",
|
||||
privateKey,
|
||||
new TextEncoder().encode(
|
||||
`(request-target): post ${userInbox.pathname}\n` +
|
||||
`host: ${userInbox.host}\n` +
|
||||
`date: ${date.toUTCString()}\n` +
|
||||
`digest: SHA-256=${btoa(
|
||||
String.fromCharCode(...new Uint8Array(digest)),
|
||||
)}\n`,
|
||||
),
|
||||
);
|
||||
|
||||
const signatureBase64 = btoa(
|
||||
String.fromCharCode(...new Uint8Array(signature)),
|
||||
);
|
||||
|
||||
return fetch(userInbox, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Date: date.toUTCString(),
|
||||
Origin: config.http.base_url,
|
||||
Signature: `keyId="${sender.uri}",algorithm="ed25519",headers="(request-target) host date digest",signature="${signatureBase64}"`,
|
||||
},
|
||||
body: JSON.stringify(statusToLysand(status)),
|
||||
});
|
||||
};
|
||||
|
||||
export const addStatusFederationJob = async (statusId: string) => {
|
||||
/* await federationQueue.add("federation", {
|
||||
id: statusId,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import type { Relationship, User } from "@prisma/client";
|
||||
import { client } from "~database/datasource";
|
||||
import type { APIRelationship } from "~types/entities/relationship";
|
||||
import type { User } from "./User";
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import { relationship } from "~drizzle/schema";
|
||||
import { db } from "~drizzle/db";
|
||||
|
||||
/**
|
||||
* Stores Mastodon API relationships
|
||||
*/
|
||||
export type Relationship = InferSelectModel<typeof relationship>;
|
||||
|
||||
/**
|
||||
* Creates a new relationship between two users.
|
||||
|
|
@ -16,25 +17,29 @@ export const createNewRelationship = async (
|
|||
owner: User,
|
||||
other: User,
|
||||
): Promise<Relationship> => {
|
||||
return await client.relationship.create({
|
||||
data: {
|
||||
ownerId: owner.id,
|
||||
subjectId: other.id,
|
||||
languages: [],
|
||||
following: false,
|
||||
showingReblogs: false,
|
||||
notifying: false,
|
||||
followedBy: false,
|
||||
blocking: false,
|
||||
blockedBy: false,
|
||||
muting: false,
|
||||
mutingNotifications: false,
|
||||
requested: false,
|
||||
domainBlocking: false,
|
||||
endorsed: false,
|
||||
note: "",
|
||||
},
|
||||
});
|
||||
return (
|
||||
await db
|
||||
.insert(relationship)
|
||||
.values({
|
||||
ownerId: owner.id,
|
||||
subjectId: other.id,
|
||||
languages: [],
|
||||
following: false,
|
||||
showingReblogs: false,
|
||||
notifying: false,
|
||||
followedBy: false,
|
||||
blocking: false,
|
||||
blockedBy: false,
|
||||
muting: false,
|
||||
mutingNotifications: false,
|
||||
requested: false,
|
||||
domainBlocking: false,
|
||||
endorsed: false,
|
||||
note: "",
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
};
|
||||
|
||||
export const checkForBidirectionalRelationships = async (
|
||||
|
|
@ -42,18 +47,14 @@ export const checkForBidirectionalRelationships = async (
|
|||
user2: User,
|
||||
createIfNotExists = true,
|
||||
): Promise<boolean> => {
|
||||
const relationship1 = await client.relationship.findFirst({
|
||||
where: {
|
||||
ownerId: user1.id,
|
||||
subjectId: user2.id,
|
||||
},
|
||||
const relationship1 = await db.query.relationship.findFirst({
|
||||
where: (rel, { and, eq }) =>
|
||||
and(eq(rel.ownerId, user1.id), eq(rel.subjectId, user2.id)),
|
||||
});
|
||||
|
||||
const relationship2 = await client.relationship.findFirst({
|
||||
where: {
|
||||
ownerId: user2.id,
|
||||
subjectId: user1.id,
|
||||
},
|
||||
const relationship2 = await db.query.relationship.findFirst({
|
||||
where: (rel, { and, eq }) =>
|
||||
and(eq(rel.ownerId, user2.id), eq(rel.subjectId, user1.id)),
|
||||
});
|
||||
|
||||
if (!relationship1 && !relationship2 && createIfNotExists) {
|
||||
|
|
@ -82,7 +83,7 @@ export const relationshipToAPI = (rel: Relationship): APIRelationship => {
|
|||
notifying: rel.notifying,
|
||||
requested: rel.requested,
|
||||
showing_reblogs: rel.showingReblogs,
|
||||
languages: rel.languages,
|
||||
languages: rel.languages ?? [],
|
||||
note: rel.note,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,35 +1,128 @@
|
|||
import { addUserToMeilisearch } from "@meilisearch";
|
||||
import type { User } from "@prisma/client";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { type Config, config } from "config-manager";
|
||||
import { htmlToText } from "html-to-text";
|
||||
import { client } from "~database/datasource";
|
||||
import type { APIAccount } from "~types/entities/account";
|
||||
import type { APISource } from "~types/entities/source";
|
||||
import type * as Lysand from "lysand-types";
|
||||
import { fetchEmoji, emojiToAPI, emojiToLysand } from "./Emoji";
|
||||
import {
|
||||
fetchEmoji,
|
||||
emojiToAPI,
|
||||
emojiToLysand,
|
||||
type EmojiWithInstance,
|
||||
} from "./Emoji";
|
||||
import { addInstanceIfNotExists } from "./Instance";
|
||||
import { userRelations } from "./relations";
|
||||
import { createNewRelationship } from "./Relationship";
|
||||
import { getBestContentType, urlToContentFormat } from "@content_types";
|
||||
import { objectToInboxRequest } from "./Federation";
|
||||
import { and, eq, sql, type InferSelectModel } from "drizzle-orm";
|
||||
import {
|
||||
emojiToUser,
|
||||
instance,
|
||||
notification,
|
||||
relationship,
|
||||
user,
|
||||
} from "~drizzle/schema";
|
||||
import { db } from "~drizzle/db";
|
||||
|
||||
export type User = InferSelectModel<typeof user> & {
|
||||
endpoints?: Partial<{
|
||||
dislikes: string;
|
||||
featured: string;
|
||||
likes: string;
|
||||
followers: string;
|
||||
following: string;
|
||||
inbox: string;
|
||||
outbox: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type UserWithRelations = User & {
|
||||
instance: InferSelectModel<typeof instance> | null;
|
||||
emojis: EmojiWithInstance[];
|
||||
followerCount: number;
|
||||
followingCount: number;
|
||||
statusCount: number;
|
||||
};
|
||||
|
||||
export type UserWithRelationsAndRelationships = UserWithRelations & {
|
||||
relationships: InferSelectModel<typeof relationship>[];
|
||||
relationshipSubjects: InferSelectModel<typeof relationship>[];
|
||||
};
|
||||
|
||||
export const userRelations: {
|
||||
instance: true;
|
||||
emojis: {
|
||||
with: {
|
||||
emoji: {
|
||||
with: {
|
||||
instance: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
} = {
|
||||
instance: true,
|
||||
emojis: {
|
||||
with: {
|
||||
emoji: {
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const userExtras = {
|
||||
followerCount:
|
||||
sql`(SELECT COUNT(*) FROM "Relationship" "relationships" WHERE ("relationships"."ownerId" = "user".id AND "relationships"."following" = true))`.as(
|
||||
"follower_count",
|
||||
),
|
||||
followingCount:
|
||||
sql`(SELECT COUNT(*) FROM "Relationship" "relationshipSubjects" WHERE ("relationshipSubjects"."subjectId" = "user".id AND "relationshipSubjects"."following" = true))`.as(
|
||||
"following_count",
|
||||
),
|
||||
statusCount:
|
||||
sql`(SELECT COUNT(*) FROM "Status" "statuses" WHERE "statuses"."authorId" = "user".id)`.as(
|
||||
"status_count",
|
||||
),
|
||||
};
|
||||
|
||||
export const userExtrasTemplate = (name: string) => ({
|
||||
// @ts-ignore
|
||||
followerCount: sql([
|
||||
`(SELECT COUNT(*) FROM "Relationship" "relationships" WHERE ("relationships"."ownerId" = "${name}".id AND "relationships"."following" = true))`,
|
||||
]).as("follower_count"),
|
||||
// @ts-ignore
|
||||
followingCount: sql([
|
||||
`(SELECT COUNT(*) FROM "Relationship" "relationshipSubjects" WHERE ("relationshipSubjects"."subjectId" = "${name}".id AND "relationshipSubjects"."following" = true))`,
|
||||
]).as("following_count"),
|
||||
// @ts-ignore
|
||||
statusCount: sql([
|
||||
`(SELECT COUNT(*) FROM "Status" "statuses" WHERE "statuses"."authorId" = "${name}".id)`,
|
||||
]).as("status_count"),
|
||||
});
|
||||
|
||||
/* const a = await db.query.user.findFirst({
|
||||
with: {
|
||||
instance: true,
|
||||
emojis: {
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
extras: {
|
||||
//
|
||||
followerCount: sql`SELECT COUNT(*) FROM relationship WHERE owner_id = user.id AND following = true`,
|
||||
},
|
||||
}); */
|
||||
|
||||
export interface AuthData {
|
||||
user: UserWithRelations | null;
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a user in the database.
|
||||
* Stores local and remote users
|
||||
*/
|
||||
|
||||
const userRelations2 = Prisma.validator<Prisma.UserDefaultArgs>()({
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
export type UserWithRelations = Prisma.UserGetPayload<typeof userRelations2>;
|
||||
|
||||
/**
|
||||
* Get the user's avatar in raw URL format
|
||||
* @param config The config to use
|
||||
|
|
@ -68,19 +161,22 @@ export const followRequestUser = async (
|
|||
reblogs = false,
|
||||
notify = false,
|
||||
languages: string[] = [],
|
||||
) => {
|
||||
): Promise<InferSelectModel<typeof relationship>> => {
|
||||
const isRemote = follower.instanceId !== followee.instanceId;
|
||||
|
||||
const relationship = await client.relationship.update({
|
||||
where: { id: relationshipId },
|
||||
data: {
|
||||
following: isRemote ? false : !followee.isLocked,
|
||||
requested: isRemote ? true : followee.isLocked,
|
||||
showingReblogs: reblogs,
|
||||
notifying: notify,
|
||||
languages: languages,
|
||||
},
|
||||
});
|
||||
const updatedRelationship = (
|
||||
await db
|
||||
.update(relationship)
|
||||
.set({
|
||||
following: isRemote ? false : !followee.isLocked,
|
||||
requested: isRemote ? true : followee.isLocked,
|
||||
showingReblogs: reblogs,
|
||||
notifying: notify,
|
||||
languages: languages,
|
||||
})
|
||||
.where(eq(relationship.id, relationshipId))
|
||||
.returning()
|
||||
)[0];
|
||||
|
||||
if (isRemote) {
|
||||
// Federate
|
||||
|
|
@ -100,35 +196,26 @@ export const followRequestUser = async (
|
|||
`Failed to federate follow request from ${follower.id} to ${followee.uri}`,
|
||||
);
|
||||
|
||||
return await client.relationship.update({
|
||||
where: { id: relationshipId },
|
||||
data: {
|
||||
following: false,
|
||||
requested: false,
|
||||
},
|
||||
});
|
||||
return (
|
||||
await db
|
||||
.update(relationship)
|
||||
.set({
|
||||
following: false,
|
||||
requested: false,
|
||||
})
|
||||
.where(eq(relationship.id, relationshipId))
|
||||
.returning()
|
||||
)[0];
|
||||
}
|
||||
} else {
|
||||
if (followee.isLocked) {
|
||||
await client.notification.create({
|
||||
data: {
|
||||
accountId: follower.id,
|
||||
type: "follow_request",
|
||||
notifiedId: followee.id,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await client.notification.create({
|
||||
data: {
|
||||
accountId: follower.id,
|
||||
type: "follow",
|
||||
notifiedId: followee.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
await db.insert(notification).values({
|
||||
accountId: followee.id,
|
||||
type: followee.isLocked ? "follow_request" : "follow",
|
||||
notifiedId: follower.id,
|
||||
});
|
||||
}
|
||||
|
||||
return relationship;
|
||||
return updatedRelationship;
|
||||
};
|
||||
|
||||
export const sendFollowAccept = async (follower: User, followee: User) => {
|
||||
|
|
@ -169,13 +256,88 @@ export const sendFollowReject = async (follower: User, followee: User) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const resolveUser = async (uri: string) => {
|
||||
// Check if user not already in database
|
||||
const foundUser = await client.user.findUnique({
|
||||
where: {
|
||||
uri,
|
||||
export const transformOutputToUserWithRelations = (
|
||||
user: Omit<User, "endpoints"> & {
|
||||
followerCount: unknown;
|
||||
followingCount: unknown;
|
||||
statusCount: unknown;
|
||||
emojis: {
|
||||
a: string;
|
||||
b: string;
|
||||
emoji?: EmojiWithInstance;
|
||||
}[];
|
||||
instance: InferSelectModel<typeof instance> | null;
|
||||
endpoints: unknown;
|
||||
},
|
||||
): UserWithRelations => {
|
||||
return {
|
||||
...user,
|
||||
followerCount: Number(user.followerCount),
|
||||
followingCount: Number(user.followingCount),
|
||||
statusCount: Number(user.statusCount),
|
||||
endpoints:
|
||||
user.endpoints ??
|
||||
({} as Partial<{
|
||||
dislikes: string;
|
||||
featured: string;
|
||||
likes: string;
|
||||
followers: string;
|
||||
following: string;
|
||||
inbox: string;
|
||||
outbox: string;
|
||||
}>),
|
||||
emojis: user.emojis.map(
|
||||
(emoji) =>
|
||||
(emoji as unknown as Record<string, object>)
|
||||
.emoji as EmojiWithInstance,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
export const findManyUsers = async (
|
||||
query: Parameters<typeof db.query.user.findMany>[0],
|
||||
): Promise<UserWithRelations[]> => {
|
||||
const output = await db.query.user.findMany({
|
||||
...query,
|
||||
with: {
|
||||
...userRelations,
|
||||
...query?.with,
|
||||
},
|
||||
include: userRelations,
|
||||
extras: {
|
||||
...userExtras,
|
||||
...query?.extras,
|
||||
},
|
||||
});
|
||||
|
||||
return output.map((user) => transformOutputToUserWithRelations(user));
|
||||
};
|
||||
|
||||
export const findFirstUser = async (
|
||||
query: Parameters<typeof db.query.user.findFirst>[0],
|
||||
): Promise<UserWithRelations | null> => {
|
||||
const output = await db.query.user.findFirst({
|
||||
...query,
|
||||
with: {
|
||||
...userRelations,
|
||||
...query?.with,
|
||||
},
|
||||
extras: {
|
||||
...userExtras,
|
||||
...query?.extras,
|
||||
},
|
||||
});
|
||||
|
||||
if (!output) return null;
|
||||
|
||||
return transformOutputToUserWithRelations(output);
|
||||
};
|
||||
|
||||
export const resolveUser = async (
|
||||
uri: string,
|
||||
): Promise<UserWithRelations | null> => {
|
||||
// Check if user not already in database
|
||||
const foundUser = await findFirstUser({
|
||||
where: (user, { eq }) => eq(user.uri, uri),
|
||||
});
|
||||
|
||||
if (foundUser) return foundUser;
|
||||
|
|
@ -192,12 +354,12 @@ export const resolveUser = async (uri: string) => {
|
|||
);
|
||||
}
|
||||
|
||||
return client.user.findUnique({
|
||||
where: {
|
||||
id: uuid[0],
|
||||
},
|
||||
include: userRelations,
|
||||
const foundLocalUser = await findFirstUser({
|
||||
where: (user, { eq }) => eq(user.id, uuid[0]),
|
||||
with: userRelations,
|
||||
});
|
||||
|
||||
return foundLocalUser || null;
|
||||
}
|
||||
|
||||
if (!URL.canParse(uri)) {
|
||||
|
|
@ -244,50 +406,62 @@ export const resolveUser = async (uri: string) => {
|
|||
emojis.push(await fetchEmoji(emoji));
|
||||
}
|
||||
|
||||
const user = await client.user.create({
|
||||
data: {
|
||||
username: data.username,
|
||||
uri: data.uri,
|
||||
createdAt: new Date(data.created_at),
|
||||
endpoints: {
|
||||
dislikes: data.dislikes,
|
||||
featured: data.featured,
|
||||
likes: data.likes,
|
||||
followers: data.followers,
|
||||
following: data.following,
|
||||
inbox: data.inbox,
|
||||
outbox: data.outbox,
|
||||
},
|
||||
emojis: {
|
||||
connect: emojis.map((emoji) => ({
|
||||
id: emoji.id,
|
||||
})),
|
||||
},
|
||||
instanceId: instance.id,
|
||||
avatar: data.avatar
|
||||
? Object.entries(data.avatar)[0][1].content
|
||||
: "",
|
||||
header: data.header
|
||||
? Object.entries(data.header)[0][1].content
|
||||
: "",
|
||||
displayName: data.display_name ?? "",
|
||||
note: getBestContentType(data.bio).content,
|
||||
publicKey: data.public_key.public_key,
|
||||
source: {
|
||||
language: null,
|
||||
note: "",
|
||||
privacy: "public",
|
||||
sensitive: false,
|
||||
fields: [],
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
const newUser = (
|
||||
await db
|
||||
.insert(user)
|
||||
.values({
|
||||
username: data.username,
|
||||
uri: data.uri,
|
||||
createdAt: new Date(data.created_at).toISOString(),
|
||||
endpoints: {
|
||||
dislikes: data.dislikes,
|
||||
featured: data.featured,
|
||||
likes: data.likes,
|
||||
followers: data.followers,
|
||||
following: data.following,
|
||||
inbox: data.inbox,
|
||||
outbox: data.outbox,
|
||||
},
|
||||
instanceId: instance.id,
|
||||
avatar: data.avatar
|
||||
? Object.entries(data.avatar)[0][1].content
|
||||
: "",
|
||||
header: data.header
|
||||
? Object.entries(data.header)[0][1].content
|
||||
: "",
|
||||
displayName: data.display_name ?? "",
|
||||
note: getBestContentType(data.bio).content,
|
||||
publicKey: data.public_key.public_key,
|
||||
source: {
|
||||
language: null,
|
||||
note: "",
|
||||
privacy: "public",
|
||||
sensitive: false,
|
||||
fields: [],
|
||||
},
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
|
||||
// Add emojis to user
|
||||
await db.insert(emojiToUser).values(
|
||||
emojis.map((emoji) => ({
|
||||
a: emoji.id,
|
||||
b: newUser.id,
|
||||
})),
|
||||
);
|
||||
|
||||
const finalUser = await findFirstUser({
|
||||
where: (user, { eq }) => eq(user.id, newUser.id),
|
||||
with: userRelations,
|
||||
});
|
||||
|
||||
// Add to Meilisearch
|
||||
await addUserToMeilisearch(user);
|
||||
if (!finalUser) return null;
|
||||
|
||||
return user;
|
||||
// Add to Meilisearch
|
||||
await addUserToMeilisearch(finalUser);
|
||||
|
||||
return finalUser;
|
||||
};
|
||||
|
||||
export const getUserUri = (user: User) => {
|
||||
|
|
@ -301,19 +475,25 @@ export const getUserUri = (user: User) => {
|
|||
* Resolves a WebFinger identifier to a user.
|
||||
* @param identifier Either a UUID or a username
|
||||
*/
|
||||
export const resolveWebFinger = async (identifier: string, host: string) => {
|
||||
export const resolveWebFinger = async (
|
||||
identifier: string,
|
||||
host: string,
|
||||
): Promise<UserWithRelations | null> => {
|
||||
// Check if user not already in database
|
||||
const foundUser = await client.user.findUnique({
|
||||
where: {
|
||||
username: identifier,
|
||||
instance: {
|
||||
base_url: host,
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
const foundUser = await db
|
||||
.select()
|
||||
.from(user)
|
||||
.innerJoin(instance, eq(user.instanceId, instance.id))
|
||||
.where(and(eq(user.username, identifier), eq(instance.baseUrl, host)))
|
||||
.limit(1);
|
||||
|
||||
if (foundUser) return foundUser;
|
||||
if (foundUser[0])
|
||||
return (
|
||||
(await findFirstUser({
|
||||
where: (user, { eq }) => eq(user.id, foundUser[0].User.id),
|
||||
with: userRelations,
|
||||
})) || null
|
||||
);
|
||||
|
||||
const hostWithProtocol = host.startsWith("http") ? host : `https://${host}`;
|
||||
|
||||
|
|
@ -383,49 +563,57 @@ export const createNewLocalUser = async (data: {
|
|||
avatar?: string;
|
||||
header?: string;
|
||||
admin?: boolean;
|
||||
}) => {
|
||||
}): Promise<UserWithRelations | null> => {
|
||||
const keys = await generateUserKeys();
|
||||
|
||||
const user = await client.user.create({
|
||||
data: {
|
||||
username: data.username,
|
||||
displayName: data.display_name ?? data.username,
|
||||
password: await Bun.password.hash(data.password),
|
||||
email: data.email,
|
||||
note: data.bio ?? "",
|
||||
avatar: data.avatar ?? config.defaults.avatar,
|
||||
header: data.header ?? config.defaults.avatar,
|
||||
isAdmin: data.admin ?? false,
|
||||
publicKey: keys.public_key,
|
||||
privateKey: keys.private_key,
|
||||
source: {
|
||||
language: null,
|
||||
note: "",
|
||||
privacy: "public",
|
||||
sensitive: false,
|
||||
fields: [],
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
const newUser = (
|
||||
await db
|
||||
.insert(user)
|
||||
.values({
|
||||
username: data.username,
|
||||
displayName: data.display_name ?? data.username,
|
||||
password: await Bun.password.hash(data.password),
|
||||
email: data.email,
|
||||
note: data.bio ?? "",
|
||||
avatar: data.avatar ?? config.defaults.avatar,
|
||||
header: data.header ?? config.defaults.avatar,
|
||||
isAdmin: data.admin ?? false,
|
||||
publicKey: keys.public_key,
|
||||
privateKey: keys.private_key,
|
||||
updatedAt: new Date().toISOString(),
|
||||
source: {
|
||||
language: null,
|
||||
note: "",
|
||||
privacy: "public",
|
||||
sensitive: false,
|
||||
fields: [],
|
||||
},
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
|
||||
const finalUser = await findFirstUser({
|
||||
where: (user, { eq }) => eq(user.id, newUser.id),
|
||||
with: userRelations,
|
||||
});
|
||||
|
||||
// Add to Meilisearch
|
||||
await addUserToMeilisearch(user);
|
||||
if (!finalUser) return null;
|
||||
|
||||
return user;
|
||||
// Add to Meilisearch
|
||||
await addUserToMeilisearch(finalUser);
|
||||
|
||||
return finalUser;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses mentions from a list of URIs
|
||||
*/
|
||||
export const parseMentionsUris = async (mentions: string[]) => {
|
||||
return await client.user.findMany({
|
||||
where: {
|
||||
uri: {
|
||||
in: mentions,
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
export const parseMentionsUris = async (
|
||||
mentions: string[],
|
||||
): Promise<UserWithRelations[]> => {
|
||||
return await findManyUsers({
|
||||
where: (user, { inArray }) => inArray(user.uri, mentions),
|
||||
with: userRelations,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -434,23 +622,22 @@ export const parseMentionsUris = async (mentions: string[]) => {
|
|||
* @param access_token The access token to retrieve the user from.
|
||||
* @returns The user associated with the given access token.
|
||||
*/
|
||||
export const retrieveUserFromToken = async (access_token: string) => {
|
||||
export const retrieveUserFromToken = async (
|
||||
access_token: string,
|
||||
): Promise<UserWithRelations | null> => {
|
||||
if (!access_token) return null;
|
||||
|
||||
const token = await client.token.findFirst({
|
||||
where: {
|
||||
access_token,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
include: userRelations,
|
||||
},
|
||||
},
|
||||
const token = await db.query.token.findFirst({
|
||||
where: (tokens, { eq }) => eq(tokens.accessToken, access_token),
|
||||
});
|
||||
|
||||
if (!token) return null;
|
||||
if (!token || !token.userId) return null;
|
||||
|
||||
return token.user;
|
||||
const user = await findFirstUser({
|
||||
where: (user, { eq }) => eq(user.id, token.userId ?? ""),
|
||||
});
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -461,34 +648,24 @@ export const retrieveUserFromToken = async (access_token: string) => {
|
|||
export const getRelationshipToOtherUser = async (
|
||||
user: UserWithRelations,
|
||||
other: User,
|
||||
) => {
|
||||
const relationship = await client.relationship.findFirst({
|
||||
where: {
|
||||
ownerId: user.id,
|
||||
subjectId: other.id,
|
||||
},
|
||||
): Promise<InferSelectModel<typeof relationship>> => {
|
||||
const foundRelationship = await db.query.relationship.findFirst({
|
||||
where: (relationship, { and, eq }) =>
|
||||
and(
|
||||
eq(relationship.ownerId, user.id),
|
||||
eq(relationship.subjectId, other.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (!relationship) {
|
||||
if (!foundRelationship) {
|
||||
// Create new relationship
|
||||
|
||||
const newRelationship = await createNewRelationship(user, other);
|
||||
|
||||
await client.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
relationships: {
|
||||
connect: {
|
||||
id: newRelationship.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return newRelationship;
|
||||
}
|
||||
|
||||
return relationship;
|
||||
return foundRelationship;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -526,40 +703,42 @@ export const generateUserKeys = async () => {
|
|||
};
|
||||
|
||||
export const userToAPI = (
|
||||
user: UserWithRelations,
|
||||
userToConvert: UserWithRelations,
|
||||
isOwnAccount = false,
|
||||
): APIAccount => {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
display_name: user.displayName,
|
||||
note: user.note,
|
||||
id: userToConvert.id,
|
||||
username: userToConvert.username,
|
||||
display_name: userToConvert.displayName,
|
||||
note: userToConvert.note,
|
||||
url:
|
||||
user.uri ||
|
||||
new URL(`/@${user.username}`, config.http.base_url).toString(),
|
||||
avatar: getAvatarUrl(user, config),
|
||||
header: getHeaderUrl(user, config),
|
||||
locked: user.isLocked,
|
||||
created_at: new Date(user.createdAt).toISOString(),
|
||||
followers_count: user.relationshipSubjects.filter((r) => r.following)
|
||||
.length,
|
||||
following_count: user.relationships.filter((r) => r.following).length,
|
||||
statuses_count: user._count.statuses,
|
||||
emojis: user.emojis.map((emoji) => emojiToAPI(emoji)),
|
||||
userToConvert.uri ||
|
||||
new URL(
|
||||
`/@${userToConvert.username}`,
|
||||
config.http.base_url,
|
||||
).toString(),
|
||||
avatar: getAvatarUrl(userToConvert, config),
|
||||
header: getHeaderUrl(userToConvert, config),
|
||||
locked: userToConvert.isLocked,
|
||||
created_at: new Date(userToConvert.createdAt).toISOString(),
|
||||
followers_count: userToConvert.followerCount,
|
||||
following_count: userToConvert.followingCount,
|
||||
statuses_count: userToConvert.statusCount,
|
||||
emojis: userToConvert.emojis.map((emoji) => emojiToAPI(emoji)),
|
||||
// TODO: Add fields
|
||||
fields: [],
|
||||
bot: user.isBot,
|
||||
bot: userToConvert.isBot,
|
||||
source:
|
||||
isOwnAccount && user.source
|
||||
? (user.source as APISource)
|
||||
isOwnAccount && userToConvert.source
|
||||
? (userToConvert.source as APISource)
|
||||
: undefined,
|
||||
// TODO: Add static avatar and header
|
||||
avatar_static: "",
|
||||
header_static: "",
|
||||
acct:
|
||||
user.instance === null
|
||||
? user.username
|
||||
: `${user.username}@${user.instance.base_url}`,
|
||||
userToConvert.instance === null
|
||||
? userToConvert.username
|
||||
: `${userToConvert.username}@${userToConvert.instance.baseUrl}`,
|
||||
// TODO: Add these fields
|
||||
limited: false,
|
||||
moved: null,
|
||||
|
|
@ -569,8 +748,8 @@ export const userToAPI = (
|
|||
mute_expires_at: undefined,
|
||||
group: false,
|
||||
pleroma: {
|
||||
is_admin: user.isAdmin,
|
||||
is_moderator: user.isAdmin,
|
||||
is_admin: userToConvert.isAdmin,
|
||||
is_moderator: userToConvert.isAdmin,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
462
drizzle-scanned/0000_third_misty_knight.sql
Normal file
462
drizzle-scanned/0000_third_misty_knight.sql
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
-- 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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[] DEFAULT 'RRAY[',
|
||||
"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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 $$;
|
||||
|
||||
*/
|
||||
1867
drizzle-scanned/meta/0000_snapshot.json
Normal file
1867
drizzle-scanned/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
13
drizzle-scanned/meta/_journal.json
Normal file
13
drizzle-scanned/meta/_journal.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": "5",
|
||||
"dialect": "pg",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "5",
|
||||
"when": 1712812153499,
|
||||
"tag": "0000_third_misty_knight",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
292
drizzle-scanned/schema.ts
Normal file
292
drizzle-scanned/schema.ts
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
import { pgTable, varchar, timestamp, text, integer, foreignKey, uuid, boolean, uniqueIndex, jsonb, index } from "drizzle-orm/pg-core"
|
||||
import { sql } from "drizzle-orm"
|
||||
|
||||
|
||||
|
||||
export const prismaMigrations = pgTable("_prisma_migrations", {
|
||||
id: varchar("id", { length: 36 }).primaryKey().notNull(),
|
||||
checksum: varchar("checksum", { length: 64 }).notNull(),
|
||||
finishedAt: timestamp("finished_at", { withTimezone: true, mode: 'string' }),
|
||||
migrationName: varchar("migration_name", { length: 255 }).notNull(),
|
||||
logs: text("logs"),
|
||||
rolledBackAt: timestamp("rolled_back_at", { withTimezone: true, mode: 'string' }),
|
||||
startedAt: timestamp("started_at", { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
|
||||
appliedStepsCount: integer("applied_steps_count").default(0).notNull(),
|
||||
});
|
||||
|
||||
export const emoji = pgTable("Emoji", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
shortcode: text("shortcode").notNull(),
|
||||
url: text("url").notNull(),
|
||||
visibleInPicker: boolean("visible_in_picker").notNull(),
|
||||
instanceId: uuid("instanceId").references(() => instance.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
alt: text("alt"),
|
||||
contentType: text("content_type").notNull(),
|
||||
});
|
||||
|
||||
export const like = pgTable("Like", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
likerId: uuid("likerId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
likedId: uuid("likedId").notNull().references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const lysandObject = pgTable("LysandObject", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
remoteId: text("remote_id").notNull(),
|
||||
type: text("type").notNull(),
|
||||
uri: text("uri").notNull(),
|
||||
createdAt: timestamp("created_at", { precision: 3, mode: 'string' }).defaultNow().notNull(),
|
||||
authorId: uuid("authorId"),
|
||||
extraData: jsonb("extra_data").notNull(),
|
||||
extensions: jsonb("extensions").notNull(),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
remoteIdKey: uniqueIndex("LysandObject_remote_id_key").on(table.remoteId),
|
||||
uriKey: uniqueIndex("LysandObject_uri_key").on(table.uri),
|
||||
lysandObjectAuthorIdFkey: foreignKey({
|
||||
columns: [table.authorId],
|
||||
foreignColumns: [table.id],
|
||||
name: "LysandObject_authorId_fkey"
|
||||
}).onUpdate("cascade").onDelete("cascade"),
|
||||
}
|
||||
});
|
||||
|
||||
export const relationship = pgTable("Relationship", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
ownerId: uuid("ownerId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
subjectId: uuid("subjectId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
following: boolean("following").notNull(),
|
||||
showingReblogs: boolean("showingReblogs").notNull(),
|
||||
notifying: boolean("notifying").notNull(),
|
||||
followedBy: boolean("followedBy").notNull(),
|
||||
blocking: boolean("blocking").notNull(),
|
||||
blockedBy: boolean("blockedBy").notNull(),
|
||||
muting: boolean("muting").notNull(),
|
||||
mutingNotifications: boolean("mutingNotifications").notNull(),
|
||||
requested: boolean("requested").notNull(),
|
||||
domainBlocking: boolean("domainBlocking").notNull(),
|
||||
endorsed: boolean("endorsed").notNull(),
|
||||
languages: text("languages").array(),
|
||||
note: text("note").notNull(),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt", { precision: 3, mode: 'string' }).notNull(),
|
||||
});
|
||||
|
||||
export const application = pgTable("Application", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
name: text("name").notNull(),
|
||||
website: text("website"),
|
||||
vapidKey: text("vapid_key"),
|
||||
clientId: text("client_id").notNull(),
|
||||
secret: text("secret").notNull(),
|
||||
scopes: text("scopes").notNull(),
|
||||
redirectUris: text("redirect_uris").notNull(),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
clientIdKey: uniqueIndex("Application_client_id_key").on(table.clientId),
|
||||
}
|
||||
});
|
||||
|
||||
export const token = pgTable("Token", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
tokenType: text("token_type").notNull(),
|
||||
scope: text("scope").notNull(),
|
||||
accessToken: text("access_token").notNull(),
|
||||
code: text("code").notNull(),
|
||||
createdAt: timestamp("created_at", { precision: 3, mode: 'string' }).defaultNow().notNull(),
|
||||
userId: uuid("userId").references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
applicationId: uuid("applicationId").references(() => application.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
});
|
||||
|
||||
export const emojiToUser = pgTable("_EmojiToUser", {
|
||||
a: uuid("A").notNull().references(() => emoji.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
b: uuid("B").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
abUnique: uniqueIndex("_EmojiToUser_AB_unique").on(table.a, table.b),
|
||||
bIdx: index().on(table.b),
|
||||
}
|
||||
});
|
||||
|
||||
export const emojiToStatus = pgTable("_EmojiToStatus", {
|
||||
a: uuid("A").notNull().references(() => emoji.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
b: uuid("B").notNull().references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
abUnique: uniqueIndex("_EmojiToStatus_AB_unique").on(table.a, table.b),
|
||||
bIdx: index().on(table.b),
|
||||
}
|
||||
});
|
||||
|
||||
export const statusToUser = pgTable("_StatusToUser", {
|
||||
a: uuid("A").notNull().references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
b: uuid("B").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
abUnique: uniqueIndex("_StatusToUser_AB_unique").on(table.a, table.b),
|
||||
bIdx: index().on(table.b),
|
||||
}
|
||||
});
|
||||
|
||||
export const userPinnedNotes = pgTable("_UserPinnedNotes", {
|
||||
a: uuid("A").notNull().references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
b: uuid("B").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
abUnique: uniqueIndex("_UserPinnedNotes_AB_unique").on(table.a, table.b),
|
||||
bIdx: index().on(table.b),
|
||||
}
|
||||
});
|
||||
|
||||
export const attachment = pgTable("Attachment", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
url: text("url").notNull(),
|
||||
remoteUrl: text("remote_url"),
|
||||
thumbnailUrl: text("thumbnail_url"),
|
||||
mimeType: text("mime_type").notNull(),
|
||||
description: text("description"),
|
||||
blurhash: text("blurhash"),
|
||||
sha256: text("sha256"),
|
||||
fps: integer("fps"),
|
||||
duration: integer("duration"),
|
||||
width: integer("width"),
|
||||
height: integer("height"),
|
||||
size: integer("size"),
|
||||
statusId: uuid("statusId").references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
});
|
||||
|
||||
export const notification = pgTable("Notification", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
type: text("type").notNull(),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(),
|
||||
notifiedId: uuid("notifiedId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
accountId: uuid("accountId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
statusId: uuid("statusId").references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
});
|
||||
|
||||
export const status = pgTable("Status", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
uri: text("uri"),
|
||||
authorId: uuid("authorId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt", { precision: 3, mode: 'string' }).notNull(),
|
||||
reblogId: uuid("reblogId"),
|
||||
content: text("content").default('').notNull(),
|
||||
contentType: text("contentType").default('text/plain').notNull(),
|
||||
visibility: text("visibility").notNull(),
|
||||
inReplyToPostId: uuid("inReplyToPostId"),
|
||||
quotingPostId: uuid("quotingPostId"),
|
||||
instanceId: uuid("instanceId").references(() => instance.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
sensitive: boolean("sensitive").notNull(),
|
||||
spoilerText: text("spoilerText").default('').notNull(),
|
||||
applicationId: uuid("applicationId").references(() => application.id, { onDelete: "set null", onUpdate: "cascade" } ),
|
||||
contentSource: text("contentSource").default('').notNull(),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
uriKey: uniqueIndex("Status_uri_key").on(table.uri),
|
||||
statusReblogIdFkey: foreignKey({
|
||||
columns: [table.reblogId],
|
||||
foreignColumns: [table.id],
|
||||
name: "Status_reblogId_fkey"
|
||||
}).onUpdate("cascade").onDelete("cascade"),
|
||||
statusInReplyToPostIdFkey: foreignKey({
|
||||
columns: [table.inReplyToPostId],
|
||||
foreignColumns: [table.id],
|
||||
name: "Status_inReplyToPostId_fkey"
|
||||
}).onUpdate("cascade").onDelete("set null"),
|
||||
statusQuotingPostIdFkey: foreignKey({
|
||||
columns: [table.quotingPostId],
|
||||
foreignColumns: [table.id],
|
||||
name: "Status_quotingPostId_fkey"
|
||||
}).onUpdate("cascade").onDelete("set null"),
|
||||
}
|
||||
});
|
||||
|
||||
export const instance = pgTable("Instance", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
baseUrl: text("base_url").notNull(),
|
||||
name: text("name").notNull(),
|
||||
version: text("version").notNull(),
|
||||
logo: jsonb("logo").notNull(),
|
||||
disableAutomoderation: boolean("disableAutomoderation").default(false).notNull(),
|
||||
});
|
||||
|
||||
export const openIdAccount = pgTable("OpenIdAccount", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
userId: uuid("userId").references(() => user.id, { onDelete: "set null", onUpdate: "cascade" } ),
|
||||
serverId: text("serverId").notNull(),
|
||||
issuerId: text("issuerId").notNull(),
|
||||
});
|
||||
|
||||
export const user = pgTable("User", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
uri: text("uri"),
|
||||
username: text("username").notNull(),
|
||||
displayName: text("displayName").notNull(),
|
||||
password: text("password"),
|
||||
email: text("email"),
|
||||
note: text("note").default('').notNull(),
|
||||
isAdmin: boolean("isAdmin").default(false).notNull(),
|
||||
endpoints: jsonb("endpoints"),
|
||||
source: jsonb("source").notNull(),
|
||||
avatar: text("avatar").notNull(),
|
||||
header: text("header").notNull(),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(),
|
||||
updatedAt: timestamp("updatedAt", { precision: 3, mode: 'string' }).notNull(),
|
||||
isBot: boolean("isBot").default(false).notNull(),
|
||||
isLocked: boolean("isLocked").default(false).notNull(),
|
||||
isDiscoverable: boolean("isDiscoverable").default(false).notNull(),
|
||||
sanctions: text("sanctions").default('RRAY[').array(),
|
||||
publicKey: text("publicKey").notNull(),
|
||||
privateKey: text("privateKey"),
|
||||
instanceId: uuid("instanceId").references(() => instance.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
disableAutomoderation: boolean("disableAutomoderation").default(false).notNull(),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
uriKey: uniqueIndex("User_uri_key").on(table.uri),
|
||||
usernameKey: uniqueIndex("User_username_key").on(table.username),
|
||||
emailKey: uniqueIndex("User_email_key").on(table.email),
|
||||
}
|
||||
});
|
||||
|
||||
export const openIdLoginFlow = pgTable("OpenIdLoginFlow", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
codeVerifier: text("codeVerifier").notNull(),
|
||||
applicationId: uuid("applicationId").references(() => application.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
issuerId: text("issuerId").notNull(),
|
||||
});
|
||||
|
||||
export const flag = pgTable("Flag", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
flagType: text("flagType").default('other').notNull(),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(),
|
||||
flaggeStatusId: uuid("flaggeStatusId").references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
flaggedUserId: uuid("flaggedUserId").references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
});
|
||||
|
||||
export const modNote = pgTable("ModNote", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
notedStatusId: uuid("notedStatusId").references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
notedUserId: uuid("notedUserId").references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
modId: uuid("modId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
note: text("note").notNull(),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(),
|
||||
});
|
||||
|
||||
export const modTag = pgTable("ModTag", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
taggedStatusId: uuid("taggedStatusId").references(() => status.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
taggedUserId: uuid("taggedUserId").references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
modId: uuid("modId").notNull().references(() => user.id, { onDelete: "cascade", onUpdate: "cascade" } ),
|
||||
tag: text("tag").notNull(),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: 'string' }).defaultNow().notNull(),
|
||||
});
|
||||
19
drizzle.config.ts
Normal file
19
drizzle.config.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { Config } from "drizzle-kit";
|
||||
import { config } from "config-manager";
|
||||
|
||||
export default {
|
||||
driver: "pg",
|
||||
out: "./drizzle",
|
||||
schema: "./drizzle/schema.ts",
|
||||
dbCredentials: {
|
||||
host: config.database.host,
|
||||
port: Number(config.database.port),
|
||||
user: config.database.username,
|
||||
password: config.database.password,
|
||||
database: config.database.database,
|
||||
},
|
||||
// Print all statements
|
||||
verbose: true,
|
||||
// Always ask for confirmation
|
||||
strict: true,
|
||||
} satisfies Config;
|
||||
462
drizzle/0000_illegal_living_lightning.sql
Normal file
462
drizzle/0000_illegal_living_lightning.sql
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
-- 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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[] DEFAULT 'RRAY[',
|
||||
"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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 uuid_generate_v7() 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 $$;
|
||||
|
||||
*/
|
||||
14
drizzle/db.ts
Normal file
14
drizzle/db.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { Client } from "pg";
|
||||
import { config } from "config-manager";
|
||||
import { drizzle } from "drizzle-orm/node-postgres";
|
||||
import * as schema from "./schema";
|
||||
|
||||
export const client = new Client({
|
||||
host: config.database.host,
|
||||
port: Number(config.database.port),
|
||||
user: config.database.username,
|
||||
password: config.database.password,
|
||||
database: config.database.database,
|
||||
});
|
||||
|
||||
export const db = drizzle(client, { schema });
|
||||
1867
drizzle/meta/0000_snapshot.json
Normal file
1867
drizzle/meta/0000_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
13
drizzle/meta/_journal.json
Normal file
13
drizzle/meta/_journal.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": "5",
|
||||
"dialect": "pg",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "5",
|
||||
"when": 1712805159664,
|
||||
"tag": "0000_illegal_living_lightning",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
677
drizzle/schema.ts
Normal file
677
drizzle/schema.ts
Normal file
|
|
@ -0,0 +1,677 @@
|
|||
import {
|
||||
pgTable,
|
||||
timestamp,
|
||||
text,
|
||||
integer,
|
||||
foreignKey,
|
||||
uuid,
|
||||
boolean,
|
||||
uniqueIndex,
|
||||
jsonb,
|
||||
index,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
|
||||
export const emoji = pgTable("Emoji", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
shortcode: text("shortcode").notNull(),
|
||||
url: text("url").notNull(),
|
||||
visibleInPicker: boolean("visible_in_picker").notNull(),
|
||||
alt: text("alt"),
|
||||
contentType: text("content_type").notNull(),
|
||||
instanceId: uuid("instanceId").references(() => instance.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const like = pgTable("Like", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
likerId: uuid("likerId")
|
||||
.notNull()
|
||||
.references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
likedId: uuid("likedId")
|
||||
.notNull()
|
||||
.references(() => status.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: "string" })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
export const lysandObject = pgTable(
|
||||
"LysandObject",
|
||||
{
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
remoteId: text("remote_id").notNull(),
|
||||
type: text("type").notNull(),
|
||||
uri: text("uri").notNull(),
|
||||
createdAt: timestamp("created_at", { precision: 3, mode: "string" })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
authorId: uuid("authorId"),
|
||||
extraData: jsonb("extra_data").notNull(),
|
||||
extensions: jsonb("extensions").notNull(),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
remoteIdKey: uniqueIndex("LysandObject_remote_id_key").on(
|
||||
table.remoteId,
|
||||
),
|
||||
uriKey: uniqueIndex("LysandObject_uri_key").on(table.uri),
|
||||
lysandObjectAuthorIdFkey: foreignKey({
|
||||
columns: [table.authorId],
|
||||
foreignColumns: [table.id],
|
||||
name: "LysandObject_authorId_fkey",
|
||||
})
|
||||
.onUpdate("cascade")
|
||||
.onDelete("cascade"),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const relationship = pgTable("Relationship", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
ownerId: uuid("ownerId")
|
||||
.notNull()
|
||||
.references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
subjectId: uuid("subjectId")
|
||||
.notNull()
|
||||
.references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
following: boolean("following").notNull(),
|
||||
showingReblogs: boolean("showingReblogs").notNull(),
|
||||
notifying: boolean("notifying").notNull(),
|
||||
followedBy: boolean("followedBy").notNull(),
|
||||
blocking: boolean("blocking").notNull(),
|
||||
blockedBy: boolean("blockedBy").notNull(),
|
||||
muting: boolean("muting").notNull(),
|
||||
mutingNotifications: boolean("mutingNotifications").notNull(),
|
||||
requested: boolean("requested").notNull(),
|
||||
domainBlocking: boolean("domainBlocking").notNull(),
|
||||
endorsed: boolean("endorsed").notNull(),
|
||||
languages: text("languages").array(),
|
||||
note: text("note").notNull(),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: "string" })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updatedAt", {
|
||||
precision: 3,
|
||||
mode: "string",
|
||||
}).notNull(),
|
||||
});
|
||||
|
||||
export const application = pgTable(
|
||||
"Application",
|
||||
{
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
name: text("name").notNull(),
|
||||
website: text("website"),
|
||||
vapidKey: text("vapid_key"),
|
||||
clientId: text("client_id").notNull(),
|
||||
secret: text("secret").notNull(),
|
||||
scopes: text("scopes").notNull(),
|
||||
redirectUris: text("redirect_uris").notNull(),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
clientIdKey: uniqueIndex("Application_client_id_key").on(
|
||||
table.clientId,
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const token = pgTable("Token", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
tokenType: text("token_type").notNull(),
|
||||
scope: text("scope").notNull(),
|
||||
accessToken: text("access_token").notNull(),
|
||||
code: text("code").notNull(),
|
||||
createdAt: timestamp("created_at", { precision: 3, mode: "string" })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
userId: uuid("userId").references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
applicationId: uuid("applicationId").references(() => application.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const attachment = pgTable("Attachment", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
url: text("url").notNull(),
|
||||
remoteUrl: text("remote_url"),
|
||||
thumbnailUrl: text("thumbnail_url"),
|
||||
mimeType: text("mime_type").notNull(),
|
||||
description: text("description"),
|
||||
blurhash: text("blurhash"),
|
||||
sha256: text("sha256"),
|
||||
fps: integer("fps"),
|
||||
duration: integer("duration"),
|
||||
width: integer("width"),
|
||||
height: integer("height"),
|
||||
size: integer("size"),
|
||||
statusId: uuid("statusId").references(() => status.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const notification = pgTable("Notification", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
type: text("type").notNull(),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: "string" })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
notifiedId: uuid("notifiedId")
|
||||
.notNull()
|
||||
.references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
accountId: uuid("accountId")
|
||||
.notNull()
|
||||
.references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
statusId: uuid("statusId").references(() => status.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const status = pgTable(
|
||||
"Status",
|
||||
{
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
uri: text("uri"),
|
||||
authorId: uuid("authorId")
|
||||
.notNull()
|
||||
.references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: "string" })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updatedAt", {
|
||||
precision: 3,
|
||||
mode: "string",
|
||||
}).notNull(),
|
||||
reblogId: uuid("reblogId"),
|
||||
content: text("content").default("").notNull(),
|
||||
contentType: text("contentType").default("text/plain").notNull(),
|
||||
visibility: text("visibility").notNull(),
|
||||
inReplyToPostId: uuid("inReplyToPostId"),
|
||||
quotingPostId: uuid("quotingPostId"),
|
||||
instanceId: uuid("instanceId").references(() => instance.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
sensitive: boolean("sensitive").notNull(),
|
||||
spoilerText: text("spoilerText").default("").notNull(),
|
||||
applicationId: uuid("applicationId").references(() => application.id, {
|
||||
onDelete: "set null",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
contentSource: text("contentSource").default("").notNull(),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
uriKey: uniqueIndex("Status_uri_key").on(table.uri),
|
||||
statusReblogIdFkey: foreignKey({
|
||||
columns: [table.reblogId],
|
||||
foreignColumns: [table.id],
|
||||
name: "Status_reblogId_fkey",
|
||||
})
|
||||
.onUpdate("cascade")
|
||||
.onDelete("cascade"),
|
||||
statusInReplyToPostIdFkey: foreignKey({
|
||||
columns: [table.inReplyToPostId],
|
||||
foreignColumns: [table.id],
|
||||
name: "Status_inReplyToPostId_fkey",
|
||||
})
|
||||
.onUpdate("cascade")
|
||||
.onDelete("set null"),
|
||||
statusQuotingPostIdFkey: foreignKey({
|
||||
columns: [table.quotingPostId],
|
||||
foreignColumns: [table.id],
|
||||
name: "Status_quotingPostId_fkey",
|
||||
})
|
||||
.onUpdate("cascade")
|
||||
.onDelete("set null"),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const instance = pgTable("Instance", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
baseUrl: text("base_url").notNull(),
|
||||
name: text("name").notNull(),
|
||||
version: text("version").notNull(),
|
||||
logo: jsonb("logo").notNull(),
|
||||
disableAutomoderation: boolean("disableAutomoderation")
|
||||
.default(false)
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
export const openIdAccount = pgTable("OpenIdAccount", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
userId: uuid("userId").references(() => user.id, {
|
||||
onDelete: "set null",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
serverId: text("serverId").notNull(),
|
||||
issuerId: text("issuerId").notNull(),
|
||||
});
|
||||
|
||||
export const user = pgTable(
|
||||
"User",
|
||||
{
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
uri: text("uri"),
|
||||
username: text("username").notNull(),
|
||||
displayName: text("displayName").notNull(),
|
||||
password: text("password"),
|
||||
email: text("email"),
|
||||
note: text("note").default("").notNull(),
|
||||
isAdmin: boolean("isAdmin").default(false).notNull(),
|
||||
endpoints: jsonb("endpoints"),
|
||||
source: jsonb("source").notNull(),
|
||||
avatar: text("avatar").notNull(),
|
||||
header: text("header").notNull(),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: "string" })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updatedAt", {
|
||||
precision: 3,
|
||||
mode: "string",
|
||||
})
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
isBot: boolean("isBot").default(false).notNull(),
|
||||
isLocked: boolean("isLocked").default(false).notNull(),
|
||||
isDiscoverable: boolean("isDiscoverable").default(false).notNull(),
|
||||
sanctions: text("sanctions").default("RRAY[").array(),
|
||||
publicKey: text("publicKey").notNull(),
|
||||
privateKey: text("privateKey"),
|
||||
instanceId: uuid("instanceId").references(() => instance.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
disableAutomoderation: boolean("disableAutomoderation")
|
||||
.default(false)
|
||||
.notNull(),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
uriKey: uniqueIndex("User_uri_key").on(table.uri),
|
||||
usernameKey: uniqueIndex("User_username_key").on(table.username),
|
||||
emailKey: uniqueIndex("User_email_key").on(table.email),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const openIdLoginFlow = pgTable("OpenIdLoginFlow", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
codeVerifier: text("codeVerifier").notNull(),
|
||||
applicationId: uuid("applicationId").references(() => application.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
issuerId: text("issuerId").notNull(),
|
||||
});
|
||||
|
||||
export const flag = pgTable("Flag", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
flagType: text("flagType").default("other").notNull(),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: "string" })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
flaggeStatusId: uuid("flaggeStatusId").references(() => status.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
flaggedUserId: uuid("flaggedUserId").references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
});
|
||||
|
||||
export const modNote = pgTable("ModNote", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
notedStatusId: uuid("notedStatusId").references(() => status.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
notedUserId: uuid("notedUserId").references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
modId: uuid("modId")
|
||||
.notNull()
|
||||
.references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
note: text("note").notNull(),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: "string" })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
export const modTag = pgTable("ModTag", {
|
||||
id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(),
|
||||
taggedStatusId: uuid("taggedStatusId").references(() => status.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
taggedUserId: uuid("taggedUserId").references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
modId: uuid("modId")
|
||||
.notNull()
|
||||
.references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
tag: text("tag").notNull(),
|
||||
createdAt: timestamp("createdAt", { precision: 3, mode: "string" })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
export const emojiToUser = pgTable(
|
||||
"_EmojiToUser",
|
||||
{
|
||||
a: uuid("A")
|
||||
.notNull()
|
||||
.references(() => emoji.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
b: uuid("B")
|
||||
.notNull()
|
||||
.references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
abUnique: uniqueIndex("_EmojiToUser_AB_unique").on(
|
||||
table.a,
|
||||
table.b,
|
||||
),
|
||||
bIdx: index().on(table.b),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const emojiToUserRelations = relations(emojiToUser, ({ one }) => ({
|
||||
emoji: one(emoji, {
|
||||
fields: [emojiToUser.a],
|
||||
references: [emoji.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [emojiToUser.b],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const emojiToStatus = pgTable(
|
||||
"_EmojiToStatus",
|
||||
{
|
||||
a: uuid("A")
|
||||
.notNull()
|
||||
.references(() => emoji.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
b: uuid("B")
|
||||
.notNull()
|
||||
.references(() => status.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
abUnique: uniqueIndex("_EmojiToStatus_AB_unique").on(
|
||||
table.a,
|
||||
table.b,
|
||||
),
|
||||
bIdx: index().on(table.b),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const statusToUser = pgTable(
|
||||
"_StatusToUser",
|
||||
{
|
||||
a: uuid("A")
|
||||
.notNull()
|
||||
.references(() => status.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
b: uuid("B")
|
||||
.notNull()
|
||||
.references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
abUnique: uniqueIndex("_StatusToUser_AB_unique").on(
|
||||
table.a,
|
||||
table.b,
|
||||
),
|
||||
bIdx: index().on(table.b),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const userPinnedNotes = pgTable(
|
||||
"_UserPinnedNotes",
|
||||
{
|
||||
a: uuid("A")
|
||||
.notNull()
|
||||
.references(() => status.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
b: uuid("B")
|
||||
.notNull()
|
||||
.references(() => user.id, {
|
||||
onDelete: "cascade",
|
||||
onUpdate: "cascade",
|
||||
}),
|
||||
},
|
||||
(table) => {
|
||||
return {
|
||||
abUnique: uniqueIndex("_UserPinnedNotes_AB_unique").on(
|
||||
table.a,
|
||||
table.b,
|
||||
),
|
||||
bIdx: index().on(table.b),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const attachmentRelations = relations(attachment, ({ one }) => ({
|
||||
status: one(status, {
|
||||
fields: [attachment.statusId],
|
||||
references: [status.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const userRelations = relations(user, ({ many, one }) => ({
|
||||
emojis: many(emojiToUser),
|
||||
pinnedNotes: many(userPinnedNotes),
|
||||
statuses: many(status, {
|
||||
relationName: "StatusToAuthor",
|
||||
}),
|
||||
likes: many(like),
|
||||
relationships: many(relationship, {
|
||||
relationName: "RelationshipToOwner",
|
||||
}),
|
||||
relationshipSubjects: many(relationship, {
|
||||
relationName: "RelationshipToSubject",
|
||||
}),
|
||||
notifications: many(notification),
|
||||
openIdAccounts: many(openIdAccount),
|
||||
flags: many(flag),
|
||||
modNotes: many(modNote),
|
||||
modTags: many(modTag),
|
||||
tokens: many(token),
|
||||
instance: one(instance, {
|
||||
fields: [user.instanceId],
|
||||
references: [instance.id],
|
||||
}),
|
||||
mentionedIn: many(statusToUser),
|
||||
}));
|
||||
|
||||
export const relationshipRelations = relations(relationship, ({ one }) => ({
|
||||
owner: one(user, {
|
||||
fields: [relationship.ownerId],
|
||||
references: [user.id],
|
||||
relationName: "RelationshipToOwner",
|
||||
}),
|
||||
subject: one(user, {
|
||||
fields: [relationship.subjectId],
|
||||
references: [user.id],
|
||||
relationName: "RelationshipToSubject",
|
||||
}),
|
||||
}));
|
||||
|
||||
export const tokenRelations = relations(token, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [token.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
application: one(application, {
|
||||
fields: [token.applicationId],
|
||||
references: [application.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const statusToUserRelations = relations(statusToUser, ({ one }) => ({
|
||||
status: one(status, {
|
||||
fields: [statusToUser.a],
|
||||
references: [status.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [statusToUser.b],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const userPinnedNotesRelations = relations(
|
||||
userPinnedNotes,
|
||||
({ one }) => ({
|
||||
status: one(status, {
|
||||
fields: [userPinnedNotes.a],
|
||||
references: [status.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [userPinnedNotes.b],
|
||||
references: [user.id],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
export const statusRelations = relations(status, ({ many, one }) => ({
|
||||
emojis: many(emojiToStatus),
|
||||
author: one(user, {
|
||||
fields: [status.authorId],
|
||||
references: [user.id],
|
||||
relationName: "StatusToAuthor",
|
||||
}),
|
||||
attachments: many(attachment),
|
||||
mentions: many(statusToUser),
|
||||
reblog: one(status, {
|
||||
fields: [status.reblogId],
|
||||
references: [status.id],
|
||||
relationName: "StatusToReblog",
|
||||
}),
|
||||
usersThatHavePinned: many(userPinnedNotes),
|
||||
inReplyTo: one(status, {
|
||||
fields: [status.inReplyToPostId],
|
||||
references: [status.id],
|
||||
relationName: "StatusToReplying",
|
||||
}),
|
||||
quoting: one(status, {
|
||||
fields: [status.quotingPostId],
|
||||
references: [status.id],
|
||||
relationName: "StatusToQuoting",
|
||||
}),
|
||||
application: one(application, {
|
||||
fields: [status.applicationId],
|
||||
references: [application.id],
|
||||
}),
|
||||
quotes: many(status, {
|
||||
relationName: "StatusToQuoting",
|
||||
}),
|
||||
replies: many(status, {
|
||||
relationName: "StatusToReplying",
|
||||
}),
|
||||
likes: many(like),
|
||||
reblogs: many(status, {
|
||||
relationName: "StatusToReblog",
|
||||
}),
|
||||
}));
|
||||
|
||||
export const likeRelations = relations(like, ({ one }) => ({
|
||||
liker: one(user, {
|
||||
fields: [like.likerId],
|
||||
references: [user.id],
|
||||
}),
|
||||
liked: one(status, {
|
||||
fields: [like.likedId],
|
||||
references: [status.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const emojiRelations = relations(emoji, ({ one, many }) => ({
|
||||
instance: one(instance, {
|
||||
fields: [emoji.instanceId],
|
||||
references: [instance.id],
|
||||
}),
|
||||
users: many(emojiToUser),
|
||||
statuses: many(emojiToStatus),
|
||||
}));
|
||||
|
||||
export const instanceRelations = relations(instance, ({ many }) => ({
|
||||
users: many(user),
|
||||
statuses: many(status),
|
||||
emojis: many(emoji),
|
||||
}));
|
||||
|
||||
export const emojiToStatusRelations = relations(emojiToStatus, ({ one }) => ({
|
||||
emoji: one(emoji, {
|
||||
fields: [emojiToStatus.a],
|
||||
references: [emoji.id],
|
||||
}),
|
||||
status: one(status, {
|
||||
fields: [emojiToStatus.b],
|
||||
references: [status.id],
|
||||
}),
|
||||
}));
|
||||
20
index.ts
20
index.ts
|
|
@ -2,13 +2,15 @@ import { exists, mkdir, writeFile } from "node:fs/promises";
|
|||
import { dirname } from "node:path";
|
||||
import { connectMeili } from "@meilisearch";
|
||||
import { moduleIsEntry } from "@module";
|
||||
import type { PrismaClientInitializationError } from "@prisma/client/runtime/library";
|
||||
import { initializeRedisCache } from "@redis";
|
||||
import { config } from "config-manager";
|
||||
import { LogLevel, LogManager, MultiLogManager } from "log-manager";
|
||||
import { client } from "~database/datasource";
|
||||
import { createServer } from "~server";
|
||||
import { db, client as pgClient } from "~drizzle/db";
|
||||
import { status } from "~drizzle/schema";
|
||||
import { count, sql } from "drizzle-orm";
|
||||
|
||||
await pgClient.connect();
|
||||
const timeAtStart = performance.now();
|
||||
|
||||
// Create requests file if it doesnt exist
|
||||
|
|
@ -43,16 +45,18 @@ if (config.meilisearch.enabled) {
|
|||
await connectMeili(dualLogger);
|
||||
}
|
||||
|
||||
if (redisCache) {
|
||||
client.$use(redisCache);
|
||||
}
|
||||
|
||||
// Check if database is reachable
|
||||
let postCount = 0;
|
||||
try {
|
||||
postCount = await client.status.count();
|
||||
postCount = (
|
||||
await db
|
||||
.select({
|
||||
count: count(),
|
||||
})
|
||||
.from(status)
|
||||
)[0].count;
|
||||
} catch (e) {
|
||||
const error = e as PrismaClientInitializationError;
|
||||
const error = e as Error;
|
||||
await logger.logError(LogLevel.CRITICAL, "Database", error);
|
||||
await consoleLogger.logError(LogLevel.CRITICAL, "Database", error);
|
||||
process.exit(1);
|
||||
|
|
|
|||
267
package.json
267
package.json
|
|
@ -1,134 +1,139 @@
|
|||
{
|
||||
"name": "lysand",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"version": "0.4.0",
|
||||
"description": "A project to build a federated social network",
|
||||
"author": {
|
||||
"email": "contact@cpluspatch.com",
|
||||
"name": "CPlusPatch",
|
||||
"url": "https://cpluspatch.com"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/lysand-org/lysand/issues"
|
||||
},
|
||||
"icon": "https://github.com/lysand-org/lysand",
|
||||
"license": "AGPL-3.0",
|
||||
"keywords": ["federated", "activitypub", "bun"],
|
||||
"workspaces": ["packages/*"],
|
||||
"maintainers": [
|
||||
{
|
||||
"email": "contact@cpluspatch.com",
|
||||
"name": "CPlusPatch",
|
||||
"url": "https://cpluspatch.com"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/lysand-org/lysand.git"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "bun run --watch index.ts",
|
||||
"vite:dev": "bunx --bun vite pages",
|
||||
"vite:build": "bunx --bun vite build pages",
|
||||
"fe:dev": "bun --bun nuxt dev packages/frontend",
|
||||
"fe:build": "bun --bun nuxt build packages/frontend",
|
||||
"fe:analyze": "bun --bun nuxt analyze packages/frontend",
|
||||
"start": "NITRO_PORT=5173 bun run dist/frontend/server/index.mjs & NODE_ENV=production bun run dist/index.js --prod",
|
||||
"migrate-dev": "bun prisma migrate dev",
|
||||
"migrate": "bun prisma migrate deploy",
|
||||
"lint": "bunx --bun eslint --config .eslintrc.cjs --ext .ts .",
|
||||
"prod-build": "bun run build.ts",
|
||||
"prisma": "DATABASE_URL=$(bun run prisma.ts) bunx prisma",
|
||||
"generate": "bun prisma generate",
|
||||
"benchmark:timeline": "bun run benchmarks/timelines.ts",
|
||||
"cloc": "cloc . --exclude-dir node_modules,dist",
|
||||
"cli": "bun run cli.ts"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@biomejs/biome",
|
||||
"@fortawesome/fontawesome-common-types",
|
||||
"@fortawesome/free-regular-svg-icons",
|
||||
"@fortawesome/free-solid-svg-icons",
|
||||
"@prisma/client",
|
||||
"@prisma/engines",
|
||||
"esbuild",
|
||||
"json-editor-vue",
|
||||
"msgpackr-extract",
|
||||
"nuxt-app",
|
||||
"prisma",
|
||||
"sharp",
|
||||
"vue-demi"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.6.4",
|
||||
"@img/sharp-wasm32": "^0.33.3",
|
||||
"@julr/unocss-preset-forms": "^0.1.0",
|
||||
"@nuxtjs/seo": "^2.0.0-rc.10",
|
||||
"@nuxtjs/tailwindcss": "^6.11.4",
|
||||
"@types/cli-table": "^0.3.4",
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
"@types/ioredis": "^5.0.0",
|
||||
"@types/jsonld": "^1.5.13",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@typescript-eslint/eslint-plugin": "latest",
|
||||
"@unocss/cli": "latest",
|
||||
"@unocss/transformer-directives": "^0.59.0",
|
||||
"@vitejs/plugin-vue": "latest",
|
||||
"@vueuse/head": "^2.0.0",
|
||||
"activitypub-types": "^1.0.3",
|
||||
"bun-types": "latest",
|
||||
"shiki": "^1.2.4",
|
||||
"typescript": "latest",
|
||||
"unocss": "latest",
|
||||
"untyped": "^1.4.2",
|
||||
"vite": "^5.2.8",
|
||||
"vite-ssr": "^0.17.1",
|
||||
"vue": "^3.3.9",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-tsc": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.461.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@json2csv/plainjs": "^7.0.6",
|
||||
"@prisma/client": "^5.6.0",
|
||||
"blurhash": "^2.0.5",
|
||||
"bullmq": "latest",
|
||||
"chalk": "^5.3.0",
|
||||
"cli-parser": "workspace:*",
|
||||
"cli-table": "^0.3.11",
|
||||
"config-manager": "workspace:*",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"extract-zip": "^2.0.1",
|
||||
"html-to-text": "^9.0.5",
|
||||
"ioredis": "^5.3.2",
|
||||
"ip-matching": "^2.1.2",
|
||||
"iso-639-1": "^3.1.0",
|
||||
"isomorphic-dompurify": "latest",
|
||||
"jsonld": "^8.3.1",
|
||||
"linkify-html": "^4.1.3",
|
||||
"linkify-string": "^4.1.3",
|
||||
"linkifyjs": "^4.1.3",
|
||||
"log-manager": "workspace:*",
|
||||
"marked": "latest",
|
||||
"media-manager": "workspace:*",
|
||||
"megalodon": "^10.0.0",
|
||||
"meilisearch": "latest",
|
||||
"merge-deep-ts": "^1.2.6",
|
||||
"mime-types": "^2.1.35",
|
||||
"next-route-matcher": "^1.0.1",
|
||||
"oauth4webapi": "^2.4.0",
|
||||
"prisma": "^5.6.0",
|
||||
"prisma-json-types-generator": "^3.0.4",
|
||||
"prisma-redis-middleware": "^4.8.0",
|
||||
"request-parser": "workspace:*",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "^0.33.3",
|
||||
"strip-ansi": "^7.1.0"
|
||||
"name": "lysand",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"version": "0.4.0",
|
||||
"description": "A project to build a federated social network",
|
||||
"author": {
|
||||
"email": "contact@cpluspatch.com",
|
||||
"name": "CPlusPatch",
|
||||
"url": "https://cpluspatch.com"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/lysand-org/lysand/issues"
|
||||
},
|
||||
"icon": "https://github.com/lysand-org/lysand",
|
||||
"license": "AGPL-3.0",
|
||||
"keywords": ["federated", "activitypub", "bun"],
|
||||
"workspaces": ["packages/*"],
|
||||
"maintainers": [
|
||||
{
|
||||
"email": "contact@cpluspatch.com",
|
||||
"name": "CPlusPatch",
|
||||
"url": "https://cpluspatch.com"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/lysand-org/lysand.git"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "bun run --watch index.ts",
|
||||
"vite:dev": "bunx --bun vite pages",
|
||||
"vite:build": "bunx --bun vite build pages",
|
||||
"fe:dev": "bun --bun nuxt dev packages/frontend",
|
||||
"fe:build": "bun --bun nuxt build packages/frontend",
|
||||
"fe:analyze": "bun --bun nuxt analyze packages/frontend",
|
||||
"start": "NITRO_PORT=5173 bun run dist/frontend/server/index.mjs & NODE_ENV=production bun run dist/index.js --prod",
|
||||
"migrate-dev": "bun prisma migrate dev",
|
||||
"migrate": "bun prisma migrate deploy",
|
||||
"lint": "bunx --bun eslint --config .eslintrc.cjs --ext .ts .",
|
||||
"prod-build": "bun run build.ts",
|
||||
"prisma": "DATABASE_URL=$(bun run prisma.ts) bunx prisma",
|
||||
"generate": "bun prisma generate",
|
||||
"benchmark:timeline": "bun run benchmarks/timelines.ts",
|
||||
"cloc": "cloc . --exclude-dir node_modules,dist",
|
||||
"cli": "bun run cli.ts"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"@biomejs/biome",
|
||||
"@fortawesome/fontawesome-common-types",
|
||||
"@fortawesome/free-regular-svg-icons",
|
||||
"@fortawesome/free-solid-svg-icons",
|
||||
"@prisma/client",
|
||||
"@prisma/engines",
|
||||
"es5-ext",
|
||||
"esbuild",
|
||||
"json-editor-vue",
|
||||
"msgpackr-extract",
|
||||
"nuxt-app",
|
||||
"prisma",
|
||||
"sharp",
|
||||
"vue-demi"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.6.4",
|
||||
"@img/sharp-wasm32": "^0.33.3",
|
||||
"@julr/unocss-preset-forms": "^0.1.0",
|
||||
"@nuxtjs/seo": "^2.0.0-rc.10",
|
||||
"@nuxtjs/tailwindcss": "^6.11.4",
|
||||
"@types/cli-table": "^0.3.4",
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
"@types/ioredis": "^5.0.0",
|
||||
"@types/jsonld": "^1.5.13",
|
||||
"@types/mime-types": "^2.1.4",
|
||||
"@types/pg": "^8.11.5",
|
||||
"@typescript-eslint/eslint-plugin": "latest",
|
||||
"@unocss/cli": "latest",
|
||||
"@unocss/transformer-directives": "^0.59.0",
|
||||
"@vitejs/plugin-vue": "latest",
|
||||
"@vueuse/head": "^2.0.0",
|
||||
"activitypub-types": "^1.0.3",
|
||||
"bun-types": "latest",
|
||||
"drizzle-kit": "^0.20.14",
|
||||
"shiki": "^1.2.4",
|
||||
"typescript": "latest",
|
||||
"unocss": "latest",
|
||||
"untyped": "^1.4.2",
|
||||
"vite": "^5.2.8",
|
||||
"vite-ssr": "^0.17.1",
|
||||
"vue": "^3.3.9",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-tsc": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.461.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@json2csv/plainjs": "^7.0.6",
|
||||
"@prisma/client": "^5.6.0",
|
||||
"blurhash": "^2.0.5",
|
||||
"bullmq": "latest",
|
||||
"chalk": "^5.3.0",
|
||||
"cli-parser": "workspace:*",
|
||||
"cli-table": "^0.3.11",
|
||||
"config-manager": "workspace:*",
|
||||
"drizzle-orm": "^0.30.7",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"extract-zip": "^2.0.1",
|
||||
"html-to-text": "^9.0.5",
|
||||
"ioredis": "^5.3.2",
|
||||
"ip-matching": "^2.1.2",
|
||||
"iso-639-1": "^3.1.0",
|
||||
"isomorphic-dompurify": "latest",
|
||||
"jsonld": "^8.3.1",
|
||||
"linkify-html": "^4.1.3",
|
||||
"linkify-string": "^4.1.3",
|
||||
"linkifyjs": "^4.1.3",
|
||||
"log-manager": "workspace:*",
|
||||
"marked": "latest",
|
||||
"media-manager": "workspace:*",
|
||||
"megalodon": "^10.0.0",
|
||||
"meilisearch": "latest",
|
||||
"merge-deep-ts": "^1.2.6",
|
||||
"mime-types": "^2.1.35",
|
||||
"next-route-matcher": "^1.0.1",
|
||||
"oauth4webapi": "^2.4.0",
|
||||
"pg": "^8.11.5",
|
||||
"prisma": "^5.6.0",
|
||||
"prisma-json-types-generator": "^3.0.4",
|
||||
"prisma-redis-middleware": "^4.8.0",
|
||||
"request-parser": "workspace:*",
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "^0.33.3",
|
||||
"strip-ansi": "^7.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { fetchTimeline } from "@timelines";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
findManyStatuses,
|
||||
statusToAPI,
|
||||
type StatusWithRelations,
|
||||
} from "~database/entities/Status";
|
||||
import { findFirstUser } from "~database/entities/User";
|
||||
import {
|
||||
statusAndUserRelations,
|
||||
userRelations,
|
||||
|
|
@ -52,38 +55,30 @@ export default apiRoute<{
|
|||
pinned,
|
||||
} = extraData.parsedRequest;
|
||||
|
||||
const user = await client.user.findUnique({
|
||||
where: { id },
|
||||
include: userRelations,
|
||||
const user = await findFirstUser({
|
||||
where: (user, { eq }) => eq(user.id, id),
|
||||
});
|
||||
|
||||
if (!user) return errorResponse("User not found", 404);
|
||||
|
||||
if (pinned) {
|
||||
const { objects, link } = await fetchTimeline<StatusWithRelations>(
|
||||
client.status,
|
||||
findManyStatuses,
|
||||
{
|
||||
where: {
|
||||
authorId: id,
|
||||
reblogId: null,
|
||||
pinnedBy: {
|
||||
some: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
// If only_media is true, only return statuses with attachments
|
||||
attachments: only_media ? { some: {} } : undefined,
|
||||
id: {
|
||||
lt: max_id,
|
||||
gt: min_id,
|
||||
gte: since_id,
|
||||
},
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
take: Number(limit),
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
// @ts-ignore
|
||||
where: (status, { and, lt, gt, gte, eq, sql }) =>
|
||||
and(
|
||||
max_id ? lt(status.id, max_id) : undefined,
|
||||
since_id ? gte(status.id, since_id) : undefined,
|
||||
min_id ? gt(status.id, min_id) : undefined,
|
||||
eq(status.authorId, id),
|
||||
sql`EXISTS (SELECT 1 FROM _UserPinnedNotes WHERE _UserPinnedNotes.status_id = ${status.id} AND _UserPinnedNotes.user_id = ${user.id})`,
|
||||
only_media
|
||||
? sql`EXISTS (SELECT 1 FROM "Attachment" WHERE "Attachment"."statusId" = ${status.id})`
|
||||
: undefined,
|
||||
),
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
orderBy: (status, { desc }) => desc(status.id),
|
||||
},
|
||||
req,
|
||||
);
|
||||
|
|
@ -100,22 +95,22 @@ export default apiRoute<{
|
|||
}
|
||||
|
||||
const { objects, link } = await fetchTimeline<StatusWithRelations>(
|
||||
client.status,
|
||||
findManyStatuses,
|
||||
{
|
||||
where: {
|
||||
authorId: id,
|
||||
reblogId: exclude_reblogs ? null : undefined,
|
||||
id: {
|
||||
lt: max_id,
|
||||
gt: min_id,
|
||||
gte: since_id,
|
||||
},
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
take: Number(limit),
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
// @ts-ignore
|
||||
where: (status, { and, lt, gt, gte, eq, sql }) =>
|
||||
and(
|
||||
max_id ? lt(status.id, max_id) : undefined,
|
||||
since_id ? gte(status.id, since_id) : undefined,
|
||||
min_id ? gt(status.id, min_id) : undefined,
|
||||
eq(status.authorId, id),
|
||||
only_media
|
||||
? sql`EXISTS (SELECT 1 FROM "Attachment" WHERE "Attachment"."statusId" = ${status.id})`
|
||||
: undefined,
|
||||
exclude_reblogs ? eq(status.reblogId, null) : undefined,
|
||||
),
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
orderBy: (status, { desc }) => desc(status.id),
|
||||
},
|
||||
req,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
return jsonResponse({
|
||||
name: application.name,
|
||||
website: application.website,
|
||||
vapid_key: application.vapid_key,
|
||||
redirect_uris: application.redirect_uris,
|
||||
vapid_key: application.vapidKey,
|
||||
redirect_uris: application.redirectUris,
|
||||
scopes: application.scopes,
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { fetchTimeline } from "@timelines";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI, type UserWithRelations } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
import {
|
||||
findManyUsers,
|
||||
userToAPI,
|
||||
type UserWithRelations,
|
||||
} from "~database/entities/User";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -28,25 +30,22 @@ export default apiRoute<{
|
|||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const { max_id, since_id, limit = 40 } = extraData.parsedRequest;
|
||||
const { max_id, since_id, min_id, limit = 40 } = extraData.parsedRequest;
|
||||
|
||||
const { objects: blocks, link } = await fetchTimeline<UserWithRelations>(
|
||||
client.user,
|
||||
findManyUsers,
|
||||
{
|
||||
where: {
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
ownerId: user.id,
|
||||
blocking: true,
|
||||
},
|
||||
},
|
||||
id: {
|
||||
lt: max_id,
|
||||
gte: since_id,
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
take: Number(limit),
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
where: (subject, { lt, gte, gt, and, sql }) =>
|
||||
and(
|
||||
max_id ? lt(subject.id, max_id) : undefined,
|
||||
since_id ? gte(subject.id, since_id) : undefined,
|
||||
min_id ? gt(subject.id, min_id) : undefined,
|
||||
sql`EXISTS (SELECT 1 FROM "Relationship" WHERE "Relationship"."subjectId" = ${subject.id} AND "Relationship"."ownerId" = ${user.id} AND "Relationship"."blocking" = true)`,
|
||||
),
|
||||
limit: Number(limit),
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
orderBy: (subject, { desc }) => desc(subject.id),
|
||||
},
|
||||
req,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { apiRoute, applyConfig } from "@api";
|
|||
import { jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { emojiToAPI } from "~database/entities/Emoji";
|
||||
import { db } from "~drizzle/db";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -16,9 +17,10 @@ export const meta = applyConfig({
|
|||
});
|
||||
|
||||
export default apiRoute(async () => {
|
||||
const emojis = await client.emoji.findMany({
|
||||
where: {
|
||||
instanceId: null,
|
||||
const emojis = await db.query.emoji.findMany({
|
||||
where: (emoji, { isNull }) => isNull(emoji.instanceId),
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { jsonResponse } from "@response";
|
||||
import { count, isNull } from "drizzle-orm";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI } from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
import { db } from "~drizzle/db";
|
||||
import { status, user } from "~drizzle/schema";
|
||||
import manifest from "~package.json";
|
||||
import type { APIInstance } from "~types/entities/instance";
|
||||
|
||||
|
|
@ -24,16 +27,23 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
// Get software version from package.json
|
||||
const version = manifest.version;
|
||||
|
||||
const statusCount = await client.status.count({
|
||||
where: {
|
||||
instanceId: null,
|
||||
},
|
||||
});
|
||||
const userCount = await client.user.count({
|
||||
where: {
|
||||
instanceId: null,
|
||||
},
|
||||
});
|
||||
const statusCount = (
|
||||
await db
|
||||
.select({
|
||||
count: count(),
|
||||
})
|
||||
.from(status)
|
||||
.where(isNull(status.instanceId))
|
||||
)[0].count;
|
||||
|
||||
const userCount = (
|
||||
await db
|
||||
.select({
|
||||
count: count(),
|
||||
})
|
||||
.from(user)
|
||||
.where(isNull(user.instanceId))
|
||||
)[0].count;
|
||||
|
||||
// Get the first created admin user
|
||||
const contactAccount = await client.user.findFirst({
|
||||
|
|
@ -47,10 +57,6 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
include: userRelations,
|
||||
});
|
||||
|
||||
if (!contactAccount) {
|
||||
throw new Error("No admin user found");
|
||||
}
|
||||
|
||||
// Get user that have posted once in the last 30 days
|
||||
const monthlyActiveUsers = await client.user.count({
|
||||
where: {
|
||||
|
|
@ -189,7 +195,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
},
|
||||
vapid_public_key: "",
|
||||
},
|
||||
contact_account: userToAPI(contactAccount),
|
||||
// @ts-expect-error Sometimes there just isnt an admin
|
||||
contact_account: contactAccount ? userToAPI(contactAccount) : undefined,
|
||||
} satisfies APIInstance & {
|
||||
pleroma: object;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ import { apiRoute, applyConfig } from "@api";
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { fetchTimeline } from "@timelines";
|
||||
import { client } from "~database/datasource";
|
||||
import { userToAPI, type UserWithRelations } from "~database/entities/User";
|
||||
import {
|
||||
findManyUsers,
|
||||
userToAPI,
|
||||
type UserWithRelations,
|
||||
} from "~database/entities/User";
|
||||
import { userRelations } from "~database/entities/relations";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -21,30 +25,28 @@ export const meta = applyConfig({
|
|||
export default apiRoute<{
|
||||
max_id?: string;
|
||||
since_id?: string;
|
||||
min_id?: string;
|
||||
limit?: number;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { user } = extraData.auth;
|
||||
const { max_id, since_id, limit = 40 } = extraData.parsedRequest;
|
||||
const { max_id, since_id, limit = 40, min_id } = extraData.parsedRequest;
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const { objects: blocks, link } = await fetchTimeline<UserWithRelations>(
|
||||
client.user,
|
||||
findManyUsers,
|
||||
{
|
||||
where: {
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
ownerId: user.id,
|
||||
muting: true,
|
||||
},
|
||||
},
|
||||
id: {
|
||||
lt: max_id,
|
||||
gte: since_id,
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
take: Number(limit),
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
where: (subject, { lt, gte, gt, and, sql }) =>
|
||||
and(
|
||||
max_id ? lt(subject.id, max_id) : undefined,
|
||||
since_id ? gte(subject.id, since_id) : undefined,
|
||||
min_id ? gt(subject.id, min_id) : undefined,
|
||||
sql`EXISTS (SELECT 1 FROM "Relationship" WHERE "Relationship"."subjectId" = ${subject.id} AND "Relationship"."ownerId" = ${user.id} AND "Relationship"."muting" = true)`,
|
||||
),
|
||||
limit: Number(limit),
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
orderBy: (subject, { desc }) => desc(subject.id),
|
||||
},
|
||||
req,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import type { Relationship } from "~database/entities/Relationship";
|
||||
import {
|
||||
findFirstStatuses,
|
||||
getAncestors,
|
||||
getDescendants,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
import { db } from "~drizzle/db";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -30,16 +31,47 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const foundStatus = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
const foundStatus = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
|
||||
if (!foundStatus) return errorResponse("Record not found", 404);
|
||||
|
||||
const relations = user
|
||||
? await db.query.relationship.findMany({
|
||||
where: (relationship, { eq }) =>
|
||||
eq(relationship.ownerId, user.id),
|
||||
})
|
||||
: null;
|
||||
|
||||
const relationSubjects = user
|
||||
? await db.query.relationship.findMany({
|
||||
where: (relationship, { eq }) =>
|
||||
eq(relationship.subjectId, user.id),
|
||||
})
|
||||
: null;
|
||||
|
||||
// Get all ancestors
|
||||
const ancestors = await getAncestors(foundStatus, user);
|
||||
const descendants = await getDescendants(foundStatus, user);
|
||||
const ancestors = await getAncestors(
|
||||
foundStatus,
|
||||
user
|
||||
? {
|
||||
...user,
|
||||
relationships: relations as Relationship[],
|
||||
relationshipSubjects: relationSubjects as Relationship[],
|
||||
}
|
||||
: null,
|
||||
);
|
||||
const descendants = await getDescendants(
|
||||
foundStatus,
|
||||
user
|
||||
? {
|
||||
...user,
|
||||
relationships: relations as Relationship[],
|
||||
relationshipSubjects: relationSubjects as Relationship[],
|
||||
}
|
||||
: null,
|
||||
);
|
||||
|
||||
return jsonResponse({
|
||||
ancestors: await Promise.all(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ import { apiRoute, applyConfig } from "@api";
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { createLike } from "~database/entities/Like";
|
||||
import { isViewableByUser, statusToAPI } from "~database/entities/Status";
|
||||
import {
|
||||
findFirstStatuses,
|
||||
isViewableByUser,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
import type { APIStatus } from "~types/entities/status";
|
||||
|
||||
|
|
@ -28,9 +32,8 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
const status = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
|
|
@ -51,6 +54,6 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
return jsonResponse({
|
||||
...(await statusToAPI(status, user)),
|
||||
favourited: true,
|
||||
favourites_count: status._count.likes + 1,
|
||||
favourites_count: status.likeCount + 1,
|
||||
} as APIStatus);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { fetchTimeline } from "@timelines";
|
||||
import { client } from "~database/datasource";
|
||||
import { isViewableByUser } from "~database/entities/Status";
|
||||
import { userToAPI, type UserWithRelations } from "~database/entities/User";
|
||||
import { findFirstStatuses, isViewableByUser } from "~database/entities/Status";
|
||||
import {
|
||||
statusAndUserRelations,
|
||||
userRelations,
|
||||
} from "~database/entities/relations";
|
||||
findManyUsers,
|
||||
userToAPI,
|
||||
type UserWithRelations,
|
||||
} from "~database/entities/User";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -34,9 +33,8 @@ export default apiRoute<{
|
|||
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
const status = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
|
|
@ -50,32 +48,18 @@ export default apiRoute<{
|
|||
if (limit < 1) return errorResponse("Invalid limit", 400);
|
||||
|
||||
const { objects, link } = await fetchTimeline<UserWithRelations>(
|
||||
client.user,
|
||||
findManyUsers,
|
||||
{
|
||||
where: {
|
||||
likes: {
|
||||
some: {
|
||||
likedId: status.id,
|
||||
},
|
||||
},
|
||||
id: {
|
||||
lt: max_id,
|
||||
gte: since_id,
|
||||
gt: min_id,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
...userRelations,
|
||||
likes: {
|
||||
where: {
|
||||
likedId: status.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
take: Number(limit),
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
// @ts-ignore
|
||||
where: (liker, { and, lt, gt, gte, eq, sql }) =>
|
||||
and(
|
||||
max_id ? lt(liker.id, max_id) : undefined,
|
||||
since_id ? gte(liker.id, since_id) : undefined,
|
||||
min_id ? gt(liker.id, min_id) : undefined,
|
||||
sql`EXISTS (SELECT 1 FROM "Like" WHERE "Like"."likedId" = ${status.id} AND "Like"."likerId" = ${liker.id})`,
|
||||
),
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
orderBy: (liker, { desc }) => desc(liker.id),
|
||||
},
|
||||
req,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { sanitizeHtml } from "@sanitization";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { parse } from "marked";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
editStatus,
|
||||
findFirstStatuses,
|
||||
isViewableByUser,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
import { db } from "~drizzle/db";
|
||||
import { status } from "~drizzle/schema";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET", "DELETE", "PUT"],
|
||||
|
|
@ -42,37 +45,32 @@ export default apiRoute<{
|
|||
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
const foundStatus = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
|
||||
const config = await extraData.configManager.getConfig();
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
if (!foundStatus || !isViewableByUser(foundStatus, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
if (req.method === "GET") {
|
||||
return jsonResponse(await statusToAPI(status));
|
||||
return jsonResponse(await statusToAPI(foundStatus));
|
||||
}
|
||||
if (req.method === "DELETE") {
|
||||
if (status.authorId !== user?.id) {
|
||||
if (foundStatus.authorId !== user?.id) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
// TODO: Implement delete and redraft functionality
|
||||
|
||||
// Get associated Status object
|
||||
|
||||
// Delete status and all associated objects
|
||||
await client.status.delete({
|
||||
where: { id },
|
||||
});
|
||||
await db.delete(status).where(eq(status.id, id));
|
||||
|
||||
return jsonResponse(
|
||||
{
|
||||
...(await statusToAPI(status, user)),
|
||||
...(await statusToAPI(foundStatus, user)),
|
||||
// TODO: Add
|
||||
// text: Add source text
|
||||
// poll: Add source poll
|
||||
|
|
@ -82,7 +80,7 @@ export default apiRoute<{
|
|||
);
|
||||
}
|
||||
if (req.method === "PUT") {
|
||||
if (status.authorId !== user?.id) {
|
||||
if (foundStatus.authorId !== user?.id) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
|
|
@ -191,21 +189,19 @@ export default apiRoute<{
|
|||
}
|
||||
|
||||
// Check if media attachments are all valid
|
||||
if (media_ids && media_ids.length > 0) {
|
||||
const foundAttachments = await db.query.attachment.findMany({
|
||||
where: (attachment, { inArray }) =>
|
||||
inArray(attachment.id, media_ids),
|
||||
});
|
||||
|
||||
const foundAttachments = await client.attachment.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: media_ids ?? [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (foundAttachments.length !== (media_ids ?? []).length) {
|
||||
return errorResponse("Invalid media IDs", 422);
|
||||
if (foundAttachments.length !== (media_ids ?? []).length) {
|
||||
return errorResponse("Invalid media IDs", 422);
|
||||
}
|
||||
}
|
||||
|
||||
// Update status
|
||||
const newStatus = await editStatus(status, {
|
||||
const newStatus = await editStatus(foundStatus, {
|
||||
content: sanitizedStatus,
|
||||
content_type,
|
||||
media_attachments: media_ids,
|
||||
|
|
@ -213,6 +209,10 @@ export default apiRoute<{
|
|||
sensitive: sensitive ?? false,
|
||||
});
|
||||
|
||||
if (!newStatus) {
|
||||
return errorResponse("Failed to update status", 500);
|
||||
}
|
||||
|
||||
return jsonResponse(await statusToAPI(newStatus, user));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { statusToAPI } from "~database/entities/Status";
|
||||
import { findFirstStatuses, statusToAPI } from "~database/entities/Status";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
import { db } from "~drizzle/db";
|
||||
import { statusToUser } from "~drizzle/schema";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
@ -26,34 +28,34 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
let status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
const foundStatus = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
|
||||
// Check if status exists
|
||||
if (!status) return errorResponse("Record not found", 404);
|
||||
if (!foundStatus) return errorResponse("Record not found", 404);
|
||||
|
||||
// Check if status is user's
|
||||
if (status.authorId !== user.id) return errorResponse("Unauthorized", 401);
|
||||
if (foundStatus.authorId !== user.id)
|
||||
return errorResponse("Unauthorized", 401);
|
||||
|
||||
await client.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
pinnedNotes: {
|
||||
connect: {
|
||||
id: status.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Check if post is already pinned
|
||||
if (
|
||||
await db.query.statusToUser.findFirst({
|
||||
where: (statusToUser, { and, eq }) =>
|
||||
and(
|
||||
eq(statusToUser.a, foundStatus.id),
|
||||
eq(statusToUser.b, user.id),
|
||||
),
|
||||
})
|
||||
) {
|
||||
return errorResponse("Already pinned", 422);
|
||||
}
|
||||
|
||||
await db.insert(statusToUser).values({
|
||||
a: foundStatus.id,
|
||||
b: user.id,
|
||||
});
|
||||
|
||||
status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
if (!status) return errorResponse("Record not found", 404);
|
||||
|
||||
return jsonResponse(statusToAPI(status, user));
|
||||
return jsonResponse(statusToAPI(foundStatus, user));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { isViewableByUser, statusToAPI } from "~database/entities/Status";
|
||||
import type { UserWithRelations } from "~database/entities/User";
|
||||
import {
|
||||
findFirstStatuses,
|
||||
isViewableByUser,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
import { db } from "~drizzle/db";
|
||||
import { notification, status } from "~drizzle/schema";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["POST"],
|
||||
|
|
@ -31,47 +35,57 @@ export default apiRoute<{
|
|||
|
||||
const { visibility = "public" } = extraData.parsedRequest;
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
const foundStatus = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
if (!foundStatus || !isViewableByUser(foundStatus, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
const existingReblog = await client.status.findFirst({
|
||||
where: {
|
||||
authorId: user.id,
|
||||
reblogId: status.id,
|
||||
},
|
||||
const existingReblog = await db.query.status.findFirst({
|
||||
where: (status, { and, eq }) =>
|
||||
and(eq(status.authorId, user.id), eq(status.reblogId, status.id)),
|
||||
});
|
||||
|
||||
if (existingReblog) {
|
||||
return errorResponse("Already reblogged", 422);
|
||||
}
|
||||
|
||||
const newReblog = await client.status.create({
|
||||
data: {
|
||||
authorId: user.id,
|
||||
reblogId: status.id,
|
||||
visibility,
|
||||
sensitive: false,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
const newReblog = (
|
||||
await db
|
||||
.insert(status)
|
||||
.values({
|
||||
authorId: user.id,
|
||||
reblogId: foundStatus.id,
|
||||
visibility,
|
||||
sensitive: false,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
|
||||
if (!newReblog) {
|
||||
return errorResponse("Failed to reblog", 500);
|
||||
}
|
||||
|
||||
const finalNewReblog = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, newReblog.id),
|
||||
});
|
||||
|
||||
if (!finalNewReblog) {
|
||||
return errorResponse("Failed to reblog", 500);
|
||||
}
|
||||
|
||||
// Create notification for reblog if reblogged user is on the same instance
|
||||
if (status.author.instanceId === user.instanceId) {
|
||||
await client.notification.create({
|
||||
data: {
|
||||
accountId: user.id,
|
||||
notifiedId: status.authorId,
|
||||
type: "reblog",
|
||||
statusId: status.reblogId,
|
||||
},
|
||||
if (foundStatus.author.instanceId === user.instanceId) {
|
||||
await db.insert(notification).values({
|
||||
accountId: user.id,
|
||||
notifiedId: foundStatus.authorId,
|
||||
type: "reblog",
|
||||
statusId: foundStatus.reblogId,
|
||||
});
|
||||
}
|
||||
|
||||
return jsonResponse(await statusToAPI(newReblog, user));
|
||||
return jsonResponse(await statusToAPI(finalNewReblog, user));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { fetchTimeline } from "@timelines";
|
||||
import { client } from "~database/datasource";
|
||||
import { isViewableByUser } from "~database/entities/Status";
|
||||
import { type UserWithRelations, userToAPI } from "~database/entities/User";
|
||||
import { findFirstStatuses, isViewableByUser } from "~database/entities/Status";
|
||||
import {
|
||||
statusAndUserRelations,
|
||||
userRelations,
|
||||
} from "~database/entities/relations";
|
||||
type UserWithRelations,
|
||||
userToAPI,
|
||||
findManyUsers,
|
||||
} from "~database/entities/User";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -34,9 +33,8 @@ export default apiRoute<{
|
|||
|
||||
const { user } = extraData.auth;
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
const status = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
|
|
@ -55,33 +53,18 @@ export default apiRoute<{
|
|||
if (limit < 1) return errorResponse("Invalid limit", 400);
|
||||
|
||||
const { objects, link } = await fetchTimeline<UserWithRelations>(
|
||||
client.user,
|
||||
findManyUsers,
|
||||
{
|
||||
where: {
|
||||
statuses: {
|
||||
some: {
|
||||
reblogId: status.id,
|
||||
},
|
||||
},
|
||||
id: {
|
||||
lt: max_id ?? undefined,
|
||||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
...userRelations,
|
||||
statuses: {
|
||||
where: {
|
||||
reblogId: status.id,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
},
|
||||
},
|
||||
take: Number(limit),
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
// @ts-ignore
|
||||
where: (reblogger, { and, lt, gt, gte, eq, sql }) =>
|
||||
and(
|
||||
max_id ? lt(reblogger.id, max_id) : undefined,
|
||||
since_id ? gte(reblogger.id, since_id) : undefined,
|
||||
min_id ? gt(reblogger.id, min_id) : undefined,
|
||||
sql`EXISTS (SELECT 1 FROM "Status" WHERE "Status"."reblogId" = ${status.id} AND "Status"."authorId" = ${reblogger.id})`,
|
||||
),
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
orderBy: (liker, { desc }) => desc(liker.id),
|
||||
},
|
||||
req,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ import { apiRoute, applyConfig } from "@api";
|
|||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { deleteLike } from "~database/entities/Like";
|
||||
import { isViewableByUser, statusToAPI } from "~database/entities/Status";
|
||||
import {
|
||||
findFirstStatuses,
|
||||
isViewableByUser,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
import type { APIStatus } from "~types/entities/status";
|
||||
|
||||
|
|
@ -28,20 +32,19 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
const foundStatus = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
if (!foundStatus || !isViewableByUser(foundStatus, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
await deleteLike(user, status);
|
||||
await deleteLike(user, foundStatus);
|
||||
|
||||
return jsonResponse({
|
||||
...(await statusToAPI(status, user)),
|
||||
...(await statusToAPI(foundStatus, user)),
|
||||
favourited: false,
|
||||
favourites_count: status._count.likes - 1,
|
||||
favourites_count: foundStatus.likeCount - 1,
|
||||
} as APIStatus);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import { apiRoute, applyConfig } from "@api";
|
||||
import { errorResponse, jsonResponse } from "@response";
|
||||
import { client } from "~database/datasource";
|
||||
import { isViewableByUser, statusToAPI } from "~database/entities/Status";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
import { eq } from "drizzle-orm";
|
||||
import {
|
||||
findFirstStatuses,
|
||||
isViewableByUser,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
import { db } from "~drizzle/db";
|
||||
import { status } from "~drizzle/schema";
|
||||
import type { APIStatus } from "~types/entities/status";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -27,33 +32,28 @@ export default apiRoute(async (req, matchedRoute, extraData) => {
|
|||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const status = await client.status.findUnique({
|
||||
where: { id },
|
||||
include: statusAndUserRelations,
|
||||
const foundStatus = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, id),
|
||||
});
|
||||
|
||||
// Check if user is authorized to view this status (if it's private)
|
||||
if (!status || !isViewableByUser(status, user))
|
||||
if (!foundStatus || !isViewableByUser(foundStatus, user))
|
||||
return errorResponse("Record not found", 404);
|
||||
|
||||
const existingReblog = await client.status.findFirst({
|
||||
where: {
|
||||
authorId: user.id,
|
||||
reblogId: status.id,
|
||||
},
|
||||
const existingReblog = await findFirstStatuses({
|
||||
where: (status, { eq }) =>
|
||||
eq(status.authorId, user.id) && eq(status.reblogId, foundStatus.id),
|
||||
});
|
||||
|
||||
if (!existingReblog) {
|
||||
return errorResponse("Not already reblogged", 422);
|
||||
}
|
||||
|
||||
await client.status.delete({
|
||||
where: { id: existingReblog.id },
|
||||
});
|
||||
await db.delete(status).where(eq(status.id, existingReblog.id));
|
||||
|
||||
return jsonResponse({
|
||||
...(await statusToAPI(status, user)),
|
||||
...(await statusToAPI(foundStatus, user)),
|
||||
reblogged: false,
|
||||
reblogs_count: status._count.reblogs - 1,
|
||||
reblogs_count: foundStatus.reblogCount - 1,
|
||||
} as APIStatus);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { StatusWithRelations } from "~database/entities/Status";
|
|||
import {
|
||||
createNewStatus,
|
||||
federateStatus,
|
||||
findFirstStatuses,
|
||||
parseTextMentions,
|
||||
statusToAPI,
|
||||
} from "~database/entities/Status";
|
||||
|
|
@ -46,9 +47,9 @@ export default apiRoute<{
|
|||
scheduled_at?: string;
|
||||
local_only?: boolean;
|
||||
content_type?: string;
|
||||
federate?: boolean;
|
||||
}>(async (req, matchedRoute, extraData) => {
|
||||
const { user, token } = extraData.auth;
|
||||
const application = await getFromToken(token);
|
||||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
|
|
@ -69,6 +70,7 @@ export default apiRoute<{
|
|||
spoiler_text,
|
||||
visibility,
|
||||
content_type,
|
||||
federate = true,
|
||||
} = extraData.parsedRequest;
|
||||
|
||||
// Validate status
|
||||
|
|
@ -173,9 +175,8 @@ export default apiRoute<{
|
|||
let quote: StatusWithRelations | null = null;
|
||||
|
||||
if (in_reply_to_id) {
|
||||
replyStatus = await client.status.findUnique({
|
||||
where: { id: in_reply_to_id },
|
||||
include: statusAndUserRelations,
|
||||
replyStatus = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, in_reply_to_id),
|
||||
});
|
||||
|
||||
if (!replyStatus) {
|
||||
|
|
@ -184,9 +185,8 @@ export default apiRoute<{
|
|||
}
|
||||
|
||||
if (quote_id) {
|
||||
quote = await client.status.findUnique({
|
||||
where: { id: quote_id },
|
||||
include: statusAndUserRelations,
|
||||
quote = await findFirstStatuses({
|
||||
where: (status, { eq }) => eq(status.id, quote_id),
|
||||
});
|
||||
|
||||
if (!quote) {
|
||||
|
|
@ -233,7 +233,13 @@ export default apiRoute<{
|
|||
quote ?? undefined,
|
||||
);
|
||||
|
||||
await federateStatus(newStatus);
|
||||
if (!newStatus) {
|
||||
return errorResponse("Failed to create status", 500);
|
||||
}
|
||||
|
||||
if (federate) {
|
||||
await federateStatus(newStatus);
|
||||
}
|
||||
|
||||
return jsonResponse(await statusToAPI(newStatus, user));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@ import { client } from "~database/datasource";
|
|||
import {
|
||||
type StatusWithRelations,
|
||||
statusToAPI,
|
||||
findManyStatuses,
|
||||
} from "~database/entities/Status";
|
||||
import { statusAndUserRelations } from "~database/entities/relations";
|
||||
import { db } from "~drizzle/db";
|
||||
|
||||
export const meta = applyConfig({
|
||||
allowedMethods: ["GET"],
|
||||
|
|
@ -39,48 +41,40 @@ export default apiRoute<{
|
|||
|
||||
if (!user) return errorResponse("Unauthorized", 401);
|
||||
|
||||
const followers = await db.query.relationship.findMany({
|
||||
where: (relationship, { eq, and }) =>
|
||||
and(
|
||||
eq(relationship.subjectId, user.id),
|
||||
eq(relationship.following, true),
|
||||
),
|
||||
});
|
||||
|
||||
const { objects, link } = await fetchTimeline<StatusWithRelations>(
|
||||
client.status,
|
||||
findManyStatuses,
|
||||
{
|
||||
where: {
|
||||
id: {
|
||||
lt: max_id ?? undefined,
|
||||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
OR: [
|
||||
{
|
||||
author: {
|
||||
OR: [
|
||||
{
|
||||
relationshipSubjects: {
|
||||
some: {
|
||||
ownerId: user.id,
|
||||
following: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: user.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// Include posts where the user is mentioned in addition to posts by followed users
|
||||
mentions: {
|
||||
some: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
take: Number(limit),
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
where: (status, { lt, gte, gt, and, or, eq, inArray, sql }) =>
|
||||
or(
|
||||
and(
|
||||
max_id ? lt(status.id, max_id) : undefined,
|
||||
since_id ? gte(status.id, since_id) : undefined,
|
||||
min_id ? gt(status.id, min_id) : undefined,
|
||||
),
|
||||
eq(status.authorId, user.id),
|
||||
/* inArray(
|
||||
status.authorId,
|
||||
followers.map((f) => f.ownerId),
|
||||
), */
|
||||
// All statuses where the user is mentioned, using table StatusToUser which has a: status.id and b: user.id
|
||||
// WHERE format (... = ...)
|
||||
sql`EXISTS (SELECT 1 FROM "StatusToUser" WHERE "StatusToUser"."a" = ${status.id} AND "StatusToUser"."b" = ${user.id})`,
|
||||
// All statuses from users that the user is following
|
||||
// WHERE format (... = ...)
|
||||
sql`EXISTS (SELECT 1 FROM "Relationship" WHERE "Relationship"."subjectId" = ${status.authorId} AND "Relationship"."ownerId" = ${user.id} AND "Relationship"."following" = true)`,
|
||||
),
|
||||
limit: Number(limit),
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
orderBy: (status, { desc }) => desc(status.id),
|
||||
},
|
||||
req,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { errorResponse, jsonResponse } from "@response";
|
|||
import { fetchTimeline } from "@timelines";
|
||||
import { client } from "~database/datasource";
|
||||
import {
|
||||
findManyStatuses,
|
||||
statusToAPI,
|
||||
type StatusWithRelations,
|
||||
} from "~database/entities/Status";
|
||||
|
|
@ -49,27 +50,23 @@ export default apiRoute<{
|
|||
}
|
||||
|
||||
const { objects, link } = await fetchTimeline<StatusWithRelations>(
|
||||
client.status,
|
||||
findManyStatuses,
|
||||
{
|
||||
where: {
|
||||
id: {
|
||||
lt: max_id ?? undefined,
|
||||
gte: since_id ?? undefined,
|
||||
gt: min_id ?? undefined,
|
||||
},
|
||||
instanceId: remote
|
||||
? {
|
||||
not: null,
|
||||
}
|
||||
: local
|
||||
? null
|
||||
: undefined,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
take: Number(limit),
|
||||
orderBy: {
|
||||
id: "desc",
|
||||
},
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
where: (status, { lt, gte, gt, and, isNull, isNotNull }) =>
|
||||
and(
|
||||
max_id ? lt(status.id, max_id) : undefined,
|
||||
since_id ? gte(status.id, since_id) : undefined,
|
||||
min_id ? gt(status.id, min_id) : undefined,
|
||||
remote
|
||||
? isNotNull(status.instanceId)
|
||||
: local
|
||||
? isNull(status.instanceId)
|
||||
: undefined,
|
||||
),
|
||||
limit: Number(limit),
|
||||
// @ts-expect-error Yes I KNOW the types are wrong
|
||||
orderBy: (status, { desc }) => desc(status.id),
|
||||
},
|
||||
req,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import { encode } from "blurhash";
|
|||
import type { MediaBackend } from "media-manager";
|
||||
import { MediaBackendType } from "media-manager";
|
||||
import sharp from "sharp";
|
||||
import { client } from "~database/datasource";
|
||||
import { attachmentToAPI, getUrl } from "~database/entities/Attachment";
|
||||
import { db } from "~drizzle/db";
|
||||
import { attachment } from "~drizzle/schema";
|
||||
import { LocalMediaBackend, S3MediaBackend } from "~packages/media-manager";
|
||||
|
||||
export const meta = applyConfig({
|
||||
|
|
@ -134,19 +135,22 @@ export default apiRoute<{
|
|||
thumbnailUrl = getUrl(path, config);
|
||||
}
|
||||
|
||||
const newAttachment = await client.attachment.create({
|
||||
data: {
|
||||
url,
|
||||
thumbnail_url: thumbnailUrl,
|
||||
sha256: sha256.update(await file.arrayBuffer()).digest("hex"),
|
||||
mime_type: file.type,
|
||||
description: description ?? "",
|
||||
size: file.size,
|
||||
blurhash: blurhash ?? undefined,
|
||||
width: metadata?.width ?? undefined,
|
||||
height: metadata?.height ?? undefined,
|
||||
},
|
||||
});
|
||||
const newAttachment = (
|
||||
await db
|
||||
.insert(attachment)
|
||||
.values({
|
||||
url,
|
||||
thumbnailUrl,
|
||||
sha256: sha256.update(await file.arrayBuffer()).digest("hex"),
|
||||
mimeType: file.type,
|
||||
description: description ?? "",
|
||||
size: file.size,
|
||||
blurhash: blurhash ?? undefined,
|
||||
width: metadata?.width ?? undefined,
|
||||
height: metadata?.height ?? undefined,
|
||||
})
|
||||
.returning()
|
||||
)[0];
|
||||
|
||||
// TODO: Add job to process videos and other media
|
||||
|
||||
|
|
|
|||
|
|
@ -10,22 +10,34 @@ import {
|
|||
import type { APIEmoji } from "~types/entities/emoji";
|
||||
import type { APIInstance } from "~types/entities/instance";
|
||||
import { sendTestRequest, wrapRelativeUrl } from "./utils";
|
||||
import { db } from "~drizzle/db";
|
||||
import { inArray } from "drizzle-orm";
|
||||
import { application, user } from "~drizzle/schema";
|
||||
|
||||
const base_url = config.http.base_url;
|
||||
|
||||
let token: Token;
|
||||
let user: UserWithRelations;
|
||||
let dummyUser: UserWithRelations;
|
||||
|
||||
describe("API Tests", () => {
|
||||
beforeAll(async () => {
|
||||
await db.delete(user).where(inArray(user.username, ["test", "test2"]));
|
||||
await db
|
||||
.delete(application)
|
||||
.where(inArray(application.clientId, ["test"]));
|
||||
|
||||
// Initialize test user
|
||||
user = await createNewLocalUser({
|
||||
dummyUser = await createNewLocalUser({
|
||||
email: "test@test.com",
|
||||
username: "test",
|
||||
password: "test",
|
||||
display_name: "",
|
||||
});
|
||||
|
||||
if (!dummyUser) {
|
||||
throw new Error("Failed to create test user");
|
||||
}
|
||||
|
||||
token = await client.token.create({
|
||||
data: {
|
||||
access_token: "test",
|
||||
|
|
@ -45,7 +57,7 @@ describe("API Tests", () => {
|
|||
token_type: TokenType.BEARER,
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
id: dummyUser.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -53,19 +65,10 @@ describe("API Tests", () => {
|
|||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await client.user.deleteMany({
|
||||
where: {
|
||||
username: {
|
||||
in: ["test", "test2"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await client.application.deleteMany({
|
||||
where: {
|
||||
client_id: "test",
|
||||
},
|
||||
});
|
||||
await db.delete(user).where(inArray(user.username, ["test", "test2"]));
|
||||
await db
|
||||
.delete(application)
|
||||
.where(inArray(application.clientId, ["test"]));
|
||||
});
|
||||
|
||||
describe("GET /api/v1/instance", () => {
|
||||
|
|
@ -89,7 +92,7 @@ describe("API Tests", () => {
|
|||
|
||||
const instance = (await response.json()) as APIInstance;
|
||||
|
||||
expect(instance.uri).toBe(new URL(config.http.base_url).hostname);
|
||||
expect(instance.uri).toBe(config.http.base_url);
|
||||
expect(instance.title).toBeDefined();
|
||||
expect(instance.description).toBeDefined();
|
||||
expect(instance.email).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ describe("API Tests", () => {
|
|||
status: "Hello, world!",
|
||||
visibility: "public",
|
||||
media_ids: [media1?.id],
|
||||
federate: false,
|
||||
}),
|
||||
},
|
||||
),
|
||||
|
|
@ -173,6 +174,7 @@ describe("API Tests", () => {
|
|||
status: "This is a reply!",
|
||||
visibility: "public",
|
||||
in_reply_to_id: status?.id,
|
||||
federate: false,
|
||||
}),
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { server } from "~index";
|
||||
// import { server } from "~index";
|
||||
|
||||
/**
|
||||
* This allows us to send a test request to the server even when it isnt running
|
||||
|
|
@ -7,7 +7,9 @@ import { server } from "~index";
|
|||
* @returns Response from the server
|
||||
*/
|
||||
export async function sendTestRequest(req: Request) {
|
||||
return server.fetch(req);
|
||||
console.log(req);
|
||||
return fetch(req);
|
||||
// return server.fetch(req);
|
||||
}
|
||||
|
||||
export function wrapRelativeUrl(url: string, base_url: string) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import type { Status, User } from "@prisma/client";
|
||||
import chalk from "chalk";
|
||||
import { config } from "config-manager";
|
||||
import { LogLevel, type LogManager, type MultiLogManager } from "log-manager";
|
||||
import { Meilisearch } from "meilisearch";
|
||||
import { client } from "~database/datasource";
|
||||
import type { Status } from "~database/entities/Status";
|
||||
import type { User } from "~database/entities/User";
|
||||
|
||||
export const meilisearch = new Meilisearch({
|
||||
host: `${config.meilisearch.host}:${config.meilisearch.port}`,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,23 @@
|
|||
import type { Status, User, Prisma, Notification } from "@prisma/client";
|
||||
import type { findManyStatuses, Status } from "~database/entities/Status";
|
||||
import type { findManyUsers, User } from "~database/entities/User";
|
||||
import type { Notification } from "~database/entities/Notification";
|
||||
import type { db } from "~drizzle/db";
|
||||
|
||||
export async function fetchTimeline<T extends User | Status | Notification>(
|
||||
model:
|
||||
| Prisma.StatusDelegate
|
||||
| Prisma.UserDelegate
|
||||
| Prisma.NotificationDelegate,
|
||||
| typeof findManyStatuses
|
||||
| typeof findManyUsers
|
||||
| typeof db.query.notification.findMany,
|
||||
args:
|
||||
| Prisma.StatusFindManyArgs
|
||||
| Prisma.UserFindManyArgs
|
||||
| Prisma.NotificationFindManyArgs,
|
||||
| Parameters<typeof findManyStatuses>[0]
|
||||
| Parameters<typeof findManyUsers>[0]
|
||||
| Parameters<typeof db.query.notification.findMany>[0],
|
||||
req: Request,
|
||||
) {
|
||||
// BEFORE: Before in a top-to-bottom order, so the most recent posts
|
||||
// AFTER: After in a top-to-bottom order, so the oldest posts
|
||||
// @ts-expect-error This is a hack to get around the fact that Prisma doesn't have a common base type for all models
|
||||
const objects = (await model.findMany(args)) as T[];
|
||||
const objects = (await model(args)) as T[];
|
||||
|
||||
// Constuct HTTP Link header (next and prev) only if there are more statuses
|
||||
const linkHeader = [];
|
||||
|
|
@ -22,15 +25,11 @@ export async function fetchTimeline<T extends User | Status | Notification>(
|
|||
if (objects.length > 0) {
|
||||
// Check if there are statuses before the first one
|
||||
// @ts-expect-error This is a hack to get around the fact that Prisma doesn't have a common base type for all models
|
||||
const objectsBefore = await model.findMany({
|
||||
const objectsBefore = await model({
|
||||
...args,
|
||||
where: {
|
||||
...args.where,
|
||||
id: {
|
||||
gt: objects[0].id,
|
||||
},
|
||||
},
|
||||
take: 1,
|
||||
// @ts-expect-error this hack breaks typing :(
|
||||
where: (object, { gt }) => gt(object.id, objects[0].id),
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (objectsBefore.length > 0) {
|
||||
|
|
@ -41,18 +40,16 @@ export async function fetchTimeline<T extends User | Status | Notification>(
|
|||
);
|
||||
}
|
||||
|
||||
if (objects.length < (args.take ?? Number.POSITIVE_INFINITY)) {
|
||||
if (
|
||||
objects.length < (Number(args?.limit) ?? Number.POSITIVE_INFINITY)
|
||||
) {
|
||||
// Check if there are statuses after the last one
|
||||
// @ts-expect-error This is a hack to get around the fact that Prisma doesn't have a common base type for all models
|
||||
const objectsAfter = await model.findMany({
|
||||
// @ts-expect-error hack again
|
||||
const objectsAfter = await model({
|
||||
...args,
|
||||
where: {
|
||||
...args.where,
|
||||
id: {
|
||||
lt: objects.at(-1)?.id,
|
||||
},
|
||||
},
|
||||
take: 1,
|
||||
// @ts-expect-error this hack breaks typing :(
|
||||
where: (object, { lt }) => lt(object.id, objects.at(-1).id),
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (objectsAfter.length > 0) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue