mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 13:59:16 +01:00
Begin moving project to use Drizzle instead of prisma
This commit is contained in:
parent
b107bed935
commit
f7abe06a60
49 changed files with 7602 additions and 1267 deletions
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue