From df2b83ac158be1e7233d8edce59033c15c193599 Mon Sep 17 00:00:00 2001 From: Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> Date: Tue, 14 Sep 2021 22:15:55 +0200 Subject: :construction: webhook --- api/src/middlewares/Authentication.ts | 4 +- api/src/middlewares/ErrorHandler.ts | 7 +- api/src/routes/discoverable-guilds.ts | 2 +- api/src/routes/guilds/#guild_id/integrations.ts | 10 +++ api/src/routes/template.ts.disabled | 2 +- api/src/routes/webhooks/#webhook_id/index.ts | 89 +++++++++++++++++++++++++ api/src/util/route.ts | 8 ++- 7 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 api/src/routes/guilds/#guild_id/integrations.ts create mode 100644 api/src/routes/webhooks/#webhook_id/index.ts (limited to 'api/src') diff --git a/api/src/middlewares/Authentication.ts b/api/src/middlewares/Authentication.ts index a300c786..32307f42 100644 --- a/api/src/middlewares/Authentication.ts +++ b/api/src/middlewares/Authentication.ts @@ -5,11 +5,11 @@ import { checkToken, Config } from "@fosscord/util"; export const NO_AUTHORIZATION_ROUTES = [ "/auth/login", "/auth/register", - "/webhooks/", "/ping", "/gateway", "/experiments", - /\/guilds\/\d+\/widget\.(json|png)/ + /\/guilds\/\d+\/widget\.(json|png)/, + /\/webhooks\/\d+\/\w+/ // only exclude webhook calls with webhook token ]; export const API_PREFIX = /^\/api(\/v\d+)?/; diff --git a/api/src/middlewares/ErrorHandler.ts b/api/src/middlewares/ErrorHandler.ts index d288f3fb..338da8d5 100644 --- a/api/src/middlewares/ErrorHandler.ts +++ b/api/src/middlewares/ErrorHandler.ts @@ -1,9 +1,10 @@ import { NextFunction, Request, Response } from "express"; import { HTTPError } from "lambert-server"; -import { EntityNotFoundError } from "typeorm"; import { FieldError } from "@fosscord/api"; import { ApiError } from "@fosscord/util"; +const EntityNotFoundErrorRegex = /"(\w+)"/; + export function ErrorHandler(error: Error, req: Request, res: Response, next: NextFunction) { if (!error) return next(); @@ -18,8 +19,8 @@ export function ErrorHandler(error: Error, req: Request, res: Response, next: Ne code = error.code; message = error.message; httpcode = error.httpStatus; - } else if (error instanceof EntityNotFoundError) { - message = `${(error as any).stringifyTarget || "Item"} could not be found`; + } else if (error.name === "EntityNotFoundError") { + message = `${error.message.match(EntityNotFoundErrorRegex)?.[1] || "Item"} could not be found`; code = 404; } else if (error instanceof FieldError) { code = Number(error.code); diff --git a/api/src/routes/discoverable-guilds.ts b/api/src/routes/discoverable-guilds.ts index f667eb2a..71789123 100644 --- a/api/src/routes/discoverable-guilds.ts +++ b/api/src/routes/discoverable-guilds.ts @@ -10,7 +10,7 @@ router.get("/", route({}), async (req: Request, res: Response) => { // ! this only works using SQL querys // TODO: implement this with default typeorm query // const guilds = await Guild.find({ where: { features: "DISCOVERABLE" } }); //, take: Math.abs(Number(limit)) }); - const guilds = await Guild.find({ where: `"features" LIKE 'COMMUNITY'`, take: Math.abs(Number(limit)) }); + const guilds = await Guild.find({ where: `"features" LIKE 'COMMUNITY'`, take: Math.abs(Number(limit) || 50) }); res.send({ guilds: guilds }); }); diff --git a/api/src/routes/guilds/#guild_id/integrations.ts b/api/src/routes/guilds/#guild_id/integrations.ts new file mode 100644 index 00000000..f6b8e99d --- /dev/null +++ b/api/src/routes/guilds/#guild_id/integrations.ts @@ -0,0 +1,10 @@ +import { route } from "@fosscord/api"; +import { Router, Request, Response } from "express"; +const router = Router(); + +router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { + // TODO: integrations (followed channels, youtube, twitch) + res.send([]); +}); + +export default router; diff --git a/api/src/routes/template.ts.disabled b/api/src/routes/template.ts.disabled index ad785f10..524e981b 100644 --- a/api/src/routes/template.ts.disabled +++ b/api/src/routes/template.ts.disabled @@ -4,7 +4,7 @@ import { Router, Request, Response } from "express"; const router = Router(); router.get("/", async (req: Request, res: Response) => { - res.send({}); + res.json({}); }); export default router; diff --git a/api/src/routes/webhooks/#webhook_id/index.ts b/api/src/routes/webhooks/#webhook_id/index.ts new file mode 100644 index 00000000..e9b40ebf --- /dev/null +++ b/api/src/routes/webhooks/#webhook_id/index.ts @@ -0,0 +1,89 @@ +import { Channel, Config, emitEvent, JWTOptions, Webhook, WebhooksUpdateEvent } from "@fosscord/util"; +import { route, Authentication, handleFile } from "@fosscord/api"; +import { Router, Request, Response, NextFunction } from "express"; +import jwt from "jsonwebtoken"; +import { HTTPError } from "lambert-server"; +const router = Router(); + +export interface WebhookModifySchema { + name?: string; + avatar?: string; + // channel_id?: string; // TODO +} + +function validateWebhookToken(req: Request, res: Response, next: NextFunction) { + const { jwtSecret } = Config.get().security; + + jwt.verify(req.params.token, jwtSecret, JWTOptions, async (err, decoded: any) => { + if (err) return next(new HTTPError("Invalid Token", 401)); + next(); + }); +} + +router.get("/", route({}), async (req: Request, res: Response) => { + res.json(await Webhook.findOneOrFail({ id: req.params.webhook_id })); +}); + +router.get("/:token", route({}), validateWebhookToken, async (req: Request, res: Response) => { + res.json(await Webhook.findOneOrFail({ id: req.params.webhook_id })); +}); + +router.patch("/", route({ body: "WebhookModifySchema", permission: "MANAGE_WEBHOOKS" }), (req: Request, res: Response) => { + return updateWebhook(req, res); +}); + +router.patch("/:token", route({ body: "WebhookModifySchema" }), validateWebhookToken, (req: Request, res: Response) => { + return updateWebhook(req, res); +}); + +async function updateWebhook(req: Request, res: Response) { + const webhook = await Webhook.findOneOrFail({ id: req.params.webhook_id }); + if (req.body.channel_id) await Channel.findOneOrFail({ id: req.body.channel_id, guild_id: webhook.guild_id }); + + webhook.assign({ + ...req.body, + avatar: await handleFile(`/icons/${req.params.webhook_id}`, req.body.avatar) + }); + + await Promise.all([ + emitEvent({ + event: "WEBHOOKS_UPDATE", + channel_id: webhook.channel_id, + data: { + channel_id: webhook.channel_id, + guild_id: webhook.guild_id + } + } as WebhooksUpdateEvent), + webhook.save() + ]); + + res.json(webhook); +} + +router.delete("/", route({ permission: "MANAGE_WEBHOOKS" }), async (req: Request, res: Response) => { + return deleteWebhook(req, res); +}); + +router.delete("/:token", route({}), validateWebhookToken, (req: Request, res: Response) => { + return deleteWebhook(req, res); +}); + +async function deleteWebhook(req: Request, res: Response) { + const webhook = await Webhook.findOneOrFail({ id: req.params.webhook_id }); + + await Promise.all([ + emitEvent({ + event: "WEBHOOKS_UPDATE", + channel_id: webhook.channel_id, + data: { + channel_id: webhook.channel_id, + guild_id: webhook.guild_id + } + } as WebhooksUpdateEvent), + webhook.remove() + ]); + + res.sendStatus(204); +} + +export default router; diff --git a/api/src/util/route.ts b/api/src/util/route.ts index 6cd8f622..1e2beb5d 100644 --- a/api/src/util/route.ts +++ b/api/src/util/route.ts @@ -1,4 +1,4 @@ -import { DiscordApiErrors, Event, EventData, getPermission, PermissionResolvable, Permissions } from "@fosscord/util"; +import { DiscordApiErrors, Event, EventData, getPermission, PermissionResolvable, Permissions, Webhook } from "@fosscord/util"; import { NextFunction, Request, Response } from "express"; import fs from "fs"; import path from "path"; @@ -54,9 +54,13 @@ export function route(opts: RouteOptions) { return async (req: Request, res: Response, next: NextFunction) => { if (opts.permission) { const required = new Permissions(opts.permission); + if (req.params.webhook_id) { + const webhook = await Webhook.findOneOrFail({ id: req.params.webhook_id }); + req.params.channel_id = webhook.channel_id; + req.params.guild_id = webhook.guild_id; + } const permission = await getPermission(req.user_id, req.params.guild_id, req.params.channel_id); - // bitfield comparison: check if user lacks certain permission if (!permission.has(required)) { throw DiscordApiErrors.MISSING_PERMISSIONS.withParams(opts.permission as string); } -- cgit 1.5.1