2024-06-06 09:04:52 +02:00
|
|
|
import { idValidator } from "@/api";
|
2024-07-26 19:21:03 +02:00
|
|
|
import { localObjectUri } from "@/constants";
|
2024-12-09 13:50:46 +01:00
|
|
|
import { mergeAndDeduplicate } from "@/lib.ts";
|
2024-05-29 02:59:49 +02:00
|
|
|
import { sanitizedHtmlStrip } from "@/sanitization";
|
2024-07-24 19:04:00 +02:00
|
|
|
import { sentry } from "@/sentry";
|
2025-02-05 22:49:07 +01:00
|
|
|
import type { z } from "@hono/zod-openapi";
|
2024-06-27 01:11:39 +02:00
|
|
|
import { getLogger } from "@logtape/logtape";
|
2024-08-26 19:06:49 +02:00
|
|
|
import { EntityValidator } from "@versia/federation";
|
2024-06-20 01:21:02 +02:00
|
|
|
import type {
|
|
|
|
|
ContentFormat,
|
2024-08-26 19:06:49 +02:00
|
|
|
Delete as VersiaDelete,
|
2024-08-19 15:16:01 +02:00
|
|
|
Note as VersiaNote,
|
2024-08-26 19:06:49 +02:00
|
|
|
} from "@versia/federation/types";
|
2024-12-09 13:50:46 +01:00
|
|
|
import { Instance, db } from "@versia/kit/db";
|
2024-11-01 21:05:54 +01:00
|
|
|
import {
|
|
|
|
|
EmojiToNote,
|
2025-01-23 20:36:09 +01:00
|
|
|
MediasToNotes,
|
2024-11-01 21:05:54 +01:00
|
|
|
NoteToMentions,
|
|
|
|
|
Notes,
|
|
|
|
|
Users,
|
|
|
|
|
} from "@versia/kit/tables";
|
2024-04-17 06:09:21 +02:00
|
|
|
import {
|
|
|
|
|
type InferInsertModel,
|
2024-11-04 14:58:17 +01:00
|
|
|
type InferSelectModel,
|
2024-04-17 06:09:21 +02:00
|
|
|
type SQL,
|
|
|
|
|
and,
|
|
|
|
|
desc,
|
|
|
|
|
eq,
|
|
|
|
|
inArray,
|
2024-04-25 05:40:27 +02:00
|
|
|
isNotNull,
|
2024-05-06 09:16:33 +02:00
|
|
|
sql,
|
2024-04-17 06:09:21 +02:00
|
|
|
} from "drizzle-orm";
|
|
|
|
|
import { htmlToText } from "html-to-text";
|
|
|
|
|
import { createRegExp, exactly, global } from "magic-regexp";
|
|
|
|
|
import {
|
|
|
|
|
contentToHtml,
|
|
|
|
|
findManyNotes,
|
2024-06-13 06:16:59 +02:00
|
|
|
parseTextMentions,
|
2024-06-29 05:50:56 +02:00
|
|
|
} from "~/classes/functions/status";
|
2025-02-12 23:25:22 +01:00
|
|
|
import type { Status as StatusSchema } from "~/classes/schemas/status.ts";
|
2024-05-29 02:59:49 +02:00
|
|
|
import { config } from "~/packages/config-manager";
|
2024-11-25 21:54:31 +01:00
|
|
|
import { DeliveryJobType, deliveryQueue } from "../queues/delivery.ts";
|
2025-02-05 22:49:07 +01:00
|
|
|
import type { Status } from "../schemas/status.ts";
|
2024-11-04 15:20:53 +01:00
|
|
|
import { Application } from "./application.ts";
|
2024-10-04 15:22:48 +02:00
|
|
|
import { BaseInterface } from "./base.ts";
|
2024-11-04 15:20:53 +01:00
|
|
|
import { Emoji } from "./emoji.ts";
|
2025-01-23 19:37:17 +01:00
|
|
|
import { Media } from "./media.ts";
|
2024-11-04 15:20:53 +01:00
|
|
|
import { User } from "./user.ts";
|
2024-04-17 06:09:21 +02:00
|
|
|
|
2024-11-04 14:58:17 +01:00
|
|
|
type NoteType = InferSelectModel<typeof Notes>;
|
|
|
|
|
|
|
|
|
|
type NoteTypeWithRelations = NoteType & {
|
|
|
|
|
author: typeof User.$type;
|
|
|
|
|
mentions: (InferSelectModel<typeof Users> & {
|
|
|
|
|
instance: typeof Instance.$type | null;
|
|
|
|
|
})[];
|
2025-01-23 16:08:42 +01:00
|
|
|
attachments: (typeof Media.$type)[];
|
2024-11-04 14:58:17 +01:00
|
|
|
reblog: NoteTypeWithoutRecursiveRelations | null;
|
|
|
|
|
emojis: (typeof Emoji.$type)[];
|
|
|
|
|
reply: NoteType | null;
|
|
|
|
|
quote: NoteType | null;
|
|
|
|
|
application: typeof Application.$type | null;
|
|
|
|
|
reblogCount: number;
|
|
|
|
|
likeCount: number;
|
|
|
|
|
replyCount: number;
|
|
|
|
|
pinned: boolean;
|
|
|
|
|
reblogged: boolean;
|
|
|
|
|
muted: boolean;
|
|
|
|
|
liked: boolean;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type NoteTypeWithoutRecursiveRelations = Omit<
|
|
|
|
|
NoteTypeWithRelations,
|
|
|
|
|
"reply" | "quote" | "reblog"
|
|
|
|
|
>;
|
|
|
|
|
|
2024-04-17 06:09:21 +02:00
|
|
|
/**
|
|
|
|
|
* Gives helpers to fetch notes from database in a nice format
|
|
|
|
|
*/
|
2024-11-04 14:58:17 +01:00
|
|
|
export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
|
|
|
|
public static $type: NoteTypeWithRelations;
|
|
|
|
|
|
|
|
|
|
public save(): Promise<NoteTypeWithRelations> {
|
2024-06-13 02:45:07 +02:00
|
|
|
return this.update(this.data);
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-24 17:20:00 +02:00
|
|
|
/**
|
|
|
|
|
* @param userRequestingNoteId Used to calculate visibility of the note
|
|
|
|
|
*/
|
2024-11-01 21:20:12 +01:00
|
|
|
public async reload(userRequestingNoteId?: string): Promise<void> {
|
2024-10-24 17:20:00 +02:00
|
|
|
const reloaded = await Note.fromId(this.data.id, userRequestingNoteId);
|
2024-06-13 02:45:07 +02:00
|
|
|
|
|
|
|
|
if (!reloaded) {
|
|
|
|
|
throw new Error("Failed to reload status");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.data = reloaded.data;
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Insert a new note into the database
|
|
|
|
|
* @param data - The data to insert
|
|
|
|
|
* @param userRequestingNoteId - The ID of the user requesting the note (used to check visibility of the note)
|
|
|
|
|
* @returns The inserted note
|
|
|
|
|
*/
|
2024-06-13 02:45:07 +02:00
|
|
|
public static async insert(
|
|
|
|
|
data: InferInsertModel<typeof Notes>,
|
|
|
|
|
userRequestingNoteId?: string,
|
|
|
|
|
): Promise<Note> {
|
|
|
|
|
const inserted = (await db.insert(Notes).values(data).returning())[0];
|
|
|
|
|
|
|
|
|
|
const note = await Note.fromId(inserted.id, userRequestingNoteId);
|
|
|
|
|
|
|
|
|
|
if (!note) {
|
|
|
|
|
throw new Error("Failed to insert status");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return note;
|
|
|
|
|
}
|
2024-04-17 06:09:21 +02:00
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Fetch a note from the database by its ID
|
|
|
|
|
* @param id - The ID of the note to fetch
|
|
|
|
|
* @param userRequestingNoteId - The ID of the user requesting the note (used to check visibility of the note)
|
|
|
|
|
* @returns The fetched note
|
|
|
|
|
*/
|
2024-11-01 21:20:12 +01:00
|
|
|
public static async fromId(
|
2024-05-09 01:19:53 +02:00
|
|
|
id: string | null,
|
2024-06-13 02:45:07 +02:00
|
|
|
userRequestingNoteId?: string,
|
2024-05-09 01:19:53 +02:00
|
|
|
): Promise<Note | null> {
|
2024-06-13 04:26:43 +02:00
|
|
|
if (!id) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2024-04-17 06:09:21 +02:00
|
|
|
|
2024-06-13 02:45:07 +02:00
|
|
|
return await Note.fromSql(
|
|
|
|
|
eq(Notes.id, id),
|
|
|
|
|
undefined,
|
|
|
|
|
userRequestingNoteId,
|
|
|
|
|
);
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Fetch multiple notes from the database by their IDs
|
|
|
|
|
* @param ids - The IDs of the notes to fetch
|
|
|
|
|
* @param userRequestingNoteId - The ID of the user requesting the note (used to check visibility of the note)
|
|
|
|
|
* @returns The fetched notes
|
|
|
|
|
*/
|
2024-11-01 21:20:12 +01:00
|
|
|
public static async fromIds(
|
2024-06-13 02:45:07 +02:00
|
|
|
ids: string[],
|
|
|
|
|
userRequestingNoteId?: string,
|
|
|
|
|
): Promise<Note[]> {
|
2024-05-09 01:19:53 +02:00
|
|
|
return await Note.manyFromSql(
|
|
|
|
|
inArray(Notes.id, ids),
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
2024-06-13 02:45:07 +02:00
|
|
|
userRequestingNoteId,
|
2024-05-09 01:19:53 +02:00
|
|
|
);
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Fetch a note from the database by a SQL query
|
|
|
|
|
* @param sql - The SQL query to fetch the note with
|
|
|
|
|
* @param orderBy - The SQL query to order the results by
|
|
|
|
|
* @param userId - The ID of the user requesting the note (used to check visibility of the note)
|
|
|
|
|
* @returns The fetched note
|
|
|
|
|
*/
|
2024-11-01 21:20:12 +01:00
|
|
|
public static async fromSql(
|
2024-04-17 06:09:21 +02:00
|
|
|
sql: SQL<unknown> | undefined,
|
2024-04-17 08:36:01 +02:00
|
|
|
orderBy: SQL<unknown> | undefined = desc(Notes.id),
|
2024-05-09 01:19:53 +02:00
|
|
|
userId?: string,
|
2024-06-13 10:52:03 +02:00
|
|
|
): Promise<Note | null> {
|
2024-05-09 01:19:53 +02:00
|
|
|
const found = await findManyNotes(
|
|
|
|
|
{
|
|
|
|
|
where: sql,
|
|
|
|
|
orderBy,
|
|
|
|
|
limit: 1,
|
|
|
|
|
},
|
|
|
|
|
userId,
|
|
|
|
|
);
|
2024-04-17 06:09:21 +02:00
|
|
|
|
2024-06-13 04:26:43 +02:00
|
|
|
if (!found[0]) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2024-05-08 23:51:47 +02:00
|
|
|
return new Note(found[0]);
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Fetch multiple notes from the database by a SQL query
|
|
|
|
|
* @param sql - The SQL query to fetch the notes with
|
|
|
|
|
* @param orderBy - The SQL query to order the results by
|
|
|
|
|
* @param limit - The maximum number of notes to fetch
|
|
|
|
|
* @param offset - The number of notes to skip
|
|
|
|
|
* @param userId - The ID of the user requesting the note (used to check visibility of the note)
|
|
|
|
|
* @returns - The fetched notes
|
|
|
|
|
*/
|
2024-11-01 21:20:12 +01:00
|
|
|
public static async manyFromSql(
|
2024-04-17 06:09:21 +02:00
|
|
|
sql: SQL<unknown> | undefined,
|
2024-04-17 08:36:01 +02:00
|
|
|
orderBy: SQL<unknown> | undefined = desc(Notes.id),
|
2024-04-17 06:09:21 +02:00
|
|
|
limit?: number,
|
|
|
|
|
offset?: number,
|
2024-05-09 01:19:53 +02:00
|
|
|
userId?: string,
|
2024-06-13 10:52:03 +02:00
|
|
|
): Promise<Note[]> {
|
2024-05-09 01:19:53 +02:00
|
|
|
const found = await findManyNotes(
|
|
|
|
|
{
|
|
|
|
|
where: sql,
|
|
|
|
|
orderBy,
|
|
|
|
|
limit,
|
|
|
|
|
offset,
|
|
|
|
|
},
|
|
|
|
|
userId,
|
|
|
|
|
);
|
2024-04-17 06:09:21 +02:00
|
|
|
|
|
|
|
|
return found.map((s) => new Note(s));
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-02 00:43:33 +01:00
|
|
|
public get id(): string {
|
2024-06-13 02:45:07 +02:00
|
|
|
return this.data.id;
|
2024-04-25 05:40:27 +02:00
|
|
|
}
|
|
|
|
|
|
2024-11-01 21:20:12 +01:00
|
|
|
public async federateToUsers(): Promise<void> {
|
2024-07-26 18:51:39 +02:00
|
|
|
const users = await this.getUsersToFederateTo();
|
|
|
|
|
|
2024-11-25 11:29:48 +01:00
|
|
|
await deliveryQueue.addBulk(
|
|
|
|
|
users.map((user) => ({
|
|
|
|
|
data: {
|
|
|
|
|
entity: this.toVersia(),
|
|
|
|
|
recipientId: user.id,
|
|
|
|
|
senderId: this.author.id,
|
|
|
|
|
},
|
|
|
|
|
name: DeliveryJobType.FederateEntity,
|
|
|
|
|
})),
|
|
|
|
|
);
|
2024-07-26 18:51:39 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Fetch the users that should be federated to for this note
|
|
|
|
|
*
|
|
|
|
|
* This includes:
|
|
|
|
|
* - Users mentioned in the note
|
|
|
|
|
* - Users that can see the note
|
|
|
|
|
* @returns The users that should be federated to
|
|
|
|
|
*/
|
2024-11-01 21:20:12 +01:00
|
|
|
public async getUsersToFederateTo(): Promise<User[]> {
|
2024-04-17 06:09:21 +02:00
|
|
|
// Mentioned users
|
|
|
|
|
const mentionedUsers =
|
2024-06-13 02:45:07 +02:00
|
|
|
this.data.mentions.length > 0
|
2024-04-25 05:40:27 +02:00
|
|
|
? await User.manyFromSql(
|
|
|
|
|
and(
|
|
|
|
|
isNotNull(Users.instanceId),
|
|
|
|
|
inArray(
|
|
|
|
|
Users.id,
|
2024-06-13 02:45:07 +02:00
|
|
|
this.data.mentions.map((mention) => mention.id),
|
2024-04-17 06:09:21 +02:00
|
|
|
),
|
2024-04-25 05:40:27 +02:00
|
|
|
),
|
|
|
|
|
)
|
2024-04-17 06:09:21 +02:00
|
|
|
: [];
|
|
|
|
|
|
2024-04-25 05:40:27 +02:00
|
|
|
const usersThatCanSeePost = await User.manyFromSql(
|
|
|
|
|
isNotNull(Users.instanceId),
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
{
|
|
|
|
|
with: {
|
|
|
|
|
relationships: {
|
2024-11-02 00:43:33 +01:00
|
|
|
where: (relationship, { eq, and }): SQL | undefined =>
|
2024-04-25 05:40:27 +02:00
|
|
|
and(
|
2024-07-26 19:21:03 +02:00
|
|
|
eq(relationship.subjectId, this.data.authorId),
|
2024-04-25 05:40:27 +02:00
|
|
|
eq(relationship.following, true),
|
|
|
|
|
),
|
|
|
|
|
},
|
2024-04-17 06:09:21 +02:00
|
|
|
},
|
|
|
|
|
},
|
2024-04-25 05:40:27 +02:00
|
|
|
);
|
2024-04-17 06:09:21 +02:00
|
|
|
|
2024-12-09 13:50:46 +01:00
|
|
|
const fusedUsers = mergeAndDeduplicate(
|
|
|
|
|
mentionedUsers,
|
|
|
|
|
usersThatCanSeePost,
|
2024-04-17 06:09:21 +02:00
|
|
|
);
|
|
|
|
|
|
2024-12-09 13:50:46 +01:00
|
|
|
return fusedUsers;
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
2024-11-02 00:43:33 +01:00
|
|
|
public get author(): User {
|
2024-06-13 02:45:07 +02:00
|
|
|
return new User(this.data.author);
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Get the number of notes in the database (excluding remote notes)
|
|
|
|
|
* @returns The number of notes in the database
|
|
|
|
|
*/
|
2024-11-01 21:20:12 +01:00
|
|
|
public static async getCount(): Promise<number> {
|
2024-10-11 15:46:05 +02:00
|
|
|
return await db.$count(
|
|
|
|
|
Notes,
|
|
|
|
|
sql`EXISTS (SELECT 1 FROM "Users" WHERE "Users"."id" = ${Notes.authorId} AND "Users"."instanceId" IS NULL)`,
|
|
|
|
|
);
|
2024-05-06 09:16:33 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Get the children of this note (replies)
|
|
|
|
|
* @param userId - The ID of the user requesting the note (used to check visibility of the note)
|
|
|
|
|
* @returns The children of this note
|
|
|
|
|
*/
|
|
|
|
|
private async getReplyChildren(userId?: string): Promise<Note[]> {
|
2024-05-09 01:19:53 +02:00
|
|
|
return await Note.manyFromSql(
|
2024-06-13 02:45:07 +02:00
|
|
|
eq(Notes.replyId, this.data.id),
|
2024-05-09 01:19:53 +02:00
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
userId,
|
|
|
|
|
);
|
2024-04-17 08:36:01 +02:00
|
|
|
}
|
|
|
|
|
|
2024-11-02 00:43:33 +01:00
|
|
|
public isRemote(): boolean {
|
2024-06-13 02:45:07 +02:00
|
|
|
return this.author.isRemote();
|
2024-06-06 09:04:52 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Update a note from remote federated servers
|
|
|
|
|
* @returns The updated note
|
|
|
|
|
*/
|
2024-11-01 21:20:12 +01:00
|
|
|
public async updateFromRemote(): Promise<Note> {
|
2024-06-06 09:04:52 +02:00
|
|
|
if (!this.isRemote()) {
|
|
|
|
|
throw new Error("Cannot refetch a local note (it is not remote)");
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-09 13:36:15 +01:00
|
|
|
const updated = await Note.fetchFromRemote(this.getUri());
|
2024-06-06 09:04:52 +02:00
|
|
|
|
|
|
|
|
if (!updated) {
|
|
|
|
|
throw new Error("Note not found after update");
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-13 02:45:07 +02:00
|
|
|
this.data = updated.data;
|
2024-06-06 09:04:52 +02:00
|
|
|
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Create a new note from user input
|
|
|
|
|
* @param data - The data to create the note from
|
|
|
|
|
* @returns The created note
|
|
|
|
|
*/
|
2024-11-01 21:20:12 +01:00
|
|
|
public static async fromData(data: {
|
2024-06-13 06:16:59 +02:00
|
|
|
author: User;
|
2024-06-20 01:21:02 +02:00
|
|
|
content: ContentFormat;
|
2025-02-12 23:25:22 +01:00
|
|
|
visibility: z.infer<typeof StatusSchema.shape.visibility>;
|
2024-06-13 06:16:59 +02:00
|
|
|
isSensitive: boolean;
|
|
|
|
|
spoilerText: string;
|
2024-06-13 06:52:01 +02:00
|
|
|
emojis?: Emoji[];
|
2024-06-13 06:16:59 +02:00
|
|
|
uri?: string;
|
|
|
|
|
mentions?: User[];
|
2024-04-17 06:09:21 +02:00
|
|
|
/** List of IDs of database Attachment objects */
|
2025-01-23 16:08:42 +01:00
|
|
|
mediaAttachments?: Media[];
|
2024-06-13 06:16:59 +02:00
|
|
|
replyId?: string;
|
|
|
|
|
quoteId?: string;
|
|
|
|
|
application?: Application;
|
|
|
|
|
}): Promise<Note> {
|
|
|
|
|
const plaintextContent =
|
|
|
|
|
data.content["text/plain"]?.content ??
|
|
|
|
|
Object.entries(data.content)[0][1].content;
|
|
|
|
|
|
2024-12-09 13:50:46 +01:00
|
|
|
const parsedMentions = mergeAndDeduplicate(
|
|
|
|
|
data.mentions ?? [],
|
|
|
|
|
await parseTextMentions(plaintextContent, data.author),
|
2024-06-13 06:16:59 +02:00
|
|
|
);
|
2024-12-09 13:50:46 +01:00
|
|
|
const parsedEmojis = mergeAndDeduplicate(
|
|
|
|
|
data.emojis ?? [],
|
|
|
|
|
await Emoji.parseFromText(plaintextContent),
|
2024-06-13 06:16:59 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const htmlContent = await contentToHtml(data.content, parsedMentions);
|
2024-04-17 06:09:21 +02:00
|
|
|
|
|
|
|
|
const newNote = await Note.insert({
|
2024-06-13 06:16:59 +02:00
|
|
|
authorId: data.author.id,
|
2024-04-17 06:09:21 +02:00
|
|
|
content: htmlContent,
|
|
|
|
|
contentSource:
|
2024-06-13 06:16:59 +02:00
|
|
|
data.content["text/plain"]?.content ||
|
|
|
|
|
data.content["text/markdown"]?.content ||
|
|
|
|
|
Object.entries(data.content)[0][1].content ||
|
2024-04-17 06:09:21 +02:00
|
|
|
"",
|
|
|
|
|
contentType: "text/html",
|
2024-06-13 06:16:59 +02:00
|
|
|
visibility: data.visibility,
|
|
|
|
|
sensitive: data.isSensitive,
|
|
|
|
|
spoilerText: await sanitizedHtmlStrip(data.spoilerText),
|
|
|
|
|
uri: data.uri || null,
|
|
|
|
|
replyId: data.replyId ?? null,
|
|
|
|
|
quotingId: data.quoteId ?? null,
|
|
|
|
|
applicationId: data.application?.id ?? null,
|
2024-04-17 06:09:21 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Connect emojis
|
2024-12-09 13:36:15 +01:00
|
|
|
await newNote.updateEmojis(parsedEmojis);
|
2024-04-17 06:09:21 +02:00
|
|
|
|
|
|
|
|
// Connect mentions
|
2024-12-09 13:36:15 +01:00
|
|
|
await newNote.updateMentions(parsedMentions);
|
2024-04-17 06:09:21 +02:00
|
|
|
|
|
|
|
|
// Set attachment parents
|
2024-12-09 13:36:15 +01:00
|
|
|
await newNote.updateAttachments(data.mediaAttachments ?? []);
|
2024-04-17 06:09:21 +02:00
|
|
|
|
|
|
|
|
// Send notifications for mentioned local users
|
2024-12-09 13:50:46 +01:00
|
|
|
for (const mention of parsedMentions) {
|
2024-04-25 05:40:27 +02:00
|
|
|
if (mention.isLocal()) {
|
2024-12-09 15:01:19 +01:00
|
|
|
await mention.notify("mention", data.author, newNote);
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-24 17:20:00 +02:00
|
|
|
await newNote.reload(data.author.id);
|
2024-06-13 06:16:59 +02:00
|
|
|
|
|
|
|
|
return newNote;
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Update a note from user input
|
|
|
|
|
* @param data - The data to update the note from
|
|
|
|
|
* @returns The updated note
|
|
|
|
|
*/
|
2024-11-01 21:20:12 +01:00
|
|
|
public async updateFromData(data: {
|
2024-06-30 10:24:10 +02:00
|
|
|
author: User;
|
2024-06-20 01:21:02 +02:00
|
|
|
content?: ContentFormat;
|
2025-02-12 23:25:22 +01:00
|
|
|
visibility?: z.infer<typeof StatusSchema.shape.visibility>;
|
2024-06-13 06:16:59 +02:00
|
|
|
isSensitive?: boolean;
|
|
|
|
|
spoilerText?: string;
|
2024-06-13 06:52:01 +02:00
|
|
|
emojis?: Emoji[];
|
2024-06-13 06:16:59 +02:00
|
|
|
uri?: string;
|
|
|
|
|
mentions?: User[];
|
2025-01-23 16:08:42 +01:00
|
|
|
mediaAttachments?: Media[];
|
2024-06-13 06:16:59 +02:00
|
|
|
replyId?: string;
|
|
|
|
|
quoteId?: string;
|
|
|
|
|
application?: Application;
|
|
|
|
|
}): Promise<Note> {
|
|
|
|
|
const plaintextContent = data.content
|
2024-09-14 17:32:32 +02:00
|
|
|
? (data.content["text/plain"]?.content ??
|
|
|
|
|
Object.entries(data.content)[0][1].content)
|
2024-04-17 06:09:21 +02:00
|
|
|
: undefined;
|
|
|
|
|
|
2024-12-09 13:50:46 +01:00
|
|
|
const parsedMentions = mergeAndDeduplicate(
|
|
|
|
|
data.mentions ?? [],
|
|
|
|
|
plaintextContent
|
2024-06-30 10:24:10 +02:00
|
|
|
? await parseTextMentions(plaintextContent, data.author)
|
2024-12-09 13:50:46 +01:00
|
|
|
: [],
|
2024-06-13 06:16:59 +02:00
|
|
|
);
|
2024-12-09 13:50:46 +01:00
|
|
|
const parsedEmojis = mergeAndDeduplicate(
|
|
|
|
|
data.emojis ?? [],
|
|
|
|
|
plaintextContent ? await Emoji.parseFromText(plaintextContent) : [],
|
2024-06-13 06:16:59 +02:00
|
|
|
);
|
2024-04-17 06:09:21 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
const htmlContent = data.content
|
|
|
|
|
? await contentToHtml(data.content, parsedMentions)
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
await this.update({
|
2024-04-17 06:09:21 +02:00
|
|
|
content: htmlContent,
|
2024-06-13 06:16:59 +02:00
|
|
|
contentSource: data.content
|
|
|
|
|
? data.content["text/plain"]?.content ||
|
|
|
|
|
data.content["text/markdown"]?.content ||
|
|
|
|
|
Object.entries(data.content)[0][1].content ||
|
2024-04-17 06:09:21 +02:00
|
|
|
""
|
|
|
|
|
: undefined,
|
|
|
|
|
contentType: "text/html",
|
2024-06-13 06:16:59 +02:00
|
|
|
visibility: data.visibility,
|
|
|
|
|
sensitive: data.isSensitive,
|
|
|
|
|
spoilerText: data.spoilerText,
|
|
|
|
|
replyId: data.replyId,
|
|
|
|
|
quotingId: data.quoteId,
|
|
|
|
|
applicationId: data.application?.id,
|
2024-04-17 06:09:21 +02:00
|
|
|
});
|
|
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
// Connect emojis
|
2024-12-09 13:36:15 +01:00
|
|
|
await this.updateEmojis(parsedEmojis);
|
2024-06-13 06:16:59 +02:00
|
|
|
|
|
|
|
|
// Connect mentions
|
2024-12-09 13:36:15 +01:00
|
|
|
await this.updateMentions(parsedMentions);
|
2024-06-13 06:16:59 +02:00
|
|
|
|
|
|
|
|
// Set attachment parents
|
2024-12-09 13:36:15 +01:00
|
|
|
await this.updateAttachments(data.mediaAttachments ?? []);
|
2024-06-13 06:16:59 +02:00
|
|
|
|
2024-10-24 17:20:00 +02:00
|
|
|
await this.reload(data.author.id);
|
2024-06-13 06:16:59 +02:00
|
|
|
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Updates the emojis associated with this note in the database
|
|
|
|
|
*
|
|
|
|
|
* Deletes all existing emojis associated with this note, then replaces them with the provided emojis.
|
|
|
|
|
* @param emojis - The emojis to associate with this note
|
|
|
|
|
*/
|
2024-12-09 13:36:15 +01:00
|
|
|
public async updateEmojis(emojis: Emoji[]): Promise<void> {
|
|
|
|
|
if (emojis.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-17 06:09:21 +02:00
|
|
|
// Connect emojis
|
|
|
|
|
await db
|
2024-04-17 08:36:01 +02:00
|
|
|
.delete(EmojiToNote)
|
2024-06-13 02:45:07 +02:00
|
|
|
.where(eq(EmojiToNote.noteId, this.data.id));
|
2024-12-09 13:36:15 +01:00
|
|
|
await db.insert(EmojiToNote).values(
|
2024-12-09 13:50:46 +01:00
|
|
|
emojis.map((emoji) => ({
|
2024-12-09 13:36:15 +01:00
|
|
|
emojiId: emoji.id,
|
|
|
|
|
noteId: this.data.id,
|
|
|
|
|
})),
|
|
|
|
|
);
|
2024-06-13 06:16:59 +02:00
|
|
|
}
|
2024-04-17 06:09:21 +02:00
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Updates the mentions associated with this note in the database
|
|
|
|
|
*
|
|
|
|
|
* Deletes all existing mentions associated with this note, then replaces them with the provided mentions.
|
|
|
|
|
* @param mentions - The mentions to associate with this note
|
|
|
|
|
*/
|
2024-12-09 13:36:15 +01:00
|
|
|
public async updateMentions(mentions: User[]): Promise<void> {
|
|
|
|
|
if (mentions.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-17 06:09:21 +02:00
|
|
|
// Connect mentions
|
|
|
|
|
await db
|
2024-04-17 08:36:01 +02:00
|
|
|
.delete(NoteToMentions)
|
2024-06-13 02:45:07 +02:00
|
|
|
.where(eq(NoteToMentions.noteId, this.data.id));
|
2024-12-09 13:36:15 +01:00
|
|
|
await db.insert(NoteToMentions).values(
|
|
|
|
|
mentions.map((mention) => ({
|
|
|
|
|
noteId: this.data.id,
|
|
|
|
|
userId: mention.id,
|
|
|
|
|
})),
|
|
|
|
|
);
|
2024-06-13 06:16:59 +02:00
|
|
|
}
|
2024-04-17 06:09:21 +02:00
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Updates the attachments associated with this note in the database
|
|
|
|
|
*
|
|
|
|
|
* Deletes all existing attachments associated with this note, then replaces them with the provided attachments.
|
|
|
|
|
* @param mediaAttachments - The IDs of the attachments to associate with this note
|
|
|
|
|
*/
|
2025-01-23 16:08:42 +01:00
|
|
|
public async updateAttachments(mediaAttachments: Media[]): Promise<void> {
|
2024-12-09 13:36:15 +01:00
|
|
|
if (mediaAttachments.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove old attachments
|
2024-06-13 06:16:59 +02:00
|
|
|
await db
|
2025-01-23 20:36:09 +01:00
|
|
|
.delete(MediasToNotes)
|
|
|
|
|
.where(eq(MediasToNotes.noteId, this.data.id));
|
|
|
|
|
|
|
|
|
|
await db.insert(MediasToNotes).values(
|
|
|
|
|
mediaAttachments.map((media) => ({
|
2024-12-09 13:36:15 +01:00
|
|
|
noteId: this.data.id,
|
2025-01-23 20:36:09 +01:00
|
|
|
mediaId: media.id,
|
|
|
|
|
})),
|
|
|
|
|
);
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Resolve a note from a URI
|
|
|
|
|
* @param uri - The URI of the note to resolve
|
|
|
|
|
* @returns The resolved note
|
|
|
|
|
*/
|
2025-02-01 16:32:18 +01:00
|
|
|
public static async resolve(uri: URL): Promise<Note | null> {
|
2024-06-06 09:04:52 +02:00
|
|
|
// Check if note not already in database
|
2025-02-01 16:32:18 +01:00
|
|
|
const foundNote = await Note.fromSql(eq(Notes.uri, uri.toString()));
|
2024-06-06 09:04:52 +02:00
|
|
|
|
2024-06-13 04:26:43 +02:00
|
|
|
if (foundNote) {
|
|
|
|
|
return foundNote;
|
|
|
|
|
}
|
2024-06-06 09:04:52 +02:00
|
|
|
|
|
|
|
|
// Check if URI is of a local note
|
2025-02-01 16:32:18 +01:00
|
|
|
if (uri.origin === config.http.base_url.origin) {
|
|
|
|
|
const uuid = uri.pathname.match(idValidator);
|
2024-06-06 09:04:52 +02:00
|
|
|
|
2024-06-13 04:26:43 +02:00
|
|
|
if (!uuid?.[0]) {
|
2024-06-06 09:04:52 +02:00
|
|
|
throw new Error(
|
|
|
|
|
`URI ${uri} is of a local note, but it could not be parsed`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await Note.fromId(uuid[0]);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-09 13:36:15 +01:00
|
|
|
return await Note.fetchFromRemote(uri);
|
2024-06-06 09:04:52 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Save a note from a remote server
|
|
|
|
|
* @param uri - The URI of the note to save
|
|
|
|
|
* @returns The saved note, or null if the note could not be fetched
|
|
|
|
|
*/
|
2025-02-01 16:32:18 +01:00
|
|
|
public static async fetchFromRemote(uri: URL): Promise<Note | null> {
|
2024-12-09 13:11:23 +01:00
|
|
|
const instance = await Instance.resolve(uri);
|
|
|
|
|
|
|
|
|
|
if (!instance) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2024-06-06 09:04:52 +02:00
|
|
|
|
2024-12-09 13:36:15 +01:00
|
|
|
const requester = await User.getFederationRequester();
|
2024-06-06 09:04:52 +02:00
|
|
|
|
2024-12-09 13:36:15 +01:00
|
|
|
const { data } = await requester.get(uri, {
|
|
|
|
|
// @ts-expect-error Bun extension
|
|
|
|
|
proxy: config.http.proxy.address,
|
|
|
|
|
});
|
2024-06-06 09:04:52 +02:00
|
|
|
|
2024-12-09 13:36:15 +01:00
|
|
|
const note = await new EntityValidator().Note(data);
|
2024-06-06 09:04:52 +02:00
|
|
|
|
2025-02-01 16:32:18 +01:00
|
|
|
const author = await User.resolve(new URL(note.author));
|
2024-06-06 09:04:52 +02:00
|
|
|
|
|
|
|
|
if (!author) {
|
|
|
|
|
throw new Error("Invalid object author");
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-09 13:11:23 +01:00
|
|
|
return await Note.fromVersia(note, author, instance);
|
2024-06-13 06:16:59 +02:00
|
|
|
}
|
2024-06-06 09:04:52 +02:00
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
2024-08-19 15:16:01 +02:00
|
|
|
* Turns a Versia Note into a database note (saved)
|
|
|
|
|
* @param note Versia Note
|
2024-06-13 10:52:03 +02:00
|
|
|
* @param author Author of the note
|
2024-12-09 13:36:15 +01:00
|
|
|
* @param instance Instance of the note
|
2024-06-13 10:52:03 +02:00
|
|
|
* @returns The saved note
|
|
|
|
|
*/
|
2024-11-01 21:20:12 +01:00
|
|
|
public static async fromVersia(
|
|
|
|
|
note: VersiaNote,
|
|
|
|
|
author: User,
|
2024-12-09 13:11:23 +01:00
|
|
|
instance: Instance,
|
2024-11-01 21:20:12 +01:00
|
|
|
): Promise<Note> {
|
2024-06-13 10:52:03 +02:00
|
|
|
const emojis: Emoji[] = [];
|
2024-11-24 23:13:29 +01:00
|
|
|
const logger = getLogger(["federation", "resolvers"]);
|
2024-06-13 06:16:59 +02:00
|
|
|
|
2024-08-26 19:27:40 +02:00
|
|
|
for (const emoji of note.extensions?.["pub.versia:custom_emojis"]
|
2024-06-13 06:16:59 +02:00
|
|
|
?.emojis ?? []) {
|
2024-12-09 13:11:23 +01:00
|
|
|
const resolvedEmoji = await Emoji.fetchFromRemote(
|
|
|
|
|
emoji,
|
|
|
|
|
instance,
|
|
|
|
|
).catch((e) => {
|
|
|
|
|
logger.error`${e}`;
|
|
|
|
|
sentry?.captureException(e);
|
|
|
|
|
return null;
|
|
|
|
|
});
|
2024-06-06 09:04:52 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
if (resolvedEmoji) {
|
|
|
|
|
emojis.push(resolvedEmoji);
|
2024-06-06 09:04:52 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-23 16:08:42 +01:00
|
|
|
const attachments: Media[] = [];
|
2024-06-06 09:04:52 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
for (const attachment of note.attachments ?? []) {
|
2025-01-23 16:08:42 +01:00
|
|
|
const resolvedAttachment = await Media.fromVersia(attachment).catch(
|
|
|
|
|
(e) => {
|
|
|
|
|
logger.error`${e}`;
|
|
|
|
|
sentry?.captureException(e);
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
);
|
2024-06-06 09:04:52 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
if (resolvedAttachment) {
|
|
|
|
|
attachments.push(resolvedAttachment);
|
2024-06-06 09:04:52 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-26 19:06:49 +02:00
|
|
|
let visibility = note.group
|
|
|
|
|
? ["public", "followers"].includes(note.group)
|
|
|
|
|
? (note.group as "public" | "private")
|
|
|
|
|
: ("url" as const)
|
|
|
|
|
: ("direct" as const);
|
|
|
|
|
|
|
|
|
|
if (visibility === "url") {
|
|
|
|
|
// TODO: Implement groups
|
|
|
|
|
visibility = "direct";
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
const newData = {
|
2024-06-06 09:04:52 +02:00
|
|
|
author,
|
2024-06-13 06:16:59 +02:00
|
|
|
content: note.content ?? {
|
2024-06-06 09:04:52 +02:00
|
|
|
"text/plain": {
|
|
|
|
|
content: "",
|
2024-08-26 19:06:49 +02:00
|
|
|
remote: false,
|
2024-06-06 09:04:52 +02:00
|
|
|
},
|
|
|
|
|
},
|
2025-02-12 23:25:22 +01:00
|
|
|
visibility,
|
2024-06-13 06:16:59 +02:00
|
|
|
isSensitive: note.is_sensitive ?? false,
|
|
|
|
|
spoilerText: note.subject ?? "",
|
2024-06-06 09:04:52 +02:00
|
|
|
emojis,
|
2024-06-13 06:16:59 +02:00
|
|
|
uri: note.uri,
|
|
|
|
|
mentions: await Promise.all(
|
2024-06-06 09:04:52 +02:00
|
|
|
(note.mentions ?? [])
|
2025-02-01 16:32:18 +01:00
|
|
|
.map((mention) => User.resolve(new URL(mention)))
|
2024-06-06 09:04:52 +02:00
|
|
|
.filter((mention) => mention !== null) as Promise<User>[],
|
|
|
|
|
),
|
2024-12-09 13:36:15 +01:00
|
|
|
mediaAttachments: attachments,
|
2024-06-13 06:16:59 +02:00
|
|
|
replyId: note.replies_to
|
2025-02-01 16:32:18 +01:00
|
|
|
? (await Note.resolve(new URL(note.replies_to)))?.data.id
|
2024-06-06 09:04:52 +02:00
|
|
|
: undefined,
|
2024-06-13 06:16:59 +02:00
|
|
|
quoteId: note.quotes
|
2025-02-01 16:32:18 +01:00
|
|
|
? (await Note.resolve(new URL(note.quotes)))?.data.id
|
2024-06-06 09:04:52 +02:00
|
|
|
: undefined,
|
2024-06-13 06:16:59 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Check if new note already exists
|
|
|
|
|
|
|
|
|
|
const foundNote = await Note.fromSql(eq(Notes.uri, note.uri));
|
2024-06-06 09:04:52 +02:00
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
// If it exists, simply update it
|
|
|
|
|
if (foundNote) {
|
|
|
|
|
await foundNote.updateFromData(newData);
|
|
|
|
|
|
|
|
|
|
return foundNote;
|
2024-06-06 09:04:52 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-13 06:16:59 +02:00
|
|
|
// Else, create a new note
|
|
|
|
|
return await Note.fromData(newData);
|
2024-06-06 09:04:52 +02:00
|
|
|
}
|
|
|
|
|
|
2024-11-01 21:20:12 +01:00
|
|
|
public async delete(ids?: string[]): Promise<void> {
|
2024-06-13 02:45:07 +02:00
|
|
|
if (Array.isArray(ids)) {
|
|
|
|
|
await db.delete(Notes).where(inArray(Notes.id, ids));
|
|
|
|
|
} else {
|
|
|
|
|
await db.delete(Notes).where(eq(Notes.id, this.id));
|
|
|
|
|
}
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
2024-11-01 21:20:12 +01:00
|
|
|
public async update(
|
2024-11-04 14:58:17 +01:00
|
|
|
newStatus: Partial<NoteTypeWithRelations>,
|
|
|
|
|
): Promise<NoteTypeWithRelations> {
|
2024-06-13 02:45:07 +02:00
|
|
|
await db.update(Notes).set(newStatus).where(eq(Notes.id, this.data.id));
|
|
|
|
|
|
|
|
|
|
const updated = await Note.fromId(this.data.id);
|
2024-04-17 06:09:21 +02:00
|
|
|
|
2024-06-13 02:45:07 +02:00
|
|
|
if (!updated) {
|
|
|
|
|
throw new Error("Failed to update status");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return updated.data;
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns whether this status is viewable by a user.
|
|
|
|
|
* @param user The user to check.
|
|
|
|
|
* @returns Whether this status is viewable by the user.
|
|
|
|
|
*/
|
2024-11-01 21:20:12 +01:00
|
|
|
public async isViewableByUser(user: User | null): Promise<boolean> {
|
2024-06-13 04:26:43 +02:00
|
|
|
if (this.author.id === user?.id) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (this.data.visibility === "public") {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (this.data.visibility === "unlisted") {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2024-06-13 02:45:07 +02:00
|
|
|
if (this.data.visibility === "private") {
|
2024-04-17 06:09:21 +02:00
|
|
|
return user
|
2024-06-13 10:52:03 +02:00
|
|
|
? !!(await db.query.Relationships.findFirst({
|
2024-11-02 00:43:33 +01:00
|
|
|
where: (relationship, { and, eq }): SQL | undefined =>
|
2024-04-17 06:09:21 +02:00
|
|
|
and(
|
|
|
|
|
eq(relationship.ownerId, user?.id),
|
2024-04-17 08:36:01 +02:00
|
|
|
eq(relationship.subjectId, Notes.authorId),
|
2024-04-17 06:09:21 +02:00
|
|
|
eq(relationship.following, true),
|
|
|
|
|
),
|
2024-06-13 10:52:03 +02:00
|
|
|
}))
|
2024-04-17 06:09:21 +02:00
|
|
|
: false;
|
|
|
|
|
}
|
|
|
|
|
return (
|
2024-06-13 10:52:03 +02:00
|
|
|
!!user &&
|
|
|
|
|
!!this.data.mentions.find((mention) => mention.id === user.id)
|
2024-04-17 06:09:21 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Convert a note to the Mastodon API format
|
|
|
|
|
* @param userFetching - The user fetching the note (used to check if the note is favourite and such)
|
|
|
|
|
* @returns The note in the Mastodon API format
|
|
|
|
|
*/
|
2025-02-05 22:49:07 +01:00
|
|
|
public async toApi(
|
|
|
|
|
userFetching?: User | null,
|
|
|
|
|
): Promise<z.infer<typeof Status>> {
|
2024-06-13 02:45:07 +02:00
|
|
|
const data = this.data;
|
2024-05-08 23:51:47 +02:00
|
|
|
|
2024-04-17 06:09:21 +02:00
|
|
|
// Convert mentions of local users from @username@host to @username
|
|
|
|
|
const mentionedLocalUsers = data.mentions.filter(
|
|
|
|
|
(mention) => mention.instanceId === null,
|
|
|
|
|
);
|
|
|
|
|
|
2024-11-22 15:06:46 +01:00
|
|
|
let replacedContent = data.content;
|
2024-04-17 06:09:21 +02:00
|
|
|
|
|
|
|
|
for (const mention of mentionedLocalUsers) {
|
|
|
|
|
replacedContent = replacedContent.replace(
|
|
|
|
|
createRegExp(
|
|
|
|
|
exactly(
|
2025-02-13 01:31:15 +01:00
|
|
|
`@${mention.username}@${config.http.base_url.host}`,
|
2024-04-17 06:09:21 +02:00
|
|
|
),
|
|
|
|
|
[global],
|
|
|
|
|
),
|
|
|
|
|
`@${mention.username}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: data.id,
|
2024-04-17 08:36:01 +02:00
|
|
|
in_reply_to_id: data.replyId || null,
|
|
|
|
|
in_reply_to_account_id: data.reply?.authorId || null,
|
2024-06-13 04:26:43 +02:00
|
|
|
account: this.author.toApi(userFetching?.id === data.authorId),
|
2024-04-17 06:09:21 +02:00
|
|
|
created_at: new Date(data.createdAt).toISOString(),
|
|
|
|
|
application: data.application
|
2024-10-23 17:56:47 +02:00
|
|
|
? new Application(data.application).toApi()
|
2025-02-05 22:49:07 +01:00
|
|
|
: undefined,
|
2024-04-17 06:09:21 +02:00
|
|
|
card: null,
|
|
|
|
|
content: replacedContent,
|
2024-06-13 06:52:01 +02:00
|
|
|
emojis: data.emojis.map((emoji) => new Emoji(emoji).toApi()),
|
2024-05-09 01:19:53 +02:00
|
|
|
favourited: data.liked,
|
2024-05-08 22:57:42 +02:00
|
|
|
favourites_count: data.likeCount,
|
2025-02-12 23:25:22 +01:00
|
|
|
media_attachments: (data.attachments ?? []).map((a) =>
|
|
|
|
|
new Media(a).toApi(),
|
2024-04-17 06:09:21 +02:00
|
|
|
),
|
2024-04-25 05:40:27 +02:00
|
|
|
mentions: data.mentions.map((mention) => ({
|
|
|
|
|
id: mention.id,
|
|
|
|
|
acct: User.getAcct(
|
|
|
|
|
mention.instanceId === null,
|
|
|
|
|
mention.username,
|
|
|
|
|
mention.instance?.baseUrl,
|
|
|
|
|
),
|
2025-02-01 16:32:18 +01:00
|
|
|
url: User.getUri(
|
|
|
|
|
mention.id,
|
|
|
|
|
mention.uri ? new URL(mention.uri) : null,
|
|
|
|
|
).toString(),
|
2024-04-25 05:40:27 +02:00
|
|
|
username: mention.username,
|
|
|
|
|
})),
|
2024-04-17 06:09:21 +02:00
|
|
|
language: null,
|
2024-05-09 01:19:53 +02:00
|
|
|
muted: data.muted,
|
|
|
|
|
pinned: data.pinned,
|
2024-04-17 06:09:21 +02:00
|
|
|
// TODO: Add polls
|
|
|
|
|
poll: null,
|
2025-02-12 23:33:07 +01:00
|
|
|
// @ts-expect-error broken recursive types
|
2024-04-28 08:15:08 +02:00
|
|
|
reblog: data.reblog
|
2024-11-04 14:58:17 +01:00
|
|
|
? await new Note(data.reblog as NoteTypeWithRelations).toApi(
|
2024-06-13 02:45:07 +02:00
|
|
|
userFetching,
|
|
|
|
|
)
|
2024-04-28 08:15:08 +02:00
|
|
|
: null,
|
2024-05-09 01:19:53 +02:00
|
|
|
reblogged: data.reblogged,
|
2024-04-17 06:09:21 +02:00
|
|
|
reblogs_count: data.reblogCount,
|
|
|
|
|
replies_count: data.replyCount,
|
|
|
|
|
sensitive: data.sensitive,
|
|
|
|
|
spoiler_text: data.spoilerText,
|
|
|
|
|
tags: [],
|
2025-02-01 16:32:18 +01:00
|
|
|
uri: data.uri || this.getUri().toString(),
|
2025-02-12 23:25:22 +01:00
|
|
|
visibility: data.visibility,
|
2025-02-01 16:32:18 +01:00
|
|
|
url: data.uri || this.getMastoUri().toString(),
|
2024-04-17 06:09:21 +02:00
|
|
|
bookmarked: false,
|
2025-02-12 23:33:07 +01:00
|
|
|
// @ts-expect-error broken recursive types
|
2024-04-28 08:15:08 +02:00
|
|
|
quote: data.quotingId
|
2024-09-14 17:32:32 +02:00
|
|
|
? ((await Note.fromId(data.quotingId, userFetching?.id).then(
|
2024-06-13 04:26:43 +02:00
|
|
|
(n) => n?.toApi(userFetching),
|
2024-09-14 17:32:32 +02:00
|
|
|
)) ?? null)
|
2024-04-28 08:15:08 +02:00
|
|
|
: null,
|
2024-06-29 08:36:15 +02:00
|
|
|
edited_at: data.updatedAt
|
|
|
|
|
? new Date(data.updatedAt).toISOString()
|
|
|
|
|
: null,
|
|
|
|
|
emoji_reactions: [],
|
|
|
|
|
plain_content: data.contentSource,
|
2024-04-17 06:09:21 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-01 16:32:18 +01:00
|
|
|
public getUri(): URL {
|
|
|
|
|
return new URL(this.data.uri || localObjectUri(this.id));
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
2025-02-01 16:32:18 +01:00
|
|
|
public static getUri(id: string | null, uri?: URL | null): URL | null {
|
2024-06-13 04:26:43 +02:00
|
|
|
if (!id) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2024-07-26 21:19:41 +02:00
|
|
|
return uri || localObjectUri(id);
|
2024-04-25 05:40:27 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
|
|
|
|
* Get the frontend URI of this note
|
|
|
|
|
* @returns The frontend URI of this note
|
|
|
|
|
*/
|
2025-02-01 16:32:18 +01:00
|
|
|
public getMastoUri(): URL {
|
2024-06-10 03:17:03 +02:00
|
|
|
return new URL(
|
2024-06-13 02:45:07 +02:00
|
|
|
`/@${this.author.data.username}/${this.id}`,
|
2024-06-10 03:17:03 +02:00
|
|
|
config.http.base_url,
|
2025-02-01 16:32:18 +01:00
|
|
|
);
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
2024-11-01 21:20:12 +01:00
|
|
|
public deleteToVersia(): VersiaDelete {
|
2024-08-26 19:06:49 +02:00
|
|
|
const id = crypto.randomUUID();
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
type: "Delete",
|
|
|
|
|
id,
|
2025-02-01 16:32:18 +01:00
|
|
|
author: this.author.getUri().toString(),
|
2024-08-26 19:06:49 +02:00
|
|
|
deleted_type: "Note",
|
2025-02-01 16:32:18 +01:00
|
|
|
deleted: this.getUri().toString(),
|
2024-08-26 19:06:49 +02:00
|
|
|
created_at: new Date().toISOString(),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-13 10:52:03 +02:00
|
|
|
/**
|
2024-08-19 15:16:01 +02:00
|
|
|
* Convert a note to the Versia format
|
|
|
|
|
* @returns The note in the Versia format
|
2024-06-13 10:52:03 +02:00
|
|
|
*/
|
2024-11-01 21:20:12 +01:00
|
|
|
public toVersia(): VersiaNote {
|
2024-06-13 02:45:07 +02:00
|
|
|
const status = this.data;
|
2024-04-17 06:09:21 +02:00
|
|
|
return {
|
|
|
|
|
type: "Note",
|
|
|
|
|
created_at: new Date(status.createdAt).toISOString(),
|
|
|
|
|
id: status.id,
|
2025-02-01 16:32:18 +01:00
|
|
|
author: this.author.getUri().toString(),
|
|
|
|
|
uri: this.getUri().toString(),
|
2024-04-17 06:09:21 +02:00
|
|
|
content: {
|
|
|
|
|
"text/html": {
|
|
|
|
|
content: status.content,
|
2024-08-26 19:06:49 +02:00
|
|
|
remote: false,
|
2024-04-17 06:09:21 +02:00
|
|
|
},
|
|
|
|
|
"text/plain": {
|
|
|
|
|
content: htmlToText(status.content),
|
2024-08-26 19:06:49 +02:00
|
|
|
remote: false,
|
2024-04-17 06:09:21 +02:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
attachments: (status.attachments ?? []).map((attachment) =>
|
2025-01-23 16:08:42 +01:00
|
|
|
new Media(attachment).toVersia(),
|
2024-04-17 06:09:21 +02:00
|
|
|
),
|
|
|
|
|
is_sensitive: status.sensitive,
|
2024-06-13 23:53:41 +02:00
|
|
|
mentions: status.mentions.map((mention) =>
|
2025-02-01 16:32:18 +01:00
|
|
|
User.getUri(
|
|
|
|
|
mention.id,
|
|
|
|
|
mention.uri ? new URL(mention.uri) : null,
|
|
|
|
|
).toString(),
|
2024-06-13 23:53:41 +02:00
|
|
|
),
|
2025-02-01 16:32:18 +01:00
|
|
|
quotes: Note.getUri(
|
|
|
|
|
status.quotingId,
|
|
|
|
|
status.quote?.uri ? new URL(status.quote.uri) : null,
|
|
|
|
|
)?.toString(),
|
|
|
|
|
replies_to: Note.getUri(
|
|
|
|
|
status.replyId,
|
|
|
|
|
status.reply?.uri ? new URL(status.reply.uri) : null,
|
|
|
|
|
)?.toString(),
|
2024-04-17 06:09:21 +02:00
|
|
|
subject: status.spoilerText,
|
2024-08-26 19:06:49 +02:00
|
|
|
// TODO: Refactor as part of groups
|
|
|
|
|
group: status.visibility === "public" ? "public" : "followers",
|
2024-04-17 06:09:21 +02:00
|
|
|
extensions: {
|
2024-08-26 19:27:40 +02:00
|
|
|
"pub.versia:custom_emojis": {
|
2024-06-13 06:52:01 +02:00
|
|
|
emojis: status.emojis.map((emoji) =>
|
2024-08-19 15:16:01 +02:00
|
|
|
new Emoji(emoji).toVersia(),
|
2024-06-13 06:52:01 +02:00
|
|
|
),
|
2024-04-17 06:09:21 +02:00
|
|
|
},
|
|
|
|
|
// TODO: Add polls and reactions
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return all the ancestors of this post,
|
2024-06-13 10:52:03 +02:00
|
|
|
* i.e. all the posts that this post is a reply to
|
|
|
|
|
* @param fetcher - The user fetching the ancestors
|
|
|
|
|
* @returns The ancestors of this post
|
2024-04-17 06:09:21 +02:00
|
|
|
*/
|
2024-11-01 21:20:12 +01:00
|
|
|
public async getAncestors(fetcher: User | null): Promise<Note[]> {
|
2024-04-17 06:09:21 +02:00
|
|
|
const ancestors: Note[] = [];
|
|
|
|
|
|
|
|
|
|
let currentStatus: Note = this;
|
|
|
|
|
|
2024-06-13 02:45:07 +02:00
|
|
|
while (currentStatus.data.replyId) {
|
2024-05-09 01:19:53 +02:00
|
|
|
const parent = await Note.fromId(
|
2024-06-13 02:45:07 +02:00
|
|
|
currentStatus.data.replyId,
|
2024-05-09 01:19:53 +02:00
|
|
|
fetcher?.id,
|
|
|
|
|
);
|
2024-04-17 06:09:21 +02:00
|
|
|
|
|
|
|
|
if (!parent) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ancestors.push(parent);
|
|
|
|
|
currentStatus = parent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filter for posts that are viewable by the user
|
2024-11-19 17:26:14 +01:00
|
|
|
const viewableAncestors = await Promise.all(
|
|
|
|
|
ancestors.map(async (ancestor) => {
|
|
|
|
|
const isViewable = await ancestor.isViewableByUser(fetcher);
|
|
|
|
|
return isViewable ? ancestor : null;
|
|
|
|
|
}),
|
|
|
|
|
).then((filteredAncestors) =>
|
|
|
|
|
filteredAncestors.filter((n) => n !== null),
|
2024-04-17 06:09:21 +02:00
|
|
|
);
|
2024-05-12 04:44:00 +02:00
|
|
|
|
|
|
|
|
// Reverse the order so that the oldest posts are first
|
|
|
|
|
return viewableAncestors.toReversed();
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return all the descendants of this post (recursive)
|
|
|
|
|
* Temporary implementation, will be replaced with a recursive SQL query when I get to it
|
2024-06-13 10:52:03 +02:00
|
|
|
* @param fetcher - The user fetching the descendants
|
|
|
|
|
* @param depth - The depth of the recursion (internal)
|
|
|
|
|
* @returns The descendants of this post
|
2024-04-17 06:09:21 +02:00
|
|
|
*/
|
2024-11-01 21:20:12 +01:00
|
|
|
public async getDescendants(
|
|
|
|
|
fetcher: User | null,
|
|
|
|
|
depth = 0,
|
|
|
|
|
): Promise<Note[]> {
|
2024-04-17 06:09:21 +02:00
|
|
|
const descendants: Note[] = [];
|
2024-05-09 01:19:53 +02:00
|
|
|
for (const child of await this.getReplyChildren(fetcher?.id)) {
|
2024-04-17 06:09:21 +02:00
|
|
|
descendants.push(child);
|
|
|
|
|
|
|
|
|
|
if (depth < 20) {
|
|
|
|
|
const childDescendants = await child.getDescendants(
|
|
|
|
|
fetcher,
|
|
|
|
|
depth + 1,
|
|
|
|
|
);
|
|
|
|
|
descendants.push(...childDescendants);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filter for posts that are viewable by the user
|
|
|
|
|
|
2024-11-19 17:26:14 +01:00
|
|
|
const viewableDescendants = await Promise.all(
|
|
|
|
|
descendants.map(async (descendant) => {
|
|
|
|
|
const isViewable = await descendant.isViewableByUser(fetcher);
|
|
|
|
|
return isViewable ? descendant : null;
|
|
|
|
|
}),
|
|
|
|
|
).then((filteredDescendants) =>
|
|
|
|
|
filteredDescendants.filter((n) => n !== null),
|
2024-04-17 06:09:21 +02:00
|
|
|
);
|
2024-05-12 04:44:00 +02:00
|
|
|
|
2024-04-17 06:09:21 +02:00
|
|
|
return viewableDescendants;
|
|
|
|
|
}
|
|
|
|
|
}
|