summary refs log tree commit diff
path: root/src/api
diff options
context:
space:
mode:
authorPuyodead1 <puyodead@proton.me>2023-03-23 11:56:17 -0400
committerPuyodead1 <puyodead@proton.me>2023-04-13 15:04:56 -0400
commit4a7811a25ce78a108e9a42fcb6c86ac0ed6e09eb (patch)
tree538718dc98a4dea8c21a0a29ea219a7304e08eb3 /src/api
parentapplications (diff)
downloadserver-4a7811a25ce78a108e9a42fcb6c86ac0ed6e09eb.tar.xz
channels
Diffstat (limited to 'src/api')
-rw-r--r--src/api/routes/channels/#channel_id/index.ts40
-rw-r--r--src/api/routes/channels/#channel_id/invites.ts32
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/ack.ts10
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts11
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/index.ts121
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts66
-rw-r--r--src/api/routes/channels/#channel_id/messages/bulk-delete.ts18
-rw-r--r--src/api/routes/channels/#channel_id/messages/index.ts223
-rw-r--r--src/api/routes/channels/#channel_id/permissions.ts12
-rw-r--r--src/api/routes/channels/#channel_id/pins.ts42
-rw-r--r--src/api/routes/channels/#channel_id/purge.ts22
-rw-r--r--src/api/routes/channels/#channel_id/recipients.ts142
-rw-r--r--src/api/routes/channels/#channel_id/typing.ts13
-rw-r--r--src/api/routes/channels/#channel_id/webhooks.ts40
14 files changed, 537 insertions, 255 deletions
diff --git a/src/api/routes/channels/#channel_id/index.ts b/src/api/routes/channels/#channel_id/index.ts
index db0d4242..74e21a02 100644
--- a/src/api/routes/channels/#channel_id/index.ts
+++ b/src/api/routes/channels/#channel_id/index.ts
@@ -16,18 +16,18 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	ChannelDeleteEvent,
+	ChannelModifySchema,
 	ChannelType,
 	ChannelUpdateEvent,
-	emitEvent,
 	Recipient,
+	emitEvent,
 	handleFile,
-	ChannelModifySchema,
 } from "@spacebar/util";
 import { Request, Response, Router } from "express";
-import { route } from "@spacebar/api";
 
 const router: Router = Router();
 // TODO: delete channel
@@ -35,7 +35,15 @@ const router: Router = Router();
 
 router.get(
 	"/",
-	route({ permission: "VIEW_CHANNEL" }),
+	route({
+		permission: "VIEW_CHANNEL",
+		responses: {
+			200: {
+				body: "Channel",
+			},
+			404: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 
@@ -49,7 +57,15 @@ router.get(
 
 router.delete(
 	"/",
-	route({ permission: "MANAGE_CHANNELS" }),
+	route({
+		permission: "MANAGE_CHANNELS",
+		responses: {
+			200: {
+				body: "Channel",
+			},
+			404: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 
@@ -90,7 +106,19 @@ router.delete(
 
 router.patch(
 	"/",
-	route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }),
+	route({
+		body: "ChannelModifySchema",
+		permission: "MANAGE_CHANNELS",
+		responses: {
+			200: {
+				body: "Channel",
+			},
+			404: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const payload = req.body as ChannelModifySchema;
 		const { channel_id } = req.params;
diff --git a/src/api/routes/channels/#channel_id/invites.ts b/src/api/routes/channels/#channel_id/invites.ts
index 9f247fe8..35cdbca8 100644
--- a/src/api/routes/channels/#channel_id/invites.ts
+++ b/src/api/routes/channels/#channel_id/invites.ts
@@ -16,19 +16,18 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Request, Response } from "express";
-import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
-import { random } from "@spacebar/api";
+import { random, route } from "@spacebar/api";
 import {
 	Channel,
+	Guild,
 	Invite,
 	InviteCreateEvent,
-	emitEvent,
-	User,
-	Guild,
 	PublicInviteRelation,
+	User,
+	emitEvent,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
 import { isTextChannel } from "./messages";
 
 const router: Router = Router();
@@ -39,6 +38,15 @@ router.post(
 		body: "InviteCreateSchema",
 		permission: "CREATE_INSTANT_INVITE",
 		right: "CREATE_INVITES",
+		responses: {
+			201: {
+				body: "Invite",
+			},
+			404: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { user_id } = req;
@@ -84,7 +92,15 @@ router.post(
 
 router.get(
 	"/",
-	route({ permission: "MANAGE_CHANNELS" }),
+	route({
+		permission: "MANAGE_CHANNELS",
+		responses: {
+			200: {
+				body: "ChannelInvitesResponse",
+			},
+			404: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 		const channel = await Channel.findOneOrFail({
diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts b/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts
index f098fa8e..f11fdcb2 100644
--- a/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts
@@ -16,6 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	emitEvent,
 	getPermission,
@@ -23,7 +24,6 @@ import {
 	ReadState,
 } from "@spacebar/util";
 import { Request, Response, Router } from "express";
-import { route } from "@spacebar/api";
 
 const router = Router();
 
@@ -33,7 +33,13 @@ const router = Router();
 
 router.post(
 	"/",
-	route({ body: "MessageAcknowledgeSchema" }),
+	route({
+		body: "MessageAcknowledgeSchema",
+		responses: {
+			200: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id, message_id } = req.params;
 
diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts b/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts
index 909a459e..5ca645c0 100644
--- a/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts
@@ -16,14 +16,21 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router = Router();
 
 router.post(
 	"/",
-	route({ permission: "MANAGE_MESSAGES" }),
+	route({
+		permission: "MANAGE_MESSAGES",
+		responses: {
+			200: {
+				body: "Message",
+			},
+		},
+	}),
 	(req: Request, res: Response) => {
 		// TODO:
 		res.json({
diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/index.ts b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts
index cd4b243e..77bc1e0e 100644
--- a/src/api/routes/channels/#channel_id/messages/#message_id/index.ts
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts
@@ -19,24 +19,23 @@
 import {
 	Attachment,
 	Channel,
-	emitEvent,
-	SpacebarApiErrors,
-	getPermission,
-	getRights,
 	Message,
 	MessageCreateEvent,
+	MessageCreateSchema,
 	MessageDeleteEvent,
+	MessageEditSchema,
 	MessageUpdateEvent,
 	Snowflake,
+	SpacebarApiErrors,
+	emitEvent,
+	getPermission,
+	getRights,
 	uploadFile,
-	MessageCreateSchema,
-	MessageEditSchema,
 } from "@spacebar/util";
-import { Router, Response, Request } from "express";
-import multer from "multer";
-import { route } from "@spacebar/api";
-import { handleMessage, postHandleMessage } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
+import multer from "multer";
+import { handleMessage, postHandleMessage, route } from "../../../../../util";
 
 const router = Router();
 // TODO: message content/embed string length limit
@@ -56,6 +55,16 @@ router.patch(
 		body: "MessageEditSchema",
 		permission: "SEND_MESSAGES",
 		right: "SEND_MESSAGES",
+		responses: {
+			200: {
+				body: "Message",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id } = req.params;
@@ -146,6 +155,16 @@ router.put(
 		body: "MessageCreateSchema",
 		permission: "SEND_MESSAGES",
 		right: "SEND_BACKDATED_EVENTS",
+		responses: {
+			200: {
+				body: "Message",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { channel_id, message_id } = req.params;
@@ -230,7 +249,19 @@ router.put(
 
 router.get(
 	"/",
-	route({ permission: "VIEW_CHANNEL" }),
+	route({
+		permission: "VIEW_CHANNEL",
+		responses: {
+			200: {
+				body: "Message",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id } = req.params;
 
@@ -252,38 +283,54 @@ router.get(
 	},
 );
 
-router.delete("/", route({}), async (req: Request, res: Response) => {
-	const { message_id, channel_id } = req.params;
+router.delete(
+	"/",
+	route({
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { message_id, channel_id } = req.params;
 
-	const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
-	const message = await Message.findOneOrFail({ where: { id: message_id } });
+		const channel = await Channel.findOneOrFail({
+			where: { id: channel_id },
+		});
+		const message = await Message.findOneOrFail({
+			where: { id: message_id },
+		});
 
-	const rights = await getRights(req.user_id);
+		const rights = await getRights(req.user_id);
 
-	if (message.author_id !== req.user_id) {
-		if (!rights.has("MANAGE_MESSAGES")) {
-			const permission = await getPermission(
-				req.user_id,
-				channel.guild_id,
-				channel_id,
-			);
-			permission.hasThrow("MANAGE_MESSAGES");
-		}
-	} else rights.hasThrow("SELF_DELETE_MESSAGES");
+		if (message.author_id !== req.user_id) {
+			if (!rights.has("MANAGE_MESSAGES")) {
+				const permission = await getPermission(
+					req.user_id,
+					channel.guild_id,
+					channel_id,
+				);
+				permission.hasThrow("MANAGE_MESSAGES");
+			}
+		} else rights.hasThrow("SELF_DELETE_MESSAGES");
 
-	await Message.delete({ id: message_id });
+		await Message.delete({ id: message_id });
 
-	await emitEvent({
-		event: "MESSAGE_DELETE",
-		channel_id,
-		data: {
-			id: message_id,
+		await emitEvent({
+			event: "MESSAGE_DELETE",
 			channel_id,
-			guild_id: channel.guild_id,
-		},
-	} as MessageDeleteEvent);
+			data: {
+				id: message_id,
+				channel_id,
+				guild_id: channel.guild_id,
+			},
+		} as MessageDeleteEvent);
 
-	res.sendStatus(204);
-});
+		res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts
index cb66cd64..c6db772b 100644
--- a/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts
@@ -16,6 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	emitEvent,
@@ -32,8 +33,7 @@ import {
 	PublicUserProjection,
 	User,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
-import { Router, Response, Request } from "express";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 import { In } from "typeorm";
 
@@ -57,7 +57,17 @@ function getEmoji(emoji: string): PartialEmoji {
 
 router.delete(
 	"/",
-	route({ permission: "MANAGE_MESSAGES" }),
+	route({
+		permission: "MANAGE_MESSAGES",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id } = req.params;
 
@@ -83,7 +93,17 @@ router.delete(
 
 router.delete(
 	"/:emoji",
-	route({ permission: "MANAGE_MESSAGES" }),
+	route({
+		permission: "MANAGE_MESSAGES",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id } = req.params;
 		const emoji = getEmoji(req.params.emoji);
@@ -120,7 +140,19 @@ router.delete(
 
 router.get(
 	"/:emoji",
-	route({ permission: "VIEW_CHANNEL" }),
+	route({
+		permission: "VIEW_CHANNEL",
+		responses: {
+			200: {
+				body: "UserPublic",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id } = req.params;
 		const emoji = getEmoji(req.params.emoji);
@@ -148,7 +180,18 @@ router.get(
 
 router.put(
 	"/:emoji/:user_id",
-	route({ permission: "READ_MESSAGE_HISTORY", right: "SELF_ADD_REACTIONS" }),
+	route({
+		permission: "READ_MESSAGE_HISTORY",
+		right: "SELF_ADD_REACTIONS",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { message_id, channel_id, user_id } = req.params;
 		if (user_id !== "@me") throw new HTTPError("Invalid user");
@@ -219,7 +262,16 @@ router.put(
 
 router.delete(
 	"/:emoji/:user_id",
-	route({}),
+	route({
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		let { user_id } = req.params;
 		const { message_id, channel_id } = req.params;
diff --git a/src/api/routes/channels/#channel_id/messages/bulk-delete.ts b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts
index 18476d5c..db1617e2 100644
--- a/src/api/routes/channels/#channel_id/messages/bulk-delete.ts
+++ b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts
@@ -16,18 +16,18 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	Config,
 	emitEvent,
 	getPermission,
 	getRights,
-	MessageDeleteBulkEvent,
 	Message,
+	MessageDeleteBulkEvent,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { route } from "@spacebar/api";
 
 const router: Router = Router();
 
@@ -38,7 +38,17 @@ export default router;
 // https://discord.com/developers/docs/resources/channel#bulk-delete-messages
 router.post(
 	"/",
-	route({ body: "BulkDeleteSchema" }),
+	route({
+		body: "BulkDeleteSchema",
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 		const channel = await Channel.findOneOrFail({
diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts
index 7f0c9fb5..8e3c43d7 100644
--- a/src/api/routes/channels/#channel_id/messages/index.ts
+++ b/src/api/routes/channels/#channel_id/messages/index.ts
@@ -16,7 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
+import { handleMessage, postHandleMessage, route } from "@spacebar/api";
 import {
 	Attachment,
 	Channel,
@@ -26,19 +26,19 @@ import {
 	emitEvent,
 	FieldErrors,
 	getPermission,
+	Member,
 	Message,
 	MessageCreateEvent,
-	Snowflake,
-	uploadFile,
-	Member,
 	MessageCreateSchema,
+	Reaction,
 	ReadState,
 	Rights,
-	Reaction,
+	Snowflake,
+	uploadFile,
 	User,
 } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
-import { handleMessage, postHandleMessage, route } from "@spacebar/api";
 import multer from "multer";
 import { FindManyOptions, FindOperator, LessThan, MoreThan } from "typeorm";
 import { URL } from "url";
@@ -73,108 +73,123 @@ export function isTextChannel(type: ChannelType): boolean {
 
 // https://discord.com/developers/docs/resources/channel#create-message
 // get messages
-router.get("/", route({}), async (req: Request, res: Response) => {
-	const channel_id = req.params.channel_id;
-	const channel = await Channel.findOneOrFail({
-		where: { id: channel_id },
-	});
-	if (!channel) throw new HTTPError("Channel not found", 404);
-
-	isTextChannel(channel.type);
-	const around = req.query.around ? `${req.query.around}` : undefined;
-	const before = req.query.before ? `${req.query.before}` : undefined;
-	const after = req.query.after ? `${req.query.after}` : undefined;
-	const limit = Number(req.query.limit) || 50;
-	if (limit < 1 || limit > 100)
-		throw new HTTPError("limit must be between 1 and 100", 422);
-
-	const halfLimit = Math.floor(limit / 2);
-
-	const permissions = await getPermission(
-		req.user_id,
-		channel.guild_id,
-		channel_id,
-	);
-	permissions.hasThrow("VIEW_CHANNEL");
-	if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
-
-	const query: FindManyOptions<Message> & {
-		where: { id?: FindOperator<string> | FindOperator<string>[] };
-	} = {
-		order: { timestamp: "DESC" },
-		take: limit,
-		where: { channel_id },
-		relations: [
-			"author",
-			"webhook",
-			"application",
-			"mentions",
-			"mention_roles",
-			"mention_channels",
-			"sticker_items",
-			"attachments",
-		],
-	};
-
-	if (after) {
-		if (BigInt(after) > BigInt(Snowflake.generate()))
-			return res.status(422);
-		query.where.id = MoreThan(after);
-	} else if (before) {
-		if (BigInt(before) < BigInt(req.params.channel_id))
-			return res.status(422);
-		query.where.id = LessThan(before);
-	} else if (around) {
-		query.where.id = [
-			MoreThan((BigInt(around) - BigInt(halfLimit)).toString()),
-			LessThan((BigInt(around) + BigInt(halfLimit)).toString()),
-		];
-
-		return res.json([]); // TODO: fix around
-	}
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "ChannelMessagesResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const channel_id = req.params.channel_id;
+		const channel = await Channel.findOneOrFail({
+			where: { id: channel_id },
+		});
+		if (!channel) throw new HTTPError("Channel not found", 404);
 
-	const messages = await Message.find(query);
-	const endpoint = Config.get().cdn.endpointPublic;
+		isTextChannel(channel.type);
+		const around = req.query.around ? `${req.query.around}` : undefined;
+		const before = req.query.before ? `${req.query.before}` : undefined;
+		const after = req.query.after ? `${req.query.after}` : undefined;
+		const limit = Number(req.query.limit) || 50;
+		if (limit < 1 || limit > 100)
+			throw new HTTPError("limit must be between 1 and 100", 422);
 
-	return res.json(
-		messages.map((x: Partial<Message>) => {
-			(x.reactions || []).forEach((y: Partial<Reaction>) => {
-				// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-				//@ts-ignore
-				if ((y.user_ids || []).includes(req.user_id)) y.me = true;
-				delete y.user_ids;
-			});
-			if (!x.author)
-				x.author = User.create({
-					id: "4",
-					discriminator: "0000",
-					username: "Spacebar Ghost",
-					public_flags: 0,
+		const halfLimit = Math.floor(limit / 2);
+
+		const permissions = await getPermission(
+			req.user_id,
+			channel.guild_id,
+			channel_id,
+		);
+		permissions.hasThrow("VIEW_CHANNEL");
+		if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
+
+		const query: FindManyOptions<Message> & {
+			where: { id?: FindOperator<string> | FindOperator<string>[] };
+		} = {
+			order: { timestamp: "DESC" },
+			take: limit,
+			where: { channel_id },
+			relations: [
+				"author",
+				"webhook",
+				"application",
+				"mentions",
+				"mention_roles",
+				"mention_channels",
+				"sticker_items",
+				"attachments",
+			],
+		};
+
+		if (after) {
+			if (BigInt(after) > BigInt(Snowflake.generate()))
+				return res.status(422);
+			query.where.id = MoreThan(after);
+		} else if (before) {
+			if (BigInt(before) < BigInt(req.params.channel_id))
+				return res.status(422);
+			query.where.id = LessThan(before);
+		} else if (around) {
+			query.where.id = [
+				MoreThan((BigInt(around) - BigInt(halfLimit)).toString()),
+				LessThan((BigInt(around) + BigInt(halfLimit)).toString()),
+			];
+
+			return res.json([]); // TODO: fix around
+		}
+
+		const messages = await Message.find(query);
+		const endpoint = Config.get().cdn.endpointPublic;
+
+		return res.json(
+			messages.map((x: Partial<Message>) => {
+				(x.reactions || []).forEach((y: Partial<Reaction>) => {
+					// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+					//@ts-ignore
+					if ((y.user_ids || []).includes(req.user_id)) y.me = true;
+					delete y.user_ids;
+				});
+				if (!x.author)
+					x.author = User.create({
+						id: "4",
+						discriminator: "0000",
+						username: "Fosscord Ghost",
+						public_flags: 0,
+					});
+				x.attachments?.forEach((y: Attachment) => {
+					// dynamically set attachment proxy_url in case the endpoint changed
+					const uri = y.proxy_url.startsWith("http")
+						? y.proxy_url
+						: `https://example.org${y.proxy_url}`;
+					y.proxy_url = `${endpoint == null ? "" : endpoint}${
+						new URL(uri).pathname
+					}`;
 				});
-			x.attachments?.forEach((y: Attachment) => {
-				// dynamically set attachment proxy_url in case the endpoint changed
-				const uri = y.proxy_url.startsWith("http")
-					? y.proxy_url
-					: `https://example.org${y.proxy_url}`;
-				y.proxy_url = `${endpoint == null ? "" : endpoint}${
-					new URL(uri).pathname
-				}`;
-			});
 
-			/**
+				/**
 			Some clients ( discord.js ) only check if a property exists within the response,
 			which causes errors when, say, the `application` property is `null`.
 			**/
 
-			// for (var curr in x) {
-			// 	if (x[curr] === null)
-			// 		delete x[curr];
-			// }
+				// for (var curr in x) {
+				// 	if (x[curr] === null)
+				// 		delete x[curr];
+				// }
 
-			return x;
-		}),
-	);
-});
+				return x;
+			}),
+		);
+	},
+);
 
 // TODO: config max upload size
 const messageUpload = multer({
@@ -208,6 +223,16 @@ router.post(
 		body: "MessageCreateSchema",
 		permission: "SEND_MESSAGES",
 		right: "SEND_MESSAGES",
+		responses: {
+			200: {
+				body: "Message",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+			404: {},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
diff --git a/src/api/routes/channels/#channel_id/permissions.ts b/src/api/routes/channels/#channel_id/permissions.ts
index 68dbc2f2..c6a9def6 100644
--- a/src/api/routes/channels/#channel_id/permissions.ts
+++ b/src/api/routes/channels/#channel_id/permissions.ts
@@ -19,13 +19,13 @@
 import {
 	Channel,
 	ChannelPermissionOverwrite,
+	ChannelPermissionOverwriteSchema,
 	ChannelUpdateEvent,
 	emitEvent,
 	Member,
 	Role,
-	ChannelPermissionOverwriteSchema,
 } from "@spacebar/util";
-import { Router, Response, Request } from "express";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 
 import { route } from "@spacebar/api";
@@ -38,6 +38,12 @@ router.put(
 	route({
 		body: "ChannelPermissionOverwriteSchema",
 		permission: "MANAGE_ROLES",
+		responses: {
+			204: {},
+			404: {},
+			501: {},
+			400: { body: "APIErrorResponse" },
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { channel_id, overwrite_id } = req.params;
@@ -92,7 +98,7 @@ router.put(
 // TODO: check permission hierarchy
 router.delete(
 	"/:overwrite_id",
-	route({ permission: "MANAGE_ROLES" }),
+	route({ permission: "MANAGE_ROLES", responses: { 204: {}, 404: {} } }),
 	async (req: Request, res: Response) => {
 		const { channel_id, overwrite_id } = req.params;
 
diff --git a/src/api/routes/channels/#channel_id/pins.ts b/src/api/routes/channels/#channel_id/pins.ts
index 32820916..7b379c30 100644
--- a/src/api/routes/channels/#channel_id/pins.ts
+++ b/src/api/routes/channels/#channel_id/pins.ts
@@ -16,23 +16,33 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	ChannelPinsUpdateEvent,
 	Config,
+	DiscordApiErrors,
 	emitEvent,
 	Message,
 	MessageUpdateEvent,
-	DiscordApiErrors,
 } from "@spacebar/util";
-import { Router, Request, Response } from "express";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
 router.put(
 	"/:message_id",
-	route({ permission: "VIEW_CHANNEL" }),
+	route({
+		permission: "VIEW_CHANNEL",
+		responses: {
+			204: {},
+			403: {},
+			404: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id, message_id } = req.params;
 
@@ -74,7 +84,17 @@ router.put(
 
 router.delete(
 	"/:message_id",
-	route({ permission: "VIEW_CHANNEL" }),
+	route({
+		permission: "VIEW_CHANNEL",
+		responses: {
+			204: {},
+			403: {},
+			404: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id, message_id } = req.params;
 
@@ -114,7 +134,17 @@ router.delete(
 
 router.get(
 	"/",
-	route({ permission: ["READ_MESSAGE_HISTORY"] }),
+	route({
+		permission: ["READ_MESSAGE_HISTORY"],
+		responses: {
+			200: {
+				body: "ChannelPinsResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 
diff --git a/src/api/routes/channels/#channel_id/purge.ts b/src/api/routes/channels/#channel_id/purge.ts
index c8da6760..cbd46bd0 100644
--- a/src/api/routes/channels/#channel_id/purge.ts
+++ b/src/api/routes/channels/#channel_id/purge.ts
@@ -16,20 +16,20 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { HTTPError } from "lambert-server";
 import { route } from "@spacebar/api";
-import { isTextChannel } from "./messages";
-import { FindManyOptions, Between, Not, FindOperator } from "typeorm";
 import {
 	Channel,
-	emitEvent,
-	getPermission,
-	getRights,
 	Message,
 	MessageDeleteBulkEvent,
 	PurgeSchema,
+	emitEvent,
+	getPermission,
+	getRights,
 } from "@spacebar/util";
-import { Router, Response, Request } from "express";
+import { Request, Response, Router } from "express";
+import { HTTPError } from "lambert-server";
+import { Between, FindManyOptions, FindOperator, Not } from "typeorm";
+import { isTextChannel } from "./messages";
 
 const router: Router = Router();
 
@@ -42,6 +42,14 @@ router.post(
 	"/",
 	route({
 		/*body: "PurgeSchema",*/
+		responses: {
+			204: {},
+			400: {
+				body: "APIErrorResponse",
+			},
+			404: {},
+			403: {},
+		},
 	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
diff --git a/src/api/routes/channels/#channel_id/recipients.ts b/src/api/routes/channels/#channel_id/recipients.ts
index f1fb48af..569bb5cd 100644
--- a/src/api/routes/channels/#channel_id/recipients.ts
+++ b/src/api/routes/channels/#channel_id/recipients.ts
@@ -16,7 +16,7 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Request, Response, Router } from "express";
+import { route } from "@spacebar/api";
 import {
 	Channel,
 	ChannelRecipientAddEvent,
@@ -28,80 +28,98 @@ import {
 	Recipient,
 	User,
 } from "@spacebar/util";
-import { route } from "@spacebar/api";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
-router.put("/:user_id", route({}), async (req: Request, res: Response) => {
-	const { channel_id, user_id } = req.params;
-	const channel = await Channel.findOneOrFail({
-		where: { id: channel_id },
-		relations: ["recipients"],
-	});
+router.put(
+	"/:user_id",
+	route({
+		responses: {
+			201: {},
+			404: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { channel_id, user_id } = req.params;
+		const channel = await Channel.findOneOrFail({
+			where: { id: channel_id },
+			relations: ["recipients"],
+		});
 
-	if (channel.type !== ChannelType.GROUP_DM) {
-		const recipients = [
-			...(channel.recipients?.map((r) => r.user_id) || []),
-			user_id,
-		].unique();
+		if (channel.type !== ChannelType.GROUP_DM) {
+			const recipients = [
+				...(channel.recipients?.map((r) => r.user_id) || []),
+				user_id,
+			].unique();
 
-		const new_channel = await Channel.createDMChannel(
-			recipients,
-			req.user_id,
-		);
-		return res.status(201).json(new_channel);
-	} else {
-		if (channel.recipients?.map((r) => r.user_id).includes(user_id)) {
-			throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
-		}
+			const new_channel = await Channel.createDMChannel(
+				recipients,
+				req.user_id,
+			);
+			return res.status(201).json(new_channel);
+		} else {
+			if (channel.recipients?.map((r) => r.user_id).includes(user_id)) {
+				throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
+			}
 
-		channel.recipients?.push(
-			Recipient.create({ channel_id: channel_id, user_id: user_id }),
-		);
-		await channel.save();
+			channel.recipients?.push(
+				Recipient.create({ channel_id: channel_id, user_id: user_id }),
+			);
+			await channel.save();
 
-		await emitEvent({
-			event: "CHANNEL_CREATE",
-			data: await DmChannelDTO.from(channel, [user_id]),
-			user_id: user_id,
-		});
+			await emitEvent({
+				event: "CHANNEL_CREATE",
+				data: await DmChannelDTO.from(channel, [user_id]),
+				user_id: user_id,
+			});
 
-		await emitEvent({
-			event: "CHANNEL_RECIPIENT_ADD",
-			data: {
+			await emitEvent({
+				event: "CHANNEL_RECIPIENT_ADD",
+				data: {
+					channel_id: channel_id,
+					user: await User.findOneOrFail({
+						where: { id: user_id },
+						select: PublicUserProjection,
+					}),
+				},
 				channel_id: channel_id,
-				user: await User.findOneOrFail({
-					where: { id: user_id },
-					select: PublicUserProjection,
-				}),
-			},
-			channel_id: channel_id,
-		} as ChannelRecipientAddEvent);
-		return res.sendStatus(204);
-	}
-});
+			} as ChannelRecipientAddEvent);
+			return res.sendStatus(204);
+		}
+	},
+);
 
-router.delete("/:user_id", route({}), async (req: Request, res: Response) => {
-	const { channel_id, user_id } = req.params;
-	const channel = await Channel.findOneOrFail({
-		where: { id: channel_id },
-		relations: ["recipients"],
-	});
-	if (
-		!(
-			channel.type === ChannelType.GROUP_DM &&
-			(channel.owner_id === req.user_id || user_id === req.user_id)
+router.delete(
+	"/:user_id",
+	route({
+		responses: {
+			204: {},
+			404: {},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		const { channel_id, user_id } = req.params;
+		const channel = await Channel.findOneOrFail({
+			where: { id: channel_id },
+			relations: ["recipients"],
+		});
+		if (
+			!(
+				channel.type === ChannelType.GROUP_DM &&
+				(channel.owner_id === req.user_id || user_id === req.user_id)
+			)
 		)
-	)
-		throw DiscordApiErrors.MISSING_PERMISSIONS;
+			throw DiscordApiErrors.MISSING_PERMISSIONS;
 
-	if (!channel.recipients?.map((r) => r.user_id).includes(user_id)) {
-		throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
-	}
+		if (!channel.recipients?.map((r) => r.user_id).includes(user_id)) {
+			throw DiscordApiErrors.INVALID_RECIPIENT; //TODO is this the right error?
+		}
 
-	await Channel.removeRecipientFromChannel(channel, user_id);
+		await Channel.removeRecipientFromChannel(channel, user_id);
 
-	return res.sendStatus(204);
-});
+		return res.sendStatus(204);
+	},
+);
 
 export default router;
diff --git a/src/api/routes/channels/#channel_id/typing.ts b/src/api/routes/channels/#channel_id/typing.ts
index 6a2fef39..b5d61d74 100644
--- a/src/api/routes/channels/#channel_id/typing.ts
+++ b/src/api/routes/channels/#channel_id/typing.ts
@@ -16,15 +16,22 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Channel, emitEvent, Member, TypingStartEvent } from "@spacebar/util";
 import { route } from "@spacebar/api";
-import { Router, Request, Response } from "express";
+import { Channel, emitEvent, Member, TypingStartEvent } from "@spacebar/util";
+import { Request, Response, Router } from "express";
 
 const router: Router = Router();
 
 router.post(
 	"/",
-	route({ permission: "SEND_MESSAGES" }),
+	route({
+		permission: "SEND_MESSAGES",
+		responses: {
+			204: {},
+			404: {},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const { channel_id } = req.params;
 		const user_id = req.user_id;
diff --git a/src/api/routes/channels/#channel_id/webhooks.ts b/src/api/routes/channels/#channel_id/webhooks.ts
index 14791a1c..4e98a1c9 100644
--- a/src/api/routes/channels/#channel_id/webhooks.ts
+++ b/src/api/routes/channels/#channel_id/webhooks.ts
@@ -16,34 +16,56 @@
 	along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
 
-import { Router, Response, Request } from "express";
 import { route } from "@spacebar/api";
 import {
 	Channel,
 	Config,
-	handleFile,
-	trimSpecial,
+	DiscordApiErrors,
 	User,
 	Webhook,
 	WebhookCreateSchema,
 	WebhookType,
+	handleFile,
+	trimSpecial,
 } from "@spacebar/util";
+import crypto from "crypto";
+import { Request, Response, Router } from "express";
 import { HTTPError } from "lambert-server";
 import { isTextChannel } from "./messages/index";
-import { DiscordApiErrors } from "@spacebar/util";
-import crypto from "crypto";
 
 const router: Router = Router();
 
 //TODO: implement webhooks
-router.get("/", route({}), async (req: Request, res: Response) => {
-	res.json([]);
-});
+router.get(
+	"/",
+	route({
+		responses: {
+			200: {
+				body: "ChannelWebhooksResponse",
+			},
+		},
+	}),
+	async (req: Request, res: Response) => {
+		res.json([]);
+	},
+);
 
 // TODO: use Image Data Type for avatar instead of String
 router.post(
 	"/",
-	route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOOKS" }),
+	route({
+		body: "WebhookCreateSchema",
+		permission: "MANAGE_WEBHOOKS",
+		responses: {
+			200: {
+				body: "WebhookCreateResponse",
+			},
+			400: {
+				body: "APIErrorResponse",
+			},
+			403: {},
+		},
+	}),
 	async (req: Request, res: Response) => {
 		const channel_id = req.params.channel_id;
 		const channel = await Channel.findOneOrFail({