summary refs log tree commit diff
path: root/api/src/routes/guilds
diff options
context:
space:
mode:
authorFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-09-12 23:28:56 +0200
committerFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-09-12 23:28:56 +0200
commit0b5534bc4144d6c708b19b1890b73eb56016e123 (patch)
tree4f17716499b03ba7ee395a7ab6f90df97a41ea46 /api/src/routes/guilds
parent:construction: :sparkles: new body parser (bans route) (diff)
downloadserver-0b5534bc4144d6c708b19b1890b73eb56016e123.tar.xz
:sparkles: #307 done
Diffstat (limited to 'api/src/routes/guilds')
-rw-r--r--api/src/routes/guilds/#guild_id/bans.ts8
-rw-r--r--api/src/routes/guilds/#guild_id/channels.ts74
-rw-r--r--api/src/routes/guilds/#guild_id/delete.ts3
-rw-r--r--api/src/routes/guilds/#guild_id/index.ts34
-rw-r--r--api/src/routes/guilds/#guild_id/invites.ts6
-rw-r--r--api/src/routes/guilds/#guild_id/members/#member_id/index.ts36
-rw-r--r--api/src/routes/guilds/#guild_id/members/#member_id/nick.ts9
-rw-r--r--api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts11
-rw-r--r--api/src/routes/guilds/#guild_id/members/index.ts6
-rw-r--r--api/src/routes/guilds/#guild_id/regions.ts4
-rw-r--r--api/src/routes/guilds/#guild_id/roles.ts44
-rw-r--r--api/src/routes/guilds/#guild_id/templates.ts51
-rw-r--r--api/src/routes/guilds/#guild_id/vanity-url.ts24
-rw-r--r--api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts57
-rw-r--r--api/src/routes/guilds/#guild_id/voice-states/@me/index.ts15
-rw-r--r--api/src/routes/guilds/#guild_id/welcome_screen.ts23
-rw-r--r--api/src/routes/guilds/#guild_id/widget.json.ts4
-rw-r--r--api/src/routes/guilds/#guild_id/widget.png.ts3
-rw-r--r--api/src/routes/guilds/#guild_id/widget.ts18
-rw-r--r--api/src/routes/guilds/index.ts19
-rw-r--r--api/src/routes/guilds/templates/index.ts12
21 files changed, 251 insertions, 210 deletions
diff --git a/api/src/routes/guilds/#guild_id/bans.ts b/api/src/routes/guilds/#guild_id/bans.ts
index 41a97b6a..c7fda9ad 100644
--- a/api/src/routes/guilds/#guild_id/bans.ts
+++ b/api/src/routes/guilds/#guild_id/bans.ts
@@ -2,7 +2,11 @@ import { Request, Response, Router } from "express";
 import { emitEvent, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, Guild, Ban, User, Member } from "@fosscord/util";
 import { HTTPError } from "lambert-server";
 import { getIpAdress, check, route } from "@fosscord/api";
-import { BanCreateSchema } from "@fosscord/api/schema/Ban";
+
+export interface BanCreateSchema {
+	delete_message_days?: string;
+	reason?: string;
+}
 
 const router: Router = Router();
 router.get("/", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => {
@@ -27,7 +31,7 @@ router.put("/:user_id", route({ body: "BanCreateSchema", permission: "BAN_MEMBER
 	const banned_user = await User.getPublicUser(banned_user_id);
 
 	if (req.user_id === banned_user_id) throw new HTTPError("You can't ban yourself", 400);
-	if (req.permission?.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400);
+	if (req.permission!.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400);
 
 	const ban = new Ban({
 		user_id: banned_user_id,
diff --git a/api/src/routes/guilds/#guild_id/channels.ts b/api/src/routes/guilds/#guild_id/channels.ts
index faeecb76..e21327d1 100644
--- a/api/src/routes/guilds/#guild_id/channels.ts
+++ b/api/src/routes/guilds/#guild_id/channels.ts
@@ -1,9 +1,8 @@
 import { Router, Response, Request } from "express";
 import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util";
 import { HTTPError } from "lambert-server";
-import { ChannelModifySchema } from "../../../schema/Channel";
-
-import { check } from "@fosscord/api";
+import { check, route } from "@fosscord/api";
+import { ChannelModifySchema } from "../../channels/#channel_id";
 const router = Router();
 
 router.get("/", async (req: Request, res: Response) => {
@@ -13,10 +12,7 @@ router.get("/", async (req: Request, res: Response) => {
 	res.json(channels);
 });
 
-// TODO: check if channel type is permitted
-// TODO: check if parent_id exists
-
-router.post("/", check(ChannelModifySchema), async (req: Request, res: Response) => {
+router.post("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => {
 	// creates a new guild channel https://discord.com/developers/docs/resources/guild#create-guild-channel
 	const { guild_id } = req.params;
 	const body = req.body as ChannelModifySchema;
@@ -26,45 +22,39 @@ router.post("/", check(ChannelModifySchema), async (req: Request, res: Response)
 	res.status(201).json(channel);
 });
 
-// TODO: check if parent_id exists
-router.patch(
-	"/",
-	check([{ id: String, $position: Number, $lock_permissions: Boolean, $parent_id: String }]),
-	async (req: Request, res: Response) => {
-		// changes guild channel position
-		const { guild_id } = req.params;
-		const body = req.body as { id: string; position?: number; lock_permissions?: boolean; parent_id?: string }[];
-
-		const permission = await getPermission(req.user_id, guild_id);
-		permission.hasThrow("MANAGE_CHANNELS");
-
-		await Promise.all([
-			body.map(async (x) => {
-				if (!x.position && !x.parent_id) throw new HTTPError(`You need to at least specify position or parent_id`, 400);
+export type ChannelReorderSchema = { id: string; position?: number; lock_permissions?: boolean; parent_id?: string }[];
 
-				const opts: any = {};
-				if (x.position) opts.position = x.position;
-
-				if (x.parent_id) {
-					opts.parent_id = x.parent_id;
-					const parent_channel = await Channel.findOneOrFail({
-						where: { id: x.parent_id, guild_id },
-						select: ["permission_overwrites"]
-					});
-					if (x.lock_permissions) {
-						opts.permission_overwrites = parent_channel.permission_overwrites;
-					}
+router.patch("/", route({ body: "ChannelReorderSchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => {
+	// changes guild channel position
+	const { guild_id } = req.params;
+	const body = req.body as ChannelReorderSchema;
+
+	await Promise.all([
+		body.map(async (x) => {
+			if (!x.position && !x.parent_id) throw new HTTPError(`You need to at least specify position or parent_id`, 400);
+
+			const opts: any = {};
+			if (x.position) opts.position = x.position;
+
+			if (x.parent_id) {
+				opts.parent_id = x.parent_id;
+				const parent_channel = await Channel.findOneOrFail({
+					where: { id: x.parent_id, guild_id },
+					select: ["permission_overwrites"]
+				});
+				if (x.lock_permissions) {
+					opts.permission_overwrites = parent_channel.permission_overwrites;
 				}
+			}
 
-				await Channel.update({ guild_id, id: x.id }, opts);
-				const channel = await Channel.findOneOrFail({ guild_id, id: x.id });
+			await Channel.update({ guild_id, id: x.id }, opts);
+			const channel = await Channel.findOneOrFail({ guild_id, id: x.id });
 
-				await emitEvent({ event: "CHANNEL_UPDATE", data: channel, channel_id: x.id, guild_id } as ChannelUpdateEvent);
-			})
-		]);
+			await emitEvent({ event: "CHANNEL_UPDATE", data: channel, channel_id: x.id, guild_id } as ChannelUpdateEvent);
+		})
+	]);
 
-		res.sendStatus(204);
-	}
-);
+	res.sendStatus(204);
+});
 
 export default router;
diff --git a/api/src/routes/guilds/#guild_id/delete.ts b/api/src/routes/guilds/#guild_id/delete.ts
index bbbd1fa4..7c3c5530 100644
--- a/api/src/routes/guilds/#guild_id/delete.ts
+++ b/api/src/routes/guilds/#guild_id/delete.ts
@@ -1,12 +1,13 @@
 import { Channel, emitEvent, GuildDeleteEvent, Guild, Member, Message, Role, Invite, Emoji } from "@fosscord/util";
 import { Router, Request, Response } from "express";
 import { HTTPError } from "lambert-server";
+import { route } from "@fosscord/api";
 
 const router = Router();
 
 // discord prefixes this route with /delete instead of using the delete method
 // docs are wrong https://discord.com/developers/docs/resources/guild#delete-guild
-router.post("/", async (req: Request, res: Response) => {
+router.post("/", route({}), async (req: Request, res: Response) => {
 	var { guild_id } = req.params;
 
 	const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: ["owner_id"] });
diff --git a/api/src/routes/guilds/#guild_id/index.ts b/api/src/routes/guilds/#guild_id/index.ts
index 244900ec..690d4103 100644
--- a/api/src/routes/guilds/#guild_id/index.ts
+++ b/api/src/routes/guilds/#guild_id/index.ts
@@ -1,23 +1,36 @@
 import { Request, Response, Router } from "express";
 import { emitEvent, getPermission, Guild, GuildUpdateEvent, Member } from "@fosscord/util";
 import { HTTPError } from "lambert-server";
-import { GuildUpdateSchema } from "../../../schema/Guild";
-
-import { check } from "@fosscord/api";
+import { check, route } from "@fosscord/api";
 import { handleFile } from "@fosscord/api";
 import "missing-native-js-functions";
+import { GuildCreateSchema } from "../index";
 
 const router = Router();
 
-router.get("/", async (req: Request, res: Response) => {
+export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> {
+	banner?: string;
+	splash?: string;
+	description?: string;
+	features?: string[];
+	verification_level?: number;
+	default_message_notifications?: number;
+	system_channel_flags?: number;
+	explicit_content_filter?: number;
+	public_updates_channel_id?: string;
+	afk_timeout?: number;
+	afk_channel_id?: string;
+	preferred_locale?: string;
+}
+
+router.get("/", route({}), async (req: Request, res: Response) => {
 	const { guild_id } = req.params;
 
-	const [guild, member_count, member] = await Promise.all([
+	const [guild, member] = await Promise.all([
 		Guild.findOneOrFail({ id: guild_id }),
-		Member.count({ guild_id: guild_id, id: req.user_id }),
-		Member.findOneOrFail({ id: req.user_id })
+		Member.findOne({ guild_id: guild_id, id: req.user_id })
 	]);
-	if (!member_count) throw new HTTPError("You are not a member of the guild you are trying to access", 401);
+	if (!member) throw new HTTPError("You are not a member of the guild you are trying to access", 401);
 
 	// @ts-ignore
 	guild.joined_at = member?.joined_at;
@@ -25,14 +38,11 @@ router.get("/", async (req: Request, res: Response) => {
 	return res.json(guild);
 });
 
-router.patch("/", check(GuildUpdateSchema), async (req: Request, res: Response) => {
+router.patch("/", route({ body: "GuildUpdateSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
 	const body = req.body as GuildUpdateSchema;
 	const { guild_id } = req.params;
 	// TODO: guild update check image
 
-	const perms = await getPermission(req.user_id, guild_id);
-	perms.hasThrow("MANAGE_GUILD");
-
 	if (body.icon) body.icon = await handleFile(`/icons/${guild_id}`, body.icon);
 	if (body.banner) body.banner = await handleFile(`/banners/${guild_id}`, body.banner);
 	if (body.splash) body.splash = await handleFile(`/splashes/${guild_id}`, body.splash);
diff --git a/api/src/routes/guilds/#guild_id/invites.ts b/api/src/routes/guilds/#guild_id/invites.ts
index 39a934ee..b7534e31 100644
--- a/api/src/routes/guilds/#guild_id/invites.ts
+++ b/api/src/routes/guilds/#guild_id/invites.ts
@@ -1,14 +1,12 @@
 import { getPermission, Invite, PublicInviteRelation } from "@fosscord/util";
+import { route } from "@fosscord/api";
 import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.get("/", async (req: Request, res: Response) => {
+router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
 	const { guild_id } = req.params;
 
-	const permissions = await getPermission(req.user_id, guild_id);
-	permissions.hasThrow("MANAGE_GUILD");
-
 	const invites = await Invite.find({ where: { guild_id }, relations: PublicInviteRelation });
 
 	return res.json(invites);
diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts
index 8b04a508..1708b7eb 100644
--- a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts
+++ b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts
@@ -1,23 +1,15 @@
 import { Request, Response, Router } from "express";
-import {
-	Guild,
-	Member,
-	User,
-	GuildMemberAddEvent,
-	getPermission,
-	PermissionResolvable,
-	Role,
-	GuildMemberUpdateEvent,
-	emitEvent
-} from "@fosscord/util";
+import { Member, getPermission, Role, GuildMemberUpdateEvent, emitEvent } from "@fosscord/util";
 import { HTTPError } from "lambert-server";
-import { check } from "@fosscord/api";
-import { MemberChangeSchema } from "@fosscord/api/schema/Member";
-import { In } from "typeorm";
+import { check, route } from "@fosscord/api";
 
 const router = Router();
 
-router.get("/", async (req: Request, res: Response) => {
+export interface MemberChangeSchema {
+	roles?: string[];
+}
+
+router.get("/", route({}), async (req: Request, res: Response) => {
 	const { guild_id, member_id } = req.params;
 	await Member.IsInGuildOrFail(req.user_id, guild_id);
 
@@ -26,8 +18,9 @@ router.get("/", async (req: Request, res: Response) => {
 	return res.json(member);
 });
 
-router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response) => {
-	const { guild_id, member_id } = req.params;
+router.patch("/", route({ body: "MemberChangeSchema" }), async (req: Request, res: Response) => {
+	let { guild_id, member_id } = req.params;
+	if (member_id === "@me") member_id = req.user_id;
 	const body = req.body as MemberChangeSchema;
 
 	const member = await Member.findOneOrFail({ where: { id: member_id, guild_id }, relations: ["roles", "user"] });
@@ -39,7 +32,7 @@ router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response)
 	}
 
 	await member.save();
-	// do not use promise.all as we have to first write to db before emitting the event
+	// do not use promise.all as we have to first write to db before emitting the event to catch errors
 	await emitEvent({
 		event: "GUILD_MEMBER_UPDATE",
 		guild_id,
@@ -49,7 +42,7 @@ router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response)
 	res.json(member);
 });
 
-router.put("/", async (req: Request, res: Response) => {
+router.put("/", route({}), async (req: Request, res: Response) => {
 	let { guild_id, member_id } = req.params;
 	if (member_id === "@me") member_id = req.user_id;
 
@@ -59,12 +52,9 @@ router.put("/", async (req: Request, res: Response) => {
 	res.sendStatus(204);
 });
 
-router.delete("/", async (req: Request, res: Response) => {
+router.delete("/", route({ permission: "KICK_MEMBERS" }), async (req: Request, res: Response) => {
 	const { guild_id, member_id } = req.params;
 
-	const perms = await getPermission(req.user_id, guild_id);
-	perms.hasThrow("KICK_MEMBERS");
-
 	await Member.removeFromGuild(member_id, guild_id);
 	res.sendStatus(204);
 });
diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts b/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts
index 3f2975e6..27f7f65d 100644
--- a/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts
+++ b/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts
@@ -1,11 +1,14 @@
 import { getPermission, Member, PermissionResolvable } from "@fosscord/util";
+import { route } from "@fosscord/api";
 import { Request, Response, Router } from "express";
-import { check } from "lambert-server";
-import { MemberNickChangeSchema } from "../../../../../schema/Member";
 
 const router = Router();
 
-router.patch("/", check(MemberNickChangeSchema), async (req: Request, res: Response) => {
+export interface MemberNickChangeSchema {
+	nick: string;
+}
+
+router.patch("/", route({ body: "MemberNickChangeSchema" }), async (req: Request, res: Response) => {
 	var { guild_id, member_id } = req.params;
 	var permissionString: PermissionResolvable = "MANAGE_NICKNAMES";
 	if (member_id === "@me") {
diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts b/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
index cb9bad9a..ae10be82 100644
--- a/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
+++ b/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
@@ -1,24 +1,19 @@
 import { getPermission, Member } from "@fosscord/util";
+import { route } from "@fosscord/api";
 import { Request, Response, Router } from "express";
 
 const router = Router();
 
-router.delete("/:member_id/roles/:role_id", async (req: Request, res: Response) => {
+router.delete("/:member_id/roles/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
 	const { guild_id, role_id, member_id } = req.params;
 
-	const perms = await getPermission(req.user_id, guild_id);
-	perms.hasThrow("MANAGE_ROLES");
-
 	await Member.removeRole(member_id, guild_id, role_id);
 	res.sendStatus(204);
 });
 
-router.put("/:member_id/roles/:role_id", async (req: Request, res: Response) => {
+router.put("/:member_id/roles/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
 	const { guild_id, role_id, member_id } = req.params;
 
-	const perms = await getPermission(req.user_id, guild_id);
-	perms.hasThrow("MANAGE_ROLES");
-
 	await Member.addRole(member_id, guild_id, role_id);
 	res.sendStatus(204);
 });
diff --git a/api/src/routes/guilds/#guild_id/members/index.ts b/api/src/routes/guilds/#guild_id/members/index.ts
index 198d6946..335f21c7 100644
--- a/api/src/routes/guilds/#guild_id/members/index.ts
+++ b/api/src/routes/guilds/#guild_id/members/index.ts
@@ -1,13 +1,15 @@
 import { Request, Response, Router } from "express";
 import { Guild, Member, PublicMemberProjection } from "@fosscord/util";
-import { instanceOf, Length } from "@fosscord/api";
+import { instanceOf, Length, route } from "@fosscord/api";
 import { MoreThan } from "typeorm";
 
 const router = Router();
 
 // TODO: not allowed for user -> only allowed for bots with privileged intents
 // TODO: send over websocket
-router.get("/", async (req: Request, res: Response) => {
+// TODO: check for GUILD_MEMBERS intent
+
+router.get("/", route({}), async (req: Request, res: Response) => {
 	const { guild_id } = req.params;
 	const guild = await Guild.findOneOrFail({ id: guild_id });
 	await Member.IsInGuildOrFail(req.user_id, guild_id);
diff --git a/api/src/routes/guilds/#guild_id/regions.ts b/api/src/routes/guilds/#guild_id/regions.ts
index 86208b79..75d24fd1 100644
--- a/api/src/routes/guilds/#guild_id/regions.ts
+++ b/api/src/routes/guilds/#guild_id/regions.ts
@@ -1,11 +1,11 @@
 import { Config, Guild, Member } from "@fosscord/util";
 import { Request, Response, Router } from "express";
-import { getVoiceRegions } from "@fosscord/api";
+import { getVoiceRegions, route } from "@fosscord/api";
 import { getIpAdress } from "@fosscord/api";
 
 const router = Router();
 
-router.get("/", async (req: Request, res: Response) => {
+router.get("/", route({}), async (req: Request, res: Response) => {
 	const { guild_id } = req.params;
 	const guild = await Guild.findOneOrFail({ id: guild_id });
 	//TODO we should use an enum for guild's features and not hardcoded strings
diff --git a/api/src/routes/guilds/#guild_id/roles.ts b/api/src/routes/guilds/#guild_id/roles.ts
index 76dd47c5..6b2902d9 100644
--- a/api/src/routes/guilds/#guild_id/roles.ts
+++ b/api/src/routes/guilds/#guild_id/roles.ts
@@ -2,23 +2,34 @@ import { Request, Response, Router } from "express";
 import {
 	Role,
 	getPermission,
-	Snowflake,
 	Member,
 	GuildRoleCreateEvent,
 	GuildRoleUpdateEvent,
 	GuildRoleDeleteEvent,
 	emitEvent,
-	Config
+	Config,
+	DiscordApiErrors
 } from "@fosscord/util";
 import { HTTPError } from "lambert-server";
-
-import { check } from "@fosscord/api";
-import { RoleModifySchema, RolePositionUpdateSchema } from "../../../schema/Roles";
-import { DiscordApiErrors } from "@fosscord/util";
+import { check, route } from "@fosscord/api";
 import { In } from "typeorm";
 
 const router: Router = Router();
 
+export interface RoleModifySchema {
+	name?: string;
+	permissions?: bigint;
+	color?: number;
+	hoist?: boolean; // whether the role should be displayed separately in the sidebar
+	mentionable?: boolean; // whether the role should be mentionable
+	position?: number;
+}
+
+export type RolePositionUpdateSchema = {
+	id: string;
+	position: number;
+}[];
+
 router.get("/", async (req: Request, res: Response) => {
 	const guild_id = req.params.guild_id;
 
@@ -29,13 +40,10 @@ router.get("/", async (req: Request, res: Response) => {
 	return res.json(roles);
 });
 
-router.post("/", check(RoleModifySchema), async (req: Request, res: Response) => {
+router.post("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
 	const guild_id = req.params.guild_id;
 	const body = req.body as RoleModifySchema;
 
-	const perms = await getPermission(req.user_id, guild_id);
-	perms.hasThrow("MANAGE_ROLES");
-
 	const role_count = await Role.count({ guild_id });
 	const { maxRoles } = Config.get().limits.guild;
 
@@ -50,7 +58,7 @@ router.post("/", check(RoleModifySchema), async (req: Request, res: Response) =>
 		...body,
 		guild_id: guild_id,
 		managed: false,
-		permissions: String(perms.bitfield & (body.permissions || 0n)),
+		permissions: String(req.permission!.bitfield & (body.permissions || 0n)),
 		tags: undefined
 	});
 
@@ -69,14 +77,11 @@ router.post("/", check(RoleModifySchema), async (req: Request, res: Response) =>
 	res.json(role);
 });
 
-router.delete("/:role_id", async (req: Request, res: Response) => {
+router.delete("/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
 	const guild_id = req.params.guild_id;
 	const { role_id } = req.params;
 	if (role_id === guild_id) throw new HTTPError("You can't delete the @everyone role");
 
-	const permissions = await getPermission(req.user_id, guild_id);
-	permissions.hasThrow("MANAGE_ROLES");
-
 	await Promise.all([
 		Role.delete({
 			id: role_id,
@@ -97,14 +102,11 @@ router.delete("/:role_id", async (req: Request, res: Response) => {
 
 // TODO: check role hierarchy
 
-router.patch("/:role_id", check(RoleModifySchema), async (req: Request, res: Response) => {
+router.patch("/:role_id", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
 	const { role_id, guild_id } = req.params;
 	const body = req.body as RoleModifySchema;
 
-	const perms = await getPermission(req.user_id, guild_id);
-	perms.hasThrow("MANAGE_ROLES");
-
-	const role = new Role({ ...body, id: role_id, guild_id, permissions: String(perms.bitfield & (body.permissions || 0n)) });
+	const role = new Role({ ...body, id: role_id, guild_id, permissions: String(req.permission!.bitfield & (body.permissions || 0n)) });
 
 	await Promise.all([
 		role.save(),
@@ -121,7 +123,7 @@ router.patch("/:role_id", check(RoleModifySchema), async (req: Request, res: Res
 	res.json(role);
 });
 
-router.patch("/", check(RolePositionUpdateSchema), async (req: Request, res: Response) => {
+router.patch("/", route({ body: "RolePositionUpdateSchema" }), async (req: Request, res: Response) => {
 	const { guild_id } = req.params;
 	const body = req.body as RolePositionUpdateSchema;
 
diff --git a/api/src/routes/guilds/#guild_id/templates.ts b/api/src/routes/guilds/#guild_id/templates.ts
index e9304e11..5179e761 100644
--- a/api/src/routes/guilds/#guild_id/templates.ts
+++ b/api/src/routes/guilds/#guild_id/templates.ts
@@ -1,8 +1,7 @@
 import { Request, Response, Router } from "express";
-import { Guild, getPermission, Template } from "@fosscord/util";
+import { Guild, Template } from "@fosscord/util";
 import { HTTPError } from "lambert-server";
-import { TemplateCreateSchema, TemplateModifySchema } from "../../../schema/Template";
-import { check } from "@fosscord/api";
+import { route } from "@fosscord/api";
 import { generateCode } from "@fosscord/api";
 
 const router: Router = Router();
@@ -24,7 +23,17 @@ const TemplateGuildProjection: (keyof Guild)[] = [
 	"icon"
 ];
 
-router.get("/", async (req: Request, res: Response) => {
+export interface TemplateCreateSchema {
+	name: string;
+	description?: string;
+}
+
+export interface TemplateModifySchema {
+	name: string;
+	description?: string;
+}
+
+router.get("/", route({}), async (req: Request, res: Response) => {
 	const { guild_id } = req.params;
 
 	var templates = await Template.find({ source_guild_id: guild_id });
@@ -32,12 +41,9 @@ router.get("/", async (req: Request, res: Response) => {
 	return res.json(templates);
 });
 
-router.post("/", check(TemplateCreateSchema), async (req: Request, res: Response) => {
+router.post("/", route({ body: "TemplateCreateSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
 	const { guild_id } = req.params;
 	const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: TemplateGuildProjection });
-	const perms = await getPermission(req.user_id, guild_id);
-	perms.hasThrow("MANAGE_GUILD");
-
 	const exists = await Template.findOneOrFail({ id: guild_id }).catch((e) => {});
 	if (exists) throw new HTTPError("Template already exists", 400);
 
@@ -54,44 +60,31 @@ router.post("/", check(TemplateCreateSchema), async (req: Request, res: Response
 	res.json(template);
 });
 
-router.delete("/:code", async (req: Request, res: Response) => {
-	const guild_id = req.params.guild_id;
-	const { code } = req.params;
-
-	const perms = await getPermission(req.user_id, guild_id);
-	perms.hasThrow("MANAGE_GUILD");
+router.delete("/:code", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
+	const { code, guild_id } = req.params;
 
 	const template = await Template.delete({
-		code
+		code,
+		source_guild_id: guild_id
 	});
 
 	res.json(template);
 });
 
-router.put("/:code", async (req: Request, res: Response) => {
-	// synchronizes the template
+router.put("/:code", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
 	const { code, guild_id } = req.params;
-
 	const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: TemplateGuildProjection });
 
-	const perms = await getPermission(req.user_id, guild_id);
-	perms.hasThrow("MANAGE_GUILD");
-
 	const template = await new Template({ code, serialized_source_guild: guild }).save();
 
 	res.json(template);
 });
 
-router.patch("/:code", check(TemplateModifySchema), async (req: Request, res: Response) => {
-	// updates the template description
-	const { guild_id } = req.params;
-	const { code } = req.params;
+router.patch("/:code", route({ body: "TemplateModifySchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
+	const { code, guild_id } = req.params;
 	const { name, description } = req.body;
 
-	const perms = await getPermission(req.user_id, guild_id);
-	perms.hasThrow("MANAGE_GUILD");
-
-	const template = await new Template({ code, name: name, description: description }).save();
+	const template = await new Template({ code, name: name, description: description, source_guild_id: guild_id }).save();
 
 	res.json(template);
 });
diff --git a/api/src/routes/guilds/#guild_id/vanity-url.ts b/api/src/routes/guilds/#guild_id/vanity-url.ts
index f1887cc0..9c0989cc 100644
--- a/api/src/routes/guilds/#guild_id/vanity-url.ts
+++ b/api/src/routes/guilds/#guild_id/vanity-url.ts
@@ -1,35 +1,37 @@
 import { Channel, ChannelType, getPermission, Guild, Invite, trimSpecial } from "@fosscord/util";
 import { Router, Request, Response } from "express";
-import { HTTPError } from "lambert-server";
-import { check, Length } from "@fosscord/api";
+import { check, Length, route } from "@fosscord/api";
 
 const router = Router();
 
 const InviteRegex = /\W/g;
 
-router.get("/", async (req: Request, res: Response) => {
+router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
 	const { guild_id } = req.params;
 
-	const permission = await getPermission(req.user_id, guild_id);
-	permission.hasThrow("MANAGE_GUILD");
-
 	const guild = await Guild.findOneOrFail({ where: { id: guild_id }, relations: ["vanity_url"] });
 	if (!guild.vanity_url) return res.json({ code: null });
 
 	return res.json({ code: guild.vanity_url_code, uses: guild.vanity_url.uses });
 });
 
+export interface VanityUrlSchema {
+	/**
+	 * @minLength 1
+	 * @maxLength 20
+	 */
+	code?: string;
+}
+
 // TODO: check if guild is elgible for vanity url
-router.patch("/", check({ code: new Length(String, 0, 20) }), async (req: Request, res: Response) => {
+router.patch("/", route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
 	const { guild_id } = req.params;
-	const code = req.body.code.replace(InviteRegex);
+	const body = req.body as VanityUrlSchema;
+	const code = body.code?.replace(InviteRegex, "");
 
 	await Invite.findOneOrFail({ code });
 
 	const guild = await Guild.findOneOrFail({ id: guild_id });
-	const permission = await getPermission(req.user_id, guild_id);
-	permission.hasThrow("MANAGE_GUILD");
-
 	const { id } = await Channel.findOneOrFail({ guild_id, type: ChannelType.GUILD_TEXT });
 	guild.vanity_url_code = code;
 
diff --git a/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts b/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts
index 447e15c1..3d76938b 100644
--- a/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts
+++ b/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts
@@ -1,14 +1,59 @@
-import { check } from "@fosscord/api";
-import { VoiceStateUpdateSchema } from "../../../../../schema";
+import { Channel, ChannelType, DiscordApiErrors, emitEvent, getPermission, VoiceState, VoiceStateUpdateEvent } from "@fosscord/util";
+import { check, route } from "@fosscord/api";
 import { Request, Response, Router } from "express";
-import { updateVoiceState } from "@fosscord/api";
 
 const router = Router();
+//TODO need more testing when community guild and voice stage channel are working
 
-router.patch("/", check(VoiceStateUpdateSchema), async (req: Request, res: Response) => {
+export interface VoiceStateUpdateSchema {
+	channel_id: string;
+	guild_id?: string;
+	suppress?: boolean;
+	request_to_speak_timestamp?: Date;
+	self_mute?: boolean;
+	self_deaf?: boolean;
+	self_video?: boolean;
+}
+
+router.patch("/", route({ body: "VoiceStateUpdateSchema" }), async (req: Request, res: Response) => {
 	const body = req.body as VoiceStateUpdateSchema;
-	const { guild_id, user_id } = req.params;
-	await updateVoiceState(body, guild_id, req.user_id, user_id);
+	var { guild_id, user_id } = req.params;
+	if (user_id === "@me") user_id = req.user_id;
+
+	const perms = await getPermission(req.user_id, guild_id, body.channel_id);
+
+	/*
+	From https://discord.com/developers/docs/resources/guild#modify-current-user-voice-state
+	You must have the MUTE_MEMBERS permission to unsuppress others. You can always suppress yourself.
+	You must have the REQUEST_TO_SPEAK permission to request to speak. You can always clear your own request to speak.
+	 */
+	if (body.suppress && user_id !== req.user_id) {
+		perms.hasThrow("MUTE_MEMBERS");
+	}
+	if (!body.suppress) body.request_to_speak_timestamp = new Date();
+	if (body.request_to_speak_timestamp) perms.hasThrow("REQUEST_TO_SPEAK");
+
+	const voice_state = await VoiceState.findOne({
+		guild_id,
+		channel_id: body.channel_id,
+		user_id
+	});
+	if (!voice_state) throw DiscordApiErrors.UNKNOWN_VOICE_STATE;
+
+	voice_state.assign(body);
+	const channel = await Channel.findOneOrFail({ guild_id, id: body.channel_id });
+	if (channel.type !== ChannelType.GUILD_STAGE_VOICE) {
+		throw DiscordApiErrors.CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE;
+	}
+
+	await Promise.all([
+		voice_state.save(),
+		emitEvent({
+			event: "VOICE_STATE_UPDATE",
+			data: voice_state,
+			guild_id
+		} as VoiceStateUpdateEvent)
+	]);
 	return res.sendStatus(204);
 });
 
diff --git a/api/src/routes/guilds/#guild_id/voice-states/@me/index.ts b/api/src/routes/guilds/#guild_id/voice-states/@me/index.ts
deleted file mode 100644
index b637ff66..00000000
--- a/api/src/routes/guilds/#guild_id/voice-states/@me/index.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { check } from "@fosscord/api";
-import { VoiceStateUpdateSchema } from "../../../../../schema";
-import { Request, Response, Router } from "express";
-import { updateVoiceState } from "@fosscord/api";
-
-const router = Router();
-
-router.patch("/", check(VoiceStateUpdateSchema), async (req: Request, res: Response) => {
-	const body = req.body as VoiceStateUpdateSchema;
-	const { guild_id } = req.params;
-	await updateVoiceState(body, guild_id, req.user_id);
-	return res.sendStatus(204);
-});
-
-export default router;
diff --git a/api/src/routes/guilds/#guild_id/welcome_screen.ts b/api/src/routes/guilds/#guild_id/welcome_screen.ts
index 7ca49b4e..7141f17e 100644
--- a/api/src/routes/guilds/#guild_id/welcome_screen.ts
+++ b/api/src/routes/guilds/#guild_id/welcome_screen.ts
@@ -1,31 +1,36 @@
 import { Request, Response, Router } from "express";
 import { Guild, getPermission, Snowflake, Member } from "@fosscord/util";
 import { HTTPError } from "lambert-server";
-
-import { check } from "@fosscord/api";
-import { GuildUpdateWelcomeScreenSchema } from "../../../schema/Guild";
+import { route } from "@fosscord/api";
 
 const router: Router = Router();
 
-router.get("/", async (req: Request, res: Response) => {
+export interface GuildUpdateWelcomeScreenSchema {
+	welcome_channels?: {
+		channel_id: string;
+		description: string;
+		emoji_id?: string;
+		emoji_name: string;
+	}[];
+	enabled?: boolean;
+	description?: string;
+}
+
+router.get("/", route({}), async (req: Request, res: Response) => {
 	const guild_id = req.params.guild_id;
 
 	const guild = await Guild.findOneOrFail({ id: guild_id });
-
 	await Member.IsInGuildOrFail(req.user_id, guild_id);
 
 	res.json(guild.welcome_screen);
 });
 
-router.patch("/", check(GuildUpdateWelcomeScreenSchema), async (req: Request, res: Response) => {
+router.patch("/", route({ body: "GuildUpdateWelcomeScreenSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
 	const guild_id = req.params.guild_id;
 	const body = req.body as GuildUpdateWelcomeScreenSchema;
 
 	const guild = await Guild.findOneOrFail({ id: guild_id });
 
-	const perms = await getPermission(req.user_id, guild_id);
-	perms.hasThrow("MANAGE_GUILD");
-
 	if (!guild.welcome_screen.enabled) throw new HTTPError("Welcome screen disabled", 400);
 	if (body.welcome_channels) guild.welcome_screen.welcome_channels = body.welcome_channels; // TODO: check if they exist and are valid
 	if (body.description) guild.welcome_screen.description = body.description;
diff --git a/api/src/routes/guilds/#guild_id/widget.json.ts b/api/src/routes/guilds/#guild_id/widget.json.ts
index f871fac8..c31519fa 100644
--- a/api/src/routes/guilds/#guild_id/widget.json.ts
+++ b/api/src/routes/guilds/#guild_id/widget.json.ts
@@ -1,7 +1,7 @@
 import { Request, Response, Router } from "express";
 import { Config, Permissions, Guild, Invite, Channel, Member } from "@fosscord/util";
 import { HTTPError } from "lambert-server";
-import { random } from "@fosscord/api";
+import { random, route } from "@fosscord/api";
 
 const router: Router = Router();
 
@@ -14,7 +14,7 @@ const router: Router = Router();
 
 // https://discord.com/developers/docs/resources/guild#get-guild-widget
 // TODO: Cache the response for a guild for 5 minutes regardless of response
-router.get("/", async (req: Request, res: Response) => {
+router.get("/", route({}), async (req: Request, res: Response) => {
 	const { guild_id } = req.params;
 
 	const guild = await Guild.findOneOrFail({ id: guild_id });
diff --git a/api/src/routes/guilds/#guild_id/widget.png.ts b/api/src/routes/guilds/#guild_id/widget.png.ts
index 89b31153..4c82b740 100644
--- a/api/src/routes/guilds/#guild_id/widget.png.ts
+++ b/api/src/routes/guilds/#guild_id/widget.png.ts
@@ -1,6 +1,7 @@
 import { Request, Response, Router } from "express";
 import { Guild } from "@fosscord/util";
 import { HTTPError } from "lambert-server";
+import { route } from "@fosscord/api";
 import fs from "fs";
 import path from "path";
 
@@ -10,7 +11,7 @@ const router: Router = Router();
 
 // https://discord.com/developers/docs/resources/guild#get-guild-widget-image
 // TODO: Cache the response
-router.get("/", async (req: Request, res: Response) => {
+router.get("/", route({}), async (req: Request, res: Response) => {
 	const { guild_id } = req.params;
 
 	const guild = await Guild.findOneOrFail({ id: guild_id });
diff --git a/api/src/routes/guilds/#guild_id/widget.ts b/api/src/routes/guilds/#guild_id/widget.ts
index d9ce817e..c8caae14 100644
--- a/api/src/routes/guilds/#guild_id/widget.ts
+++ b/api/src/routes/guilds/#guild_id/widget.ts
@@ -1,31 +1,29 @@
 import { Request, Response, Router } from "express";
 import { getPermission, Guild } from "@fosscord/util";
 import { HTTPError } from "lambert-server";
-import { check } from "@fosscord/api";
-import { WidgetModifySchema } from "../../../schema/Widget";
+import { check, route } from "@fosscord/api";
+
+export interface WidgetModifySchema {
+	enabled: boolean; // whether the widget is enabled
+	channel_id: string; // the widget channel id
+}
 
 const router: Router = Router();
 
 // https://discord.com/developers/docs/resources/guild#get-guild-widget-settings
-router.get("/", async (req: Request, res: Response) => {
+router.get("/", route({}), async (req: Request, res: Response) => {
 	const { guild_id } = req.params;
 
-	const perms = await getPermission(req.user_id, guild_id);
-	perms.hasThrow("MANAGE_GUILD");
-
 	const guild = await Guild.findOneOrFail({ id: guild_id });
 
 	return res.json({ enabled: guild.widget_enabled || false, channel_id: guild.widget_channel_id || null });
 });
 
 // https://discord.com/developers/docs/resources/guild#modify-guild-widget
-router.patch("/", check(WidgetModifySchema), async (req: Request, res: Response) => {
+router.patch("/", route({ body: "WidgetModifySchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
 	const body = req.body as WidgetModifySchema;
 	const { guild_id } = req.params;
 
-	const perms = await getPermission(req.user_id, guild_id);
-	perms.hasThrow("MANAGE_GUILD");
-
 	await Guild.update({ id: guild_id }, { widget_enabled: body.enabled, widget_channel_id: body.channel_id });
 	// Widget invite for the widget_channel_id gets created as part of the /guilds/{guild.id}/widget.json request
 
diff --git a/api/src/routes/guilds/index.ts b/api/src/routes/guilds/index.ts
index a1b199e7..ba951f96 100644
--- a/api/src/routes/guilds/index.ts
+++ b/api/src/routes/guilds/index.ts
@@ -1,15 +1,28 @@
 import { Router, Request, Response } from "express";
 import { Role, Guild, Snowflake, Config, User, Member, Channel } from "@fosscord/util";
 import { HTTPError } from "lambert-server";
-import { check } from "@fosscord/api";
-import { GuildCreateSchema } from "../../schema/Guild";
+import { check, route } from "@fosscord/api";
 import { DiscordApiErrors } from "@fosscord/util";
+import { ChannelModifySchema } from "../channels/#channel_id";
 
 const router: Router = Router();
 
+export interface GuildCreateSchema {
+	/**
+	 * @maxLength 100
+	 */
+	name: string;
+	region?: string;
+	icon?: string;
+	channels?: ChannelModifySchema[];
+	guild_template_code?: string;
+	system_channel_id?: string;
+	rules_channel_id?: string;
+}
+
 //TODO: create default channel
 
-router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) => {
+router.post("/", route({ body: "GuildCreateSchema" }), async (req: Request, res: Response) => {
 	const body = req.body as GuildCreateSchema;
 
 	const { maxGuilds } = Config.get().limits.user;
diff --git a/api/src/routes/guilds/templates/index.ts b/api/src/routes/guilds/templates/index.ts
index 1d0f2716..d7a42044 100644
--- a/api/src/routes/guilds/templates/index.ts
+++ b/api/src/routes/guilds/templates/index.ts
@@ -2,11 +2,15 @@ import { Request, Response, Router } from "express";
 const router: Router = Router();
 import { Template, Guild, Role, Snowflake, Config, User, Member } from "@fosscord/util";
 import { HTTPError } from "lambert-server";
-import { GuildTemplateCreateSchema } from "../../../schema/Guild";
-import { check } from "@fosscord/api";
+import { check, route } from "@fosscord/api";
 import { DiscordApiErrors } from "@fosscord/util";
 
-router.get("/:code", async (req: Request, res: Response) => {
+export interface GuildTemplateCreateSchema {
+	name: string;
+	avatar?: string;
+}
+
+router.get("/:code", route({}), async (req: Request, res: Response) => {
 	const { code } = req.params;
 
 	const template = await Template.findOneOrFail({ code: code });
@@ -14,7 +18,7 @@ router.get("/:code", async (req: Request, res: Response) => {
 	res.json(template);
 });
 
-router.post("/:code", check(GuildTemplateCreateSchema), async (req: Request, res: Response) => {
+router.post("/:code", route({ body: "GuildTemplateCreateSchema" }), async (req: Request, res: Response) => {
 	const { code } = req.params;
 	const body = req.body as GuildTemplateCreateSchema;