2024-05-05 07:13:23 +02:00
|
|
|
import { sanitizedHtmlStrip } from "@sanitization";
|
2024-04-17 06:09:21 +02:00
|
|
|
import {
|
|
|
|
|
type InferInsertModel,
|
|
|
|
|
type SQL,
|
|
|
|
|
and,
|
2024-05-06 09:16:33 +02:00
|
|
|
count,
|
2024-04-17 06:09:21 +02:00
|
|
|
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 type * as Lysand from "lysand-types";
|
|
|
|
|
import { createRegExp, exactly, global } from "magic-regexp";
|
|
|
|
|
import {
|
|
|
|
|
type Application,
|
|
|
|
|
applicationToAPI,
|
|
|
|
|
} from "~database/entities/Application";
|
|
|
|
|
import {
|
|
|
|
|
attachmentToAPI,
|
|
|
|
|
attachmentToLysand,
|
|
|
|
|
} from "~database/entities/Attachment";
|
|
|
|
|
import {
|
|
|
|
|
type EmojiWithInstance,
|
|
|
|
|
emojiToAPI,
|
|
|
|
|
emojiToLysand,
|
|
|
|
|
parseEmojis,
|
|
|
|
|
} from "~database/entities/Emoji";
|
|
|
|
|
import { localObjectURI } from "~database/entities/Federation";
|
|
|
|
|
import {
|
|
|
|
|
type Status,
|
|
|
|
|
type StatusWithRelations,
|
|
|
|
|
contentToHtml,
|
|
|
|
|
findManyNotes,
|
|
|
|
|
} from "~database/entities/Status";
|
|
|
|
|
import { db } from "~drizzle/db";
|
|
|
|
|
import {
|
2024-04-17 08:36:01 +02:00
|
|
|
Attachments,
|
|
|
|
|
EmojiToNote,
|
|
|
|
|
NoteToMentions,
|
|
|
|
|
Notes,
|
|
|
|
|
Notifications,
|
|
|
|
|
Users,
|
2024-04-17 06:09:21 +02:00
|
|
|
} from "~drizzle/schema";
|
|
|
|
|
import { config } from "~packages/config-manager";
|
|
|
|
|
import type { Attachment as APIAttachment } from "~types/mastodon/attachment";
|
|
|
|
|
import type { Status as APIStatus } from "~types/mastodon/status";
|
2024-04-25 05:40:27 +02:00
|
|
|
import { User } from "./user";
|
2024-04-17 06:09:21 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Gives helpers to fetch notes from database in a nice format
|
|
|
|
|
*/
|
|
|
|
|
export class Note {
|
|
|
|
|
private constructor(private status: StatusWithRelations) {}
|
|
|
|
|
|
|
|
|
|
static async fromId(id: string | null): Promise<Note | null> {
|
|
|
|
|
if (!id) return null;
|
|
|
|
|
|
2024-04-17 08:36:01 +02:00
|
|
|
return await Note.fromSql(eq(Notes.id, id));
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async fromIds(ids: string[]): Promise<Note[]> {
|
2024-04-17 08:36:01 +02:00
|
|
|
return await Note.manyFromSql(inArray(Notes.id, ids));
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async fromSql(
|
|
|
|
|
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
|
|
|
) {
|
2024-05-08 23:51:47 +02:00
|
|
|
const found = await findManyNotes({
|
2024-04-17 06:09:21 +02:00
|
|
|
where: sql,
|
|
|
|
|
orderBy,
|
2024-05-08 23:51:47 +02:00
|
|
|
limit: 1,
|
2024-04-17 06:09:21 +02:00
|
|
|
});
|
|
|
|
|
|
2024-05-08 23:51:47 +02:00
|
|
|
if (!found[0]) return null;
|
|
|
|
|
return new Note(found[0]);
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async manyFromSql(
|
|
|
|
|
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,
|
|
|
|
|
) {
|
|
|
|
|
const found = await findManyNotes({
|
|
|
|
|
where: sql,
|
|
|
|
|
orderBy,
|
|
|
|
|
limit,
|
|
|
|
|
offset,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return found.map((s) => new Note(s));
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-25 05:40:27 +02:00
|
|
|
get id() {
|
|
|
|
|
return this.status.id;
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-17 06:09:21 +02:00
|
|
|
async getUsersToFederateTo() {
|
|
|
|
|
// Mentioned users
|
|
|
|
|
const mentionedUsers =
|
|
|
|
|
this.getStatus().mentions.length > 0
|
2024-04-25 05:40:27 +02:00
|
|
|
? await User.manyFromSql(
|
|
|
|
|
and(
|
|
|
|
|
isNotNull(Users.instanceId),
|
|
|
|
|
inArray(
|
|
|
|
|
Users.id,
|
|
|
|
|
this.getStatus().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: {
|
|
|
|
|
where: (relationship, { eq, and }) =>
|
|
|
|
|
and(
|
|
|
|
|
eq(relationship.subjectId, Users.id),
|
|
|
|
|
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
|
|
|
|
|
|
|
|
const fusedUsers = [...mentionedUsers, ...usersThatCanSeePost];
|
|
|
|
|
|
|
|
|
|
const deduplicatedUsersById = fusedUsers.filter(
|
|
|
|
|
(user, index, self) =>
|
|
|
|
|
index === self.findIndex((t) => t.id === user.id),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return deduplicatedUsersById;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static fromStatus(status: StatusWithRelations) {
|
|
|
|
|
return new Note(status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static fromStatuses(statuses: StatusWithRelations[]) {
|
|
|
|
|
return statuses.map((s) => new Note(s));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isNull() {
|
|
|
|
|
return this.status === null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getStatus() {
|
|
|
|
|
return this.status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getAuthor() {
|
2024-04-25 05:40:27 +02:00
|
|
|
return new User(this.status.author);
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
2024-05-06 09:16:33 +02:00
|
|
|
static async getCount() {
|
|
|
|
|
return (
|
|
|
|
|
await db
|
|
|
|
|
.select({
|
|
|
|
|
count: count(),
|
|
|
|
|
})
|
|
|
|
|
.from(Notes)
|
|
|
|
|
.where(
|
|
|
|
|
sql`EXISTS (SELECT 1 FROM "Users" WHERE "Users"."id" = ${Notes.authorId} AND "Users"."instanceId" IS NULL)`,
|
|
|
|
|
)
|
|
|
|
|
)[0].count;
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-17 06:09:21 +02:00
|
|
|
async getReplyChildren() {
|
2024-04-17 08:36:01 +02:00
|
|
|
return await Note.manyFromSql(eq(Notes.replyId, this.status.id));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async insert(values: InferInsertModel<typeof Notes>) {
|
|
|
|
|
return (await db.insert(Notes).values(values).returning())[0];
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async fromData(
|
|
|
|
|
author: User,
|
|
|
|
|
content: Lysand.ContentFormat,
|
|
|
|
|
visibility: APIStatus["visibility"],
|
|
|
|
|
is_sensitive: boolean,
|
|
|
|
|
spoiler_text: string,
|
|
|
|
|
emojis: EmojiWithInstance[],
|
|
|
|
|
uri?: string,
|
2024-04-25 05:40:27 +02:00
|
|
|
mentions?: User[],
|
2024-04-17 06:09:21 +02:00
|
|
|
/** List of IDs of database Attachment objects */
|
|
|
|
|
media_attachments?: string[],
|
|
|
|
|
replyId?: string,
|
|
|
|
|
quoteId?: string,
|
|
|
|
|
application?: Application,
|
|
|
|
|
): Promise<Note | null> {
|
|
|
|
|
const htmlContent = await contentToHtml(content, mentions);
|
|
|
|
|
|
|
|
|
|
// Parse emojis and fuse with existing emojis
|
|
|
|
|
let foundEmojis = emojis;
|
|
|
|
|
|
2024-04-25 05:40:27 +02:00
|
|
|
if (author.isLocal()) {
|
2024-04-17 06:09:21 +02:00
|
|
|
const parsedEmojis = await parseEmojis(htmlContent);
|
|
|
|
|
// Fuse and deduplicate
|
|
|
|
|
foundEmojis = [...emojis, ...parsedEmojis].filter(
|
|
|
|
|
(emoji, index, self) =>
|
|
|
|
|
index === self.findIndex((t) => t.id === emoji.id),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newNote = await Note.insert({
|
|
|
|
|
authorId: author.id,
|
|
|
|
|
content: htmlContent,
|
|
|
|
|
contentSource:
|
|
|
|
|
content["text/plain"]?.content ||
|
|
|
|
|
content["text/markdown"]?.content ||
|
|
|
|
|
Object.entries(content)[0][1].content ||
|
|
|
|
|
"",
|
|
|
|
|
contentType: "text/html",
|
|
|
|
|
visibility,
|
|
|
|
|
sensitive: is_sensitive,
|
2024-05-03 05:20:24 +02:00
|
|
|
spoilerText: await sanitizedHtmlStrip(spoiler_text),
|
2024-04-17 06:09:21 +02:00
|
|
|
uri: uri || null,
|
2024-04-17 08:36:01 +02:00
|
|
|
replyId: replyId ?? null,
|
|
|
|
|
quotingId: quoteId ?? null,
|
2024-04-17 06:09:21 +02:00
|
|
|
applicationId: application?.id ?? null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Connect emojis
|
|
|
|
|
for (const emoji of foundEmojis) {
|
|
|
|
|
await db
|
2024-04-17 08:36:01 +02:00
|
|
|
.insert(EmojiToNote)
|
2024-04-17 06:09:21 +02:00
|
|
|
.values({
|
|
|
|
|
emojiId: emoji.id,
|
2024-04-17 08:36:01 +02:00
|
|
|
noteId: newNote.id,
|
2024-04-17 06:09:21 +02:00
|
|
|
})
|
|
|
|
|
.execute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Connect mentions
|
|
|
|
|
for (const mention of mentions ?? []) {
|
|
|
|
|
await db
|
2024-04-17 08:36:01 +02:00
|
|
|
.insert(NoteToMentions)
|
2024-04-17 06:09:21 +02:00
|
|
|
.values({
|
2024-04-17 08:36:01 +02:00
|
|
|
noteId: newNote.id,
|
2024-04-17 06:09:21 +02:00
|
|
|
userId: mention.id,
|
|
|
|
|
})
|
|
|
|
|
.execute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set attachment parents
|
|
|
|
|
if (media_attachments && media_attachments.length > 0) {
|
|
|
|
|
await db
|
2024-04-17 08:36:01 +02:00
|
|
|
.update(Attachments)
|
2024-04-17 06:09:21 +02:00
|
|
|
.set({
|
2024-04-17 08:36:01 +02:00
|
|
|
noteId: newNote.id,
|
2024-04-17 06:09:21 +02:00
|
|
|
})
|
2024-04-17 08:36:01 +02:00
|
|
|
.where(inArray(Attachments.id, media_attachments));
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send notifications for mentioned local users
|
|
|
|
|
for (const mention of mentions ?? []) {
|
2024-04-25 05:40:27 +02:00
|
|
|
if (mention.isLocal()) {
|
2024-04-17 08:36:01 +02:00
|
|
|
await db.insert(Notifications).values({
|
2024-04-17 06:09:21 +02:00
|
|
|
accountId: author.id,
|
|
|
|
|
notifiedId: mention.id,
|
|
|
|
|
type: "mention",
|
2024-04-17 08:36:01 +02:00
|
|
|
noteId: newNote.id,
|
2024-04-17 06:09:21 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await Note.fromId(newNote.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateFromData(
|
|
|
|
|
content?: Lysand.ContentFormat,
|
|
|
|
|
visibility?: APIStatus["visibility"],
|
|
|
|
|
is_sensitive?: boolean,
|
|
|
|
|
spoiler_text?: string,
|
|
|
|
|
emojis: EmojiWithInstance[] = [],
|
2024-04-25 05:40:27 +02:00
|
|
|
mentions: User[] = [],
|
2024-04-17 06:09:21 +02:00
|
|
|
/** List of IDs of database Attachment objects */
|
|
|
|
|
media_attachments: string[] = [],
|
|
|
|
|
) {
|
|
|
|
|
const htmlContent = content
|
|
|
|
|
? await contentToHtml(content, mentions)
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
// Parse emojis and fuse with existing emojis
|
|
|
|
|
let foundEmojis = emojis;
|
|
|
|
|
|
2024-04-25 05:40:27 +02:00
|
|
|
if (this.getAuthor().isLocal() && htmlContent) {
|
2024-04-17 06:09:21 +02:00
|
|
|
const parsedEmojis = await parseEmojis(htmlContent);
|
|
|
|
|
// Fuse and deduplicate
|
|
|
|
|
foundEmojis = [...emojis, ...parsedEmojis].filter(
|
|
|
|
|
(emoji, index, self) =>
|
|
|
|
|
index === self.findIndex((t) => t.id === emoji.id),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newNote = await this.update({
|
|
|
|
|
content: htmlContent,
|
|
|
|
|
contentSource: content
|
|
|
|
|
? content["text/plain"]?.content ||
|
|
|
|
|
content["text/markdown"]?.content ||
|
|
|
|
|
Object.entries(content)[0][1].content ||
|
|
|
|
|
""
|
|
|
|
|
: undefined,
|
|
|
|
|
contentType: "text/html",
|
|
|
|
|
visibility,
|
|
|
|
|
sensitive: is_sensitive,
|
|
|
|
|
spoilerText: spoiler_text,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Connect emojis
|
|
|
|
|
await db
|
2024-04-17 08:36:01 +02:00
|
|
|
.delete(EmojiToNote)
|
|
|
|
|
.where(eq(EmojiToNote.noteId, this.status.id));
|
2024-04-17 06:09:21 +02:00
|
|
|
|
|
|
|
|
for (const emoji of foundEmojis) {
|
|
|
|
|
await db
|
2024-04-17 08:36:01 +02:00
|
|
|
.insert(EmojiToNote)
|
2024-04-17 06:09:21 +02:00
|
|
|
.values({
|
|
|
|
|
emojiId: emoji.id,
|
2024-04-17 08:36:01 +02:00
|
|
|
noteId: this.status.id,
|
2024-04-17 06:09:21 +02:00
|
|
|
})
|
|
|
|
|
.execute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Connect mentions
|
|
|
|
|
await db
|
2024-04-17 08:36:01 +02:00
|
|
|
.delete(NoteToMentions)
|
|
|
|
|
.where(eq(NoteToMentions.noteId, this.status.id));
|
2024-04-17 06:09:21 +02:00
|
|
|
|
|
|
|
|
for (const mention of mentions ?? []) {
|
|
|
|
|
await db
|
2024-04-17 08:36:01 +02:00
|
|
|
.insert(NoteToMentions)
|
2024-04-17 06:09:21 +02:00
|
|
|
.values({
|
2024-04-17 08:36:01 +02:00
|
|
|
noteId: this.status.id,
|
2024-04-17 06:09:21 +02:00
|
|
|
userId: mention.id,
|
|
|
|
|
})
|
|
|
|
|
.execute();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set attachment parents
|
|
|
|
|
if (media_attachments && media_attachments.length > 0) {
|
|
|
|
|
await db
|
2024-04-17 08:36:01 +02:00
|
|
|
.update(Attachments)
|
2024-04-17 06:09:21 +02:00
|
|
|
.set({
|
2024-04-17 08:36:01 +02:00
|
|
|
noteId: this.status.id,
|
2024-04-17 06:09:21 +02:00
|
|
|
})
|
2024-04-17 08:36:01 +02:00
|
|
|
.where(inArray(Attachments.id, media_attachments));
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return await Note.fromId(newNote.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async delete() {
|
|
|
|
|
return (
|
|
|
|
|
await db
|
2024-04-17 08:36:01 +02:00
|
|
|
.delete(Notes)
|
|
|
|
|
.where(eq(Notes.id, this.status.id))
|
2024-04-17 06:09:21 +02:00
|
|
|
.returning()
|
|
|
|
|
)[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async update(newStatus: Partial<Status>) {
|
|
|
|
|
return (
|
|
|
|
|
await db
|
2024-04-17 08:36:01 +02:00
|
|
|
.update(Notes)
|
2024-04-17 06:09:21 +02:00
|
|
|
.set(newStatus)
|
2024-04-17 08:36:01 +02:00
|
|
|
.where(eq(Notes.id, this.status.id))
|
2024-04-17 06:09:21 +02:00
|
|
|
.returning()
|
|
|
|
|
)[0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async deleteMany(ids: string[]) {
|
2024-04-17 08:36:01 +02:00
|
|
|
return await db.delete(Notes).where(inArray(Notes.id, ids)).returning();
|
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-04-25 05:40:27 +02:00
|
|
|
async isViewableByUser(user: User | null) {
|
2024-04-17 06:09:21 +02:00
|
|
|
if (this.getAuthor().id === user?.id) return true;
|
|
|
|
|
if (this.getStatus().visibility === "public") return true;
|
|
|
|
|
if (this.getStatus().visibility === "unlisted") return true;
|
|
|
|
|
if (this.getStatus().visibility === "private") {
|
|
|
|
|
return user
|
2024-04-17 08:36:01 +02:00
|
|
|
? await db.query.Relationships.findFirst({
|
2024-04-17 06:09:21 +02:00
|
|
|
where: (relationship, { and, eq }) =>
|
|
|
|
|
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),
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
: false;
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
user &&
|
|
|
|
|
this.getStatus().mentions.find((mention) => mention.id === user.id)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-25 05:40:27 +02:00
|
|
|
async toAPI(userFetching?: User | null): Promise<APIStatus> {
|
2024-04-17 06:09:21 +02:00
|
|
|
const data = this.getStatus();
|
2024-05-08 23:51:47 +02:00
|
|
|
|
|
|
|
|
const [pinnedByUser, rebloggedByUser, mutedByUser, likedByUser] = (
|
|
|
|
|
await Promise.all([
|
|
|
|
|
userFetching
|
|
|
|
|
? db.query.UserToPinnedNotes.findFirst({
|
|
|
|
|
where: (relation, { and, eq }) =>
|
|
|
|
|
and(
|
|
|
|
|
eq(relation.noteId, data.id),
|
|
|
|
|
eq(relation.userId, userFetching?.id),
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
: false,
|
|
|
|
|
userFetching
|
|
|
|
|
? Note.fromSql(
|
|
|
|
|
and(
|
|
|
|
|
eq(Notes.authorId, userFetching?.id),
|
|
|
|
|
eq(Notes.reblogId, data.id),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
: false,
|
|
|
|
|
userFetching
|
|
|
|
|
? db.query.Relationships.findFirst({
|
|
|
|
|
where: (relationship, { and, eq }) =>
|
|
|
|
|
and(
|
|
|
|
|
eq(relationship.ownerId, userFetching.id),
|
|
|
|
|
eq(relationship.subjectId, data.authorId),
|
|
|
|
|
eq(relationship.muting, true),
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
: false,
|
|
|
|
|
userFetching
|
|
|
|
|
? db.query.Likes.findFirst({
|
|
|
|
|
where: (like, { and, eq }) =>
|
|
|
|
|
and(
|
|
|
|
|
eq(like.likedId, data.id),
|
|
|
|
|
eq(like.likerId, userFetching.id),
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
: false,
|
|
|
|
|
])
|
|
|
|
|
).map((r) => !!r);
|
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,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let replacedContent = data.content;
|
|
|
|
|
|
|
|
|
|
for (const mention of mentionedLocalUsers) {
|
|
|
|
|
replacedContent = replacedContent.replace(
|
|
|
|
|
createRegExp(
|
|
|
|
|
exactly(
|
|
|
|
|
`@${mention.username}@${
|
|
|
|
|
new URL(config.http.base_url).host
|
|
|
|
|
}`,
|
|
|
|
|
),
|
|
|
|
|
[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-04-25 05:40:27 +02:00
|
|
|
account: this.getAuthor().toAPI(userFetching?.id === data.authorId),
|
2024-04-17 06:09:21 +02:00
|
|
|
created_at: new Date(data.createdAt).toISOString(),
|
|
|
|
|
application: data.application
|
|
|
|
|
? applicationToAPI(data.application)
|
|
|
|
|
: null,
|
|
|
|
|
card: null,
|
|
|
|
|
content: replacedContent,
|
|
|
|
|
emojis: data.emojis.map((emoji) => emojiToAPI(emoji)),
|
2024-05-08 23:51:47 +02:00
|
|
|
favourited: likedByUser,
|
2024-05-08 22:57:42 +02:00
|
|
|
favourites_count: data.likeCount,
|
2024-04-17 06:09:21 +02:00
|
|
|
media_attachments: (data.attachments ?? []).map(
|
|
|
|
|
(a) => attachmentToAPI(a) as APIAttachment,
|
|
|
|
|
),
|
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,
|
|
|
|
|
),
|
|
|
|
|
url: User.getUri(mention.id, mention.uri, config.http.base_url),
|
|
|
|
|
username: mention.username,
|
|
|
|
|
})),
|
2024-04-17 06:09:21 +02:00
|
|
|
language: null,
|
2024-05-08 23:51:47 +02:00
|
|
|
muted: mutedByUser,
|
|
|
|
|
pinned: pinnedByUser,
|
2024-04-17 06:09:21 +02:00
|
|
|
// TODO: Add polls
|
|
|
|
|
poll: null,
|
2024-04-28 08:15:08 +02:00
|
|
|
reblog: data.reblog
|
|
|
|
|
? await Note.fromStatus(
|
|
|
|
|
data.reblog as StatusWithRelations,
|
|
|
|
|
).toAPI(userFetching)
|
|
|
|
|
: null,
|
2024-05-08 23:51:47 +02:00
|
|
|
reblogged: rebloggedByUser,
|
2024-04-17 06:09:21 +02:00
|
|
|
reblogs_count: data.reblogCount,
|
|
|
|
|
replies_count: data.replyCount,
|
|
|
|
|
sensitive: data.sensitive,
|
|
|
|
|
spoiler_text: data.spoilerText,
|
|
|
|
|
tags: [],
|
2024-05-05 07:13:23 +02:00
|
|
|
uri: data.uri || this.getMastoURI(),
|
2024-04-17 06:09:21 +02:00
|
|
|
visibility: data.visibility as APIStatus["visibility"],
|
|
|
|
|
url: data.uri || this.getMastoURI(),
|
|
|
|
|
bookmarked: false,
|
2024-04-28 08:15:08 +02:00
|
|
|
// @ts-expect-error Glitch-SOC extension
|
|
|
|
|
quote: data.quotingId
|
|
|
|
|
? (await Note.fromId(data.quotingId).then((n) =>
|
|
|
|
|
n?.toAPI(userFetching),
|
|
|
|
|
)) ?? null
|
|
|
|
|
: null,
|
2024-04-17 08:36:01 +02:00
|
|
|
quote_id: data.quotingId || undefined,
|
2024-04-17 06:09:21 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getURI() {
|
|
|
|
|
return localObjectURI(this.getStatus().id);
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-25 05:40:27 +02:00
|
|
|
static getURI(id?: string | null) {
|
|
|
|
|
if (!id) return null;
|
|
|
|
|
return localObjectURI(id);
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-17 06:09:21 +02:00
|
|
|
getMastoURI() {
|
2024-04-25 05:40:27 +02:00
|
|
|
return `/@${this.getAuthor().getUser().username}/${this.id}`;
|
2024-04-17 06:09:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toLysand(): Lysand.Note {
|
|
|
|
|
const status = this.getStatus();
|
|
|
|
|
return {
|
|
|
|
|
type: "Note",
|
|
|
|
|
created_at: new Date(status.createdAt).toISOString(),
|
|
|
|
|
id: status.id,
|
2024-04-25 05:40:27 +02:00
|
|
|
author: this.getAuthor().getUri(),
|
2024-04-17 06:09:21 +02:00
|
|
|
uri: this.getURI(),
|
|
|
|
|
content: {
|
|
|
|
|
"text/html": {
|
|
|
|
|
content: status.content,
|
|
|
|
|
},
|
|
|
|
|
"text/plain": {
|
|
|
|
|
content: htmlToText(status.content),
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
attachments: (status.attachments ?? []).map((attachment) =>
|
|
|
|
|
attachmentToLysand(attachment),
|
|
|
|
|
),
|
|
|
|
|
is_sensitive: status.sensitive,
|
|
|
|
|
mentions: status.mentions.map((mention) => mention.uri || ""),
|
2024-04-25 05:40:27 +02:00
|
|
|
quotes: Note.getURI(status.quotingId) ?? undefined,
|
|
|
|
|
replies_to: Note.getURI(status.replyId) ?? undefined,
|
2024-04-17 06:09:21 +02:00
|
|
|
subject: status.spoilerText,
|
|
|
|
|
visibility: status.visibility as Lysand.Visibility,
|
|
|
|
|
extensions: {
|
|
|
|
|
"org.lysand:custom_emojis": {
|
|
|
|
|
emojis: status.emojis.map((emoji) => emojiToLysand(emoji)),
|
|
|
|
|
},
|
|
|
|
|
// TODO: Add polls and reactions
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return all the ancestors of this post,
|
|
|
|
|
*/
|
2024-04-25 05:40:27 +02:00
|
|
|
async getAncestors(fetcher: User | null) {
|
2024-04-17 06:09:21 +02:00
|
|
|
const ancestors: Note[] = [];
|
|
|
|
|
|
|
|
|
|
let currentStatus: Note = this;
|
|
|
|
|
|
2024-04-17 08:36:01 +02:00
|
|
|
while (currentStatus.getStatus().replyId) {
|
|
|
|
|
const parent = await Note.fromId(currentStatus.getStatus().replyId);
|
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
|
|
|
|
|
const viewableAncestors = ancestors.filter((ancestor) =>
|
|
|
|
|
ancestor.isViewableByUser(fetcher),
|
|
|
|
|
);
|
|
|
|
|
return viewableAncestors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return all the descendants of this post (recursive)
|
|
|
|
|
* Temporary implementation, will be replaced with a recursive SQL query when I get to it
|
|
|
|
|
*/
|
2024-04-25 05:40:27 +02:00
|
|
|
async getDescendants(fetcher: User | null, depth = 0) {
|
2024-04-17 06:09:21 +02:00
|
|
|
const descendants: Note[] = [];
|
|
|
|
|
for (const child of await this.getReplyChildren()) {
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
const viewableDescendants = descendants.filter((descendant) =>
|
|
|
|
|
descendant.isViewableByUser(fetcher),
|
|
|
|
|
);
|
|
|
|
|
return viewableDescendants;
|
|
|
|
|
}
|
|
|
|
|
}
|