mirror of
https://github.com/versia-pub/server.git
synced 2025-12-06 08:28:19 +01:00
refactor(api): 🎨 Refactor complex functions into smaller ones
This commit is contained in:
parent
a1e02d0d78
commit
c61f519a34
|
|
@ -2,7 +2,7 @@ import { idValidator } from "@/api";
|
||||||
import { dualLogger } from "@/loggers";
|
import { dualLogger } from "@/loggers";
|
||||||
import { proxyUrl } from "@/response";
|
import { proxyUrl } from "@/response";
|
||||||
import { sanitizedHtmlStrip } from "@/sanitization";
|
import { sanitizedHtmlStrip } from "@/sanitization";
|
||||||
import type { EntityValidator } from "@lysand-org/federation";
|
import { EntityValidator } from "@lysand-org/federation";
|
||||||
import {
|
import {
|
||||||
type InferInsertModel,
|
type InferInsertModel,
|
||||||
type SQL,
|
type SQL,
|
||||||
|
|
@ -33,6 +33,7 @@ import {
|
||||||
type StatusWithRelations,
|
type StatusWithRelations,
|
||||||
contentToHtml,
|
contentToHtml,
|
||||||
findManyNotes,
|
findManyNotes,
|
||||||
|
parseTextMentions,
|
||||||
} from "~/database/entities/status";
|
} from "~/database/entities/status";
|
||||||
import { db } from "~/drizzle/db";
|
import { db } from "~/drizzle/db";
|
||||||
import {
|
import {
|
||||||
|
|
@ -249,90 +250,79 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
static async fromData(
|
static async fromData(data: {
|
||||||
author: User,
|
author: User;
|
||||||
content: typeof EntityValidator.$ContentFormat,
|
content: typeof EntityValidator.$ContentFormat;
|
||||||
visibility: apiStatus["visibility"],
|
visibility: apiStatus["visibility"];
|
||||||
isSensitive: boolean,
|
isSensitive: boolean;
|
||||||
spoilerText: string,
|
spoilerText: string;
|
||||||
emojis: EmojiWithInstance[],
|
emojis?: EmojiWithInstance[];
|
||||||
uri?: string,
|
uri?: string;
|
||||||
mentions?: User[],
|
mentions?: User[];
|
||||||
/** List of IDs of database Attachment objects */
|
/** List of IDs of database Attachment objects */
|
||||||
mediaAttachments?: string[],
|
mediaAttachments?: string[];
|
||||||
replyId?: string,
|
replyId?: string;
|
||||||
quoteId?: string,
|
quoteId?: string;
|
||||||
application?: Application,
|
application?: Application;
|
||||||
): Promise<Note | null> {
|
}): Promise<Note> {
|
||||||
const htmlContent = await contentToHtml(content, mentions);
|
const plaintextContent =
|
||||||
|
data.content["text/plain"]?.content ??
|
||||||
|
Object.entries(data.content)[0][1].content;
|
||||||
|
|
||||||
// Parse emojis and fuse with existing emojis
|
const parsedMentions = [
|
||||||
let foundEmojis = emojis;
|
...(data.mentions ?? []),
|
||||||
|
...(await parseTextMentions(plaintextContent)),
|
||||||
|
// Deduplicate by .id
|
||||||
|
].filter(
|
||||||
|
(mention, index, self) =>
|
||||||
|
index === self.findIndex((t) => t.id === mention.id),
|
||||||
|
);
|
||||||
|
|
||||||
if (author.isLocal()) {
|
const parsedEmojis = [
|
||||||
const parsedEmojis = await parseEmojis(htmlContent);
|
...(data.emojis ?? []),
|
||||||
// Fuse and deduplicate
|
...(await parseEmojis(plaintextContent)),
|
||||||
foundEmojis = [...emojis, ...parsedEmojis].filter(
|
// Deduplicate by .id
|
||||||
(emoji, index, self) =>
|
].filter(
|
||||||
index === self.findIndex((t) => t.id === emoji.id),
|
(emoji, index, self) =>
|
||||||
);
|
index === self.findIndex((t) => t.id === emoji.id),
|
||||||
}
|
);
|
||||||
|
|
||||||
|
const htmlContent = await contentToHtml(data.content, parsedMentions);
|
||||||
|
|
||||||
const newNote = await Note.insert({
|
const newNote = await Note.insert({
|
||||||
authorId: author.id,
|
authorId: data.author.id,
|
||||||
content: htmlContent,
|
content: htmlContent,
|
||||||
contentSource:
|
contentSource:
|
||||||
content["text/plain"]?.content ||
|
data.content["text/plain"]?.content ||
|
||||||
content["text/markdown"]?.content ||
|
data.content["text/markdown"]?.content ||
|
||||||
Object.entries(content)[0][1].content ||
|
Object.entries(data.content)[0][1].content ||
|
||||||
"",
|
"",
|
||||||
contentType: "text/html",
|
contentType: "text/html",
|
||||||
visibility,
|
visibility: data.visibility,
|
||||||
sensitive: isSensitive,
|
sensitive: data.isSensitive,
|
||||||
spoilerText: await sanitizedHtmlStrip(spoilerText),
|
spoilerText: await sanitizedHtmlStrip(data.spoilerText),
|
||||||
uri: uri || null,
|
uri: data.uri || null,
|
||||||
replyId: replyId ?? null,
|
replyId: data.replyId ?? null,
|
||||||
quotingId: quoteId ?? null,
|
quotingId: data.quoteId ?? null,
|
||||||
applicationId: application?.id ?? null,
|
applicationId: data.application?.id ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connect emojis
|
// Connect emojis
|
||||||
for (const emoji of foundEmojis) {
|
await newNote.recalculateDatabaseEmojis(parsedEmojis);
|
||||||
await db
|
|
||||||
.insert(EmojiToNote)
|
|
||||||
.values({
|
|
||||||
emojiId: emoji.id,
|
|
||||||
noteId: newNote.id,
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect mentions
|
// Connect mentions
|
||||||
for (const mention of mentions ?? []) {
|
await newNote.recalculateDatabaseMentions(parsedMentions);
|
||||||
await db
|
|
||||||
.insert(NoteToMentions)
|
|
||||||
.values({
|
|
||||||
noteId: newNote.id,
|
|
||||||
userId: mention.id,
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set attachment parents
|
// Set attachment parents
|
||||||
if (mediaAttachments && mediaAttachments.length > 0) {
|
await newNote.recalculateDatabaseAttachments(
|
||||||
await db
|
data.mediaAttachments ?? [],
|
||||||
.update(Attachments)
|
);
|
||||||
.set({
|
|
||||||
noteId: newNote.id,
|
|
||||||
})
|
|
||||||
.where(inArray(Attachments.id, mediaAttachments));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send notifications for mentioned local users
|
// Send notifications for mentioned local users
|
||||||
for (const mention of mentions ?? []) {
|
for (const mention of parsedMentions ?? []) {
|
||||||
if (mention.isLocal()) {
|
if (mention.isLocal()) {
|
||||||
await db.insert(Notifications).values({
|
await db.insert(Notifications).values({
|
||||||
accountId: author.id,
|
accountId: data.author.id,
|
||||||
notifiedId: mention.id,
|
notifiedId: mention.id,
|
||||||
type: "mention",
|
type: "mention",
|
||||||
noteId: newNote.id,
|
noteId: newNote.id,
|
||||||
|
|
@ -340,61 +330,101 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return await Note.fromId(newNote.id, newNote.data.authorId);
|
await newNote.reload();
|
||||||
|
|
||||||
|
return newNote;
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFromData(
|
async updateFromData(data: {
|
||||||
content?: typeof EntityValidator.$ContentFormat,
|
author?: User;
|
||||||
visibility?: apiStatus["visibility"],
|
content?: typeof EntityValidator.$ContentFormat;
|
||||||
isSensitive?: boolean,
|
visibility?: apiStatus["visibility"];
|
||||||
spoilerText?: string,
|
isSensitive?: boolean;
|
||||||
emojis: EmojiWithInstance[] = [],
|
spoilerText?: string;
|
||||||
mentions: User[] = [],
|
emojis?: EmojiWithInstance[];
|
||||||
|
uri?: string;
|
||||||
|
mentions?: User[];
|
||||||
/** List of IDs of database Attachment objects */
|
/** List of IDs of database Attachment objects */
|
||||||
mediaAttachments: string[] = [],
|
mediaAttachments?: string[];
|
||||||
replyId?: string,
|
replyId?: string;
|
||||||
quoteId?: string,
|
quoteId?: string;
|
||||||
application?: Application,
|
application?: Application;
|
||||||
) {
|
}): Promise<Note> {
|
||||||
const htmlContent = content
|
const plaintextContent = data.content
|
||||||
? await contentToHtml(content, mentions)
|
? data.content["text/plain"]?.content ??
|
||||||
|
Object.entries(data.content)[0][1].content
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// Parse emojis and fuse with existing emojis
|
const parsedMentions = [
|
||||||
let foundEmojis = emojis;
|
...(data.mentions ?? []),
|
||||||
|
...(plaintextContent
|
||||||
|
? await parseTextMentions(plaintextContent)
|
||||||
|
: []),
|
||||||
|
// Deduplicate by .id
|
||||||
|
].filter(
|
||||||
|
(mention, index, self) =>
|
||||||
|
index === self.findIndex((t) => t.id === mention.id),
|
||||||
|
);
|
||||||
|
|
||||||
if (this.author.isLocal() && htmlContent) {
|
const parsedEmojis = [
|
||||||
const parsedEmojis = await parseEmojis(htmlContent);
|
...(data.emojis ?? []),
|
||||||
// Fuse and deduplicate
|
...(plaintextContent ? await parseEmojis(plaintextContent) : []),
|
||||||
foundEmojis = [...emojis, ...parsedEmojis].filter(
|
// Deduplicate by .id
|
||||||
(emoji, index, self) =>
|
].filter(
|
||||||
index === self.findIndex((t) => t.id === emoji.id),
|
(emoji, index, self) =>
|
||||||
);
|
index === self.findIndex((t) => t.id === emoji.id),
|
||||||
}
|
);
|
||||||
|
|
||||||
const newNote = await this.update({
|
const htmlContent = data.content
|
||||||
|
? await contentToHtml(data.content, parsedMentions)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
await this.update({
|
||||||
content: htmlContent,
|
content: htmlContent,
|
||||||
contentSource: content
|
contentSource: data.content
|
||||||
? content["text/plain"]?.content ||
|
? data.content["text/plain"]?.content ||
|
||||||
content["text/markdown"]?.content ||
|
data.content["text/markdown"]?.content ||
|
||||||
Object.entries(content)[0][1].content ||
|
Object.entries(data.content)[0][1].content ||
|
||||||
""
|
""
|
||||||
: undefined,
|
: undefined,
|
||||||
contentType: "text/html",
|
contentType: "text/html",
|
||||||
visibility,
|
visibility: data.visibility,
|
||||||
sensitive: isSensitive,
|
sensitive: data.isSensitive,
|
||||||
spoilerText: spoilerText,
|
spoilerText: data.spoilerText,
|
||||||
replyId,
|
replyId: data.replyId,
|
||||||
quotingId: quoteId,
|
quotingId: data.quoteId,
|
||||||
applicationId: application?.id,
|
applicationId: data.application?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Connect emojis
|
||||||
|
await this.recalculateDatabaseEmojis(parsedEmojis);
|
||||||
|
|
||||||
|
// Connect mentions
|
||||||
|
await this.recalculateDatabaseMentions(parsedMentions);
|
||||||
|
|
||||||
|
// Set attachment parents
|
||||||
|
await this.recalculateDatabaseAttachments(data.mediaAttachments ?? []);
|
||||||
|
|
||||||
|
await this.reload();
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async recalculateDatabaseEmojis(
|
||||||
|
emojis: EmojiWithInstance[],
|
||||||
|
): Promise<void> {
|
||||||
|
// Fuse and deduplicate
|
||||||
|
const fusedEmojis = emojis.filter(
|
||||||
|
(emoji, index, self) =>
|
||||||
|
index === self.findIndex((t) => t.id === emoji.id),
|
||||||
|
);
|
||||||
|
|
||||||
// Connect emojis
|
// Connect emojis
|
||||||
await db
|
await db
|
||||||
.delete(EmojiToNote)
|
.delete(EmojiToNote)
|
||||||
.where(eq(EmojiToNote.noteId, this.data.id));
|
.where(eq(EmojiToNote.noteId, this.data.id));
|
||||||
|
|
||||||
for (const emoji of foundEmojis) {
|
for (const emoji of fusedEmojis) {
|
||||||
await db
|
await db
|
||||||
.insert(EmojiToNote)
|
.insert(EmojiToNote)
|
||||||
.values({
|
.values({
|
||||||
|
|
@ -403,13 +433,15 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async recalculateDatabaseMentions(mentions: User[]): Promise<void> {
|
||||||
// Connect mentions
|
// Connect mentions
|
||||||
await db
|
await db
|
||||||
.delete(NoteToMentions)
|
.delete(NoteToMentions)
|
||||||
.where(eq(NoteToMentions.noteId, this.data.id));
|
.where(eq(NoteToMentions.noteId, this.data.id));
|
||||||
|
|
||||||
for (const mention of mentions ?? []) {
|
for (const mention of mentions) {
|
||||||
await db
|
await db
|
||||||
.insert(NoteToMentions)
|
.insert(NoteToMentions)
|
||||||
.values({
|
.values({
|
||||||
|
|
@ -418,27 +450,27 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
})
|
})
|
||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async recalculateDatabaseAttachments(
|
||||||
|
mediaAttachments: string[],
|
||||||
|
): Promise<void> {
|
||||||
// Set attachment parents
|
// Set attachment parents
|
||||||
if (mediaAttachments) {
|
await db
|
||||||
|
.update(Attachments)
|
||||||
|
.set({
|
||||||
|
noteId: null,
|
||||||
|
})
|
||||||
|
.where(eq(Attachments.noteId, this.data.id));
|
||||||
|
|
||||||
|
if (mediaAttachments.length > 0) {
|
||||||
await db
|
await db
|
||||||
.update(Attachments)
|
.update(Attachments)
|
||||||
.set({
|
.set({
|
||||||
noteId: null,
|
noteId: this.data.id,
|
||||||
})
|
})
|
||||||
.where(eq(Attachments.noteId, this.data.id));
|
.where(inArray(Attachments.id, mediaAttachments));
|
||||||
|
|
||||||
if (mediaAttachments.length > 0) {
|
|
||||||
await db
|
|
||||||
.update(Attachments)
|
|
||||||
.set({
|
|
||||||
noteId: this.data.id,
|
|
||||||
})
|
|
||||||
.where(inArray(Attachments.id, mediaAttachments));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return await Note.fromId(newNote.id, newNote.authorId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async resolve(
|
static async resolve(
|
||||||
|
|
@ -476,10 +508,6 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
throw new Error("No URI or note provided");
|
throw new Error("No URI or note provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
const foundStatus = await Note.fromSql(
|
|
||||||
eq(Notes.uri, uri ?? providedNote?.uri ?? ""),
|
|
||||||
);
|
|
||||||
|
|
||||||
let note = providedNote || null;
|
let note = providedNote || null;
|
||||||
|
|
||||||
if (uri) {
|
if (uri) {
|
||||||
|
|
@ -494,27 +522,44 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
note = (await response.json()) as typeof EntityValidator.$Note;
|
note = await new EntityValidator().Note(await response.json());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!note) {
|
if (!note) {
|
||||||
throw new Error("No note was able to be fetched");
|
throw new Error("No note was able to be fetched");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (note.type !== "Note") {
|
|
||||||
throw new Error("Invalid object type");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!note.author) {
|
|
||||||
throw new Error("Invalid object author");
|
|
||||||
}
|
|
||||||
|
|
||||||
const author = await User.resolve(note.author);
|
const author = await User.resolve(note.author);
|
||||||
|
|
||||||
if (!author) {
|
if (!author) {
|
||||||
throw new Error("Invalid object author");
|
throw new Error("Invalid object author");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return await Note.fromLysand(note, author);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async fromLysand(
|
||||||
|
note: typeof EntityValidator.$Note,
|
||||||
|
author: User,
|
||||||
|
): Promise<Note> {
|
||||||
|
const emojis = [];
|
||||||
|
|
||||||
|
for (const emoji of note.extensions?.["org.lysand:custom_emojis"]
|
||||||
|
?.emojis ?? []) {
|
||||||
|
const resolvedEmoji = await fetchEmoji(emoji).catch((e) => {
|
||||||
|
dualLogger.logError(
|
||||||
|
LogLevel.Error,
|
||||||
|
"Federation.StatusResolver",
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resolvedEmoji) {
|
||||||
|
emojis.push(resolvedEmoji);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const attachments = [];
|
const attachments = [];
|
||||||
|
|
||||||
for (const attachment of note.attachments ?? []) {
|
for (const attachment of note.attachments ?? []) {
|
||||||
|
|
@ -534,85 +579,45 @@ export class Note extends BaseInterface<typeof Notes, StatusWithRelations> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const emojis = [];
|
const newData = {
|
||||||
|
|
||||||
for (const emoji of note.extensions?.["org.lysand:custom_emojis"]
|
|
||||||
?.emojis ?? []) {
|
|
||||||
const resolvedEmoji = await fetchEmoji(emoji).catch((e) => {
|
|
||||||
dualLogger.logError(
|
|
||||||
LogLevel.Error,
|
|
||||||
"Federation.StatusResolver",
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resolvedEmoji) {
|
|
||||||
emojis.push(resolvedEmoji);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundStatus) {
|
|
||||||
return await foundStatus.updateFromData(
|
|
||||||
note.content ?? {
|
|
||||||
"text/plain": {
|
|
||||||
content: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
note.visibility as apiStatus["visibility"],
|
|
||||||
note.is_sensitive ?? false,
|
|
||||||
note.subject ?? "",
|
|
||||||
emojis,
|
|
||||||
note.mentions
|
|
||||||
? await Promise.all(
|
|
||||||
(note.mentions ?? [])
|
|
||||||
.map((mention) => User.resolve(mention))
|
|
||||||
.filter(
|
|
||||||
(mention) => mention !== null,
|
|
||||||
) as Promise<User>[],
|
|
||||||
)
|
|
||||||
: [],
|
|
||||||
attachments.map((a) => a.id),
|
|
||||||
note.replies_to
|
|
||||||
? (await Note.resolve(note.replies_to))?.data.id
|
|
||||||
: undefined,
|
|
||||||
note.quotes
|
|
||||||
? (await Note.resolve(note.quotes))?.data.id
|
|
||||||
: undefined,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const createdNote = await Note.fromData(
|
|
||||||
author,
|
author,
|
||||||
note.content ?? {
|
content: note.content ?? {
|
||||||
"text/plain": {
|
"text/plain": {
|
||||||
content: "",
|
content: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
note.visibility as apiStatus["visibility"],
|
visibility: note.visibility as apiStatus["visibility"],
|
||||||
note.is_sensitive ?? false,
|
isSensitive: note.is_sensitive ?? false,
|
||||||
note.subject ?? "",
|
spoilerText: note.subject ?? "",
|
||||||
emojis,
|
emojis,
|
||||||
note.uri,
|
uri: note.uri,
|
||||||
await Promise.all(
|
mentions: await Promise.all(
|
||||||
(note.mentions ?? [])
|
(note.mentions ?? [])
|
||||||
.map((mention) => User.resolve(mention))
|
.map((mention) => User.resolve(mention))
|
||||||
.filter((mention) => mention !== null) as Promise<User>[],
|
.filter((mention) => mention !== null) as Promise<User>[],
|
||||||
),
|
),
|
||||||
attachments.map((a) => a.id),
|
mediaAttachments: attachments.map((a) => a.id),
|
||||||
note.replies_to
|
replyId: note.replies_to
|
||||||
? (await Note.resolve(note.replies_to))?.data.id
|
? (await Note.resolve(note.replies_to))?.data.id
|
||||||
: undefined,
|
: undefined,
|
||||||
note.quotes
|
quoteId: note.quotes
|
||||||
? (await Note.resolve(note.quotes))?.data.id
|
? (await Note.resolve(note.quotes))?.data.id
|
||||||
: undefined,
|
: undefined,
|
||||||
);
|
};
|
||||||
|
|
||||||
if (!createdNote) {
|
// Check if new note already exists
|
||||||
throw new Error("Failed to create status");
|
|
||||||
|
const foundNote = await Note.fromSql(eq(Notes.uri, note.uri));
|
||||||
|
|
||||||
|
// If it exists, simply update it
|
||||||
|
if (foundNote) {
|
||||||
|
await foundNote.updateFromData(newData);
|
||||||
|
|
||||||
|
return foundNote;
|
||||||
}
|
}
|
||||||
|
|
||||||
return createdNote;
|
// Else, create a new note
|
||||||
|
return await Note.fromData(newData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(ids: string[]): Promise<void>;
|
async delete(ids: string[]): Promise<void>;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { applyConfig, auth, handleZodError, qs } from "@/api";
|
import { applyConfig, auth, handleZodError, jsonOrForm } from "@/api";
|
||||||
import { errorResponse, jsonResponse } from "@/response";
|
import { errorResponse, jsonResponse } from "@/response";
|
||||||
import { sanitizedHtmlStrip } from "@/sanitization";
|
import { sanitizedHtmlStrip } from "@/sanitization";
|
||||||
import { zValidator } from "@hono/zod-validator";
|
import { zValidator } from "@hono/zod-validator";
|
||||||
|
|
@ -99,7 +99,7 @@ export default (app: Hono) =>
|
||||||
app.on(
|
app.on(
|
||||||
meta.allowedMethods,
|
meta.allowedMethods,
|
||||||
meta.route,
|
meta.route,
|
||||||
qs(),
|
jsonOrForm(),
|
||||||
zValidator("form", schemas.form, handleZodError),
|
zValidator("form", schemas.form, handleZodError),
|
||||||
auth(meta.auth, meta.permissions),
|
auth(meta.auth, meta.permissions),
|
||||||
async (context) => {
|
async (context) => {
|
||||||
|
|
|
||||||
|
|
@ -159,21 +159,18 @@ export default (app: Hono) =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newNote = await foundStatus.updateFromData(
|
const newNote = await foundStatus.updateFromData({
|
||||||
statusText
|
content: statusText
|
||||||
? {
|
? {
|
||||||
[content_type]: {
|
[content_type]: {
|
||||||
content: statusText,
|
content: statusText,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
undefined,
|
isSensitive: sensitive,
|
||||||
sensitive,
|
spoilerText: spoiler_text,
|
||||||
spoiler_text,
|
mediaAttachments: media_ids,
|
||||||
undefined,
|
});
|
||||||
undefined,
|
|
||||||
media_ids,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!newNote) {
|
if (!newNote) {
|
||||||
return errorResponse("Failed to update status", 500);
|
return errorResponse("Failed to update status", 500);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { config } from "config-manager";
|
||||||
import type { Hono } from "hono";
|
import type { Hono } from "hono";
|
||||||
import ISO6391 from "iso-639-1";
|
import ISO6391 from "iso-639-1";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { federateNote, parseTextMentions } from "~/database/entities/status";
|
import { federateNote } from "~/database/entities/status";
|
||||||
import { db } from "~/drizzle/db";
|
import { db } from "~/drizzle/db";
|
||||||
import { RolePermissions } from "~/drizzle/schema";
|
import { RolePermissions } from "~/drizzle/schema";
|
||||||
import { Note } from "~/packages/database-interface/note";
|
import { Note } from "~/packages/database-interface/note";
|
||||||
|
|
@ -175,26 +175,21 @@ export default (app: Hono) =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mentions = await parseTextMentions(status ?? "");
|
const newNote = await Note.fromData({
|
||||||
|
author: user,
|
||||||
const newNote = await Note.fromData(
|
content: {
|
||||||
user,
|
|
||||||
{
|
|
||||||
[content_type]: {
|
[content_type]: {
|
||||||
content: status ?? "",
|
content: status ?? "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
visibility,
|
visibility,
|
||||||
sensitive ?? false,
|
isSensitive: sensitive ?? false,
|
||||||
spoiler_text ?? "",
|
spoilerText: spoiler_text ?? "",
|
||||||
[],
|
mediaAttachments: media_ids,
|
||||||
undefined,
|
replyId: in_reply_to_id ?? undefined,
|
||||||
mentions,
|
quoteId: quote_id ?? undefined,
|
||||||
media_ids,
|
application: application ?? undefined,
|
||||||
in_reply_to_id ?? undefined,
|
});
|
||||||
quote_id ?? undefined,
|
|
||||||
application ?? undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!newNote) {
|
if (!newNote) {
|
||||||
return errorResponse("Failed to create status", 500);
|
return errorResponse("Failed to create status", 500);
|
||||||
|
|
|
||||||
254
utils/api.ts
254
utils/api.ts
|
|
@ -3,7 +3,6 @@ import chalk from "chalk";
|
||||||
import { config } from "config-manager";
|
import { config } from "config-manager";
|
||||||
import type { Context } from "hono";
|
import type { Context } from "hono";
|
||||||
import { createMiddleware } from "hono/factory";
|
import { createMiddleware } from "hono/factory";
|
||||||
import type { BodyData } from "hono/utils/body";
|
|
||||||
import { validator } from "hono/validator";
|
import { validator } from "hono/validator";
|
||||||
import {
|
import {
|
||||||
anyOf,
|
anyOf,
|
||||||
|
|
@ -198,148 +197,45 @@ export const auth = (
|
||||||
return checkRouteNeedsAuth(auth, authData, context);
|
return checkRouteNeedsAuth(auth, authData, context);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* export const auth = (
|
// Helper function to parse form data
|
||||||
authData: ApiRouteMetadata["auth"],
|
async function parseFormData(context: Context) {
|
||||||
permissionData?: ApiRouteMetadata["permissions"],
|
const formData = await context.req.formData();
|
||||||
) =>
|
const urlparams = new URLSearchParams();
|
||||||
validator("header", async (value, context) => {
|
const files = new Map<string, File>();
|
||||||
const auth = value.authorization
|
for (const [key, value] of [...formData.entries()]) {
|
||||||
? await getFromHeader(value.authorization)
|
if (Array.isArray(value)) {
|
||||||
: null;
|
for (const val of value) {
|
||||||
|
urlparams.append(key, val);
|
||||||
const error = errorResponse("Unauthorized", 401);
|
|
||||||
|
|
||||||
// Permissions check
|
|
||||||
if (permissionData) {
|
|
||||||
const userPerms = auth?.user
|
|
||||||
? auth.user.getAllPermissions()
|
|
||||||
: config.permissions.anonymous;
|
|
||||||
|
|
||||||
const requiredPerms =
|
|
||||||
permissionData.methodOverrides?.[
|
|
||||||
context.req.method as HttpVerb
|
|
||||||
] ?? permissionData.required;
|
|
||||||
|
|
||||||
if (!requiredPerms.every((perm) => userPerms.includes(perm))) {
|
|
||||||
const missingPerms = requiredPerms.filter(
|
|
||||||
(perm) => !userPerms.includes(perm),
|
|
||||||
);
|
|
||||||
|
|
||||||
return context.json(
|
|
||||||
{
|
|
||||||
error: `You do not have the required permissions to access this route. Missing: ${missingPerms.join(
|
|
||||||
", ",
|
|
||||||
)}`,
|
|
||||||
},
|
|
||||||
403,
|
|
||||||
error.headers.toJSON(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
} else if (value instanceof File) {
|
||||||
|
if (!files.has(key)) {
|
||||||
if (auth?.user) {
|
files.set(key, value);
|
||||||
return {
|
|
||||||
user: auth.user as User,
|
|
||||||
token: auth.token as string,
|
|
||||||
application: auth.application as Application | null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (authData.required) {
|
|
||||||
return context.json(
|
|
||||||
{
|
|
||||||
error: "Unauthorized",
|
|
||||||
},
|
|
||||||
401,
|
|
||||||
error.headers.toJSON(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
authData.requiredOnMethods?.includes(context.req.method as HttpVerb)
|
|
||||||
) {
|
|
||||||
return context.json(
|
|
||||||
{
|
|
||||||
error: "Unauthorized",
|
|
||||||
},
|
|
||||||
401,
|
|
||||||
error.headers.toJSON(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: null,
|
|
||||||
token: null,
|
|
||||||
application: null,
|
|
||||||
};
|
|
||||||
}); */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Middleware to magically unfuck forms
|
|
||||||
* Add it to random Hono routes and hope it works
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const qs = () => {
|
|
||||||
return createMiddleware(async (context, next) => {
|
|
||||||
const contentType = context.req.header("content-type");
|
|
||||||
|
|
||||||
if (contentType?.includes("multipart/form-data")) {
|
|
||||||
// Get it as a query format to pass on to qs, then insert back files
|
|
||||||
const formData = await context.req.formData();
|
|
||||||
const urlparams = new URLSearchParams();
|
|
||||||
const files = new Map<string, File>();
|
|
||||||
for (const [key, value] of [...formData.entries()]) {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
for (const val of value) {
|
|
||||||
urlparams.append(key, val);
|
|
||||||
}
|
|
||||||
} else if (value instanceof File) {
|
|
||||||
if (!files.has(key)) {
|
|
||||||
files.set(key, value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
urlparams.append(key, String(value));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
const parsed = parse(urlparams.toString(), {
|
urlparams.append(key, String(value));
|
||||||
parseArrays: true,
|
|
||||||
interpretNumericEntities: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// @ts-ignore Very bad hack
|
|
||||||
context.req.parseBody = <T extends BodyData = BodyData>() =>
|
|
||||||
Promise.resolve({
|
|
||||||
...parsed,
|
|
||||||
...Object.fromEntries(files),
|
|
||||||
} as T);
|
|
||||||
|
|
||||||
context.req.formData = () =>
|
|
||||||
// @ts-ignore I'm so sorry for this
|
|
||||||
Promise.resolve({
|
|
||||||
...parsed,
|
|
||||||
...Object.fromEntries(files),
|
|
||||||
});
|
|
||||||
// @ts-ignore I'm so sorry for this
|
|
||||||
context.req.bodyCache.formData = {
|
|
||||||
...parsed,
|
|
||||||
...Object.fromEntries(files),
|
|
||||||
};
|
|
||||||
} else if (contentType?.includes("application/x-www-form-urlencoded")) {
|
|
||||||
const parsed = parse(await context.req.text(), {
|
|
||||||
parseArrays: true,
|
|
||||||
interpretNumericEntities: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
context.req.parseBody = <T extends BodyData = BodyData>() =>
|
|
||||||
Promise.resolve(parsed as T);
|
|
||||||
// @ts-ignore Very bad hack
|
|
||||||
context.req.formData = () => Promise.resolve(parsed);
|
|
||||||
// @ts-ignore I'm so sorry for this
|
|
||||||
context.req.bodyCache.formData = parsed;
|
|
||||||
}
|
}
|
||||||
await next();
|
}
|
||||||
|
|
||||||
|
const parsed = parse(urlparams.toString(), {
|
||||||
|
parseArrays: true,
|
||||||
|
interpretNumericEntities: true,
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
return {
|
||||||
|
parsed,
|
||||||
|
files,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse urlencoded data
|
||||||
|
async function parseUrlEncoded(context: Context) {
|
||||||
|
const parsed = parse(await context.req.text(), {
|
||||||
|
parseArrays: true,
|
||||||
|
interpretNumericEntities: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
export const qsQuery = () => {
|
export const qsQuery = () => {
|
||||||
return createMiddleware(async (context, next) => {
|
return createMiddleware(async (context, next) => {
|
||||||
|
|
@ -350,77 +246,49 @@ export const qsQuery = () => {
|
||||||
|
|
||||||
// @ts-ignore Very bad hack
|
// @ts-ignore Very bad hack
|
||||||
context.req.query = () => parsed;
|
context.req.query = () => parsed;
|
||||||
|
|
||||||
// @ts-ignore I'm so sorry for this
|
// @ts-ignore I'm so sorry for this
|
||||||
context.req.queries = () => parsed;
|
context.req.queries = () => parsed;
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fill in queries, formData and json
|
export const setContextFormDataToObject = (
|
||||||
|
context: Context,
|
||||||
|
setTo: object,
|
||||||
|
): Context => {
|
||||||
|
// @ts-expect-error HACK
|
||||||
|
context.req.bodyCache.formData = setTo;
|
||||||
|
context.req.parseBody = async () =>
|
||||||
|
context.req.bodyCache.formData as FormData;
|
||||||
|
context.req.formData = async () =>
|
||||||
|
context.req.bodyCache.formData as FormData;
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Middleware to magically unfuck forms
|
||||||
|
* Add it to random Hono routes and hope it works
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
export const jsonOrForm = () => {
|
export const jsonOrForm = () => {
|
||||||
return createMiddleware(async (context, next) => {
|
return createMiddleware(async (context, next) => {
|
||||||
const contentType = context.req.header("content-type");
|
const contentType = context.req.header("content-type");
|
||||||
|
|
||||||
if (contentType?.includes("application/json")) {
|
if (contentType?.includes("application/json")) {
|
||||||
context.req.parseBody = async <T extends BodyData = BodyData>() =>
|
setContextFormDataToObject(context, await context.req.json());
|
||||||
(await context.req.json()) as T;
|
|
||||||
context.req.bodyCache.formData = await context.req.json();
|
|
||||||
context.req.formData = async () =>
|
|
||||||
context.req.bodyCache.formData as FormData;
|
|
||||||
} else if (contentType?.includes("application/x-www-form-urlencoded")) {
|
} else if (contentType?.includes("application/x-www-form-urlencoded")) {
|
||||||
const parsed = parse(await context.req.text(), {
|
const parsed = await parseUrlEncoded(context);
|
||||||
parseArrays: true,
|
|
||||||
interpretNumericEntities: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
context.req.parseBody = <T extends BodyData = BodyData>() =>
|
setContextFormDataToObject(context, parsed);
|
||||||
Promise.resolve(parsed as T);
|
|
||||||
// @ts-ignore Very bad hack
|
|
||||||
context.req.formData = () => Promise.resolve(parsed);
|
|
||||||
// @ts-ignore I'm so sorry for this
|
|
||||||
context.req.bodyCache.formData = parsed;
|
|
||||||
} else if (contentType?.includes("multipart/form-data")) {
|
} else if (contentType?.includes("multipart/form-data")) {
|
||||||
// Get it as a query format to pass on to qs, then insert back files
|
const { parsed, files } = await parseFormData(context);
|
||||||
const formData = await context.req.formData();
|
|
||||||
const urlparams = new URLSearchParams();
|
|
||||||
const files = new Map<string, File>();
|
|
||||||
for (const [key, value] of [...formData.entries()]) {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
for (const val of value) {
|
|
||||||
urlparams.append(key, val);
|
|
||||||
}
|
|
||||||
} else if (value instanceof File) {
|
|
||||||
if (!files.has(key)) {
|
|
||||||
files.set(key, value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
urlparams.append(key, String(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = parse(urlparams.toString(), {
|
setContextFormDataToObject(context, {
|
||||||
parseArrays: true,
|
|
||||||
interpretNumericEntities: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// @ts-ignore Very bad hack
|
|
||||||
context.req.parseBody = <T extends BodyData = BodyData>() =>
|
|
||||||
Promise.resolve({
|
|
||||||
...parsed,
|
|
||||||
...Object.fromEntries(files),
|
|
||||||
} as T);
|
|
||||||
|
|
||||||
context.req.formData = () =>
|
|
||||||
// @ts-ignore I'm so sorry for this
|
|
||||||
Promise.resolve({
|
|
||||||
...parsed,
|
|
||||||
...Object.fromEntries(files),
|
|
||||||
});
|
|
||||||
// @ts-ignore I'm so sorry for this
|
|
||||||
context.req.bodyCache.formData = {
|
|
||||||
...parsed,
|
...parsed,
|
||||||
...Object.fromEntries(files),
|
...Object.fromEntries(files),
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue