refactor(api): 🎨 Simplify expressions

This commit is contained in:
Jesse Wierzbinski 2024-06-12 20:20:49 -10:00
parent 36d70fb612
commit 98f3ab23d8
No known key found for this signature in database
7 changed files with 203 additions and 288 deletions

View file

@ -6,9 +6,7 @@ import { config } from "config-manager";
import { and, eq } from "drizzle-orm";
import type { Hono } from "hono";
import ISO6391 from "iso-639-1";
import { MediaBackendType } from "media-manager";
import type { MediaBackend } from "media-manager";
import { LocalMediaBackend, S3MediaBackend } from "media-manager";
import { MediaBackend } from "media-manager";
import { z } from "zod";
import { getUrl } from "~/database/entities/attachment";
import { parseEmojis } from "~/database/entities/emoji";
@ -127,19 +125,10 @@ export default (app: Hono) =>
display_name ?? "",
);
let mediaManager: MediaBackend;
switch (config.media.backend as MediaBackendType) {
case MediaBackendType.Local:
mediaManager = new LocalMediaBackend(config);
break;
case MediaBackendType.S3:
mediaManager = new S3MediaBackend(config);
break;
default:
// TODO: Replace with logger
throw new Error("Invalid media backend");
}
const mediaManager = await MediaBackend.fromBackendType(
config.media.backend,
config,
);
if (display_name) {
// Check if display name doesnt match filters

View file

@ -3,9 +3,7 @@ import { errorResponse, jsonResponse, response } from "@/response";
import { zValidator } from "@hono/zod-validator";
import { config } from "config-manager";
import type { Hono } from "hono";
import type { MediaBackend } from "media-manager";
import { MediaBackendType } from "media-manager";
import { LocalMediaBackend, S3MediaBackend } from "media-manager";
import { MediaBackend } from "media-manager";
import { z } from "zod";
import { getUrl } from "~/database/entities/attachment";
import { RolePermissions } from "~/drizzle/schema";
@ -74,19 +72,10 @@ export default (app: Hono) =>
let thumbnailUrl = attachment.data.thumbnailUrl;
let mediaManager: MediaBackend;
switch (config.media.backend as MediaBackendType) {
case MediaBackendType.Local:
mediaManager = new LocalMediaBackend(config);
break;
case MediaBackendType.S3:
mediaManager = new S3MediaBackend(config);
break;
default:
// TODO: Replace with logger
throw new Error("Invalid media backend");
}
const mediaManager = await MediaBackend.fromBackendType(
config.media.backend,
config,
);
if (thumbnail) {
const { path } = await mediaManager.addFile(thumbnail);

View file

@ -4,9 +4,7 @@ import { zValidator } from "@hono/zod-validator";
import { encode } from "blurhash";
import { config } from "config-manager";
import type { Hono } from "hono";
import { MediaBackendType } from "media-manager";
import type { MediaBackend } from "media-manager";
import { LocalMediaBackend, S3MediaBackend } from "media-manager";
import { MediaBackend } from "media-manager";
import sharp from "sharp";
import { z } from "zod";
import { getUrl } from "~/database/entities/attachment";
@ -101,19 +99,10 @@ export default (app: Hono) =>
let url = "";
let mediaManager: MediaBackend;
switch (config.media.backend as MediaBackendType) {
case MediaBackendType.Local:
mediaManager = new LocalMediaBackend(config);
break;
case MediaBackendType.S3:
mediaManager = new S3MediaBackend(config);
break;
default:
// TODO: Replace with logger
throw new Error("Invalid media backend");
}
const mediaManager = await MediaBackend.fromBackendType(
config.media.backend,
config,
);
const { path } = await mediaManager.addFile(file);

View file

@ -12,8 +12,8 @@ import type { Hono } from "hono";
import ISO6391 from "iso-639-1";
import { z } from "zod";
import { undoFederationRequest } from "~/database/entities/federation";
import { db } from "~/drizzle/db";
import { RolePermissions } from "~/drizzle/schema";
import { Attachment } from "~/packages/database-interface/attachment";
import { Note } from "~/packages/database-interface/note";
export const meta = applyConfig({
@ -40,13 +40,24 @@ export const schemas = {
param: z.object({
id: z.string().regex(idValidator),
}),
form: z.object({
status: z.string().max(config.validation.max_note_size).optional(),
form: z
.object({
status: z
.string()
.max(config.validation.max_note_size)
.refine(
(s) =>
!config.filters.note_content.some((filter) =>
s.match(filter),
),
"Status contains blocked words",
)
.optional(),
content_type: z.string().optional().default("text/plain"),
media_ids: z
.array(z.string().regex(idValidator))
.max(config.validation.max_media_attachments)
.optional(),
.default([]),
spoiler_text: z.string().max(255).optional(),
sensitive: z
.string()
@ -76,7 +87,11 @@ export const schemas = {
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.or(z.boolean())
.optional(),
}),
})
.refine(
(obj) => !(obj.media_ids.length > 0 && obj["poll[options]"]),
"Cannot attach poll to media",
),
};
export default (app: Hono) =>
@ -91,75 +106,51 @@ export default (app: Hono) =>
const { id } = context.req.valid("param");
const { user } = context.req.valid("header");
const foundStatus = await Note.fromId(id, user?.id);
if (!foundStatus?.isViewableByUser(user)) {
return errorResponse("Record not found", 404);
}
if (context.req.method === "GET") {
return jsonResponse(await foundStatus.toApi(user));
}
if (context.req.method === "DELETE") {
if (foundStatus.author.id !== user?.id) {
return errorResponse("Unauthorized", 401);
}
// TODO: Delete and redraft
await foundStatus.delete();
await user.federateToFollowers(
undoFederationRequest(user, foundStatus.getUri()),
);
return jsonResponse(await foundStatus.toApi(user), 200);
}
// TODO: Polls
const {
status: statusText,
content_type,
"poll[options]": options,
media_ids,
spoiler_text,
sensitive,
} = context.req.valid("form");
if (!(statusText || (media_ids && media_ids.length > 0))) {
return errorResponse(
"Status is required unless media is attached",
422,
const note = await Note.fromId(id, user?.id);
if (!note?.isViewableByUser(user)) {
return errorResponse("Record not found", 404);
}
switch (context.req.method) {
case "GET": {
return jsonResponse(await note.toApi(user));
}
case "DELETE": {
if (note.author.id !== user?.id) {
return errorResponse("Unauthorized", 401);
}
// TODO: Delete and redraft
await note.delete();
await user.federateToFollowers(
undoFederationRequest(user, note.getUri()),
);
return jsonResponse(await note.toApi(user), 200);
}
case "PUT": {
if (media_ids.length > 0) {
const foundAttachments =
await Attachment.fromIds(media_ids);
if (media_ids && media_ids.length > 0 && options) {
return errorResponse(
"Cannot attach poll to post with media",
422,
);
}
if (
config.filters.note_content.some((filter) =>
statusText?.match(filter),
)
) {
return errorResponse("Status contains blocked words", 422);
}
if (media_ids && media_ids.length > 0) {
const foundAttachments = await db.query.Attachments.findMany({
where: (attachment, { inArray }) =>
inArray(attachment.id, media_ids),
});
if (foundAttachments.length !== (media_ids ?? []).length) {
if (foundAttachments.length !== media_ids.length) {
return errorResponse("Invalid media IDs", 422);
}
}
const newNote = await foundStatus.updateFromData({
const newNote = await note.updateFromData({
content: statusText
? {
[content_type]: {
@ -172,10 +163,8 @@ export default (app: Hono) =>
mediaAttachments: media_ids,
});
if (!newNote) {
return errorResponse("Failed to update status", 500);
}
return jsonResponse(await newNote.toApi(user));
}
}
},
);

View file

@ -28,7 +28,9 @@ describe(meta.route, () => {
const response = await sendTestRequest(
new Request(new URL(meta.route, config.http.base_url), {
method: "POST",
body: new URLSearchParams(),
body: new URLSearchParams({
status: "Hello, world!",
}),
}),
);

View file

@ -6,8 +6,8 @@ import type { Hono } from "hono";
import ISO6391 from "iso-639-1";
import { z } from "zod";
import { federateNote } from "~/database/entities/status";
import { db } from "~/drizzle/db";
import { RolePermissions } from "~/drizzle/schema";
import { Attachment } from "~/packages/database-interface/attachment";
import { Note } from "~/packages/database-interface/note";
export const meta = applyConfig({
@ -26,18 +26,26 @@ export const meta = applyConfig({
});
export const schemas = {
form: z.object({
form: z
.object({
status: z
.string()
.max(config.validation.max_note_size)
.trim()
.refine(
(s) =>
!config.filters.note_content.some((filter) =>
s.match(filter),
),
"Status contains blocked words",
)
.optional(),
// TODO: Add regex to validate
content_type: z.string().optional().default("text/plain"),
media_ids: z
.array(z.string().uuid())
.max(config.validation.max_media_attachments)
.optional(),
.default([]),
spoiler_text: z.string().max(255).trim().optional(),
sensitive: z
.string()
@ -73,14 +81,26 @@ export const schemas = {
.enum(["public", "unlisted", "private", "direct"])
.optional()
.default("public"),
scheduled_at: z.string().optional().nullable(),
scheduled_at: z.coerce
.date()
.min(new Date(), "Scheduled time must be in the future")
.optional()
.nullable(),
local_only: z
.string()
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
.or(z.boolean())
.optional()
.default(false),
}),
})
.refine(
(obj) => obj.status || obj.media_ids.length > 0,
"Status is required unless media is attached",
)
.refine(
(obj) => !(obj.media_ids.length > 0 && obj["poll[options]"]),
"Cannot attach poll to media",
),
};
export default (app: Hono) =>
@ -100,10 +120,8 @@ export default (app: Hono) =>
const {
status,
media_ids,
"poll[options]": options,
in_reply_to_id,
quote_id,
scheduled_at,
sensitive,
spoiler_text,
visibility,
@ -111,69 +129,23 @@ export default (app: Hono) =>
local_only,
} = context.req.valid("form");
// Validate status
if (!(status || (media_ids && media_ids.length > 0))) {
return errorResponse(
"Status is required unless media is attached",
422,
);
}
if (media_ids && media_ids.length > 0 && options) {
// Disallow poll
return errorResponse("Cannot attach poll to media", 422);
}
if (scheduled_at) {
if (
Number.isNaN(new Date(scheduled_at).getTime()) ||
new Date(scheduled_at).getTime() < Date.now()
) {
return errorResponse(
"Scheduled time must be in the future",
422,
);
}
}
// Check if status body doesnt match filters
if (
config.filters.note_content.some((filter) =>
status?.match(filter),
)
) {
return errorResponse("Status contains blocked words", 422);
}
// Check if media attachments are all valid
if (media_ids && media_ids.length > 0) {
const foundAttachments = await db.query.Attachments.findMany({
where: (attachment, { inArray }) =>
inArray(attachment.id, media_ids),
}).catch(() => []);
if (media_ids.length > 0) {
const foundAttachments = await Attachment.fromIds(media_ids);
if (foundAttachments.length !== (media_ids ?? []).length) {
if (foundAttachments.length !== media_ids.length) {
return errorResponse("Invalid media IDs", 422);
}
}
// Check that in_reply_to_id and quote_id are real posts if provided
if (in_reply_to_id) {
const foundReply = await Note.fromId(in_reply_to_id);
if (!foundReply) {
return errorResponse(
"Invalid in_reply_to_id (not found)",
422,
);
}
if (in_reply_to_id && !(await Note.fromId(in_reply_to_id))) {
return errorResponse("Invalid in_reply_to_id (not found)", 422);
}
if (quote_id) {
const foundQuote = await Note.fromId(quote_id);
if (!foundQuote) {
if (quote_id && !(await Note.fromId(quote_id))) {
return errorResponse("Invalid quote_id (not found)", 422);
}
}
const newNote = await Note.fromData({
author: user,
@ -191,10 +163,6 @@ export default (app: Hono) =>
application: application ?? undefined,
});
if (!newNote) {
return errorResponse("Failed to create status", 500);
}
if (!local_only) {
await federateNote(newNote);
}

View file

@ -4,9 +4,7 @@ import { zValidator } from "@hono/zod-validator";
import { encode } from "blurhash";
import { config } from "config-manager";
import type { Hono } from "hono";
import type { MediaBackend } from "media-manager";
import { MediaBackendType } from "media-manager";
import { LocalMediaBackend, S3MediaBackend } from "media-manager";
import { MediaBackend } from "media-manager";
import sharp from "sharp";
import { z } from "zod";
import { getUrl } from "~/database/entities/attachment";
@ -101,19 +99,10 @@ export default (app: Hono) =>
let url = "";
let mediaManager: MediaBackend;
switch (config.media.backend as MediaBackendType) {
case MediaBackendType.Local:
mediaManager = new LocalMediaBackend(config);
break;
case MediaBackendType.S3:
mediaManager = new S3MediaBackend(config);
break;
default:
// TODO: Replace with logger
throw new Error("Invalid media backend");
}
const mediaManager = await MediaBackend.fromBackendType(
config.media.backend,
config,
);
const { path } = await mediaManager.addFile(file);