mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
Replace eslint and prettier with Biome
This commit is contained in:
parent
4a5a2ea590
commit
af0d627f19
199 changed files with 16493 additions and 16361 deletions
|
|
@ -3,7 +3,7 @@ import { PrismaClient } from "@prisma/client";
|
|||
import { config } from "config-manager";
|
||||
|
||||
const client = new PrismaClient({
|
||||
datasourceUrl: `postgresql://${config.database.username}:${config.database.password}@${config.database.host}:${config.database.port}/${config.database.database}`,
|
||||
datasourceUrl: `postgresql://${config.database.username}:${config.database.password}@${config.database.host}:${config.database.port}/${config.database.database}`,
|
||||
});
|
||||
|
||||
/* const federationQueue = new Queue("federation", {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { APIApplication } from "~types/entities/application";
|
||||
import type { Application } from "@prisma/client";
|
||||
import { client } from "~database/datasource";
|
||||
import type { APIApplication } from "~types/entities/application";
|
||||
|
||||
/**
|
||||
* Represents an application that can authenticate with the API.
|
||||
|
|
@ -12,18 +12,18 @@ import { client } from "~database/datasource";
|
|||
* @returns The application associated with the given access token, or null if no such application exists.
|
||||
*/
|
||||
export const getFromToken = async (
|
||||
token: string
|
||||
token: string,
|
||||
): Promise<Application | null> => {
|
||||
const dbToken = await client.token.findFirst({
|
||||
where: {
|
||||
access_token: token,
|
||||
},
|
||||
include: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
const dbToken = await client.token.findFirst({
|
||||
where: {
|
||||
access_token: token,
|
||||
},
|
||||
include: {
|
||||
application: true,
|
||||
},
|
||||
});
|
||||
|
||||
return dbToken?.application || null;
|
||||
return dbToken?.application || null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -31,9 +31,9 @@ export const getFromToken = async (
|
|||
* @returns The API application representation of this application.
|
||||
*/
|
||||
export const applicationToAPI = (app: Application): APIApplication => {
|
||||
return {
|
||||
name: app.name,
|
||||
website: app.website,
|
||||
vapid_key: app.vapid_key,
|
||||
};
|
||||
return {
|
||||
name: app.name,
|
||||
website: app.website,
|
||||
vapid_key: app.vapid_key,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,69 +1,70 @@
|
|||
import type { Attachment } from "@prisma/client";
|
||||
import type { ConfigType } from "config-manager";
|
||||
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";
|
||||
|
||||
export const attachmentToAPI = (
|
||||
attachment: Attachment
|
||||
attachment: Attachment,
|
||||
): APIAsyncAttachment | APIAttachment => {
|
||||
let type = "unknown";
|
||||
let type = "unknown";
|
||||
|
||||
if (attachment.mime_type.startsWith("image/")) {
|
||||
type = "image";
|
||||
} else if (attachment.mime_type.startsWith("video/")) {
|
||||
type = "video";
|
||||
} else if (attachment.mime_type.startsWith("audio/")) {
|
||||
type = "audio";
|
||||
}
|
||||
if (attachment.mime_type.startsWith("image/")) {
|
||||
type = "image";
|
||||
} else if (attachment.mime_type.startsWith("video/")) {
|
||||
type = "video";
|
||||
} else if (attachment.mime_type.startsWith("audio/")) {
|
||||
type = "audio";
|
||||
}
|
||||
|
||||
return {
|
||||
id: attachment.id,
|
||||
type: type as any,
|
||||
url: attachment.url,
|
||||
remote_url: attachment.remote_url,
|
||||
preview_url: attachment.thumbnail_url,
|
||||
text_url: null,
|
||||
meta: {
|
||||
width: attachment.width || undefined,
|
||||
height: attachment.height || undefined,
|
||||
fps: attachment.fps || undefined,
|
||||
size:
|
||||
attachment.width && attachment.height
|
||||
? `${attachment.width}x${attachment.height}`
|
||||
: undefined,
|
||||
duration: attachment.duration || undefined,
|
||||
length: attachment.size?.toString() || undefined,
|
||||
aspect:
|
||||
attachment.width && attachment.height
|
||||
? attachment.width / attachment.height
|
||||
: undefined,
|
||||
original: {
|
||||
width: attachment.width || undefined,
|
||||
height: attachment.height || undefined,
|
||||
size:
|
||||
attachment.width && attachment.height
|
||||
? `${attachment.width}x${attachment.height}`
|
||||
: undefined,
|
||||
aspect:
|
||||
attachment.width && attachment.height
|
||||
? attachment.width / attachment.height
|
||||
: undefined,
|
||||
},
|
||||
// Idk whether size or length is the right value
|
||||
},
|
||||
description: attachment.description,
|
||||
blurhash: attachment.blurhash,
|
||||
};
|
||||
return {
|
||||
id: attachment.id,
|
||||
type: type as "image" | "video" | "audio" | "unknown",
|
||||
url: attachment.url,
|
||||
remote_url: attachment.remote_url,
|
||||
preview_url: attachment.thumbnail_url,
|
||||
text_url: null,
|
||||
meta: {
|
||||
width: attachment.width || undefined,
|
||||
height: attachment.height || undefined,
|
||||
fps: attachment.fps || undefined,
|
||||
size:
|
||||
attachment.width && attachment.height
|
||||
? `${attachment.width}x${attachment.height}`
|
||||
: undefined,
|
||||
duration: attachment.duration || undefined,
|
||||
length: attachment.size?.toString() || undefined,
|
||||
aspect:
|
||||
attachment.width && attachment.height
|
||||
? attachment.width / attachment.height
|
||||
: undefined,
|
||||
original: {
|
||||
width: attachment.width || undefined,
|
||||
height: attachment.height || undefined,
|
||||
size:
|
||||
attachment.width && attachment.height
|
||||
? `${attachment.width}x${attachment.height}`
|
||||
: undefined,
|
||||
aspect:
|
||||
attachment.width && attachment.height
|
||||
? attachment.width / attachment.height
|
||||
: undefined,
|
||||
},
|
||||
// Idk whether size or length is the right value
|
||||
},
|
||||
description: attachment.description,
|
||||
blurhash: attachment.blurhash,
|
||||
};
|
||||
};
|
||||
|
||||
export const getUrl = (name: string, config: ConfigType) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||
if (config.media.backend === MediaBackendType.LOCAL) {
|
||||
return `${config.http.base_url}/media/${name}`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison, @typescript-eslint/no-unnecessary-condition
|
||||
} else if (config.media.backend === MediaBackendType.S3) {
|
||||
return `${config.s3.public_url}/${name}`;
|
||||
}
|
||||
return "";
|
||||
export const getUrl = (name: string, config: Config) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
||||
if (config.media.backend === MediaBackendType.LOCAL) {
|
||||
return `${config.http.base_url}/media/${name}`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison, @typescript-eslint/no-unnecessary-condition
|
||||
}
|
||||
if (config.media.backend === MediaBackendType.S3) {
|
||||
return `${config.s3.public_url}/${name}`;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { Emoji } from "@prisma/client";
|
||||
import { client } from "~database/datasource";
|
||||
import type { APIEmoji } from "~types/entities/emoji";
|
||||
import type { Emoji as LysandEmoji } from "~types/lysand/extensions/org.lysand/custom_emojis";
|
||||
import { client } from "~database/datasource";
|
||||
import type { Emoji } from "@prisma/client";
|
||||
|
||||
/**
|
||||
* Represents an emoji entity in the database.
|
||||
|
|
@ -13,41 +13,41 @@ import type { Emoji } from "@prisma/client";
|
|||
* @returns An array of emojis
|
||||
*/
|
||||
export const parseEmojis = async (text: string): Promise<Emoji[]> => {
|
||||
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: {
|
||||
instance: true,
|
||||
},
|
||||
});
|
||||
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: {
|
||||
instance: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const addEmojiIfNotExists = async (emoji: LysandEmoji) => {
|
||||
const existingEmoji = await client.emoji.findFirst({
|
||||
where: {
|
||||
shortcode: emoji.name,
|
||||
instance: null,
|
||||
},
|
||||
});
|
||||
const existingEmoji = await client.emoji.findFirst({
|
||||
where: {
|
||||
shortcode: emoji.name,
|
||||
instance: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingEmoji) return existingEmoji;
|
||||
if (existingEmoji) return existingEmoji;
|
||||
|
||||
return await client.emoji.create({
|
||||
data: {
|
||||
shortcode: emoji.name,
|
||||
url: emoji.url[0].content,
|
||||
alt: emoji.alt || null,
|
||||
content_type: emoji.url[0].content_type,
|
||||
visible_in_picker: true,
|
||||
},
|
||||
});
|
||||
return await client.emoji.create({
|
||||
data: {
|
||||
shortcode: emoji.name,
|
||||
url: emoji.url[0].content,
|
||||
alt: emoji.alt || null,
|
||||
content_type: emoji.url[0].content_type,
|
||||
visible_in_picker: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -55,43 +55,43 @@ export const addEmojiIfNotExists = async (emoji: LysandEmoji) => {
|
|||
* @returns The APIEmoji object.
|
||||
*/
|
||||
export const emojiToAPI = (emoji: Emoji): APIEmoji => {
|
||||
return {
|
||||
shortcode: emoji.shortcode,
|
||||
static_url: emoji.url, // TODO: Add static version
|
||||
url: emoji.url,
|
||||
visible_in_picker: emoji.visible_in_picker,
|
||||
category: undefined,
|
||||
};
|
||||
return {
|
||||
shortcode: emoji.shortcode,
|
||||
static_url: emoji.url, // TODO: Add static version
|
||||
url: emoji.url,
|
||||
visible_in_picker: emoji.visible_in_picker,
|
||||
category: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
export const emojiToLysand = (emoji: Emoji): LysandEmoji => {
|
||||
return {
|
||||
name: emoji.shortcode,
|
||||
url: [
|
||||
{
|
||||
content: emoji.url,
|
||||
content_type: emoji.content_type,
|
||||
},
|
||||
],
|
||||
alt: emoji.alt || undefined,
|
||||
};
|
||||
return {
|
||||
name: emoji.shortcode,
|
||||
url: [
|
||||
{
|
||||
content: emoji.url,
|
||||
content_type: emoji.content_type,
|
||||
},
|
||||
],
|
||||
alt: emoji.alt || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts the emoji to an ActivityPub object.
|
||||
* @returns The ActivityPub object.
|
||||
*/
|
||||
export const emojiToActivityPub = (emoji: Emoji): any => {
|
||||
// replace any with your ActivityPub Emoji type
|
||||
return {
|
||||
type: "Emoji",
|
||||
name: `:${emoji.shortcode}:`,
|
||||
updated: new Date().toISOString(),
|
||||
icon: {
|
||||
type: "Image",
|
||||
url: emoji.url,
|
||||
mediaType: emoji.content_type,
|
||||
alt: emoji.alt || undefined,
|
||||
},
|
||||
};
|
||||
export const emojiToActivityPub = (emoji: Emoji): object => {
|
||||
// replace any with your ActivityPub Emoji type
|
||||
return {
|
||||
type: "Emoji",
|
||||
name: `:${emoji.shortcode}:`,
|
||||
updated: new Date().toISOString(),
|
||||
icon: {
|
||||
type: "Image",
|
||||
url: emoji.url,
|
||||
mediaType: emoji.content_type,
|
||||
alt: emoji.alt || undefined,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,38 +12,38 @@ import type { ServerMetadata } from "~types/lysand/Object";
|
|||
* @returns Either the database instance if it already exists, or a newly created instance.
|
||||
*/
|
||||
export const addInstanceIfNotExists = async (
|
||||
url: string
|
||||
url: string,
|
||||
): Promise<Instance> => {
|
||||
const origin = new URL(url).origin;
|
||||
const hostname = new URL(url).hostname;
|
||||
const origin = new URL(url).origin;
|
||||
const hostname = new URL(url).hostname;
|
||||
|
||||
const found = await client.instance.findFirst({
|
||||
where: {
|
||||
base_url: hostname,
|
||||
},
|
||||
});
|
||||
const found = await client.instance.findFirst({
|
||||
where: {
|
||||
base_url: hostname,
|
||||
},
|
||||
});
|
||||
|
||||
if (found) return found;
|
||||
if (found) return found;
|
||||
|
||||
// Fetch the instance configuration
|
||||
const metadata = (await fetch(`${origin}/.well-known/lysand`).then(res =>
|
||||
res.json()
|
||||
)) as Partial<ServerMetadata>;
|
||||
// Fetch the instance configuration
|
||||
const metadata = (await fetch(`${origin}/.well-known/lysand`).then((res) =>
|
||||
res.json(),
|
||||
)) as Partial<ServerMetadata>;
|
||||
|
||||
if (metadata.type !== "ServerMetadata") {
|
||||
throw new Error("Invalid instance metadata");
|
||||
}
|
||||
if (metadata.type !== "ServerMetadata") {
|
||||
throw new Error("Invalid instance metadata");
|
||||
}
|
||||
|
||||
if (!(metadata.name && metadata.version)) {
|
||||
throw new Error("Invalid instance metadata");
|
||||
}
|
||||
if (!(metadata.name && metadata.version)) {
|
||||
throw new Error("Invalid instance metadata");
|
||||
}
|
||||
|
||||
return await client.instance.create({
|
||||
data: {
|
||||
base_url: hostname,
|
||||
name: metadata.name,
|
||||
version: metadata.version,
|
||||
logo: metadata.logo as any,
|
||||
},
|
||||
});
|
||||
return await client.instance.create({
|
||||
data: {
|
||||
base_url: hostname,
|
||||
name: metadata.name,
|
||||
version: metadata.version,
|
||||
logo: metadata.logo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
import type { Like, Prisma } from "@prisma/client";
|
||||
import { config } from "config-manager";
|
||||
import { client } from "~database/datasource";
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import type { Like as LysandLike } from "~types/lysand/Object";
|
||||
import type { Like } from "@prisma/client";
|
||||
import { client } from "~database/datasource";
|
||||
import type { UserWithRelations } from "./User";
|
||||
import type { StatusWithRelations } from "./Status";
|
||||
import { config } from "config-manager";
|
||||
import type { UserWithRelations } from "./User";
|
||||
|
||||
/**
|
||||
* Represents a Like entity in the database.
|
||||
*/
|
||||
export const toLysand = (like: Like): LysandLike => {
|
||||
return {
|
||||
id: like.id,
|
||||
author: (like as any).liker?.uri,
|
||||
type: "Like",
|
||||
created_at: new Date(like.createdAt).toISOString(),
|
||||
object: (like as any).liked?.uri,
|
||||
uri: `${config.http.base_url}/actions/${like.id}`,
|
||||
};
|
||||
return {
|
||||
id: like.id,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
|
||||
author: (like as any).liker?.uri,
|
||||
type: "Like",
|
||||
created_at: new Date(like.createdAt).toISOString(),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: to be rewritten
|
||||
object: (like as any).liked?.uri,
|
||||
uri: `${config.http.base_url}/actions/${like.id}`,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -26,29 +28,29 @@ export const toLysand = (like: Like): LysandLike => {
|
|||
* @param status Status being liked
|
||||
*/
|
||||
export const createLike = async (
|
||||
user: UserWithRelations,
|
||||
status: StatusWithRelations
|
||||
user: UserWithRelations,
|
||||
status: StatusWithRelations,
|
||||
) => {
|
||||
await client.like.create({
|
||||
data: {
|
||||
likedId: status.id,
|
||||
likerId: user.id,
|
||||
},
|
||||
});
|
||||
await client.like.create({
|
||||
data: {
|
||||
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,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// TODO: Add database jobs for federating this
|
||||
}
|
||||
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,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// TODO: Add database jobs for federating this
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -57,28 +59,28 @@ export const createLike = async (
|
|||
* @param status Status being unliked
|
||||
*/
|
||||
export const deleteLike = async (
|
||||
user: UserWithRelations,
|
||||
status: StatusWithRelations
|
||||
user: UserWithRelations,
|
||||
status: StatusWithRelations,
|
||||
) => {
|
||||
await client.like.deleteMany({
|
||||
where: {
|
||||
likedId: status.id,
|
||||
likerId: user.id,
|
||||
},
|
||||
});
|
||||
await client.like.deleteMany({
|
||||
where: {
|
||||
likedId: status.id,
|
||||
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,
|
||||
},
|
||||
});
|
||||
// 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,
|
||||
},
|
||||
});
|
||||
|
||||
if (user.instanceId === null && status.author.instanceId !== null) {
|
||||
// User is local, federate the delete
|
||||
// TODO: Federate this
|
||||
}
|
||||
if (user.instanceId === null && status.author.instanceId !== null) {
|
||||
// User is local, federate the delete
|
||||
// TODO: Federate this
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,20 +4,20 @@ import { type StatusWithRelations, statusToAPI } from "./Status";
|
|||
import { type UserWithRelations, userToAPI } from "./User";
|
||||
|
||||
export type NotificationWithRelations = Notification & {
|
||||
status: StatusWithRelations | null;
|
||||
account: UserWithRelations;
|
||||
status: StatusWithRelations | null;
|
||||
account: UserWithRelations;
|
||||
};
|
||||
|
||||
export const notificationToAPI = async (
|
||||
notification: NotificationWithRelations
|
||||
notification: NotificationWithRelations,
|
||||
): Promise<APINotification> => {
|
||||
return {
|
||||
account: userToAPI(notification.account),
|
||||
created_at: new Date(notification.createdAt).toISOString(),
|
||||
id: notification.id,
|
||||
type: notification.type,
|
||||
status: notification.status
|
||||
? await statusToAPI(notification.status, notification.account)
|
||||
: undefined,
|
||||
};
|
||||
return {
|
||||
account: userToAPI(notification.account),
|
||||
created_at: new Date(notification.createdAt).toISOString(),
|
||||
id: notification.id,
|
||||
type: notification.type,
|
||||
status: notification.status
|
||||
? await statusToAPI(notification.status, notification.account)
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,79 +9,79 @@ import type { LysandObjectType } from "~types/lysand/Object";
|
|||
*/
|
||||
|
||||
export const createFromObject = async (object: LysandObjectType) => {
|
||||
const foundObject = await client.lysandObject.findFirst({
|
||||
where: { remote_id: object.id },
|
||||
include: {
|
||||
author: true,
|
||||
},
|
||||
});
|
||||
const foundObject = await client.lysandObject.findFirst({
|
||||
where: { remote_id: object.id },
|
||||
include: {
|
||||
author: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (foundObject) {
|
||||
return foundObject;
|
||||
}
|
||||
if (foundObject) {
|
||||
return foundObject;
|
||||
}
|
||||
|
||||
const author = await client.lysandObject.findFirst({
|
||||
where: { uri: (object as any).author },
|
||||
});
|
||||
const author = await client.lysandObject.findFirst({
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
where: { uri: (object as any).author },
|
||||
});
|
||||
|
||||
return await client.lysandObject.create({
|
||||
data: {
|
||||
authorId: author?.id,
|
||||
created_at: new Date(object.created_at),
|
||||
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 client.lysandObject.create({
|
||||
data: {
|
||||
authorId: author?.id,
|
||||
created_at: new Date(object.created_at),
|
||||
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),
|
||||
),
|
||||
),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const toLysand = (lyObject: LysandObject): LysandObjectType => {
|
||||
return {
|
||||
id: lyObject.remote_id || lyObject.id,
|
||||
created_at: new Date(lyObject.created_at).toISOString(),
|
||||
type: lyObject.type,
|
||||
uri: lyObject.uri,
|
||||
// @ts-expect-error This works, I promise
|
||||
...lyObject.extra_data,
|
||||
extensions: lyObject.extensions,
|
||||
};
|
||||
return {
|
||||
id: lyObject.remote_id || lyObject.id,
|
||||
created_at: new Date(lyObject.created_at).toISOString(),
|
||||
type: lyObject.type,
|
||||
uri: lyObject.uri,
|
||||
...lyObject.extra_data,
|
||||
extensions: lyObject.extensions,
|
||||
};
|
||||
};
|
||||
|
||||
export const isPublication = (lyObject: LysandObject): boolean => {
|
||||
return lyObject.type === "Note" || lyObject.type === "Patch";
|
||||
return lyObject.type === "Note" || lyObject.type === "Patch";
|
||||
};
|
||||
|
||||
export const isAction = (lyObject: LysandObject): boolean => {
|
||||
return [
|
||||
"Like",
|
||||
"Follow",
|
||||
"Dislike",
|
||||
"FollowAccept",
|
||||
"FollowReject",
|
||||
"Undo",
|
||||
"Announce",
|
||||
].includes(lyObject.type);
|
||||
return [
|
||||
"Like",
|
||||
"Follow",
|
||||
"Dislike",
|
||||
"FollowAccept",
|
||||
"FollowReject",
|
||||
"Undo",
|
||||
"Announce",
|
||||
].includes(lyObject.type);
|
||||
};
|
||||
|
||||
export const isActor = (lyObject: LysandObject): boolean => {
|
||||
return lyObject.type === "User";
|
||||
return lyObject.type === "User";
|
||||
};
|
||||
|
||||
export const isExtension = (lyObject: LysandObject): boolean => {
|
||||
return lyObject.type === "Extension";
|
||||
return lyObject.type === "Extension";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// import { Worker } from "bullmq";
|
||||
import { statusToLysand, type StatusWithRelations } from "./Status";
|
||||
import type { User } from "@prisma/client";
|
||||
import { config } from "config-manager";
|
||||
// import { Worker } from "bullmq";
|
||||
import { type StatusWithRelations, statusToLysand } from "./Status";
|
||||
|
||||
/* export const federationWorker = new Worker(
|
||||
"federation",
|
||||
|
|
@ -123,68 +123,68 @@ import { config } from "config-manager";
|
|||
* 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;
|
||||
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
|
||||
status: StatusWithRelations,
|
||||
sender: User,
|
||||
user: User,
|
||||
) => {
|
||||
const privateKey = await crypto.subtle.importKey(
|
||||
"pkcs8",
|
||||
str2ab(atob(user.privateKey ?? "")),
|
||||
"Ed25519",
|
||||
false,
|
||||
["sign"]
|
||||
);
|
||||
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 digest = await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
new TextEncoder().encode("request_body"),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const userInbox = new URL(user.endpoints.inbox);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const userInbox = new URL(user.endpoints.inbox);
|
||||
|
||||
const date = new Date();
|
||||
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 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))
|
||||
);
|
||||
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)),
|
||||
});
|
||||
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", {
|
||||
/* await federationQueue.add("federation", {
|
||||
id: statusId,
|
||||
}); */
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { Relationship, User } from "@prisma/client";
|
||||
import type { APIRelationship } from "~types/entities/relationship";
|
||||
import { client } from "~database/datasource";
|
||||
import type { APIRelationship } from "~types/entities/relationship";
|
||||
|
||||
/**
|
||||
* Stores Mastodon API relationships
|
||||
|
|
@ -13,55 +13,55 @@ import { client } from "~database/datasource";
|
|||
* @returns The newly created relationship.
|
||||
*/
|
||||
export const createNewRelationship = async (
|
||||
owner: User,
|
||||
other: User
|
||||
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 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: "",
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const checkForBidirectionalRelationships = async (
|
||||
user1: User,
|
||||
user2: User,
|
||||
createIfNotExists = true
|
||||
user1: User,
|
||||
user2: User,
|
||||
createIfNotExists = true,
|
||||
): Promise<boolean> => {
|
||||
const relationship1 = await client.relationship.findFirst({
|
||||
where: {
|
||||
ownerId: user1.id,
|
||||
subjectId: user2.id,
|
||||
},
|
||||
});
|
||||
const relationship1 = await client.relationship.findFirst({
|
||||
where: {
|
||||
ownerId: user1.id,
|
||||
subjectId: user2.id,
|
||||
},
|
||||
});
|
||||
|
||||
const relationship2 = await client.relationship.findFirst({
|
||||
where: {
|
||||
ownerId: user2.id,
|
||||
subjectId: user1.id,
|
||||
},
|
||||
});
|
||||
const relationship2 = await client.relationship.findFirst({
|
||||
where: {
|
||||
ownerId: user2.id,
|
||||
subjectId: user1.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!relationship1 && !relationship2 && createIfNotExists) {
|
||||
await createNewRelationship(user1, user2);
|
||||
await createNewRelationship(user2, user1);
|
||||
}
|
||||
if (!relationship1 && !relationship2 && createIfNotExists) {
|
||||
await createNewRelationship(user1, user2);
|
||||
await createNewRelationship(user2, user1);
|
||||
}
|
||||
|
||||
return !!relationship1 && !!relationship2;
|
||||
return !!relationship1 && !!relationship2;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -69,20 +69,20 @@ export const checkForBidirectionalRelationships = async (
|
|||
* @returns The API-friendly relationship.
|
||||
*/
|
||||
export const relationshipToAPI = (rel: Relationship): APIRelationship => {
|
||||
return {
|
||||
blocked_by: rel.blockedBy,
|
||||
blocking: rel.blocking,
|
||||
domain_blocking: rel.domainBlocking,
|
||||
endorsed: rel.endorsed,
|
||||
followed_by: rel.followedBy,
|
||||
following: rel.following,
|
||||
id: rel.subjectId,
|
||||
muting: rel.muting,
|
||||
muting_notifications: rel.mutingNotifications,
|
||||
notifying: rel.notifying,
|
||||
requested: rel.requested,
|
||||
showing_reblogs: rel.showingReblogs,
|
||||
languages: rel.languages,
|
||||
note: rel.note,
|
||||
};
|
||||
return {
|
||||
blocked_by: rel.blockedBy,
|
||||
blocking: rel.blocking,
|
||||
domain_blocking: rel.domainBlocking,
|
||||
endorsed: rel.endorsed,
|
||||
followed_by: rel.followedBy,
|
||||
following: rel.following,
|
||||
id: rel.subjectId,
|
||||
muting: rel.muting,
|
||||
muting_notifications: rel.mutingNotifications,
|
||||
notifying: rel.notifying,
|
||||
requested: rel.requested,
|
||||
showing_reblogs: rel.showingReblogs,
|
||||
languages: rel.languages,
|
||||
note: rel.note,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,37 +1,37 @@
|
|||
import { getBestContentType } from "@content_types";
|
||||
import { addStausToMeilisearch } from "@meilisearch";
|
||||
import {
|
||||
type Application,
|
||||
type Emoji,
|
||||
Prisma,
|
||||
type Relationship,
|
||||
type Status,
|
||||
type User,
|
||||
} from "@prisma/client";
|
||||
import { sanitizeHtml } from "@sanitization";
|
||||
import { config } from "config-manager";
|
||||
import { htmlToText } from "html-to-text";
|
||||
import linkifyHtml from "linkify-html";
|
||||
import linkifyStr from "linkify-string";
|
||||
import { parse } from "marked";
|
||||
import { client } from "~database/datasource";
|
||||
import type { APIAttachment } from "~types/entities/attachment";
|
||||
import type { APIStatus } from "~types/entities/status";
|
||||
import type { LysandPublication, Note } from "~types/lysand/Object";
|
||||
import { applicationToAPI } from "./Application";
|
||||
import { attachmentToAPI } from "./Attachment";
|
||||
import { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji";
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
import type { UserWithRelations } from "./User";
|
||||
import { fetchRemoteUser, parseMentionsUris, userToAPI } from "./User";
|
||||
import { client } from "~database/datasource";
|
||||
import type { LysandPublication, Note } from "~types/lysand/Object";
|
||||
import { htmlToText } from "html-to-text";
|
||||
import { getBestContentType } from "@content_types";
|
||||
import {
|
||||
Prisma,
|
||||
type Application,
|
||||
type Emoji,
|
||||
type Relationship,
|
||||
type Status,
|
||||
type User,
|
||||
} from "@prisma/client";
|
||||
import { emojiToAPI, emojiToLysand, parseEmojis } from "./Emoji";
|
||||
import type { APIStatus } from "~types/entities/status";
|
||||
import { applicationToAPI } from "./Application";
|
||||
import { attachmentToAPI } from "./Attachment";
|
||||
import type { APIAttachment } from "~types/entities/attachment";
|
||||
import { sanitizeHtml } from "@sanitization";
|
||||
import { parse } from "marked";
|
||||
import linkifyStr from "linkify-string";
|
||||
import linkifyHtml from "linkify-html";
|
||||
import { addStausToMeilisearch } from "@meilisearch";
|
||||
import { config } from "config-manager";
|
||||
import { statusAndUserRelations, userRelations } from "./relations";
|
||||
|
||||
const statusRelations = Prisma.validator<Prisma.StatusDefaultArgs>()({
|
||||
include: statusAndUserRelations,
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
export type StatusWithRelations = Prisma.StatusGetPayload<
|
||||
typeof statusRelations
|
||||
typeof statusRelations
|
||||
>;
|
||||
|
||||
/**
|
||||
|
|
@ -44,76 +44,75 @@ export type StatusWithRelations = Prisma.StatusGetPayload<
|
|||
* @returns Whether this status is viewable by the user.
|
||||
*/
|
||||
export const isViewableByUser = (status: Status, user: User | null) => {
|
||||
if (status.authorId === user?.id) return true;
|
||||
if (status.visibility === "public") return true;
|
||||
else if (status.visibility === "unlisted") return true;
|
||||
else if (status.visibility === "private") {
|
||||
// @ts-expect-error Prisma TypeScript types dont include relations
|
||||
return !!(user?.relationships as Relationship[]).find(
|
||||
rel => rel.id === status.authorId
|
||||
);
|
||||
} else {
|
||||
// @ts-expect-error Prisma TypeScript types dont include relations
|
||||
return user && (status.mentions as User[]).includes(user);
|
||||
}
|
||||
if (status.authorId === user?.id) return true;
|
||||
if (status.visibility === "public") return true;
|
||||
if (status.visibility === "unlisted") return true;
|
||||
if (status.visibility === "private") {
|
||||
// @ts-expect-error Prisma TypeScript types dont include relations
|
||||
return !!(user?.relationships as Relationship[]).find(
|
||||
(rel) => rel.id === status.authorId,
|
||||
);
|
||||
}
|
||||
// @ts-expect-error Prisma TypeScript types dont include relations
|
||||
return user && (status.mentions as User[]).includes(user);
|
||||
};
|
||||
|
||||
export const fetchFromRemote = async (uri: string): Promise<Status | null> => {
|
||||
// Check if already in database
|
||||
// Check if already in database
|
||||
|
||||
const existingStatus: StatusWithRelations | null =
|
||||
await client.status.findFirst({
|
||||
where: {
|
||||
uri: uri,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const existingStatus: StatusWithRelations | null =
|
||||
await client.status.findFirst({
|
||||
where: {
|
||||
uri: uri,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
if (existingStatus) return existingStatus;
|
||||
if (existingStatus) return existingStatus;
|
||||
|
||||
const status = await fetch(uri);
|
||||
const status = await fetch(uri);
|
||||
|
||||
if (status.status === 404) return null;
|
||||
if (status.status === 404) return null;
|
||||
|
||||
const body = (await status.json()) as LysandPublication;
|
||||
const body = (await status.json()) as LysandPublication;
|
||||
|
||||
const content = getBestContentType(body.contents);
|
||||
const content = getBestContentType(body.contents);
|
||||
|
||||
const emojis = await parseEmojis(content?.content || "");
|
||||
const emojis = await parseEmojis(content?.content || "");
|
||||
|
||||
const author = await fetchRemoteUser(body.author);
|
||||
const author = await fetchRemoteUser(body.author);
|
||||
|
||||
let replyStatus: Status | null = null;
|
||||
let quotingStatus: Status | null = null;
|
||||
let replyStatus: Status | null = null;
|
||||
let quotingStatus: Status | null = null;
|
||||
|
||||
if (body.replies_to.length > 0) {
|
||||
replyStatus = await fetchFromRemote(body.replies_to[0]);
|
||||
}
|
||||
if (body.replies_to.length > 0) {
|
||||
replyStatus = await fetchFromRemote(body.replies_to[0]);
|
||||
}
|
||||
|
||||
if (body.quotes.length > 0) {
|
||||
quotingStatus = await fetchFromRemote(body.quotes[0]);
|
||||
}
|
||||
if (body.quotes.length > 0) {
|
||||
quotingStatus = await fetchFromRemote(body.quotes[0]);
|
||||
}
|
||||
|
||||
return await createNewStatus({
|
||||
account: author,
|
||||
content: content?.content || "",
|
||||
content_type: content?.content_type,
|
||||
application: null,
|
||||
// TODO: Add visibility
|
||||
visibility: "public",
|
||||
spoiler_text: body.subject || "",
|
||||
uri: body.uri,
|
||||
sensitive: body.is_sensitive,
|
||||
emojis: emojis,
|
||||
mentions: await parseMentionsUris(body.mentions),
|
||||
reply: replyStatus
|
||||
? {
|
||||
status: replyStatus,
|
||||
user: (replyStatus as any).author,
|
||||
}
|
||||
: undefined,
|
||||
quote: quotingStatus || undefined,
|
||||
});
|
||||
return await createNewStatus({
|
||||
account: author,
|
||||
content: content?.content || "",
|
||||
content_type: content?.content_type,
|
||||
application: null,
|
||||
// TODO: Add visibility
|
||||
visibility: "public",
|
||||
spoiler_text: body.subject || "",
|
||||
uri: body.uri,
|
||||
sensitive: body.is_sensitive,
|
||||
emojis: emojis,
|
||||
mentions: await parseMentionsUris(body.mentions),
|
||||
reply: replyStatus
|
||||
? {
|
||||
status: replyStatus,
|
||||
user: (replyStatus as StatusWithRelations).author,
|
||||
}
|
||||
: undefined,
|
||||
quote: quotingStatus || undefined,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -121,34 +120,34 @@ export const fetchFromRemote = async (uri: string): Promise<Status | null> => {
|
|||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
|
||||
export const getAncestors = async (
|
||||
status: StatusWithRelations,
|
||||
fetcher: UserWithRelations | null
|
||||
status: StatusWithRelations,
|
||||
fetcher: UserWithRelations | null,
|
||||
) => {
|
||||
const ancestors: StatusWithRelations[] = [];
|
||||
const ancestors: StatusWithRelations[] = [];
|
||||
|
||||
let currentStatus = status;
|
||||
let currentStatus = status;
|
||||
|
||||
while (currentStatus.inReplyToPostId) {
|
||||
const parent = await client.status.findFirst({
|
||||
where: {
|
||||
id: currentStatus.inReplyToPostId,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
while (currentStatus.inReplyToPostId) {
|
||||
const parent = await client.status.findFirst({
|
||||
where: {
|
||||
id: currentStatus.inReplyToPostId,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
if (!parent) break;
|
||||
if (!parent) break;
|
||||
|
||||
ancestors.push(parent);
|
||||
ancestors.push(parent);
|
||||
|
||||
currentStatus = parent;
|
||||
}
|
||||
currentStatus = parent;
|
||||
}
|
||||
|
||||
// Filter for posts that are viewable by the user
|
||||
// Filter for posts that are viewable by the user
|
||||
|
||||
const viewableAncestors = ancestors.filter(ancestor =>
|
||||
isViewableByUser(ancestor, fetcher)
|
||||
);
|
||||
return viewableAncestors;
|
||||
const viewableAncestors = ancestors.filter((ancestor) =>
|
||||
isViewableByUser(ancestor, fetcher),
|
||||
);
|
||||
return viewableAncestors;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -157,42 +156,42 @@ export const getAncestors = async (
|
|||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/require-await, @typescript-eslint/no-unused-vars
|
||||
export const getDescendants = async (
|
||||
status: StatusWithRelations,
|
||||
fetcher: UserWithRelations | null,
|
||||
depth = 0
|
||||
status: StatusWithRelations,
|
||||
fetcher: UserWithRelations | null,
|
||||
depth = 0,
|
||||
) => {
|
||||
const descendants: StatusWithRelations[] = [];
|
||||
const descendants: StatusWithRelations[] = [];
|
||||
|
||||
const currentStatus = status;
|
||||
const currentStatus = status;
|
||||
|
||||
// Fetch all children of children of children recursively calling getDescendants
|
||||
// Fetch all children of children of children recursively calling getDescendants
|
||||
|
||||
const children = await client.status.findMany({
|
||||
where: {
|
||||
inReplyToPostId: currentStatus.id,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const children = await client.status.findMany({
|
||||
where: {
|
||||
inReplyToPostId: currentStatus.id,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
for (const child of children) {
|
||||
descendants.push(child);
|
||||
for (const child of children) {
|
||||
descendants.push(child);
|
||||
|
||||
if (depth < 20) {
|
||||
const childDescendants = await getDescendants(
|
||||
child,
|
||||
fetcher,
|
||||
depth + 1
|
||||
);
|
||||
descendants.push(...childDescendants);
|
||||
}
|
||||
}
|
||||
if (depth < 20) {
|
||||
const childDescendants = await getDescendants(
|
||||
child,
|
||||
fetcher,
|
||||
depth + 1,
|
||||
);
|
||||
descendants.push(...childDescendants);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter for posts that are viewable by the user
|
||||
// Filter for posts that are viewable by the user
|
||||
|
||||
const viewableDescendants = descendants.filter(descendant =>
|
||||
isViewableByUser(descendant, fetcher)
|
||||
);
|
||||
return viewableDescendants;
|
||||
const viewableDescendants = descendants.filter((descendant) =>
|
||||
isViewableByUser(descendant, fetcher),
|
||||
);
|
||||
return viewableDescendants;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -201,250 +200,250 @@ export const getDescendants = async (
|
|||
* @returns A promise that resolves with the new status.
|
||||
*/
|
||||
export const createNewStatus = async (data: {
|
||||
account: User;
|
||||
application: Application | null;
|
||||
content: string;
|
||||
visibility: APIStatus["visibility"];
|
||||
sensitive: boolean;
|
||||
spoiler_text: string;
|
||||
emojis?: Emoji[];
|
||||
content_type?: string;
|
||||
uri?: string;
|
||||
mentions?: User[];
|
||||
media_attachments?: string[];
|
||||
reply?: {
|
||||
status: Status;
|
||||
user: User;
|
||||
};
|
||||
quote?: Status;
|
||||
account: User;
|
||||
application: Application | null;
|
||||
content: string;
|
||||
visibility: APIStatus["visibility"];
|
||||
sensitive: boolean;
|
||||
spoiler_text: string;
|
||||
emojis?: Emoji[];
|
||||
content_type?: string;
|
||||
uri?: string;
|
||||
mentions?: User[];
|
||||
media_attachments?: string[];
|
||||
reply?: {
|
||||
status: Status;
|
||||
user: User;
|
||||
};
|
||||
quote?: Status;
|
||||
}) => {
|
||||
// Get people mentioned in the content (match @username or @username@domain.com mentions)
|
||||
const mentionedPeople =
|
||||
data.content.match(/@[a-zA-Z0-9_]+(@[a-zA-Z0-9_]+)?/g) ?? [];
|
||||
// Get people mentioned in the content (match @username or @username@domain.com mentions)
|
||||
const mentionedPeople =
|
||||
data.content.match(/@[a-zA-Z0-9_]+(@[a-zA-Z0-9_]+)?/g) ?? [];
|
||||
|
||||
let mentions = data.mentions || [];
|
||||
let mentions = data.mentions || [];
|
||||
|
||||
// Parse emojis
|
||||
const emojis = await parseEmojis(data.content);
|
||||
// Parse emojis
|
||||
const emojis = await parseEmojis(data.content);
|
||||
|
||||
data.emojis = data.emojis ? [...data.emojis, ...emojis] : emojis;
|
||||
data.emojis = data.emojis ? [...data.emojis, ...emojis] : emojis;
|
||||
|
||||
// Get list of mentioned users
|
||||
if (mentions.length === 0) {
|
||||
mentions = await client.user.findMany({
|
||||
where: {
|
||||
OR: mentionedPeople.map(person => ({
|
||||
username: person.split("@")[1],
|
||||
instance: {
|
||||
base_url: person.split("@")[2],
|
||||
},
|
||||
})),
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
}
|
||||
// Get list of mentioned users
|
||||
if (mentions.length === 0) {
|
||||
mentions = await client.user.findMany({
|
||||
where: {
|
||||
OR: mentionedPeople.map((person) => ({
|
||||
username: person.split("@")[1],
|
||||
instance: {
|
||||
base_url: person.split("@")[2],
|
||||
},
|
||||
})),
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
}
|
||||
|
||||
let formattedContent;
|
||||
let formattedContent = "";
|
||||
|
||||
// Get HTML version of content
|
||||
if (data.content_type === "text/markdown") {
|
||||
formattedContent = linkifyHtml(
|
||||
await sanitizeHtml(await parse(data.content))
|
||||
);
|
||||
} else if (data.content_type === "text/x.misskeymarkdown") {
|
||||
// Parse as MFM
|
||||
} else {
|
||||
// Parse as plaintext
|
||||
formattedContent = linkifyStr(data.content);
|
||||
// Get HTML version of content
|
||||
if (data.content_type === "text/markdown") {
|
||||
formattedContent = linkifyHtml(
|
||||
await sanitizeHtml(await parse(data.content)),
|
||||
);
|
||||
} else if (data.content_type === "text/x.misskeymarkdown") {
|
||||
// Parse as MFM
|
||||
} else {
|
||||
// Parse as plaintext
|
||||
formattedContent = linkifyStr(data.content);
|
||||
|
||||
// Split by newline and add <p> tags
|
||||
formattedContent = formattedContent
|
||||
.split("\n")
|
||||
.map(line => `<p>${line}</p>`)
|
||||
.join("\n");
|
||||
}
|
||||
// Split by newline and add <p> tags
|
||||
formattedContent = formattedContent
|
||||
.split("\n")
|
||||
.map((line) => `<p>${line}</p>`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
let status = await client.status.create({
|
||||
data: {
|
||||
authorId: data.account.id,
|
||||
applicationId: data.application?.id,
|
||||
content: formattedContent,
|
||||
contentSource: data.content,
|
||||
contentType: data.content_type,
|
||||
visibility: data.visibility,
|
||||
sensitive: data.sensitive,
|
||||
spoilerText: data.spoiler_text,
|
||||
emojis: {
|
||||
connect: data.emojis.map(emoji => {
|
||||
return {
|
||||
id: emoji.id,
|
||||
};
|
||||
}),
|
||||
},
|
||||
attachments: data.media_attachments
|
||||
? {
|
||||
connect: data.media_attachments.map(attachment => {
|
||||
return {
|
||||
id: attachment,
|
||||
};
|
||||
}),
|
||||
}
|
||||
: undefined,
|
||||
inReplyToPostId: data.reply?.status.id,
|
||||
quotingPostId: data.quote?.id,
|
||||
instanceId: data.account.instanceId || undefined,
|
||||
isReblog: false,
|
||||
uri:
|
||||
data.uri ||
|
||||
`${config.http.base_url}/statuses/FAKE-${crypto.randomUUID()}`,
|
||||
mentions: {
|
||||
connect: mentions.map(mention => {
|
||||
return {
|
||||
id: mention.id,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
let status = await client.status.create({
|
||||
data: {
|
||||
authorId: data.account.id,
|
||||
applicationId: data.application?.id,
|
||||
content: formattedContent,
|
||||
contentSource: data.content,
|
||||
contentType: data.content_type,
|
||||
visibility: data.visibility,
|
||||
sensitive: data.sensitive,
|
||||
spoilerText: data.spoiler_text,
|
||||
emojis: {
|
||||
connect: data.emojis.map((emoji) => {
|
||||
return {
|
||||
id: emoji.id,
|
||||
};
|
||||
}),
|
||||
},
|
||||
attachments: data.media_attachments
|
||||
? {
|
||||
connect: data.media_attachments.map((attachment) => {
|
||||
return {
|
||||
id: attachment,
|
||||
};
|
||||
}),
|
||||
}
|
||||
: undefined,
|
||||
inReplyToPostId: data.reply?.status.id,
|
||||
quotingPostId: data.quote?.id,
|
||||
instanceId: data.account.instanceId || undefined,
|
||||
isReblog: false,
|
||||
uri:
|
||||
data.uri ||
|
||||
`${config.http.base_url}/statuses/FAKE-${crypto.randomUUID()}`,
|
||||
mentions: {
|
||||
connect: mentions.map((mention) => {
|
||||
return {
|
||||
id: mention.id,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
// Update URI
|
||||
status = await client.status.update({
|
||||
where: {
|
||||
id: status.id,
|
||||
},
|
||||
data: {
|
||||
uri: data.uri || `${config.http.base_url}/statuses/${status.id}`,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
// Update URI
|
||||
status = await client.status.update({
|
||||
where: {
|
||||
id: status.id,
|
||||
},
|
||||
data: {
|
||||
uri: data.uri || `${config.http.base_url}/statuses/${status.id}`,
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
// Create notification
|
||||
if (status.inReplyToPost) {
|
||||
await client.notification.create({
|
||||
data: {
|
||||
notifiedId: status.inReplyToPost.authorId,
|
||||
accountId: status.authorId,
|
||||
type: "mention",
|
||||
statusId: status.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
// Create notification
|
||||
if (status.inReplyToPost) {
|
||||
await client.notification.create({
|
||||
data: {
|
||||
notifiedId: status.inReplyToPost.authorId,
|
||||
accountId: status.authorId,
|
||||
type: "mention",
|
||||
statusId: status.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Add to search index
|
||||
await addStausToMeilisearch(status);
|
||||
// Add to search index
|
||||
await addStausToMeilisearch(status);
|
||||
|
||||
return status;
|
||||
return status;
|
||||
};
|
||||
|
||||
export const editStatus = async (
|
||||
status: StatusWithRelations,
|
||||
data: {
|
||||
content: string;
|
||||
visibility?: APIStatus["visibility"];
|
||||
sensitive: boolean;
|
||||
spoiler_text: string;
|
||||
emojis?: Emoji[];
|
||||
content_type?: string;
|
||||
uri?: string;
|
||||
mentions?: User[];
|
||||
media_attachments?: string[];
|
||||
}
|
||||
status: StatusWithRelations,
|
||||
data: {
|
||||
content: string;
|
||||
visibility?: APIStatus["visibility"];
|
||||
sensitive: boolean;
|
||||
spoiler_text: string;
|
||||
emojis?: Emoji[];
|
||||
content_type?: string;
|
||||
uri?: string;
|
||||
mentions?: User[];
|
||||
media_attachments?: string[];
|
||||
},
|
||||
) => {
|
||||
// Get people mentioned in the content (match @username or @username@domain.com mentions
|
||||
const mentionedPeople =
|
||||
data.content.match(/@[a-zA-Z0-9_]+(@[a-zA-Z0-9_]+)?/g) ?? [];
|
||||
// Get people mentioned in the content (match @username or @username@domain.com mentions
|
||||
const mentionedPeople =
|
||||
data.content.match(/@[a-zA-Z0-9_]+(@[a-zA-Z0-9_]+)?/g) ?? [];
|
||||
|
||||
let mentions = data.mentions || [];
|
||||
let mentions = data.mentions || [];
|
||||
|
||||
// Parse emojis
|
||||
const emojis = await parseEmojis(data.content);
|
||||
// Parse emojis
|
||||
const emojis = await parseEmojis(data.content);
|
||||
|
||||
data.emojis = data.emojis ? [...data.emojis, ...emojis] : emojis;
|
||||
data.emojis = data.emojis ? [...data.emojis, ...emojis] : emojis;
|
||||
|
||||
// Get list of mentioned users
|
||||
if (mentions.length === 0) {
|
||||
mentions = await client.user.findMany({
|
||||
where: {
|
||||
OR: mentionedPeople.map(person => ({
|
||||
username: person.split("@")[1],
|
||||
instance: {
|
||||
base_url: person.split("@")[2],
|
||||
},
|
||||
})),
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
}
|
||||
// Get list of mentioned users
|
||||
if (mentions.length === 0) {
|
||||
mentions = await client.user.findMany({
|
||||
where: {
|
||||
OR: mentionedPeople.map((person) => ({
|
||||
username: person.split("@")[1],
|
||||
instance: {
|
||||
base_url: person.split("@")[2],
|
||||
},
|
||||
})),
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
}
|
||||
|
||||
let formattedContent;
|
||||
let formattedContent = "";
|
||||
|
||||
// Get HTML version of content
|
||||
if (data.content_type === "text/markdown") {
|
||||
formattedContent = linkifyHtml(
|
||||
await sanitizeHtml(await parse(data.content))
|
||||
);
|
||||
} else if (data.content_type === "text/x.misskeymarkdown") {
|
||||
// Parse as MFM
|
||||
} else {
|
||||
// Parse as plaintext
|
||||
formattedContent = linkifyStr(data.content);
|
||||
// Get HTML version of content
|
||||
if (data.content_type === "text/markdown") {
|
||||
formattedContent = linkifyHtml(
|
||||
await sanitizeHtml(await parse(data.content)),
|
||||
);
|
||||
} else if (data.content_type === "text/x.misskeymarkdown") {
|
||||
// Parse as MFM
|
||||
} else {
|
||||
// Parse as plaintext
|
||||
formattedContent = linkifyStr(data.content);
|
||||
|
||||
// Split by newline and add <p> tags
|
||||
formattedContent = formattedContent
|
||||
.split("\n")
|
||||
.map(line => `<p>${line}</p>`)
|
||||
.join("\n");
|
||||
}
|
||||
// Split by newline and add <p> tags
|
||||
formattedContent = formattedContent
|
||||
.split("\n")
|
||||
.map((line) => `<p>${line}</p>`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
const newStatus = await client.status.update({
|
||||
where: {
|
||||
id: status.id,
|
||||
},
|
||||
data: {
|
||||
content: formattedContent,
|
||||
contentSource: data.content,
|
||||
contentType: data.content_type,
|
||||
visibility: data.visibility,
|
||||
sensitive: data.sensitive,
|
||||
spoilerText: data.spoiler_text,
|
||||
emojis: {
|
||||
connect: data.emojis.map(emoji => {
|
||||
return {
|
||||
id: emoji.id,
|
||||
};
|
||||
}),
|
||||
},
|
||||
attachments: data.media_attachments
|
||||
? {
|
||||
connect: data.media_attachments.map(attachment => {
|
||||
return {
|
||||
id: attachment,
|
||||
};
|
||||
}),
|
||||
}
|
||||
: undefined,
|
||||
mentions: {
|
||||
connect: mentions.map(mention => {
|
||||
return {
|
||||
id: mention.id,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
const newStatus = await client.status.update({
|
||||
where: {
|
||||
id: status.id,
|
||||
},
|
||||
data: {
|
||||
content: formattedContent,
|
||||
contentSource: data.content,
|
||||
contentType: data.content_type,
|
||||
visibility: data.visibility,
|
||||
sensitive: data.sensitive,
|
||||
spoilerText: data.spoiler_text,
|
||||
emojis: {
|
||||
connect: data.emojis.map((emoji) => {
|
||||
return {
|
||||
id: emoji.id,
|
||||
};
|
||||
}),
|
||||
},
|
||||
attachments: data.media_attachments
|
||||
? {
|
||||
connect: data.media_attachments.map((attachment) => {
|
||||
return {
|
||||
id: attachment,
|
||||
};
|
||||
}),
|
||||
}
|
||||
: undefined,
|
||||
mentions: {
|
||||
connect: mentions.map((mention) => {
|
||||
return {
|
||||
id: mention.id,
|
||||
};
|
||||
}),
|
||||
},
|
||||
},
|
||||
include: statusAndUserRelations,
|
||||
});
|
||||
|
||||
return newStatus;
|
||||
return newStatus;
|
||||
};
|
||||
|
||||
export const isFavouritedBy = async (status: Status, user: User) => {
|
||||
return !!(await client.like.findFirst({
|
||||
where: {
|
||||
likerId: user.id,
|
||||
likedId: status.id,
|
||||
},
|
||||
}));
|
||||
return !!(await client.like.findFirst({
|
||||
where: {
|
||||
likerId: user.id,
|
||||
likedId: status.id,
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -452,67 +451,67 @@ export const isFavouritedBy = async (status: Status, user: User) => {
|
|||
* @returns A promise that resolves with the API status.
|
||||
*/
|
||||
export const statusToAPI = async (
|
||||
status: StatusWithRelations,
|
||||
user?: UserWithRelations
|
||||
status: StatusWithRelations,
|
||||
user?: UserWithRelations,
|
||||
): Promise<APIStatus> => {
|
||||
return {
|
||||
id: status.id,
|
||||
in_reply_to_id: status.inReplyToPostId || null,
|
||||
in_reply_to_account_id: status.inReplyToPost?.authorId || null,
|
||||
// @ts-expect-error Prisma TypeScript types dont include relations
|
||||
account: userToAPI(status.author),
|
||||
created_at: new Date(status.createdAt).toISOString(),
|
||||
application: status.application
|
||||
? applicationToAPI(status.application)
|
||||
: null,
|
||||
card: null,
|
||||
content: status.content,
|
||||
emojis: status.emojis.map(emoji => emojiToAPI(emoji)),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
favourited: !!(status.likes ?? []).find(
|
||||
like => like.likerId === user?.id
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
favourites_count: (status.likes ?? []).length,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
media_attachments: (status.attachments ?? []).map(
|
||||
a => attachmentToAPI(a) as APIAttachment
|
||||
),
|
||||
// @ts-expect-error Prisma TypeScript types dont include relations
|
||||
mentions: status.mentions.map(mention => userToAPI(mention)),
|
||||
language: null,
|
||||
muted: user
|
||||
? user.relationships.find(r => r.subjectId == status.authorId)
|
||||
?.muting || false
|
||||
: false,
|
||||
pinned: status.pinnedBy.find(u => u.id === user?.id) ? true : false,
|
||||
// TODO: Add pols
|
||||
poll: null,
|
||||
reblog: status.reblog
|
||||
? await statusToAPI(status.reblog as unknown as StatusWithRelations)
|
||||
: null,
|
||||
reblogged: !!(await client.status.findFirst({
|
||||
where: {
|
||||
authorId: user?.id,
|
||||
reblogId: status.id,
|
||||
},
|
||||
})),
|
||||
reblogs_count: status._count.reblogs,
|
||||
replies_count: status._count.replies,
|
||||
sensitive: status.sensitive,
|
||||
spoiler_text: status.spoilerText,
|
||||
tags: [],
|
||||
uri: `${config.http.base_url}/statuses/${status.id}`,
|
||||
visibility: "public",
|
||||
url: `${config.http.base_url}/statuses/${status.id}`,
|
||||
bookmarked: false,
|
||||
quote: status.quotingPost
|
||||
? await statusToAPI(
|
||||
status.quotingPost as unknown as StatusWithRelations
|
||||
)
|
||||
: null,
|
||||
quote_id: status.quotingPost?.id || undefined,
|
||||
};
|
||||
return {
|
||||
id: status.id,
|
||||
in_reply_to_id: status.inReplyToPostId || null,
|
||||
in_reply_to_account_id: status.inReplyToPost?.authorId || null,
|
||||
// @ts-expect-error Prisma TypeScript types dont include relations
|
||||
account: userToAPI(status.author),
|
||||
created_at: new Date(status.createdAt).toISOString(),
|
||||
application: status.application
|
||||
? applicationToAPI(status.application)
|
||||
: null,
|
||||
card: null,
|
||||
content: status.content,
|
||||
emojis: status.emojis.map((emoji) => emojiToAPI(emoji)),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
favourited: !!(status.likes ?? []).find(
|
||||
(like) => like.likerId === user?.id,
|
||||
),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
favourites_count: (status.likes ?? []).length,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
media_attachments: (status.attachments ?? []).map(
|
||||
(a) => attachmentToAPI(a) as APIAttachment,
|
||||
),
|
||||
// @ts-expect-error Prisma TypeScript types dont include relations
|
||||
mentions: status.mentions.map((mention) => userToAPI(mention)),
|
||||
language: null,
|
||||
muted: user
|
||||
? user.relationships.find((r) => r.subjectId === status.authorId)
|
||||
?.muting || false
|
||||
: false,
|
||||
pinned: status.pinnedBy.find((u) => u.id === user?.id) ? true : false,
|
||||
// TODO: Add pols
|
||||
poll: null,
|
||||
reblog: status.reblog
|
||||
? await statusToAPI(status.reblog as unknown as StatusWithRelations)
|
||||
: null,
|
||||
reblogged: !!(await client.status.findFirst({
|
||||
where: {
|
||||
authorId: user?.id,
|
||||
reblogId: status.id,
|
||||
},
|
||||
})),
|
||||
reblogs_count: status._count.reblogs,
|
||||
replies_count: status._count.replies,
|
||||
sensitive: status.sensitive,
|
||||
spoiler_text: status.spoilerText,
|
||||
tags: [],
|
||||
uri: `${config.http.base_url}/statuses/${status.id}`,
|
||||
visibility: "public",
|
||||
url: `${config.http.base_url}/statuses/${status.id}`,
|
||||
bookmarked: false,
|
||||
quote: status.quotingPost
|
||||
? await statusToAPI(
|
||||
status.quotingPost as unknown as StatusWithRelations,
|
||||
)
|
||||
: null,
|
||||
quote_id: status.quotingPost?.id || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
/* export const statusToActivityPub = async (
|
||||
|
|
@ -563,35 +562,35 @@ export const statusToAPI = async (
|
|||
}; */
|
||||
|
||||
export const statusToLysand = (status: StatusWithRelations): Note => {
|
||||
return {
|
||||
type: "Note",
|
||||
created_at: new Date(status.createdAt).toISOString(),
|
||||
id: status.id,
|
||||
author: status.authorId,
|
||||
uri: `${config.http.base_url}/users/${status.authorId}/statuses/${status.id}`,
|
||||
contents: [
|
||||
{
|
||||
content: status.content,
|
||||
content_type: "text/html",
|
||||
},
|
||||
{
|
||||
// Content converted to plaintext
|
||||
content: htmlToText(status.content),
|
||||
content_type: "text/plain",
|
||||
},
|
||||
],
|
||||
// TODO: Add attachments
|
||||
attachments: [],
|
||||
is_sensitive: status.sensitive,
|
||||
mentions: status.mentions.map(mention => mention.uri),
|
||||
quotes: status.quotingPost ? [status.quotingPost.uri] : [],
|
||||
replies_to: status.inReplyToPostId ? [status.inReplyToPostId] : [],
|
||||
subject: status.spoilerText,
|
||||
extensions: {
|
||||
"org.lysand:custom_emojis": {
|
||||
emojis: status.emojis.map(emoji => emojiToLysand(emoji)),
|
||||
},
|
||||
// TODO: Add polls and reactions
|
||||
},
|
||||
};
|
||||
return {
|
||||
type: "Note",
|
||||
created_at: new Date(status.createdAt).toISOString(),
|
||||
id: status.id,
|
||||
author: status.authorId,
|
||||
uri: `${config.http.base_url}/users/${status.authorId}/statuses/${status.id}`,
|
||||
contents: [
|
||||
{
|
||||
content: status.content,
|
||||
content_type: "text/html",
|
||||
},
|
||||
{
|
||||
// Content converted to plaintext
|
||||
content: htmlToText(status.content),
|
||||
content_type: "text/plain",
|
||||
},
|
||||
],
|
||||
// TODO: Add attachments
|
||||
attachments: [],
|
||||
is_sensitive: status.sensitive,
|
||||
mentions: status.mentions.map((mention) => mention.uri),
|
||||
quotes: status.quotingPost ? [status.quotingPost.uri] : [],
|
||||
replies_to: status.inReplyToPostId ? [status.inReplyToPostId] : [],
|
||||
subject: status.spoilerText,
|
||||
extensions: {
|
||||
"org.lysand:custom_emojis": {
|
||||
emojis: status.emojis.map((emoji) => emojiToLysand(emoji)),
|
||||
},
|
||||
// TODO: Add polls and reactions
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@
|
|||
* The type of token.
|
||||
*/
|
||||
export enum TokenType {
|
||||
BEARER = "Bearer",
|
||||
BEARER = "Bearer",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
import type { APIAccount } from "~types/entities/account";
|
||||
import type { LysandUser } from "~types/lysand/Object";
|
||||
import { htmlToText } from "html-to-text";
|
||||
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 { MediaBackendType } from "~packages/media-manager";
|
||||
import type { APIAccount } from "~types/entities/account";
|
||||
import type { APISource } from "~types/entities/source";
|
||||
import type { LysandUser } from "~types/lysand/Object";
|
||||
import { addEmojiIfNotExists, emojiToAPI, emojiToLysand } from "./Emoji";
|
||||
import { addInstanceIfNotExists } from "./Instance";
|
||||
import type { APISource } from "~types/entities/source";
|
||||
import { addUserToMeilisearch } from "@meilisearch";
|
||||
import { config, type Config } from "config-manager";
|
||||
import { userRelations } from "./relations";
|
||||
import { MediaBackendType } from "~packages/media-manager";
|
||||
|
||||
export interface AuthData {
|
||||
user: UserWithRelations | null;
|
||||
token: string;
|
||||
user: UserWithRelations | null;
|
||||
token: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -23,7 +23,7 @@ export interface AuthData {
|
|||
*/
|
||||
|
||||
const userRelations2 = Prisma.validator<Prisma.UserDefaultArgs>()({
|
||||
include: userRelations,
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
export type UserWithRelations = Prisma.UserGetPayload<typeof userRelations2>;
|
||||
|
|
@ -34,14 +34,15 @@ export type UserWithRelations = Prisma.UserGetPayload<typeof userRelations2>;
|
|||
* @returns The raw URL for the user's avatar
|
||||
*/
|
||||
export const getAvatarUrl = (user: User, config: Config) => {
|
||||
if (!user.avatar) return config.defaults.avatar;
|
||||
if (config.media.backend === MediaBackendType.LOCAL) {
|
||||
return `${config.http.base_url}/media/${user.avatar}`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
} else if (config.media.backend === MediaBackendType.S3) {
|
||||
return `${config.s3.public_url}/${user.avatar}`;
|
||||
}
|
||||
return "";
|
||||
if (!user.avatar) return config.defaults.avatar;
|
||||
if (config.media.backend === MediaBackendType.LOCAL) {
|
||||
return `${config.http.base_url}/media/${user.avatar}`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
}
|
||||
if (config.media.backend === MediaBackendType.S3) {
|
||||
return `${config.s3.public_url}/${user.avatar}`;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -50,128 +51,129 @@ export const getAvatarUrl = (user: User, config: Config) => {
|
|||
* @returns The raw URL for the user's header
|
||||
*/
|
||||
export const getHeaderUrl = (user: User, config: Config) => {
|
||||
if (!user.header) return config.defaults.header;
|
||||
if (config.media.backend === MediaBackendType.LOCAL) {
|
||||
return `${config.http.base_url}/media/${user.header}`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
} else if (config.media.backend === MediaBackendType.S3) {
|
||||
return `${config.s3.public_url}/${user.header}`;
|
||||
}
|
||||
return "";
|
||||
if (!user.header) return config.defaults.header;
|
||||
if (config.media.backend === MediaBackendType.LOCAL) {
|
||||
return `${config.http.base_url}/media/${user.header}`;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
}
|
||||
if (config.media.backend === MediaBackendType.S3) {
|
||||
return `${config.s3.public_url}/${user.header}`;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
export const getFromRequest = async (req: Request): Promise<AuthData> => {
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || "";
|
||||
// Check auth token
|
||||
const token = req.headers.get("Authorization")?.split(" ")[1] || "";
|
||||
|
||||
return { user: await retrieveUserFromToken(token), token };
|
||||
return { user: await retrieveUserFromToken(token), token };
|
||||
};
|
||||
|
||||
export const fetchRemoteUser = async (uri: string) => {
|
||||
// Check if user not already in database
|
||||
const foundUser = await client.user.findUnique({
|
||||
where: {
|
||||
uri,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
// Check if user not already in database
|
||||
const foundUser = await client.user.findUnique({
|
||||
where: {
|
||||
uri,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
|
||||
if (foundUser) return foundUser;
|
||||
if (foundUser) return foundUser;
|
||||
|
||||
const response = await fetch(uri, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
const response = await fetch(uri, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const data = (await response.json()) as Partial<LysandUser>;
|
||||
const data = (await response.json()) as Partial<LysandUser>;
|
||||
|
||||
if (
|
||||
!(
|
||||
data.id &&
|
||||
data.username &&
|
||||
data.uri &&
|
||||
data.created_at &&
|
||||
data.disliked &&
|
||||
data.featured &&
|
||||
data.liked &&
|
||||
data.followers &&
|
||||
data.following &&
|
||||
data.inbox &&
|
||||
data.outbox &&
|
||||
data.public_key
|
||||
)
|
||||
) {
|
||||
throw new Error("Invalid user data");
|
||||
}
|
||||
if (
|
||||
!(
|
||||
data.id &&
|
||||
data.username &&
|
||||
data.uri &&
|
||||
data.created_at &&
|
||||
data.disliked &&
|
||||
data.featured &&
|
||||
data.liked &&
|
||||
data.followers &&
|
||||
data.following &&
|
||||
data.inbox &&
|
||||
data.outbox &&
|
||||
data.public_key
|
||||
)
|
||||
) {
|
||||
throw new Error("Invalid user data");
|
||||
}
|
||||
|
||||
// Parse emojis and add them to database
|
||||
const userEmojis =
|
||||
data.extensions?.["org.lysand:custom_emojis"]?.emojis ?? [];
|
||||
// Parse emojis and add them to database
|
||||
const userEmojis =
|
||||
data.extensions?.["org.lysand:custom_emojis"]?.emojis ?? [];
|
||||
|
||||
const user = await client.user.create({
|
||||
data: {
|
||||
username: data.username,
|
||||
uri: data.uri,
|
||||
createdAt: new Date(data.created_at),
|
||||
endpoints: {
|
||||
disliked: data.disliked,
|
||||
featured: data.featured,
|
||||
liked: data.liked,
|
||||
followers: data.followers,
|
||||
following: data.following,
|
||||
inbox: data.inbox,
|
||||
outbox: data.outbox,
|
||||
},
|
||||
avatar: (data.avatar && data.avatar[0].content) || "",
|
||||
header: (data.header && data.header[0].content) || "",
|
||||
displayName: data.display_name ?? "",
|
||||
note: data.bio?.[0].content ?? "",
|
||||
publicKey: data.public_key.public_key,
|
||||
source: {
|
||||
language: null,
|
||||
note: "",
|
||||
privacy: "public",
|
||||
sensitive: false,
|
||||
fields: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
const user = await client.user.create({
|
||||
data: {
|
||||
username: data.username,
|
||||
uri: data.uri,
|
||||
createdAt: new Date(data.created_at),
|
||||
endpoints: {
|
||||
disliked: data.disliked,
|
||||
featured: data.featured,
|
||||
liked: data.liked,
|
||||
followers: data.followers,
|
||||
following: data.following,
|
||||
inbox: data.inbox,
|
||||
outbox: data.outbox,
|
||||
},
|
||||
avatar: data.avatar?.[0].content || "",
|
||||
header: data.header?.[0].content || "",
|
||||
displayName: data.display_name ?? "",
|
||||
note: data.bio?.[0].content ?? "",
|
||||
publicKey: data.public_key.public_key,
|
||||
source: {
|
||||
language: null,
|
||||
note: "",
|
||||
privacy: "public",
|
||||
sensitive: false,
|
||||
fields: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Add to Meilisearch
|
||||
await addUserToMeilisearch(user);
|
||||
// Add to Meilisearch
|
||||
await addUserToMeilisearch(user);
|
||||
|
||||
const emojis = [];
|
||||
const emojis = [];
|
||||
|
||||
for (const emoji of userEmojis) {
|
||||
emojis.push(await addEmojiIfNotExists(emoji));
|
||||
}
|
||||
for (const emoji of userEmojis) {
|
||||
emojis.push(await addEmojiIfNotExists(emoji));
|
||||
}
|
||||
|
||||
const uriData = new URL(data.uri);
|
||||
const uriData = new URL(data.uri);
|
||||
|
||||
return await client.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
emojis: {
|
||||
connect: emojis.map(emoji => ({
|
||||
id: emoji.id,
|
||||
})),
|
||||
},
|
||||
instanceId: (await addInstanceIfNotExists(uriData.origin)).id,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
return await client.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
emojis: {
|
||||
connect: emojis.map((emoji) => ({
|
||||
id: emoji.id,
|
||||
})),
|
||||
},
|
||||
instanceId: (await addInstanceIfNotExists(uriData.origin)).id,
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the list of followers associated with the actor and updates the user's followers
|
||||
*/
|
||||
export const fetchFollowers = () => {
|
||||
//
|
||||
//
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -180,75 +182,75 @@ export const fetchFollowers = () => {
|
|||
* @returns The newly created user.
|
||||
*/
|
||||
export const createNewLocalUser = async (data: {
|
||||
username: string;
|
||||
display_name?: string;
|
||||
password: string;
|
||||
email: string;
|
||||
bio?: string;
|
||||
avatar?: string;
|
||||
header?: string;
|
||||
admin?: boolean;
|
||||
username: string;
|
||||
display_name?: string;
|
||||
password: string;
|
||||
email: string;
|
||||
bio?: string;
|
||||
avatar?: string;
|
||||
header?: string;
|
||||
admin?: boolean;
|
||||
}) => {
|
||||
const keys = await generateUserKeys();
|
||||
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,
|
||||
uri: "",
|
||||
publicKey: keys.public_key,
|
||||
privateKey: keys.private_key,
|
||||
source: {
|
||||
language: null,
|
||||
note: "",
|
||||
privacy: "public",
|
||||
sensitive: false,
|
||||
fields: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
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,
|
||||
uri: "",
|
||||
publicKey: keys.public_key,
|
||||
privateKey: keys.private_key,
|
||||
source: {
|
||||
language: null,
|
||||
note: "",
|
||||
privacy: "public",
|
||||
sensitive: false,
|
||||
fields: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Add to Meilisearch
|
||||
await addUserToMeilisearch(user);
|
||||
// Add to Meilisearch
|
||||
await addUserToMeilisearch(user);
|
||||
|
||||
return await client.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
uri: `${config.http.base_url}/users/${user.id}`,
|
||||
endpoints: {
|
||||
disliked: `${config.http.base_url}/users/${user.id}/disliked`,
|
||||
featured: `${config.http.base_url}/users/${user.id}/featured`,
|
||||
liked: `${config.http.base_url}/users/${user.id}/liked`,
|
||||
followers: `${config.http.base_url}/users/${user.id}/followers`,
|
||||
following: `${config.http.base_url}/users/${user.id}/following`,
|
||||
inbox: `${config.http.base_url}/users/${user.id}/inbox`,
|
||||
outbox: `${config.http.base_url}/users/${user.id}/outbox`,
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
return await client.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
uri: `${config.http.base_url}/users/${user.id}`,
|
||||
endpoints: {
|
||||
disliked: `${config.http.base_url}/users/${user.id}/disliked`,
|
||||
featured: `${config.http.base_url}/users/${user.id}/featured`,
|
||||
liked: `${config.http.base_url}/users/${user.id}/liked`,
|
||||
followers: `${config.http.base_url}/users/${user.id}/followers`,
|
||||
following: `${config.http.base_url}/users/${user.id}/following`,
|
||||
inbox: `${config.http.base_url}/users/${user.id}/inbox`,
|
||||
outbox: `${config.http.base_url}/users/${user.id}/outbox`,
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses mentions from a list of URIs
|
||||
*/
|
||||
export const parseMentionsUris = async (mentions: string[]) => {
|
||||
return await client.user.findMany({
|
||||
where: {
|
||||
uri: {
|
||||
in: mentions,
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
return await client.user.findMany({
|
||||
where: {
|
||||
uri: {
|
||||
in: mentions,
|
||||
},
|
||||
},
|
||||
include: userRelations,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -257,22 +259,22 @@ export const parseMentionsUris = async (mentions: string[]) => {
|
|||
* @returns The user associated with the given access token.
|
||||
*/
|
||||
export const retrieveUserFromToken = async (access_token: string) => {
|
||||
if (!access_token) return null;
|
||||
if (!access_token) return null;
|
||||
|
||||
const token = await client.token.findFirst({
|
||||
where: {
|
||||
access_token,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
include: userRelations,
|
||||
},
|
||||
},
|
||||
});
|
||||
const token = await client.token.findFirst({
|
||||
where: {
|
||||
access_token,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
include: userRelations,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!token) return null;
|
||||
if (!token) return null;
|
||||
|
||||
return token.user;
|
||||
return token.user;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -281,174 +283,174 @@ export const retrieveUserFromToken = async (access_token: string) => {
|
|||
* @returns The relationship to the other user.
|
||||
*/
|
||||
export const getRelationshipToOtherUser = async (
|
||||
user: UserWithRelations,
|
||||
other: User
|
||||
user: UserWithRelations,
|
||||
other: User,
|
||||
) => {
|
||||
return await client.relationship.findFirst({
|
||||
where: {
|
||||
ownerId: user.id,
|
||||
subjectId: other.id,
|
||||
},
|
||||
});
|
||||
return await client.relationship.findFirst({
|
||||
where: {
|
||||
ownerId: user.id,
|
||||
subjectId: other.id,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates keys for the user.
|
||||
*/
|
||||
export const generateUserKeys = async () => {
|
||||
const keys = await crypto.subtle.generateKey("Ed25519", true, [
|
||||
"sign",
|
||||
"verify",
|
||||
]);
|
||||
const keys = await crypto.subtle.generateKey("Ed25519", true, [
|
||||
"sign",
|
||||
"verify",
|
||||
]);
|
||||
|
||||
const privateKey = btoa(
|
||||
String.fromCharCode.apply(null, [
|
||||
...new Uint8Array(
|
||||
// jesus help me what do these letters mean
|
||||
await crypto.subtle.exportKey("pkcs8", keys.privateKey)
|
||||
),
|
||||
])
|
||||
);
|
||||
const publicKey = btoa(
|
||||
String.fromCharCode(
|
||||
...new Uint8Array(
|
||||
// why is exporting a key so hard
|
||||
await crypto.subtle.exportKey("spki", keys.publicKey)
|
||||
)
|
||||
)
|
||||
);
|
||||
const privateKey = btoa(
|
||||
String.fromCharCode.apply(null, [
|
||||
...new Uint8Array(
|
||||
// jesus help me what do these letters mean
|
||||
await crypto.subtle.exportKey("pkcs8", keys.privateKey),
|
||||
),
|
||||
]),
|
||||
);
|
||||
const publicKey = btoa(
|
||||
String.fromCharCode(
|
||||
...new Uint8Array(
|
||||
// why is exporting a key so hard
|
||||
await crypto.subtle.exportKey("spki", keys.publicKey),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Add header, footer and newlines later on
|
||||
// These keys are base64 encrypted
|
||||
return {
|
||||
private_key: privateKey,
|
||||
public_key: publicKey,
|
||||
};
|
||||
// Add header, footer and newlines later on
|
||||
// These keys are base64 encrypted
|
||||
return {
|
||||
private_key: privateKey,
|
||||
public_key: publicKey,
|
||||
};
|
||||
};
|
||||
|
||||
export const userToAPI = (
|
||||
user: UserWithRelations,
|
||||
isOwnAccount = false
|
||||
user: UserWithRelations,
|
||||
isOwnAccount = false,
|
||||
): APIAccount => {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
display_name: user.displayName,
|
||||
note: user.note,
|
||||
url: user.uri,
|
||||
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)),
|
||||
// TODO: Add fields
|
||||
fields: [],
|
||||
bot: user.isBot,
|
||||
source:
|
||||
isOwnAccount && user.source
|
||||
? (user.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}`,
|
||||
// TODO: Add these fields
|
||||
limited: false,
|
||||
moved: null,
|
||||
noindex: false,
|
||||
suspended: false,
|
||||
discoverable: undefined,
|
||||
mute_expires_at: undefined,
|
||||
group: false,
|
||||
pleroma: {
|
||||
is_admin: user.isAdmin,
|
||||
is_moderator: user.isAdmin,
|
||||
},
|
||||
};
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
display_name: user.displayName,
|
||||
note: user.note,
|
||||
url: user.uri,
|
||||
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)),
|
||||
// TODO: Add fields
|
||||
fields: [],
|
||||
bot: user.isBot,
|
||||
source:
|
||||
isOwnAccount && user.source
|
||||
? (user.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}`,
|
||||
// TODO: Add these fields
|
||||
limited: false,
|
||||
moved: null,
|
||||
noindex: false,
|
||||
suspended: false,
|
||||
discoverable: undefined,
|
||||
mute_expires_at: undefined,
|
||||
group: false,
|
||||
pleroma: {
|
||||
is_admin: user.isAdmin,
|
||||
is_moderator: user.isAdmin,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Should only return local users
|
||||
*/
|
||||
export const userToLysand = (user: UserWithRelations): LysandUser => {
|
||||
if (user.instanceId !== null) {
|
||||
throw new Error("Cannot convert remote user to Lysand format");
|
||||
}
|
||||
if (user.instanceId !== null) {
|
||||
throw new Error("Cannot convert remote user to Lysand format");
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
type: "User",
|
||||
uri: user.uri,
|
||||
bio: [
|
||||
{
|
||||
content: user.note,
|
||||
content_type: "text/html",
|
||||
},
|
||||
{
|
||||
content: htmlToText(user.note),
|
||||
content_type: "text/plain",
|
||||
},
|
||||
],
|
||||
created_at: new Date(user.createdAt).toISOString(),
|
||||
disliked: `${user.uri}/disliked`,
|
||||
featured: `${user.uri}/featured`,
|
||||
liked: `${user.uri}/liked`,
|
||||
followers: `${user.uri}/followers`,
|
||||
following: `${user.uri}/following`,
|
||||
inbox: `${user.uri}/inbox`,
|
||||
outbox: `${user.uri}/outbox`,
|
||||
indexable: false,
|
||||
username: user.username,
|
||||
avatar: [
|
||||
{
|
||||
content: getAvatarUrl(user, config) || "",
|
||||
content_type: `image/${user.avatar.split(".")[1]}`,
|
||||
},
|
||||
],
|
||||
header: [
|
||||
{
|
||||
content: getHeaderUrl(user, config) || "",
|
||||
content_type: `image/${user.header.split(".")[1]}`,
|
||||
},
|
||||
],
|
||||
display_name: user.displayName,
|
||||
fields: (user.source as any as APISource).fields.map(field => ({
|
||||
key: [
|
||||
{
|
||||
content: field.name,
|
||||
content_type: "text/html",
|
||||
},
|
||||
{
|
||||
content: htmlToText(field.name),
|
||||
content_type: "text/plain",
|
||||
},
|
||||
],
|
||||
value: [
|
||||
{
|
||||
content: field.value,
|
||||
content_type: "text/html",
|
||||
},
|
||||
{
|
||||
content: htmlToText(field.value),
|
||||
content_type: "text/plain",
|
||||
},
|
||||
],
|
||||
})),
|
||||
public_key: {
|
||||
actor: `${config.http.base_url}/users/${user.id}`,
|
||||
public_key: user.publicKey,
|
||||
},
|
||||
extensions: {
|
||||
"org.lysand:custom_emojis": {
|
||||
emojis: user.emojis.map(emoji => emojiToLysand(emoji)),
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
id: user.id,
|
||||
type: "User",
|
||||
uri: user.uri,
|
||||
bio: [
|
||||
{
|
||||
content: user.note,
|
||||
content_type: "text/html",
|
||||
},
|
||||
{
|
||||
content: htmlToText(user.note),
|
||||
content_type: "text/plain",
|
||||
},
|
||||
],
|
||||
created_at: new Date(user.createdAt).toISOString(),
|
||||
disliked: `${user.uri}/disliked`,
|
||||
featured: `${user.uri}/featured`,
|
||||
liked: `${user.uri}/liked`,
|
||||
followers: `${user.uri}/followers`,
|
||||
following: `${user.uri}/following`,
|
||||
inbox: `${user.uri}/inbox`,
|
||||
outbox: `${user.uri}/outbox`,
|
||||
indexable: false,
|
||||
username: user.username,
|
||||
avatar: [
|
||||
{
|
||||
content: getAvatarUrl(user, config) || "",
|
||||
content_type: `image/${user.avatar.split(".")[1]}`,
|
||||
},
|
||||
],
|
||||
header: [
|
||||
{
|
||||
content: getHeaderUrl(user, config) || "",
|
||||
content_type: `image/${user.header.split(".")[1]}`,
|
||||
},
|
||||
],
|
||||
display_name: user.displayName,
|
||||
fields: (user.source as APISource).fields.map((field) => ({
|
||||
key: [
|
||||
{
|
||||
content: field.name,
|
||||
content_type: "text/html",
|
||||
},
|
||||
{
|
||||
content: htmlToText(field.name),
|
||||
content_type: "text/plain",
|
||||
},
|
||||
],
|
||||
value: [
|
||||
{
|
||||
content: field.value,
|
||||
content_type: "text/html",
|
||||
},
|
||||
{
|
||||
content: htmlToText(field.value),
|
||||
content_type: "text/plain",
|
||||
},
|
||||
],
|
||||
})),
|
||||
public_key: {
|
||||
actor: `${config.http.base_url}/users/${user.id}`,
|
||||
public_key: user.publicKey,
|
||||
},
|
||||
extensions: {
|
||||
"org.lysand:custom_emojis": {
|
||||
emojis: user.emojis.map((emoji) => emojiToLysand(emoji)),
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,111 +1,111 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
|
||||
export const userRelations: Prisma.UserInclude = {
|
||||
emojis: true,
|
||||
instance: true,
|
||||
likes: true,
|
||||
relationships: true,
|
||||
relationshipSubjects: true,
|
||||
pinnedNotes: true,
|
||||
_count: {
|
||||
select: {
|
||||
statuses: true,
|
||||
likes: true,
|
||||
},
|
||||
},
|
||||
emojis: true,
|
||||
instance: true,
|
||||
likes: true,
|
||||
relationships: true,
|
||||
relationshipSubjects: true,
|
||||
pinnedNotes: true,
|
||||
_count: {
|
||||
select: {
|
||||
statuses: true,
|
||||
likes: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const statusAndUserRelations: Prisma.StatusInclude = {
|
||||
author: {
|
||||
include: userRelations,
|
||||
},
|
||||
application: true,
|
||||
emojis: true,
|
||||
inReplyToPost: {
|
||||
include: {
|
||||
author: {
|
||||
include: userRelations,
|
||||
},
|
||||
application: true,
|
||||
emojis: true,
|
||||
inReplyToPost: {
|
||||
include: {
|
||||
author: true,
|
||||
},
|
||||
},
|
||||
instance: true,
|
||||
mentions: true,
|
||||
pinnedBy: true,
|
||||
_count: {
|
||||
select: {
|
||||
replies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
reblogs: true,
|
||||
attachments: true,
|
||||
instance: true,
|
||||
mentions: {
|
||||
include: userRelations,
|
||||
},
|
||||
pinnedBy: true,
|
||||
_count: {
|
||||
select: {
|
||||
replies: true,
|
||||
likes: true,
|
||||
reblogs: true,
|
||||
},
|
||||
},
|
||||
reblog: {
|
||||
include: {
|
||||
author: {
|
||||
include: userRelations,
|
||||
},
|
||||
application: true,
|
||||
emojis: true,
|
||||
inReplyToPost: {
|
||||
include: {
|
||||
author: true,
|
||||
},
|
||||
},
|
||||
instance: true,
|
||||
mentions: {
|
||||
include: userRelations,
|
||||
},
|
||||
pinnedBy: true,
|
||||
_count: {
|
||||
select: {
|
||||
replies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
quotingPost: {
|
||||
include: {
|
||||
author: {
|
||||
include: userRelations,
|
||||
},
|
||||
application: true,
|
||||
emojis: true,
|
||||
inReplyToPost: {
|
||||
include: {
|
||||
author: true,
|
||||
},
|
||||
},
|
||||
instance: true,
|
||||
mentions: true,
|
||||
pinnedBy: true,
|
||||
_count: {
|
||||
select: {
|
||||
replies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
likes: {
|
||||
include: {
|
||||
liker: true,
|
||||
},
|
||||
},
|
||||
author: {
|
||||
include: userRelations,
|
||||
},
|
||||
application: true,
|
||||
emojis: true,
|
||||
inReplyToPost: {
|
||||
include: {
|
||||
author: {
|
||||
include: userRelations,
|
||||
},
|
||||
application: true,
|
||||
emojis: true,
|
||||
inReplyToPost: {
|
||||
include: {
|
||||
author: true,
|
||||
},
|
||||
},
|
||||
instance: true,
|
||||
mentions: true,
|
||||
pinnedBy: true,
|
||||
_count: {
|
||||
select: {
|
||||
replies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
reblogs: true,
|
||||
attachments: true,
|
||||
instance: true,
|
||||
mentions: {
|
||||
include: userRelations,
|
||||
},
|
||||
pinnedBy: true,
|
||||
_count: {
|
||||
select: {
|
||||
replies: true,
|
||||
likes: true,
|
||||
reblogs: true,
|
||||
},
|
||||
},
|
||||
reblog: {
|
||||
include: {
|
||||
author: {
|
||||
include: userRelations,
|
||||
},
|
||||
application: true,
|
||||
emojis: true,
|
||||
inReplyToPost: {
|
||||
include: {
|
||||
author: true,
|
||||
},
|
||||
},
|
||||
instance: true,
|
||||
mentions: {
|
||||
include: userRelations,
|
||||
},
|
||||
pinnedBy: true,
|
||||
_count: {
|
||||
select: {
|
||||
replies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
quotingPost: {
|
||||
include: {
|
||||
author: {
|
||||
include: userRelations,
|
||||
},
|
||||
application: true,
|
||||
emojis: true,
|
||||
inReplyToPost: {
|
||||
include: {
|
||||
author: true,
|
||||
},
|
||||
},
|
||||
instance: true,
|
||||
mentions: true,
|
||||
pinnedBy: true,
|
||||
_count: {
|
||||
select: {
|
||||
replies: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
likes: {
|
||||
include: {
|
||||
liker: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue