mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 22:09:16 +01:00
feat(api): ✨ Add user emoji upload capabilities
This commit is contained in:
parent
980f4c8021
commit
da2520e60e
36 changed files with 5440 additions and 3340 deletions
162
server/api/api/v1/custom_emojis/index.test.ts
Normal file
162
server/api/api/v1/custom_emojis/index.test.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { config } from "config-manager";
|
||||
import { inArray } from "drizzle-orm";
|
||||
import { db } from "~drizzle/db";
|
||||
import { Emojis } from "~drizzle/schema";
|
||||
import { getTestUsers, sendTestRequest } from "~tests/utils";
|
||||
import { meta } from "./index";
|
||||
|
||||
const { users, tokens, deleteUsers } = await getTestUsers(2);
|
||||
|
||||
// Make user 2 an admin
|
||||
beforeAll(async () => {
|
||||
await users[1].update({ isAdmin: true });
|
||||
|
||||
// Upload one emoji as admin, then one as each user
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL("/api/v1/emojis", config.http.base_url), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[1].accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
shortcode: "test1",
|
||||
element: "https://cdn.lysand.org/logo.webp",
|
||||
global: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
await sendTestRequest(
|
||||
new Request(new URL("/api/v1/emojis", config.http.base_url), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
shortcode: "test2",
|
||||
element: "https://cdn.lysand.org/logo.webp",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
await sendTestRequest(
|
||||
new Request(new URL("/api/v1/emojis", config.http.base_url), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[1].accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
shortcode: "test3",
|
||||
element: "https://cdn.lysand.org/logo.webp",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await deleteUsers();
|
||||
|
||||
await db
|
||||
.delete(Emojis)
|
||||
.where(inArray(Emojis.shortcode, ["test1", "test2", "test3"]));
|
||||
});
|
||||
|
||||
describe(meta.route, () => {
|
||||
test("should return all global emojis", async () => {
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[1].accessToken}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toBe("application/json");
|
||||
|
||||
const emojis = await response.json();
|
||||
|
||||
// Should contain test1 and test2, but not test2
|
||||
expect(emojis).toContainEqual(
|
||||
expect.objectContaining({
|
||||
shortcode: "test1",
|
||||
}),
|
||||
);
|
||||
expect(emojis).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
shortcode: "test2",
|
||||
}),
|
||||
);
|
||||
expect(emojis).toContainEqual(
|
||||
expect.objectContaining({
|
||||
shortcode: "test3",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("should return all user emojis", async () => {
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toBe("application/json");
|
||||
|
||||
const emojis = await response.json();
|
||||
|
||||
// Should contain test1 and test2, but not test3
|
||||
expect(emojis).toContainEqual(
|
||||
expect.objectContaining({
|
||||
shortcode: "test1",
|
||||
}),
|
||||
);
|
||||
expect(emojis).toContainEqual(
|
||||
expect.objectContaining({
|
||||
shortcode: "test2",
|
||||
}),
|
||||
);
|
||||
expect(emojis).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
shortcode: "test3",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("should return all global emojis when signed out", async () => {
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url)),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get("content-type")).toBe("application/json");
|
||||
|
||||
const emojis = await response.json();
|
||||
|
||||
// Should contain test1, but not test2 or test3
|
||||
expect(emojis).toContainEqual(
|
||||
expect.objectContaining({
|
||||
shortcode: "test1",
|
||||
}),
|
||||
);
|
||||
expect(emojis).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
shortcode: "test2",
|
||||
}),
|
||||
);
|
||||
expect(emojis).not.toContainEqual(
|
||||
expect.objectContaining({
|
||||
shortcode: "test3",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { applyConfig } from "@api";
|
||||
import { applyConfig, auth } from "@api";
|
||||
import { jsonResponse } from "@response";
|
||||
import type { Hono } from "hono";
|
||||
import { emojiToAPI } from "~database/entities/Emoji";
|
||||
|
|
@ -17,15 +17,29 @@ export const meta = applyConfig({
|
|||
});
|
||||
|
||||
export default (app: Hono) =>
|
||||
app.on(meta.allowedMethods, meta.route, async () => {
|
||||
const emojis = await db.query.Emojis.findMany({
|
||||
where: (emoji, { isNull }) => isNull(emoji.instanceId),
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
});
|
||||
app.on(
|
||||
meta.allowedMethods,
|
||||
meta.route,
|
||||
auth(meta.auth),
|
||||
async (context) => {
|
||||
const { user } = context.req.valid("header");
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(emojis.map((emoji) => emojiToAPI(emoji))),
|
||||
);
|
||||
});
|
||||
const emojis = await db.query.Emojis.findMany({
|
||||
where: (emoji, { isNull, and, eq, or }) =>
|
||||
and(
|
||||
isNull(emoji.instanceId),
|
||||
or(
|
||||
isNull(emoji.ownerId),
|
||||
user ? eq(emoji.ownerId, user.id) : undefined,
|
||||
),
|
||||
),
|
||||
with: {
|
||||
instance: true,
|
||||
},
|
||||
});
|
||||
|
||||
return jsonResponse(
|
||||
await Promise.all(emojis.map((emoji) => emojiToAPI(emoji))),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { config } from "config-manager";
|
||||
import { inArray } from "drizzle-orm";
|
||||
import { db } from "~drizzle/db";
|
||||
import { Emojis } from "~drizzle/schema";
|
||||
import { getTestUsers, sendTestRequest } from "~tests/utils";
|
||||
import { meta } from "./index";
|
||||
|
||||
|
|
@ -21,6 +24,7 @@ beforeAll(async () => {
|
|||
body: JSON.stringify({
|
||||
shortcode: "test",
|
||||
element: "https://cdn.lysand.org/logo.webp",
|
||||
global: true,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
|
@ -32,6 +36,10 @@ beforeAll(async () => {
|
|||
|
||||
afterAll(async () => {
|
||||
await deleteUsers();
|
||||
|
||||
await db
|
||||
.delete(Emojis)
|
||||
.where(inArray(Emojis.shortcode, ["test", "test2", "test3", "test4"]));
|
||||
});
|
||||
|
||||
// /api/v1/emojis/:id (PATCH, DELETE, GET)
|
||||
|
|
@ -71,15 +79,19 @@ describe(meta.route, () => {
|
|||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
test("should return 403 if not an admin", async () => {
|
||||
test("should not work if the user is trying to update an emoji they don't own", async () => {
|
||||
const response = await sendTestRequest(
|
||||
new Request(
|
||||
new URL(meta.route.replace(":id", id), config.http.base_url),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "GET",
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({
|
||||
shortcode: "test2",
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -150,6 +162,43 @@ describe(meta.route, () => {
|
|||
expect(emoji.shortcode).toBe("test2");
|
||||
});
|
||||
|
||||
test("should update the emoji to be non-global", async () => {
|
||||
const response = await sendTestRequest(
|
||||
new Request(
|
||||
new URL(meta.route.replace(":id", id), config.http.base_url),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[1].accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({
|
||||
global: false,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
|
||||
// Check if the other user can see it
|
||||
const response2 = await sendTestRequest(
|
||||
new Request(
|
||||
new URL("/api/v1/custom_emojis", config.http.base_url),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||
},
|
||||
method: "GET",
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
expect(response2.ok).toBe(true);
|
||||
const emojis = await response2.json();
|
||||
expect(emojis).not.toContainEqual(expect.objectContaining({ id: id }));
|
||||
});
|
||||
|
||||
test("should delete the emoji", async () => {
|
||||
const response = await sendTestRequest(
|
||||
new Request(
|
||||
|
|
|
|||
|
|
@ -52,7 +52,13 @@ export const schemas = {
|
|||
.max(2000)
|
||||
.url()
|
||||
.or(z.instanceof(File)),
|
||||
category: z.string().max(64).optional(),
|
||||
alt: z.string().max(1000).optional(),
|
||||
global: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.or(z.boolean())
|
||||
.optional(),
|
||||
})
|
||||
.partial()
|
||||
.optional(),
|
||||
|
|
@ -74,16 +80,6 @@ export default (app: Hono) =>
|
|||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
// Check if user is admin
|
||||
if (!user.getUser().isAdmin) {
|
||||
return jsonResponse(
|
||||
{
|
||||
error: "You do not have permission to modify emojis (must be an administrator)",
|
||||
},
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
const emoji = await db.query.Emojis.findFirst({
|
||||
where: (emoji, { eq }) => eq(emoji.id, id),
|
||||
with: {
|
||||
|
|
@ -93,6 +89,19 @@ export default (app: Hono) =>
|
|||
|
||||
if (!emoji) return errorResponse("Emoji not found", 404);
|
||||
|
||||
// Check if user is admin
|
||||
if (
|
||||
!user.getUser().isAdmin &&
|
||||
emoji.ownerId !== user.getUser().id
|
||||
) {
|
||||
return jsonResponse(
|
||||
{
|
||||
error: "You do not have permission to modify this emoji, as it is either global or not owned by you",
|
||||
},
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
switch (context.req.method) {
|
||||
case "DELETE": {
|
||||
await db.delete(Emojis).where(eq(Emojis.id, id));
|
||||
|
|
@ -105,18 +114,31 @@ export default (app: Hono) =>
|
|||
|
||||
if (!form) {
|
||||
return errorResponse(
|
||||
"Invalid form data (must supply shortcode and/or element and/or alt)",
|
||||
"Invalid form data (must supply at least one of: shortcode, element, alt, category)",
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
if (!form.shortcode && !form.element && !form.alt) {
|
||||
if (
|
||||
!form.shortcode &&
|
||||
!form.element &&
|
||||
!form.alt &&
|
||||
!form.category &&
|
||||
form.global === undefined
|
||||
) {
|
||||
return errorResponse(
|
||||
"Invalid form data (must supply shortcode and/or element and/or alt)",
|
||||
"Invalid form data (must supply shortcode and/or element and/or alt and/or global)",
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
||||
if (!user.getUser().isAdmin && form.global) {
|
||||
return errorResponse(
|
||||
"Only administrators can make an emoji global or not",
|
||||
401,
|
||||
);
|
||||
}
|
||||
|
||||
if (form.element) {
|
||||
// Check of emoji is an image
|
||||
const contentType =
|
||||
|
|
@ -159,7 +181,9 @@ export default (app: Hono) =>
|
|||
shortcode: form.shortcode ?? emoji.shortcode,
|
||||
alt: form.alt ?? emoji.alt,
|
||||
url: emoji.url,
|
||||
ownerId: form.global ? null : user.id,
|
||||
contentType: emoji.contentType,
|
||||
category: form.category ?? emoji.category,
|
||||
})
|
||||
.where(eq(Emojis.id, id))
|
||||
.returning()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Emojis } from "~drizzle/schema";
|
|||
import { getTestUsers, sendTestRequest } from "~tests/utils";
|
||||
import { meta } from "./index";
|
||||
|
||||
const { users, tokens, deleteUsers } = await getTestUsers(2);
|
||||
const { users, tokens, deleteUsers } = await getTestUsers(3);
|
||||
|
||||
// Make user 2 an admin
|
||||
beforeAll(async () => {
|
||||
|
|
@ -18,7 +18,7 @@ afterAll(async () => {
|
|||
|
||||
await db
|
||||
.delete(Emojis)
|
||||
.where(inArray(Emojis.shortcode, ["test1", "test2", "test3"]));
|
||||
.where(inArray(Emojis.shortcode, ["test1", "test2", "test3", "test4"]));
|
||||
});
|
||||
|
||||
describe(meta.route, () => {
|
||||
|
|
@ -39,99 +39,146 @@ describe(meta.route, () => {
|
|||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
test("should return 403 if not an admin", async () => {
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
shortcode: "test",
|
||||
element: "https://cdn.lysand.org/logo.webp",
|
||||
describe("Admin tests", () => {
|
||||
test("should upload a file and create an emoji", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("shortcode", "test1");
|
||||
formData.append("element", Bun.file("tests/test-image.webp"));
|
||||
formData.append("global", "true");
|
||||
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[1].accessToken}`,
|
||||
},
|
||||
body: formData,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
);
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
expect(response.ok).toBe(true);
|
||||
const emoji = await response.json();
|
||||
expect(emoji.shortcode).toBe("test1");
|
||||
expect(emoji.url).toContain("/media/proxy");
|
||||
});
|
||||
|
||||
test("should upload a file and create an emoji", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("shortcode", "test1");
|
||||
formData.append("element", Bun.file("tests/test-image.webp"));
|
||||
test("should try to upload a non-image", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("shortcode", "test2");
|
||||
formData.append("element", new File(["test"], "test.txt"));
|
||||
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[1].accessToken}`,
|
||||
},
|
||||
body: formData,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
const emoji = await response.json();
|
||||
expect(emoji.shortcode).toBe("test1");
|
||||
expect(emoji.url).toContain("/media/proxy");
|
||||
});
|
||||
|
||||
test("should try to upload a non-image", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("shortcode", "test2");
|
||||
formData.append("element", new File(["test"], "test.txt"));
|
||||
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[1].accessToken}`,
|
||||
},
|
||||
body: formData,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
});
|
||||
|
||||
test("should upload an emoji by url", async () => {
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[1].accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
shortcode: "test3",
|
||||
element: "https://cdn.lysand.org/logo.webp",
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[1].accessToken}`,
|
||||
},
|
||||
body: formData,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
const emoji = await response.json();
|
||||
expect(emoji.shortcode).toBe("test3");
|
||||
expect(emoji.url).toContain("/media/proxy/");
|
||||
expect(response.status).toBe(422);
|
||||
});
|
||||
|
||||
test("should upload an emoji by url", async () => {
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[1].accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
shortcode: "test3",
|
||||
element: "https://cdn.lysand.org/logo.webp",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
const emoji = await response.json();
|
||||
expect(emoji.shortcode).toBe("test3");
|
||||
expect(emoji.url).toContain("/media/proxy/");
|
||||
});
|
||||
|
||||
test("should fail when uploading an already existing emoji", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("shortcode", "test1");
|
||||
formData.append("element", Bun.file("tests/test-image.webp"));
|
||||
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[1].accessToken}`,
|
||||
},
|
||||
body: formData,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
});
|
||||
});
|
||||
|
||||
test("should fail when uploading an already existing emoji", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("shortcode", "test1");
|
||||
formData.append("element", Bun.file("tests/test-image.webp"));
|
||||
describe("User tests", () => {
|
||||
test("should upload a file and create an emoji", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("shortcode", "test4");
|
||||
formData.append("element", Bun.file("tests/test-image.webp"));
|
||||
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[1].accessToken}`,
|
||||
},
|
||||
body: formData,
|
||||
}),
|
||||
);
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||
},
|
||||
body: formData,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
expect(response.ok).toBe(true);
|
||||
const emoji = await response.json();
|
||||
expect(emoji.shortcode).toBe("test4");
|
||||
expect(emoji.url).toContain("/media/proxy/");
|
||||
});
|
||||
|
||||
test("should fail when uploading an already existing global emoji", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("shortcode", "test1");
|
||||
formData.append("element", Bun.file("tests/test-image.webp"));
|
||||
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[0].accessToken}`,
|
||||
},
|
||||
body: formData,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
});
|
||||
|
||||
test("should create an emoji as another user with the same shortcode", async () => {
|
||||
const formData = new FormData();
|
||||
formData.append("shortcode", "test4");
|
||||
formData.append("element", Bun.file("tests/test-image.webp"));
|
||||
|
||||
const response = await sendTestRequest(
|
||||
new Request(new URL(meta.route, config.http.base_url), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${tokens[2].accessToken}`,
|
||||
},
|
||||
body: formData,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(response.ok).toBe(true);
|
||||
const emoji = await response.json();
|
||||
expect(emoji.shortcode).toBe("test4");
|
||||
expect(emoji.url).toContain("/media/proxy/");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -47,7 +47,13 @@ export const schemas = {
|
|||
.max(2000)
|
||||
.url()
|
||||
.or(z.instanceof(File)),
|
||||
category: z.string().max(64).optional(),
|
||||
alt: z.string().max(1000).optional(),
|
||||
global: z
|
||||
.string()
|
||||
.transform((v) => ["true", "1", "on"].includes(v.toLowerCase()))
|
||||
.or(z.boolean())
|
||||
.optional(),
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
@ -59,31 +65,34 @@ export default (app: Hono) =>
|
|||
zValidator("form", schemas.form, handleZodError),
|
||||
auth(meta.auth),
|
||||
async (context) => {
|
||||
const { shortcode, element, alt } = context.req.valid("form");
|
||||
const { shortcode, element, alt, global, category } =
|
||||
context.req.valid("form");
|
||||
const { user } = context.req.valid("header");
|
||||
|
||||
if (!user) {
|
||||
return errorResponse("Unauthorized", 401);
|
||||
}
|
||||
|
||||
// Check if user is admin
|
||||
if (!user.getUser().isAdmin) {
|
||||
return jsonResponse(
|
||||
{
|
||||
error: "You do not have permission to add emojis (must be an administrator)",
|
||||
},
|
||||
403,
|
||||
if (!user.getUser().isAdmin && global) {
|
||||
return errorResponse(
|
||||
"Only administrators can upload global emojis",
|
||||
401,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if emoji already exists
|
||||
const existing = await db.query.Emojis.findFirst({
|
||||
where: (emoji, { eq }) => eq(emoji.shortcode, shortcode),
|
||||
where: (emoji, { eq, and, isNull, or }) =>
|
||||
and(
|
||||
eq(emoji.shortcode, shortcode),
|
||||
isNull(emoji.instanceId),
|
||||
or(eq(emoji.ownerId, user.id), isNull(emoji.ownerId)),
|
||||
),
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return errorResponse(
|
||||
`An emoji with the shortcode ${shortcode} already exists.`,
|
||||
`An emoji with the shortcode ${shortcode} already exists, either owned by you or global.`,
|
||||
422,
|
||||
);
|
||||
}
|
||||
|
|
@ -123,6 +132,8 @@ export default (app: Hono) =>
|
|||
shortcode,
|
||||
url: getUrl(url, config),
|
||||
visibleInPicker: true,
|
||||
ownerId: global ? null : user.id,
|
||||
category,
|
||||
contentType,
|
||||
alt,
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue