Newer
Older
percord / src / api / routes / webhooks / #webhook_id / index.ts
import { route } from "@spacebar/api";
import {
    Config,
    DiscordApiErrors,
    getPermission,
    Webhook,
    WebhooksUpdateEvent,
    emitEvent,
    Channel,
    handleFile,
    ValidateName,
    Message,
    MessageDeleteBulkEvent,
} from "@spacebar/util";
import { Request, Response, Router } from "express";
import { HTTPError } from "lambert-server";
import { WebhookUpdateSchema } from "@spacebar/schemas";
import { In } from "typeorm";
const router = Router({ mergeParams: true });

router.get(
    "/",
    route({
        description: "Returns a webhook object for the given id. Requires the MANAGE_WEBHOOKS permission or to be the owner of the webhook.",
        responses: {
            200: {
                body: "APIWebhook",
            },
            404: {},
        },
    }),
    async (req: Request, res: Response) => {
        const { webhook_id } = req.params as { [key: string]: string };
        const webhook = await Webhook.findOneOrFail({
            where: { id: webhook_id },
            relations: { user: true, channel: true, source_channel: true, guild: true, source_guild: true, application: true },
        });

        if (webhook.guild_id) {
            const permission = await getPermission(req.user_id, webhook.guild_id);

            if (!permission.has("MANAGE_WEBHOOKS")) throw DiscordApiErrors.UNKNOWN_WEBHOOK;
        } else if (webhook.user_id != req.user_id) throw DiscordApiErrors.UNKNOWN_WEBHOOK;

        return res.json({
            ...webhook,
            url: Config.get().api.endpointPublic + "/webhooks/" + webhook.id + "/" + webhook.token,
        });
    },
);

router.delete(
    "/",
    route({
        responses: {
            204: {},
            400: {
                body: "APIErrorResponse",
            },
            404: {},
        },
    }),
    async (req: Request, res: Response) => {
        const { webhook_id } = req.params as { [key: string]: string };

        const webhook = await Webhook.findOneOrFail({
            where: { id: webhook_id },
            relations: { user: true, channel: true, source_channel: true, guild: true, source_guild: true, application: true },
        });

        if (webhook.guild_id) {
            const permission = await getPermission(req.user_id, webhook.guild_id);

            if (!permission.has("MANAGE_WEBHOOKS")) throw DiscordApiErrors.UNKNOWN_WEBHOOK;
        } else if (webhook.user_id != req.user_id) throw DiscordApiErrors.UNKNOWN_WEBHOOK;

        const channel_id = webhook.channel_id;
        const channel = await Channel.findOneOrFail({ where: { id: channel_id } });

        // work around foreign key constraint
        while (await Message.count({ where: { webhook_id, channel_id } })) {
            const ids = (await Message.find({ where: { webhook_id, channel_id }, select: { id: true }, order: { id: "asc" }, take: 100 })).map((x) => x.id);
            await Message.delete({ id: In(ids) });
            await emitEvent({
                event: "MESSAGE_DELETE_BULK",
                channel_id,
                origin: "webhook delete",
                data: {
                    channel_id,
                    guild_id: channel.guild_id,
                    ids,
                },
            } satisfies MessageDeleteBulkEvent);
        }

        await Webhook.delete({ id: webhook_id });

        await emitEvent({
            event: "WEBHOOKS_UPDATE",
            channel_id,
            data: {
                channel_id,
                guild_id: webhook.guild_id!, // TODO: is this even the right fix?
            },
        } satisfies WebhooksUpdateEvent);

        res.sendStatus(204);
    },
);

router.patch(
    "/",
    route({
        requestBody: "WebhookUpdateSchema",
        responses: {
            200: {
                body: "WebhookCreateResponse",
            },
            400: {
                body: "APIErrorResponse",
            },
            403: {},
            404: {},
        },
    }),
    async (req: Request, res: Response) => {
        const { webhook_id } = req.params as { [key: string]: string };
        const body = req.body as WebhookUpdateSchema;

        const webhook = await Webhook.findOneOrFail({
            where: { id: webhook_id },
            relations: { user: true, channel: true, source_channel: true, guild: true, source_guild: true, application: true },
        });

        if (webhook.guild_id) {
            const permission = await getPermission(req.user_id, webhook.guild_id);

            if (!permission.has("MANAGE_WEBHOOKS")) throw DiscordApiErrors.UNKNOWN_WEBHOOK;
        } else if (webhook.user_id != req.user_id) throw DiscordApiErrors.UNKNOWN_WEBHOOK;

        if (!body.name && !body.avatar && !body.channel_id) {
            throw new HTTPError("Empty webhook updates are not allowed", 50006);
        }

        if (body.avatar) body.avatar = await handleFile(`/avatars/${webhook_id}`, body.avatar as string);

        if (body.name) {
            ValidateName(body.name);
        }

        const channel_id = body.channel_id || webhook.channel_id;
        webhook.assign(body);

        if (body.channel_id)
            webhook.assign({
                channel: await Channel.findOneOrFail({
                    where: { id: channel_id },
                }),
            });

        await Promise.all([
            webhook.save(),
            emitEvent({
                event: "WEBHOOKS_UPDATE",
                channel_id,
                data: {
                    channel_id,
                    guild_id: webhook.guild_id!, //TODO: is this even the right fix?
                },
            } satisfies WebhooksUpdateEvent),
        ]);

        res.json(webhook);
    },
);

export default router;