summary refs log tree commit diff
path: root/api
diff options
context:
space:
mode:
authorFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-09-14 22:15:55 +0200
committerFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-09-14 22:15:55 +0200
commitf691aa4c5aa47c8a8085c7b01912a1c403ce8732 (patch)
tree532673ee0955f5eec2c8a33c7abad8ed64dc9a48 /api
parentfix #129 (diff)
downloadserver-f691aa4c5aa47c8a8085c7b01912a1c403ce8732.tar.xz
:construction: webhook
Diffstat (limited to 'api')
-rw-r--r--api/assets/schemas.json255
-rw-r--r--api/client_test/index.html2
-rw-r--r--api/src/middlewares/Authentication.ts4
-rw-r--r--api/src/middlewares/ErrorHandler.ts7
-rw-r--r--api/src/routes/discoverable-guilds.ts2
-rw-r--r--api/src/routes/guilds/#guild_id/integrations.ts10
-rw-r--r--api/src/routes/template.ts.disabled2
-rw-r--r--api/src/routes/webhooks/#webhook_id/index.ts89
-rw-r--r--api/src/util/route.ts8
9 files changed, 365 insertions, 14 deletions
diff --git a/api/assets/schemas.json b/api/assets/schemas.json
index 9c34f968..88558cfa 100644
--- a/api/assets/schemas.json
+++ b/api/assets/schemas.json
@@ -1770,10 +1770,6 @@
             }
         },
         "additionalProperties": false,
-        "required": [
-            "avatar",
-            "name"
-        ],
         "definitions": {
             "ChannelType": {
                 "enum": [
@@ -7446,5 +7442,256 @@
             }
         },
         "$schema": "http://json-schema.org/draft-07/schema#"
+    },
+    "WebhookModifySchema": {
+        "type": "object",
+        "properties": {
+            "name": {
+                "type": "string"
+            },
+            "avatar": {
+                "type": "string"
+            }
+        },
+        "additionalProperties": false,
+        "definitions": {
+            "ChannelType": {
+                "enum": [
+                    0,
+                    1,
+                    10,
+                    11,
+                    12,
+                    13,
+                    2,
+                    3,
+                    4,
+                    5,
+                    6
+                ],
+                "type": "number"
+            },
+            "ChannelPermissionOverwriteType": {
+                "enum": [
+                    0,
+                    1
+                ],
+                "type": "number"
+            },
+            "Embed": {
+                "type": "object",
+                "properties": {
+                    "title": {
+                        "type": "string"
+                    },
+                    "type": {
+                        "enum": [
+                            "article",
+                            "gifv",
+                            "image",
+                            "link",
+                            "rich",
+                            "video"
+                        ],
+                        "type": "string"
+                    },
+                    "description": {
+                        "type": "string"
+                    },
+                    "url": {
+                        "type": "string"
+                    },
+                    "timestamp": {
+                        "type": "string",
+                        "format": "date-time"
+                    },
+                    "color": {
+                        "type": "integer"
+                    },
+                    "footer": {
+                        "type": "object",
+                        "properties": {
+                            "text": {
+                                "type": "string"
+                            },
+                            "icon_url": {
+                                "type": "string"
+                            },
+                            "proxy_icon_url": {
+                                "type": "string"
+                            }
+                        },
+                        "additionalProperties": false,
+                        "required": [
+                            "text"
+                        ]
+                    },
+                    "image": {
+                        "$ref": "#/definitions/EmbedImage"
+                    },
+                    "thumbnail": {
+                        "$ref": "#/definitions/EmbedImage"
+                    },
+                    "video": {
+                        "$ref": "#/definitions/EmbedImage"
+                    },
+                    "provider": {
+                        "type": "object",
+                        "properties": {
+                            "name": {
+                                "type": "string"
+                            },
+                            "url": {
+                                "type": "string"
+                            }
+                        },
+                        "additionalProperties": false
+                    },
+                    "author": {
+                        "type": "object",
+                        "properties": {
+                            "name": {
+                                "type": "string"
+                            },
+                            "url": {
+                                "type": "string"
+                            },
+                            "icon_url": {
+                                "type": "string"
+                            },
+                            "proxy_icon_url": {
+                                "type": "string"
+                            }
+                        },
+                        "additionalProperties": false
+                    },
+                    "fields": {
+                        "type": "array",
+                        "items": {
+                            "type": "object",
+                            "properties": {
+                                "name": {
+                                    "type": "string"
+                                },
+                                "value": {
+                                    "type": "string"
+                                },
+                                "inline": {
+                                    "type": "boolean"
+                                }
+                            },
+                            "additionalProperties": false,
+                            "required": [
+                                "name",
+                                "value"
+                            ]
+                        }
+                    }
+                },
+                "additionalProperties": false
+            },
+            "EmbedImage": {
+                "type": "object",
+                "properties": {
+                    "url": {
+                        "type": "string"
+                    },
+                    "proxy_url": {
+                        "type": "string"
+                    },
+                    "height": {
+                        "type": "integer"
+                    },
+                    "width": {
+                        "type": "integer"
+                    }
+                },
+                "additionalProperties": false
+            },
+            "ChannelModifySchema": {
+                "type": "object",
+                "properties": {
+                    "name": {
+                        "maxLength": 100,
+                        "type": "string"
+                    },
+                    "type": {
+                        "$ref": "#/definitions/ChannelType"
+                    },
+                    "topic": {
+                        "type": "string"
+                    },
+                    "bitrate": {
+                        "type": "integer"
+                    },
+                    "user_limit": {
+                        "type": "integer"
+                    },
+                    "rate_limit_per_user": {
+                        "type": "integer"
+                    },
+                    "position": {
+                        "type": "integer"
+                    },
+                    "permission_overwrites": {
+                        "type": "array",
+                        "items": {
+                            "type": "object",
+                            "properties": {
+                                "id": {
+                                    "type": "string"
+                                },
+                                "type": {
+                                    "$ref": "#/definitions/ChannelPermissionOverwriteType"
+                                },
+                                "allow": {
+                                    "type": "bigint"
+                                },
+                                "deny": {
+                                    "type": "bigint"
+                                }
+                            },
+                            "additionalProperties": false,
+                            "required": [
+                                "allow",
+                                "deny",
+                                "id",
+                                "type"
+                            ]
+                        }
+                    },
+                    "parent_id": {
+                        "type": "string"
+                    },
+                    "id": {
+                        "type": "string"
+                    },
+                    "nsfw": {
+                        "type": "boolean"
+                    },
+                    "rtc_region": {
+                        "type": "string"
+                    },
+                    "default_auto_archive_duration": {
+                        "type": "integer"
+                    }
+                },
+                "additionalProperties": false,
+                "required": [
+                    "name",
+                    "type"
+                ]
+            },
+            "RelationshipType": {
+                "enum": [
+                    1,
+                    2,
+                    3,
+                    4
+                ],
+                "type": "number"
+            }
+        },
+        "$schema": "http://json-schema.org/draft-07/schema#"
     }
 }
\ No newline at end of file
diff --git a/api/client_test/index.html b/api/client_test/index.html
index ac66df06..335b477c 100644
--- a/api/client_test/index.html
+++ b/api/client_test/index.html
@@ -11,7 +11,7 @@
 			window.__OVERLAY__ = /overlay/.test(location.pathname);
 			window.__BILLING_STANDALONE__ = /^\/billing/.test(location.pathname);
 			window.GLOBAL_ENV = {
-				API_ENDPOINT: "/api",
+				API_ENDPOINT: `//${location.host}/api`,
 				API_VERSION: 9,
 				GATEWAY_ENDPOINT: `${location.protocol === "https:" ? "wss://" : "ws://"}${location.hostname}:3002`,
 				WEBAPP_ENDPOINT: "",
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);
 			}