summary refs log tree commit diff
path: root/src/api/routes
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/routes')
-rw-r--r--src/api/routes/-/healthz.ts17
-rw-r--r--src/api/routes/-/readyz.ts17
-rw-r--r--src/api/routes/applications/#id/entitlements.ts12
-rw-r--r--src/api/routes/applications/detectable.ts11
-rw-r--r--src/api/routes/applications/index.ts34
-rw-r--r--src/api/routes/auth/location-metadata.ts13
-rw-r--r--src/api/routes/auth/login.ts87
-rw-r--r--src/api/routes/auth/mfa/totp.ts42
-rw-r--r--src/api/routes/auth/register.ts157
-rw-r--r--src/api/routes/channels/#channel_id/followers.ts14
-rw-r--r--src/api/routes/channels/#channel_id/index.ts72
-rw-r--r--src/api/routes/channels/#channel_id/invites.ts58
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/ack.ts33
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts28
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/index.ts199
-rw-r--r--src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts185
-rw-r--r--src/api/routes/channels/#channel_id/messages/bulk-delete.ts43
-rw-r--r--src/api/routes/channels/#channel_id/messages/index.ts241
-rw-r--r--src/api/routes/channels/#channel_id/permissions.ts81
-rw-r--r--src/api/routes/channels/#channel_id/pins.ts90
-rw-r--r--src/api/routes/channels/#channel_id/purge.ts64
-rw-r--r--src/api/routes/channels/#channel_id/recipients.ts68
-rw-r--r--src/api/routes/channels/#channel_id/typing.ts29
-rw-r--r--src/api/routes/channels/#channel_id/webhooks.ts34
-rw-r--r--src/api/routes/discoverable-guilds.ts39
-rw-r--r--src/api/routes/discovery.ts18
-rw-r--r--src/api/routes/downloads.ts20
-rw-r--r--src/api/routes/experiments.ts11
-rw-r--r--src/api/routes/gateway/bot.ts40
-rw-r--r--src/api/routes/gateway/index.ts24
-rw-r--r--src/api/routes/gifs/search.ts28
-rw-r--r--src/api/routes/gifs/trending-gifs.ts28
-rw-r--r--src/api/routes/gifs/trending.ts62
-rw-r--r--src/api/routes/guild-recommendations.ts24
-rw-r--r--src/api/routes/guilds/#guild_id/audit-logs.ts17
-rw-r--r--src/api/routes/guilds/#guild_id/bans.ts157
-rw-r--r--src/api/routes/guilds/#guild_id/channels.ts57
-rw-r--r--src/api/routes/guilds/#guild_id/delete.ts30
-rw-r--r--src/api/routes/guilds/#guild_id/discovery-requirements.ts39
-rw-r--r--src/api/routes/guilds/#guild_id/emojis.ts107
-rw-r--r--src/api/routes/guilds/#guild_id/index.ts60
-rw-r--r--src/api/routes/guilds/#guild_id/integrations.ts11
-rw-r--r--src/api/routes/guilds/#guild_id/invites.ts15
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/index.ts98
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/nick.ts22
-rw-r--r--src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts21
-rw-r--r--src/api/routes/guilds/#guild_id/members/index.ts31
-rw-r--r--src/api/routes/guilds/#guild_id/premium.ts10
-rw-r--r--src/api/routes/guilds/#guild_id/prune.ts79
-rw-r--r--src/api/routes/guilds/#guild_id/regions.ts15
-rw-r--r--src/api/routes/guilds/#guild_id/roles/#role_id/index.ts68
-rw-r--r--src/api/routes/guilds/#guild_id/roles/index.ts98
-rw-r--r--src/api/routes/guilds/#guild_id/stickers.ts121
-rw-r--r--src/api/routes/guilds/#guild_id/templates.ts83
-rw-r--r--src/api/routes/guilds/#guild_id/vanity-url.ts59
-rw-r--r--src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts51
-rw-r--r--src/api/routes/guilds/#guild_id/webhooks.ts11
-rw-r--r--src/api/routes/guilds/#guild_id/welcome_screen.ts31
-rw-r--r--src/api/routes/guilds/#guild_id/widget.json.ts83
-rw-r--r--src/api/routes/guilds/#guild_id/widget.png.ts111
-rw-r--r--src/api/routes/guilds/#guild_id/widget.ts27
-rw-r--r--src/api/routes/guilds/index.ts32
-rw-r--r--src/api/routes/guilds/templates/index.ts79
-rw-r--r--src/api/routes/invites/index.ts57
-rw-r--r--src/api/routes/oauth2/tokens.ts10
-rw-r--r--src/api/routes/outbound-promotions.ts11
-rw-r--r--src/api/routes/partners/#guild_id/requirements.ts40
-rw-r--r--src/api/routes/ping.ts26
-rw-r--r--src/api/routes/policies/instance/domains.ts18
-rw-r--r--src/api/routes/policies/instance/index.ts12
-rw-r--r--src/api/routes/policies/instance/limits.ts11
-rw-r--r--src/api/routes/scheduled-maintenances/upcoming_json.ts12
-rw-r--r--src/api/routes/science.ts11
-rw-r--r--src/api/routes/stage-instances.ts11
-rw-r--r--src/api/routes/sticker-packs/index.ts13
-rw-r--r--src/api/routes/stickers/#sticker_id/index.ts12
-rw-r--r--src/api/routes/stop.ts26
-rw-r--r--src/api/routes/store/published-listings/applications.ts79
-rw-r--r--src/api/routes/store/published-listings/applications/#id/subscription-plans.ts25
-rw-r--r--src/api/routes/store/published-listings/skus.ts79
-rw-r--r--src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts142
-rw-r--r--src/api/routes/teams.ts11
-rw-r--r--src/api/routes/template.ts.disabled11
-rw-r--r--src/api/routes/track.ts11
-rw-r--r--src/api/routes/updates.ts20
-rw-r--r--src/api/routes/users/#id/index.ts13
-rw-r--r--src/api/routes/users/#id/profile.ts58
-rw-r--r--src/api/routes/users/#id/relationships.ts41
-rw-r--r--src/api/routes/users/@me/activities/statistics/applications.ts11
-rw-r--r--src/api/routes/users/@me/affinities/guilds.ts11
-rw-r--r--src/api/routes/users/@me/affinities/users.ts11
-rw-r--r--src/api/routes/users/@me/applications/#app_id/entitlements.ts11
-rw-r--r--src/api/routes/users/@me/billing/country-code.ts11
-rw-r--r--src/api/routes/users/@me/billing/payment-sources.ts11
-rw-r--r--src/api/routes/users/@me/billing/subscriptions.ts11
-rw-r--r--src/api/routes/users/@me/channels.ts20
-rw-r--r--src/api/routes/users/@me/connections.ts11
-rw-r--r--src/api/routes/users/@me/delete.ts32
-rw-r--r--src/api/routes/users/@me/devices.ts11
-rw-r--r--src/api/routes/users/@me/disable.ts26
-rw-r--r--src/api/routes/users/@me/email-settings.ts20
-rw-r--r--src/api/routes/users/@me/entitlements.ts11
-rw-r--r--src/api/routes/users/@me/guilds.ts57
-rw-r--r--src/api/routes/users/@me/guilds/premium/subscription-slots.ts11
-rw-r--r--src/api/routes/users/@me/index.ts72
-rw-r--r--src/api/routes/users/@me/library.ts11
-rw-r--r--src/api/routes/users/@me/mfa/codes.ts45
-rw-r--r--src/api/routes/users/@me/mfa/totp/disable.ts41
-rw-r--r--src/api/routes/users/@me/mfa/totp/enable.ts48
-rw-r--r--src/api/routes/users/@me/notes.ts60
-rw-r--r--src/api/routes/users/@me/relationships.ts204
-rw-r--r--src/api/routes/users/@me/settings.ts18
-rw-r--r--src/api/routes/voice/regions.ts11
113 files changed, 5092 insertions, 0 deletions
diff --git a/src/api/routes/-/healthz.ts b/src/api/routes/-/healthz.ts
new file mode 100644
index 00000000..f7bcfebf
--- /dev/null
+++ b/src/api/routes/-/healthz.ts
@@ -0,0 +1,17 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+import { getConnection } from "typeorm";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	try {
+		// test that the database is alive & responding
+		getConnection();
+		return res.sendStatus(200);
+	} catch (e) {
+		res.sendStatus(503);
+	}
+});
+
+export default router;
diff --git a/src/api/routes/-/readyz.ts b/src/api/routes/-/readyz.ts
new file mode 100644
index 00000000..f7bcfebf
--- /dev/null
+++ b/src/api/routes/-/readyz.ts
@@ -0,0 +1,17 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+import { getConnection } from "typeorm";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	try {
+		// test that the database is alive & responding
+		getConnection();
+		return res.sendStatus(200);
+	} catch (e) {
+		res.sendStatus(503);
+	}
+});
+
+export default router;
diff --git a/src/api/routes/applications/#id/entitlements.ts b/src/api/routes/applications/#id/entitlements.ts
new file mode 100644
index 00000000..cfcfe40f
--- /dev/null
+++ b/src/api/routes/applications/#id/entitlements.ts
@@ -0,0 +1,12 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	//const { exclude_consumed } = req.query;
+	res.status(200).send([]);
+});
+
+export default router;
diff --git a/src/api/routes/applications/detectable.ts b/src/api/routes/applications/detectable.ts
new file mode 100644
index 00000000..28ce42da
--- /dev/null
+++ b/src/api/routes/applications/detectable.ts
@@ -0,0 +1,11 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	//TODO
+	res.send([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/applications/index.ts b/src/api/routes/applications/index.ts
new file mode 100644
index 00000000..033dcc51
--- /dev/null
+++ b/src/api/routes/applications/index.ts
@@ -0,0 +1,34 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+import { Application, OrmUtils, Team, trimSpecial, User } from "@fosscord/util";
+
+const router: Router = Router();
+
+export interface ApplicationCreateSchema {
+	name: string;
+	team_id?: string | number;
+}
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	//TODO
+	let results = await Application.find({where: {owner: {id: req.user_id}}, relations: ["owner", "bot"] });
+	res.json(results).status(200);
+});
+
+router.post("/", route({}), async (req: Request, res: Response) => {
+	const body = req.body as ApplicationCreateSchema;
+	const user = await User.findOne({where: {id: req.user_id}})
+	if(!user) res.status(420);
+	let app = OrmUtils.mergeDeep(new Application(), {
+		name: trimSpecial(body.name),
+		description: "",
+		bot_public: true,
+		owner: user,
+		verify_key: "IMPLEMENTME",
+		flags: 0
+	});
+	await app.save();
+	res.json(app).status(200);
+});
+
+export default router;
\ No newline at end of file
diff --git a/src/api/routes/auth/location-metadata.ts b/src/api/routes/auth/location-metadata.ts
new file mode 100644
index 00000000..f4c2bd16
--- /dev/null
+++ b/src/api/routes/auth/location-metadata.ts
@@ -0,0 +1,13 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { getIpAdress, IPAnalysis } from "@fosscord/api";
+const router = Router();
+
+router.get("/",route({}), async (req: Request, res: Response) => {
+    //TODO
+    //Note: It's most likely related to legal. At the moment Discord hasn't finished this too
+    const country_code = (await IPAnalysis(getIpAdress(req))).country_code;
+	res.json({ consent_required: false, country_code: country_code, promotional_email_opt_in: { required: true, pre_checked: false}});
+});
+
+export default router;
diff --git a/src/api/routes/auth/login.ts b/src/api/routes/auth/login.ts
new file mode 100644
index 00000000..9fc5924d
--- /dev/null
+++ b/src/api/routes/auth/login.ts
@@ -0,0 +1,87 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+import bcrypt from "bcrypt";
+import { Config, User, generateToken, adjustEmail, FieldErrors, LoginSchema } from "@fosscord/util";
+import crypto from "crypto";
+
+const router: Router = Router();
+export default router;
+
+router.post("/", route({ body: "LoginSchema" }), async (req: Request, res: Response) => {
+	const { login, password, captcha_key, undelete } = req.body as LoginSchema;
+	const email = adjustEmail(login);
+
+	const config = Config.get();
+
+	if (config.login.requireCaptcha && config.security.captcha.enabled) {
+		if (!captcha_key) {
+			const { sitekey, service } = config.security.captcha;
+			return res.status(400).json({
+				captcha_key: ["captcha-required"],
+				captcha_sitekey: sitekey,
+				captcha_service: service
+			});
+		}
+
+		// TODO: check captcha
+	}
+
+	const user = await User.findOneOrFail({
+		where: [{ phone: login }, { email: login }],
+		select: ["data", "id", "disabled", "deleted", "settings", "totp_secret", "mfa_enabled"]
+	}).catch((e) => {
+		throw FieldErrors({ login: { message: req.t("auth:login.INVALID_LOGIN"), code: "INVALID_LOGIN" } });
+	});
+
+	if (undelete) {
+		// undelete refers to un'disable' here
+		if (user.disabled) await User.update({ id: user.id }, { disabled: false });
+		if (user.deleted) await User.update({ id: user.id }, { deleted: false });
+	} else {
+		if (user.deleted) return res.status(400).json({ message: "This account is scheduled for deletion.", code: 20011 });
+		if (user.disabled) return res.status(400).json({ message: req.t("auth:login.ACCOUNT_DISABLED"), code: 20013 });
+	}
+
+	// the salt is saved in the password refer to bcrypt docs
+	const same_password = await bcrypt.compare(password, user.data.hash || "");
+	if (!same_password) {
+		throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } });
+	}
+
+	if (user.mfa_enabled) {
+		// TODO: This is not a discord.com ticket. I'm not sure what it is but I'm lazy
+		const ticket = crypto.randomBytes(40).toString("hex");
+
+		await User.update({ id: user.id }, { totp_last_ticket: ticket });
+
+		return res.json({
+			ticket: ticket,
+			mfa: true,
+			sms: false,	// TODO
+			token: null,
+		})
+	}
+
+	const token = await generateToken(user.id);
+
+	// Notice this will have a different token structure, than discord
+	// Discord header is just the user id as string, which is not possible with npm-jsonwebtoken package
+	// https://user-images.githubusercontent.com/6506416/81051916-dd8c9900-8ec2-11ea-8794-daf12d6f31f0.png
+
+	res.json({ token, settings: user.settings });
+});
+
+/**
+ * POST /auth/login
+ * @argument { login: "email@gmail.com", password: "cleartextpassword", undelete: false, captcha_key: null, login_source: null, gift_code_sku_id: null, }
+
+ * MFA required:
+ * @returns {"token": null, "mfa": true, "sms": true, "ticket": "SOME TICKET JWT TOKEN"}
+
+ * Captcha required:
+ * @returns {"captcha_key": ["captcha-required"], "captcha_sitekey": null, "captcha_service": "recaptcha"}
+
+ * Sucess:
+ * @returns {"token": "USERTOKEN", "settings": {"locale": "en", "theme": "dark"}}
+
+ */
diff --git a/src/api/routes/auth/mfa/totp.ts b/src/api/routes/auth/mfa/totp.ts
new file mode 100644
index 00000000..421dbafa
--- /dev/null
+++ b/src/api/routes/auth/mfa/totp.ts
@@ -0,0 +1,42 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { BackupCode, FieldErrors, generateToken, TotpSchema, User } from "@fosscord/util";
+import { verifyToken } from "node-2fa";
+import { HTTPError } from "lambert-server";
+const router = Router();
+
+router.post("/", route({ body: "TotpSchema" }), async (req: Request, res: Response) => {
+	const { code, ticket, gift_code_sku_id, login_source } = req.body as TotpSchema;
+
+	const user = await User.findOneOrFail({
+		where: {
+			totp_last_ticket: ticket,
+		},
+		select: [
+			"id",
+			"totp_secret",
+			"settings",
+		],
+	});
+
+	const backup = await BackupCode.findOne({ where: { code: code, expired: false, consumed: false, user: { id: user.id } } });
+
+	if (!backup) {
+		const ret = verifyToken(user.totp_secret!, code);
+		if (!ret || ret.delta != 0)
+			throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+	}
+	else {
+		backup.consumed = true;
+		await backup.save();
+	}
+
+	await User.update({ id: user.id }, { totp_last_ticket: "" });
+
+	return res.json({
+		token: await generateToken(user.id),
+		user_settings: user.settings,
+	});
+});
+
+export default router;
diff --git a/src/api/routes/auth/register.ts b/src/api/routes/auth/register.ts
new file mode 100644
index 00000000..09366a12
--- /dev/null
+++ b/src/api/routes/auth/register.ts
@@ -0,0 +1,157 @@
+import { Request, Response, Router } from "express";
+import { Config, generateToken, Invite, FieldErrors, User, adjustEmail, trimSpecial, RegisterSchema } from "@fosscord/util";
+import { route, getIpAdress, IPAnalysis, isProxy } from "@fosscord/api";
+import bcrypt from "bcrypt";
+import { HTTPError } from "@fosscord/util";
+
+const router: Router = Router();
+
+router.post("/", route({ body: "RegisterSchema" }), async (req: Request, res: Response) => {
+	const body = req.body as RegisterSchema;
+	const { register, security } = Config.get();
+	const ip = getIpAdress(req);
+
+	// email will be slightly modified version of the user supplied email -> e.g. protection against GMail Trick
+	let email = adjustEmail(body.email);
+
+	// check if registration is allowed
+	if (!register.allowNewRegistration) {
+		throw FieldErrors({
+			email: { code: "REGISTRATION_DISABLED", message: req.t("auth:register.REGISTRATION_DISABLED") }
+		});
+	}
+
+	// check if the user agreed to the Terms of Service
+	if (!body.consent) {
+		throw FieldErrors({
+			consent: { code: "CONSENT_REQUIRED", message: req.t("auth:register.CONSENT_REQUIRED") }
+		});
+	}
+
+	if (register.disabled) {
+		throw FieldErrors({
+			email: {
+				code: "DISABLED",
+				message: "registration is disabled on this instance"
+			}
+		});
+	}
+
+	if (register.requireCaptcha && security.captcha.enabled) {
+		if (!body.captcha_key) {
+			const { sitekey, service } = security.captcha;
+			return res?.status(400).json({
+				captcha_key: ["captcha-required"],
+				captcha_sitekey: sitekey,
+				captcha_service: service
+			});
+		}
+
+		// TODO: check captcha
+	}
+
+	if (!register.allowMultipleAccounts) {
+		// TODO: check if fingerprint was eligible generated
+		const exists = await User.findOne({ where: { fingerprints: body.fingerprint }, select: ["id"] });
+
+		if (exists) {
+			throw FieldErrors({
+				email: {
+					code: "EMAIL_ALREADY_REGISTERED",
+					message: req.t("auth:register.EMAIL_ALREADY_REGISTERED")
+				}
+			});
+		}
+	}
+
+	if (register.blockProxies) {
+		if (isProxy(await IPAnalysis(ip))) {
+			console.log(`proxy ${ip} blocked from registration`);
+			throw new HTTPError("Your IP is blocked from registration");
+		}
+	}
+
+	// TODO: gift_code_sku_id?
+	// TODO: check password strength
+
+	if (email) {
+		// replace all dots and chars after +, if its a gmail.com email
+		if (!email) {
+			throw FieldErrors({ email: { code: "INVALID_EMAIL", message: req?.t("auth:register.INVALID_EMAIL") } });
+		}
+
+		// check if there is already an account with this email
+		const exists = await User.findOne({ where: { email: email } });
+
+		if (exists) {
+			throw FieldErrors({
+				email: {
+					code: "EMAIL_ALREADY_REGISTERED",
+					message: req.t("auth:register.EMAIL_ALREADY_REGISTERED")
+				}
+			});
+		}
+	} else if (register.email.required) {
+		throw FieldErrors({
+			email: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }
+		});
+	}
+
+	if (register.dateOfBirth.required && !body.date_of_birth) {
+		throw FieldErrors({
+			date_of_birth: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }
+		});
+	} else if (register.dateOfBirth.required && register.dateOfBirth.minimum) {
+		const minimum = new Date();
+		minimum.setFullYear(minimum.getFullYear() - register.dateOfBirth.minimum);
+		body.date_of_birth = new Date(body.date_of_birth as Date);
+
+		// higher is younger
+		if (body.date_of_birth > minimum) {
+			throw FieldErrors({
+				date_of_birth: {
+					code: "DATE_OF_BIRTH_UNDERAGE",
+					message: req.t("auth:register.DATE_OF_BIRTH_UNDERAGE", { years: register.dateOfBirth.minimum })
+				}
+			});
+		}
+	}
+
+	if (body.password) {
+		// the salt is saved in the password refer to bcrypt docs
+		body.password = await bcrypt.hash(body.password, 12);
+	} else if (register.password.required) {
+		throw FieldErrors({
+			password: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }
+		});
+	}
+
+	if (!body.invite && (register.requireInvite || (register.guestsRequireInvite && !register.email))) {
+		// require invite to register -> e.g. for organizations to send invites to their employees
+		throw FieldErrors({
+			email: { code: "INVITE_ONLY", message: req.t("auth:register.INVITE_ONLY") }
+		});
+	}
+
+	const user = await User.register({ ...body, req });
+
+	if (body.invite) {
+		// await to fail if the invite doesn't exist (necessary for requireInvite to work properly) (username only signups are possible)
+		await Invite.joinGuild(user.id, body.invite);
+	}
+
+	return res.json({ token: await generateToken(user.id) });
+});
+
+export default router;
+
+/**
+ * POST /auth/register
+ * @argument { "fingerprint":"805826570869932034.wR8vi8lGlFBJerErO9LG5NViJFw", "email":"qo8etzvaf@gmail.com", "username":"qp39gr98", "password":"wtp9gep9gw", "invite":null, "consent":true, "date_of_birth":"2000-04-04", "gift_code_sku_id":null, "captcha_key":null}
+ *
+ * Field Error
+ * @returns { "code": 50035, "errors": { "consent": { "_errors": [{ "code": "CONSENT_REQUIRED", "message": "You must agree to Discord's Terms of Service and Privacy Policy." }]}}, "message": "Invalid Form Body"}
+ *
+ * Success 200:
+ * @returns {token: "OMITTED"}
+ */
diff --git a/src/api/routes/channels/#channel_id/followers.ts b/src/api/routes/channels/#channel_id/followers.ts
new file mode 100644
index 00000000..641af4f8
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/followers.ts
@@ -0,0 +1,14 @@
+import { Router, Response, Request } from "express";
+const router: Router = Router();
+// TODO:
+
+export default router;
+
+/**
+ *
+ * @param {"webhook_channel_id":"754001514330062952"}
+ *
+ * Creates a WebHook in the channel and returns the id of it
+ *
+ * @returns {"channel_id": "816382962056560690", "webhook_id": "834910735095037962"}
+ */
diff --git a/src/api/routes/channels/#channel_id/index.ts b/src/api/routes/channels/#channel_id/index.ts
new file mode 100644
index 00000000..bb8b868b
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/index.ts
@@ -0,0 +1,72 @@
+import {
+	Channel,
+	ChannelDeleteEvent,
+	ChannelPermissionOverwriteType,
+	ChannelType,
+	ChannelUpdateEvent,
+	emitEvent,
+	Recipient,
+	handleFile,
+	ChannelModifySchema
+} from "@fosscord/util";
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+import { OrmUtils } from "@fosscord/util";
+
+const router: Router = Router();
+// TODO: delete channel
+// TODO: Get channel
+
+router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => {
+	const { channel_id } = req.params;
+
+	const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
+
+	return res.send(channel);
+});
+
+router.delete("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => {
+	const { channel_id } = req.params;
+
+	const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] });
+
+	if (channel.type === ChannelType.DM) {
+		const recipient = await Recipient.findOneOrFail({ where: { channel_id, user_id: req.user_id } });
+		recipient.closed = true;
+		await Promise.all([
+			recipient.save(),
+			emitEvent({ event: "CHANNEL_DELETE", data: channel, user_id: req.user_id } as ChannelDeleteEvent)
+		]);
+	} else if (channel.type === ChannelType.GROUP_DM) {
+		await Channel.removeRecipientFromChannel(channel, req.user_id);
+	} else {
+		await Promise.all([
+			Channel.delete({ id: channel_id }),
+			emitEvent({ event: "CHANNEL_DELETE", data: channel, channel_id } as ChannelDeleteEvent)
+		]);
+	}
+
+	res.send(channel);
+});
+
+router.patch("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => {
+	let payload = req.body as ChannelModifySchema;
+	const { channel_id } = req.params;
+	if (payload.icon) payload.icon = await handleFile(`/channel-icons/${channel_id}`, payload.icon);
+
+	let channel = await Channel.findOneOrFail({ where: { id: channel_id } });
+	channel = OrmUtils.mergeDeep(channel, payload);
+
+	await Promise.all([
+		channel.save(),
+		emitEvent({
+			event: "CHANNEL_UPDATE",
+			data: channel,
+			channel_id
+		} as ChannelUpdateEvent)
+	]);
+
+	res.send(channel);
+});
+
+export default router;
diff --git a/src/api/routes/channels/#channel_id/invites.ts b/src/api/routes/channels/#channel_id/invites.ts
new file mode 100644
index 00000000..b5c65c0d
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/invites.ts
@@ -0,0 +1,58 @@
+import { Router, Request, Response } from "express";
+import { HTTPError } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { random } from "@fosscord/api";
+import { Channel, Invite, InviteCreateEvent, emitEvent, User, Guild, PublicInviteRelation } from "@fosscord/util";
+import { isTextChannel } from "./messages";
+import { OrmUtils } from "@fosscord/util";
+
+const router: Router = Router();
+
+router.post("/", route({ body: "InviteCreateSchema", permission: "CREATE_INSTANT_INVITE", right: "CREATE_INVITES" }),
+			async (req: Request, res: Response) => {
+	const { user_id } = req;
+	const { channel_id } = req.params;
+	const channel = await Channel.findOneOrFail({ where: { id: channel_id }, select: ["id", "name", "type", "guild_id"] });
+	isTextChannel(channel.type);
+
+	if (!channel.guild_id) {
+		throw new HTTPError("This channel doesn't exist", 404);
+	}
+	const { guild_id } = channel;
+
+	const expires_at = new Date(req.body.max_age * 1000 + Date.now());
+
+	const invite = await OrmUtils.mergeDeep(new Invite(),{
+		temporary: req.body.temporary || true,
+		max_uses: req.body.max_uses,
+		max_age: req.body.max_age,
+		expires_at,
+		guild_id,
+		channel_id,
+		inviter_id: user_id
+	}).save();
+	//TODO: check this, removed toJSON call
+	const data = JSON.parse(JSON.stringify(invite));
+	data.inviter = await User.getPublicUser(req.user_id);
+	data.guild = await Guild.findOne({ where: { id: guild_id } });
+	data.channel = channel;
+
+	await emitEvent({ event: "INVITE_CREATE", data, guild_id } as InviteCreateEvent);
+	res.status(201).send(data);
+});
+
+router.get("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => {
+	const { channel_id } = req.params;
+	const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
+
+	if (!channel.guild_id) {
+		throw new HTTPError("This channel doesn't exist", 404);
+	}
+	const { guild_id } = channel;
+
+	const invites = await Invite.find({ where: { guild_id }, relations: PublicInviteRelation });
+
+	res.status(200).send(invites);
+});
+
+export default router;
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
new file mode 100644
index 00000000..041f4d5e
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/ack.ts
@@ -0,0 +1,33 @@
+import { emitEvent, getPermission, MessageAckEvent, ReadState, Snowflake } from "@fosscord/util";
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+import { OrmUtils } from "@fosscord/util";
+
+const router = Router();
+
+router.post("/", route({ body: "MessageAcknowledgeSchema" }), async (req: Request, res: Response) => {
+	const { channel_id, message_id } = req.params;
+
+	const permission = await getPermission(req.user_id, undefined, channel_id);
+	permission.hasThrow("VIEW_CHANNEL");
+
+	let read_state = await ReadState.findOne({ where: { user_id: req.user_id, channel_id } });
+	if (!read_state) read_state = OrmUtils.mergeDeep(new ReadState(), { user_id: req.user_id, channel_id }) as ReadState;
+	read_state.last_message_id = message_id;
+
+	await read_state.save();
+
+	await emitEvent({
+		event: "MESSAGE_ACK",
+		user_id: req.user_id,
+		data: {
+			channel_id,
+			message_id,
+			version: 3763
+		}
+	} as MessageAckEvent);
+
+	res.sendStatus(204);
+});
+
+export default router;
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
new file mode 100644
index 00000000..b2cb6763
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/crosspost.ts
@@ -0,0 +1,28 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.post("/", route({ permission: "MANAGE_MESSAGES" }), (req: Request, res: Response) => {
+	// TODO:
+	res.json({
+		id: "",
+		type: 0,
+		content: "",
+		channel_id: "",
+		author: { id: "", username: "", avatar: "", discriminator: "", public_flags: 64 },
+		attachments: [],
+		embeds: [],
+		mentions: [],
+		mention_roles: [],
+		pinned: false,
+		mention_everyone: false,
+		tts: false,
+		timestamp: "",
+		edited_timestamp: null,
+		flags: 1,
+		components: []
+	}).status(200);
+});
+
+export default router;
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
new file mode 100644
index 00000000..d7e27062
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/index.ts
@@ -0,0 +1,199 @@
+import {
+	Attachment,
+	Channel,
+	Embed,
+	DiscordApiErrors,
+	emitEvent,
+	FosscordApiErrors,
+	getPermission,
+	getRights,
+ 	Message,
+	MessageCreateEvent,
+	MessageDeleteEvent,
+	MessageUpdateEvent,
+	Snowflake,
+	uploadFile, 
+	MessageCreateSchema
+} from "@fosscord/util";
+import { Router, Response, Request } from "express";
+import multer from "multer";
+import { route } from "@fosscord/api";
+import { handleMessage, postHandleMessage } from "@fosscord/api";
+import { HTTPError } from "@fosscord/util";
+
+const router = Router();
+// TODO: message content/embed string length limit
+
+const messageUpload = multer({
+	limits: {
+		fileSize: 1024 * 1024 * 100,
+		fields: 10,
+		files: 1
+	},
+	storage: multer.memoryStorage()
+}); // max upload 50 mb
+
+router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES" }), async (req: Request, res: Response) => {
+	const { message_id, channel_id } = req.params;
+	let body = req.body as MessageCreateSchema;
+
+	const message = await Message.findOneOrFail({ where: { id: message_id, channel_id }, relations: ["attachments"] });
+
+	const permissions = await getPermission(req.user_id, undefined, channel_id);
+	
+	const rights = await getRights(req.user_id);
+
+	if ((req.user_id !== message.author_id)) {
+		if (!rights.has("MANAGE_MESSAGES")) {
+			permissions.hasThrow("MANAGE_MESSAGES");
+			body = { flags: body.flags };
+// guild admins can only suppress embeds of other messages, no such restriction imposed to instance-wide admins
+		}
+	} else rights.hasThrow("SELF_EDIT_MESSAGES");
+
+	const new_message = await handleMessage({
+		...message,
+		// TODO: should message_reference be overridable?
+		// @ts-ignore
+		message_reference: message.message_reference,
+		...body,
+		author_id: message.author_id,
+		channel_id,
+		id: message_id,
+		edited_timestamp: new Date()
+	});
+
+	await Promise.all([
+		new_message!.save(),
+		await emitEvent({
+			event: "MESSAGE_UPDATE",
+			channel_id,
+			data: { ...new_message, nonce: undefined }
+		} as MessageUpdateEvent)
+	]);
+
+	postHandleMessage(message);
+
+	return res.json(message);
+});
+
+
+// Backfill message with specific timestamp
+router.put(
+	"/",
+	messageUpload.single("file"),
+	async (req, res, next) => {
+		if (req.body.payload_json) {
+			req.body = JSON.parse(req.body.payload_json);
+		}
+
+		next();
+	},
+	route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_BACKDATED_EVENTS" }),
+	async (req: Request, res: Response) => {
+		const { channel_id, message_id } = req.params;
+		let body = req.body as MessageCreateSchema;
+		const attachments: Attachment[] = [];
+		
+		const rights = await getRights(req.user_id);
+		rights.hasThrow("SEND_MESSAGES");
+
+		// regex to check if message contains anything other than numerals ( also no decimals )
+		if (!message_id.match(/^\+?\d+$/)) {
+			throw new HTTPError("Message IDs must be positive integers", 400);
+		}
+
+		const snowflake = Snowflake.deconstruct(message_id)
+		if (Date.now() < snowflake.timestamp) {
+			// message is in the future
+			throw FosscordApiErrors.CANNOT_BACKFILL_TO_THE_FUTURE;
+		}
+
+		const exists = await Message.findOne({ where: { id: message_id, channel_id: channel_id }});
+		if (exists) {
+			throw FosscordApiErrors.CANNOT_REPLACE_BY_BACKFILL;
+		}
+
+		if (req.file) {
+			try {
+				const file: any = await uploadFile(`/attachments/${req.params.channel_id}`, req.file);
+				attachments.push({ ...file, proxy_url: file.url });
+			} catch (error) {
+				return res.status(400).json(error);
+			}
+		}
+		const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] });
+
+		const embeds = body.embeds || [];
+		if (body.embed) embeds.push(body.embed);
+		let message = await handleMessage({
+			...body,
+			type: 0,
+			pinned: false,
+			author_id: req.user_id,
+			id: message_id,
+			embeds,
+			channel_id,
+			attachments,
+			edited_timestamp: undefined,
+			timestamp: new Date(snowflake.timestamp),
+		});
+
+		//Fix for the client bug
+		delete message.member
+		
+		await Promise.all([
+			message.save(),
+			emitEvent({ event: "MESSAGE_CREATE", channel_id: channel_id, data: message } as MessageCreateEvent),
+			channel.save()
+		]);
+
+		postHandleMessage(message).catch((e) => { }); // no await as it shouldnt block the message send function and silently catch error
+
+		return res.json(message);
+	}
+);
+
+router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => {
+	const { message_id, channel_id } = req.params;
+
+	const message = await Message.findOneOrFail({ where: { id: message_id, channel_id }, relations: ["attachments"] });
+
+	const permissions = await getPermission(req.user_id, undefined, channel_id);
+	
+	if (message.author_id !== req.user_id) permissions.hasThrow("READ_MESSAGE_HISTORY");
+
+	return res.json(message);
+});
+
+router.delete("/", route({}), 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 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");
+
+	await Message.delete({ id: message_id });
+
+	await emitEvent({
+		event: "MESSAGE_DELETE",
+		channel_id,
+		data: {
+			id: message_id,
+			channel_id,
+			guild_id: channel.guild_id
+		}
+	} as MessageDeleteEvent);
+
+	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
new file mode 100644
index 00000000..d0ab35bb
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/messages/#message_id/reactions.ts
@@ -0,0 +1,185 @@
+import {
+	Channel,
+	emitEvent,
+	Emoji,
+	getPermission,
+	Member,
+	Message,
+	MessageReactionAddEvent,
+	MessageReactionRemoveAllEvent,
+	MessageReactionRemoveEmojiEvent,
+	MessageReactionRemoveEvent,
+	PartialEmoji,
+	PublicUserProjection,
+	User
+} from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { Router, Response, Request } from "express";
+import { HTTPError } from "@fosscord/util";
+import { In } from "typeorm";
+
+const router = Router();
+// TODO: check if emoji is really an unicode emoji or a prperly encoded external emoji
+
+function getEmoji(emoji: string): PartialEmoji {
+	emoji = decodeURIComponent(emoji);
+	const parts = emoji.includes(":") && emoji.split(":");
+	if (parts)
+		return {
+			name: parts[0],
+			id: parts[1]
+		};
+
+	return {
+		id: undefined,
+		name: emoji
+	};
+}
+
+router.delete("/", route({ permission: "MANAGE_MESSAGES" }), async (req: Request, res: Response) => {
+	const { message_id, channel_id } = req.params;
+
+	const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
+
+	await Message.update({ id: message_id, channel_id }, { reactions: [] });
+
+	await emitEvent({
+		event: "MESSAGE_REACTION_REMOVE_ALL",
+		channel_id,
+		data: {
+			channel_id,
+			message_id,
+			guild_id: channel.guild_id
+		}
+	} as MessageReactionRemoveAllEvent);
+
+	res.sendStatus(204);
+});
+
+router.delete("/:emoji", route({ permission: "MANAGE_MESSAGES" }), async (req: Request, res: Response) => {
+	const { message_id, channel_id } = req.params;
+	const emoji = getEmoji(req.params.emoji);
+
+	const message = await Message.findOneOrFail({ where: { id: message_id, channel_id } });
+
+	const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
+	if (!already_added) throw new HTTPError("Reaction not found", 404);
+	message.reactions.remove(already_added);
+
+	await Promise.all([
+		message.save(),
+		emitEvent({
+			event: "MESSAGE_REACTION_REMOVE_EMOJI",
+			channel_id,
+			data: {
+				channel_id,
+				message_id,
+				guild_id: message.guild_id,
+				emoji
+			}
+		} as MessageReactionRemoveEmojiEvent)
+	]);
+
+	res.sendStatus(204);
+});
+
+router.get("/:emoji", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => {
+	const { message_id, channel_id } = req.params;
+	const emoji = getEmoji(req.params.emoji);
+
+	const message = await Message.findOneOrFail({ where: { id: message_id, channel_id } });
+	const reaction = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
+	if (!reaction) throw new HTTPError("Reaction not found", 404);
+
+	const users = await User.find({
+		where: {
+			id: In(reaction.user_ids)
+		},
+		select: PublicUserProjection
+	});
+
+	res.json(users);
+});
+
+router.put("/:emoji/:user_id", route({ permission: "READ_MESSAGE_HISTORY", right: "SELF_ADD_REACTIONS" }), async (req: Request, res: Response) => {
+	const { message_id, channel_id, user_id } = req.params;
+	if (user_id !== "@me") throw new HTTPError("Invalid user");
+	const emoji = getEmoji(req.params.emoji);
+
+	const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
+	const message = await Message.findOneOrFail({ where: { id: message_id, channel_id } });
+	const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
+
+	if (!already_added) req.permission!.hasThrow("ADD_REACTIONS");
+
+	if (emoji.id) {
+		const external_emoji = await Emoji.findOneOrFail({ where: { id: emoji.id } });
+		if (!already_added) req.permission!.hasThrow("USE_EXTERNAL_EMOJIS");
+		emoji.animated = external_emoji.animated;
+		emoji.name = external_emoji.name;
+	}
+
+	if (already_added) {
+		if (already_added.user_ids.includes(req.user_id)) return res.sendStatus(204); // Do not throw an error ¯\_(ツ)_/¯ as discord also doesn't throw any error
+		already_added.count++;
+	} else message.reactions.push({ count: 1, emoji, user_ids: [req.user_id] });
+
+	await message.save();
+
+	const member = channel.guild_id && (await Member.findOneOrFail({ where: { id: req.user_id } }));
+
+	await emitEvent({
+		event: "MESSAGE_REACTION_ADD",
+		channel_id,
+		data: {
+			user_id: req.user_id,
+			channel_id,
+			message_id,
+			guild_id: channel.guild_id,
+			emoji,
+			member
+		}
+	} as MessageReactionAddEvent);
+
+	res.sendStatus(204);
+});
+
+router.delete("/:emoji/:user_id", route({}), async (req: Request, res: Response) => {
+	let { message_id, channel_id, user_id } = req.params;
+
+	const emoji = getEmoji(req.params.emoji);
+
+	const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
+	const message = await Message.findOneOrFail({ where: { id: message_id, channel_id } });
+
+	if (user_id === "@me") user_id = req.user_id;
+	else {
+		const permissions = await getPermission(req.user_id, undefined, channel_id);
+		permissions.hasThrow("MANAGE_MESSAGES");
+	}
+
+	const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
+	if (!already_added || !already_added.user_ids.includes(user_id)) throw new HTTPError("Reaction not found", 404);
+
+	already_added.count--;
+
+	if (already_added.count <= 0) message.reactions.remove(already_added);
+
+	await message.save();
+
+	await emitEvent({
+		event: "MESSAGE_REACTION_REMOVE",
+		channel_id,
+		data: {
+			user_id: req.user_id,
+			channel_id,
+			message_id,
+			guild_id: channel.guild_id,
+			emoji
+		}
+	} as MessageReactionRemoveEvent);
+
+	res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/channels/#channel_id/messages/bulk-delete.ts b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts
new file mode 100644
index 00000000..af44b522
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/messages/bulk-delete.ts
@@ -0,0 +1,43 @@
+import { Router, Response, Request } from "express";
+import { Channel, Config, emitEvent, getPermission, getRights, MessageDeleteBulkEvent, Message } from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { In } from "typeorm";
+
+const router: Router = Router();
+
+export default router;
+
+// should users be able to bulk delete messages or only bots? ANSWER: all users
+// should this request fail, if you provide messages older than 14 days/invalid ids? ANSWER: NO
+// https://discord.com/developers/docs/resources/channel#bulk-delete-messages
+router.post("/", route({ body: "BulkDeleteSchema" }), async (req: Request, res: Response) => {
+	const { channel_id } = req.params;
+	const channel = await Channel.findOneOrFail({where:{ id: channel_id} });
+	if (!channel.guild_id) throw new HTTPError("Can't bulk delete dm channel messages", 400);
+
+	const rights = await getRights(req.user_id);
+	rights.hasThrow("SELF_DELETE_MESSAGES");
+
+	let superuser = rights.has("MANAGE_MESSAGES");
+	const permission = await getPermission(req.user_id, channel?.guild_id, channel_id);
+
+	const { maxBulkDelete } = Config.get().limits.message;
+
+	const { messages } = req.body as { messages: string[] };
+	if (messages.length === 0) throw new HTTPError("You must specify messages to bulk delete");
+	if (!superuser) {
+		permission.hasThrow("MANAGE_MESSAGES");
+		if (messages.length > maxBulkDelete) throw new HTTPError(`You cannot delete more than ${maxBulkDelete} messages`);
+	}
+
+	await Message.delete({ id: In(messages) });
+
+	await emitEvent({
+		event: "MESSAGE_DELETE_BULK",
+		channel_id,
+		data: { ids: messages, channel_id, guild_id: channel.guild_id }
+	} as MessageDeleteBulkEvent);
+
+	res.sendStatus(204);
+});
diff --git a/src/api/routes/channels/#channel_id/messages/index.ts b/src/api/routes/channels/#channel_id/messages/index.ts
new file mode 100644
index 00000000..9ab0d97d
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/messages/index.ts
@@ -0,0 +1,241 @@
+import { Router, Response, Request } from "express";
+import {
+	Attachment,
+	Channel,
+	ChannelType,
+	Config,
+	DmChannelDTO,
+	emitEvent,
+	getPermission,
+	getRights,
+	Message,
+	MessageCreateEvent,
+	Snowflake,
+	uploadFile,
+	Member,
+	MessageCreateSchema
+} from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { handleMessage, postHandleMessage, route } from "@fosscord/api";
+import multer from "multer";
+import { FindManyOptions, LessThan, MoreThan } from "typeorm";
+import { URL } from "url";
+
+const router: Router = Router();
+
+export default router;
+
+export function isTextChannel(type: ChannelType): boolean {
+	switch (type) {
+		case ChannelType.GUILD_STORE:
+		case ChannelType.GUILD_VOICE:
+		case ChannelType.GUILD_STAGE_VOICE:
+		case ChannelType.GUILD_CATEGORY:
+		case ChannelType.GUILD_FORUM:
+		case ChannelType.DIRECTORY:
+			throw new HTTPError("not a text channel", 400);
+		case ChannelType.DM:
+		case ChannelType.GROUP_DM:
+		case ChannelType.GUILD_NEWS:
+		case ChannelType.GUILD_NEWS_THREAD:
+		case ChannelType.GUILD_PUBLIC_THREAD:
+		case ChannelType.GUILD_PRIVATE_THREAD:
+		case ChannelType.GUILD_TEXT:
+		case ChannelType.ENCRYPTED:
+		case ChannelType.ENCRYPTED_THREAD:
+			return true;
+		default:
+			throw new HTTPError("unimplemented", 400);
+	}
+}
+
+// https://discord.com/developers/docs/resources/channel#create-message
+// get messages
+router.get("/", 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);
+
+	let 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([]);
+
+	let query: FindManyOptions<Message> & { where: { id?: any; }; } = {
+		order: { id: "DESC" },
+		take: limit,
+		where: { channel_id },
+		relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"]
+	};
+	
+
+	if (after) {
+		if (after > new Snowflake()) return res.status(422);
+		query.where.id = MoreThan(after);
+	}
+	else if (before) { 
+		if (before < 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())
+		];
+	}
+
+	const messages = await Message.find(query);
+	const endpoint = Config.get().cdn.endpointPublic;
+
+	return res.json(
+		messages.map((x: any) => {
+			(x.reactions || []).forEach((x: any) => {
+				// @ts-ignore
+				if ((x.user_ids || []).includes(req.user_id)) x.me = true;
+				// @ts-ignore
+				delete x.user_ids;
+			});
+			// @ts-ignore
+			if (!x.author) x.author = { id: "4", discriminator: "0000", username: "Fosscord Ghost", public_flags: "0", avatar: null };
+			x.attachments?.forEach((y: any) => {
+				// 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 erorrs when, say, the `application` property is `null`.
+			**/
+			
+			for (let curr in x) {
+				if (x[curr] === null)
+					delete x[curr];
+			}
+
+			return x;
+		})
+	);
+});
+
+// TODO: config max upload size
+const messageUpload = multer({
+	limits: {
+		fileSize: 1024 * 1024 * 100,
+		fields: 10,
+		// files: 1
+	},
+	storage: multer.memoryStorage()
+}); // max upload 50 mb
+/**
+ TODO: dynamically change limit of MessageCreateSchema with config
+
+ https://discord.com/developers/docs/resources/channel#create-message
+ TODO: text channel slowdown (per-user and across-users)
+ Q: trim and replace message content and every embed field A: NO, given this cannot be implemented in E2EE channels
+ TODO: only dispatch notifications for mentions denoted in allowed_mentions
+**/
+// Send message
+router.post(
+	"/",
+	messageUpload.any(),
+	async (req, res, next) => {
+		if (req.body.payload_json) {
+			req.body = JSON.parse(req.body.payload_json);
+		}
+
+		next();
+	},
+	route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES" }),
+	async (req: Request, res: Response) => {
+		const { channel_id } = req.params;
+		let body = req.body as MessageCreateSchema;
+		const attachments: Attachment[] = [];
+
+		const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] });
+		if (!channel.isWritable()) {
+			throw new HTTPError(`Cannot send messages to channel of type ${channel.type}`, 400)
+		}
+
+		const files = req.files as Express.Multer.File[] ?? [];
+		for (let currFile of files) {
+			try {
+				const file: any = await uploadFile(`/attachments/${channel.id}`, currFile);
+				attachments.push({ ...file, proxy_url: file.url });
+			}
+			catch (error) {
+				return res.status(400).json(error);
+			}
+		}
+
+		const embeds = body.embeds || [];
+		if (body.embed) embeds.push(body.embed);
+		let message = await handleMessage({
+			...body,
+			type: 0,
+			pinned: false,
+			author_id: req.user_id,
+			embeds,
+			channel_id,
+			attachments,
+			edited_timestamp: undefined,
+			timestamp: new Date()
+		});
+
+		channel.last_message_id = message.id;
+
+		if (channel.isDm()) {
+			const channel_dto = await DmChannelDTO.from(channel);
+
+			// Only one recipients should be closed here, since in group DMs the recipient is deleted not closed
+			Promise.all(
+				channel.recipients!.map((recipient) => {
+					if (recipient.closed) {
+						recipient.closed = false;
+						return Promise.all([
+							recipient.save(),
+							emitEvent({
+								event: "CHANNEL_CREATE",
+								data: channel_dto.excludedRecipients([recipient.user_id]),
+								user_id: recipient.user_id
+							})
+						]);
+					}
+				})
+			);
+		}
+	
+	    //Defining member fields
+		var member = await Member.findOneOrFail({ where: { id: req.user_id }, relations: ["roles"] });
+		// TODO: This doesn't work either
+        // member.roles = member.roles.filter((role) => {
+		// 	return role.id !== role.guild_id;
+		// }).map((role) => {
+		// 	return role.id;
+		// });
+		message.member = member;
+		// TODO: Figure this out
+		// delete message.member.last_message_id;
+		// delete message.member.index;
+		
+		await Promise.all([
+			message.save(),
+			emitEvent({ event: "MESSAGE_CREATE", channel_id: channel_id, data: message } as MessageCreateEvent),
+			message.guild_id ? Member.update({ id: req.user_id, guild_id: message.guild_id }, { last_message_id: message.id }) : null,
+			channel.save()
+		]);
+
+		postHandleMessage(message).catch((e) => { }); // no await as it shouldnt block the message send function and silently catch error
+
+		return res.json(message);
+	}
+);
+
diff --git a/src/api/routes/channels/#channel_id/permissions.ts b/src/api/routes/channels/#channel_id/permissions.ts
new file mode 100644
index 00000000..34052fe5
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/permissions.ts
@@ -0,0 +1,81 @@
+import {
+	Channel,
+	ChannelPermissionOverwrite,
+	ChannelPermissionOverwriteSchema,
+	ChannelPermissionOverwriteType,
+	ChannelUpdateEvent,
+	emitEvent,
+	getPermission,
+	Member,
+	Role
+} from "@fosscord/util";
+import { Router, Response, Request } from "express";
+import { HTTPError } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.put(
+	"/:overwrite_id",
+	route({ body: "ChannelPermissionOverwriteSchema", permission: "MANAGE_ROLES" }),
+	async (req: Request, res: Response) => {
+		const { channel_id, overwrite_id } = req.params;
+		const body = req.body as ChannelPermissionOverwriteSchema;
+
+		let channel = await Channel.findOneOrFail({ where: {id: channel_id} });
+		if (!channel.guild_id) throw new HTTPError("Channel not found", 404);
+
+		if (body.type === 0) {
+			if (!(await Role.count({ where: { id: overwrite_id } }))) throw new HTTPError("role not found", 404);
+		} else if (body.type === 1) {
+			if (!(await Member.count({ where: { id: overwrite_id } }))) throw new HTTPError("user not found", 404);
+		} else throw new HTTPError("type not supported", 501);
+
+		// @ts-ignore
+		let overwrite: ChannelPermissionOverwrite = channel.permission_overwrites.find((x) => x.id === overwrite_id);
+		if (!overwrite) {
+			// @ts-ignore
+			overwrite = {
+				id: overwrite_id,
+				type: body.type
+			};
+			channel.permission_overwrites!.push(overwrite);
+		}
+		overwrite.allow = String(req.permission!.bitfield & (BigInt(body.allow) || BigInt("0")));
+		overwrite.deny = String(req.permission!.bitfield & (BigInt(body.deny) || BigInt("0")));
+
+		await Promise.all([
+			channel.save(),
+			emitEvent({
+				event: "CHANNEL_UPDATE",
+				channel_id,
+				data: channel
+			} as ChannelUpdateEvent)
+		]);
+
+		return res.sendStatus(204);
+	}
+);
+
+// TODO: check permission hierarchy
+router.delete("/:overwrite_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
+	const { channel_id, overwrite_id } = req.params;
+
+	const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
+	if (!channel.guild_id) throw new HTTPError("Channel not found", 404);
+
+	channel.permission_overwrites = channel.permission_overwrites!.filter((x) => x.id === overwrite_id);
+
+	await Promise.all([
+		channel.save(),
+		emitEvent({
+			event: "CHANNEL_UPDATE",
+			channel_id,
+			data: channel
+		} as ChannelUpdateEvent)
+	]);
+
+	return res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/channels/#channel_id/pins.ts b/src/api/routes/channels/#channel_id/pins.ts
new file mode 100644
index 00000000..003638c5
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/pins.ts
@@ -0,0 +1,90 @@
+import {
+	Channel,
+	ChannelPinsUpdateEvent,
+	Config,
+	emitEvent,
+	getPermission,
+	Message,
+	MessageUpdateEvent,
+	DiscordApiErrors
+} from "@fosscord/util";
+import { Router, Request, Response } from "express";
+import { HTTPError } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.put("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => {
+	const { channel_id, message_id } = req.params;
+
+	const message = await Message.findOneOrFail({ where: { id: message_id } });
+
+	// * in dm channels anyone can pin messages -> only check for guilds
+	if (message.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES");
+
+	const pinned_count = await Message.count({ where: { channel: { id: channel_id }, pinned: true } });
+	const { maxPins } = Config.get().limits.channel;
+	if (pinned_count >= maxPins) throw DiscordApiErrors.MAXIMUM_PINS.withParams(maxPins);
+
+	await Promise.all([
+		Message.update({ id: message_id }, { pinned: true }),
+		emitEvent({
+			event: "MESSAGE_UPDATE",
+			channel_id,
+			data: message
+		} as MessageUpdateEvent),
+		emitEvent({
+			event: "CHANNEL_PINS_UPDATE",
+			channel_id,
+			data: {
+				channel_id,
+				guild_id: message.guild_id,
+				last_pin_timestamp: undefined
+			}
+		} as ChannelPinsUpdateEvent)
+	]);
+
+	res.sendStatus(204);
+});
+
+router.delete("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => {
+	const { channel_id, message_id } = req.params;
+
+	const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
+	if (channel.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES");
+
+	const message = await Message.findOneOrFail({ where: { id: message_id } });
+	message.pinned = false;
+
+	await Promise.all([
+		message.save(),
+
+		emitEvent({
+			event: "MESSAGE_UPDATE",
+			channel_id,
+			data: message
+		} as MessageUpdateEvent),
+
+		emitEvent({
+			event: "CHANNEL_PINS_UPDATE",
+			channel_id,
+			data: {
+				channel_id,
+				guild_id: channel.guild_id,
+				last_pin_timestamp: undefined
+			}
+		} as ChannelPinsUpdateEvent)
+	]);
+
+	res.sendStatus(204);
+});
+
+router.get("/", route({ permission: ["READ_MESSAGE_HISTORY"] }), async (req: Request, res: Response) => {
+	const { channel_id } = req.params;
+
+	let pins = await Message.find({ where: { channel_id, pinned: true } });
+
+	res.send(pins);
+});
+
+export default router;
diff --git a/src/api/routes/channels/#channel_id/purge.ts b/src/api/routes/channels/#channel_id/purge.ts
new file mode 100644
index 00000000..1ef6e1d7
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/purge.ts
@@ -0,0 +1,64 @@
+import { HTTPError, PurgeSchema } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { isTextChannel } from "./messages";
+import { FindManyOptions, Between, Not } from "typeorm";
+import { Channel, Config, emitEvent, getPermission, getRights, Message, MessageDeleteBulkEvent } from "@fosscord/util";
+import { Router, Response, Request } from "express";
+import { In } from "typeorm";
+
+const router: Router = Router();
+
+export default router;
+
+/**
+TODO: apply the delete bit by bit to prevent client and database stress
+**/
+router.post("/",route({ /*body: "PurgeSchema",*/ }), async (req: Request, res: Response) => {
+		const { channel_id } = req.params;
+		const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
+
+		if (!channel.guild_id) throw new HTTPError("Can't purge dm channels", 400);
+		isTextChannel(channel.type);
+
+		const rights = await getRights(req.user_id);
+		if (!rights.has("MANAGE_MESSAGES")) {
+			const permissions = await getPermission(req.user_id, channel.guild_id, channel_id);
+			permissions.hasThrow("MANAGE_MESSAGES");
+			permissions.hasThrow("MANAGE_CHANNELS");
+		}
+
+		const { before, after } = req.body as PurgeSchema;
+
+		// TODO: send the deletion event bite-by-bite to prevent client stress
+
+		let query: FindManyOptions<Message> & { where: { id?: any } } = {
+			order: { id: "ASC" },
+			// take: limit,
+			where: {
+				channel_id,
+				id: Between(after, before), // the right way around
+				author_id: rights.has("SELF_DELETE_MESSAGES") ? undefined : Not(req.user_id)
+				// if you lack the right of self-deletion, you can't delete your own messages, even in purges
+			},
+			relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"]
+		};
+
+		const messages = await Message.find(query);
+		const endpoint = Config.get().cdn.endpointPublic;
+
+		if (messages.length == 0) {
+			res.sendStatus(304);
+			return;
+		}
+
+		await Message.delete({ id: In(messages) });
+
+		await emitEvent({
+			event: "MESSAGE_DELETE_BULK",
+			channel_id,
+			data: { ids: messages.map((x) => x.id), channel_id, guild_id: channel.guild_id }
+		} as MessageDeleteBulkEvent);
+
+		res.sendStatus(204);
+	}
+);
diff --git a/src/api/routes/channels/#channel_id/recipients.ts b/src/api/routes/channels/#channel_id/recipients.ts
new file mode 100644
index 00000000..069212e2
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/recipients.ts
@@ -0,0 +1,68 @@
+import { Request, Response, Router } from "express";
+import {
+	Channel,
+	ChannelRecipientAddEvent,
+	ChannelType,
+	DiscordApiErrors,
+	DmChannelDTO,
+	emitEvent,
+	PublicUserProjection,
+	Recipient,
+	User
+} from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { OrmUtils } from "@fosscord/util";
+
+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"] });
+
+	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?
+		}
+
+		channel.recipients!.push(OrmUtils.mergeDeep(new Recipient(), { 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_RECIPIENT_ADD",
+			data: {
+				channel_id: channel_id,
+				user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection })
+			},
+			channel_id: channel_id
+		} 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)))
+		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?
+	}
+
+	await Channel.removeRecipientFromChannel(channel, user_id);
+
+	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
new file mode 100644
index 00000000..99460f6e
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/typing.ts
@@ -0,0 +1,29 @@
+import { Channel, emitEvent, Member, TypingStartEvent } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { Router, Request, Response } from "express";
+
+const router: Router = Router();
+
+router.post("/", route({ permission: "SEND_MESSAGES" }), async (req: Request, res: Response) => {
+	const { channel_id } = req.params;
+	const user_id = req.user_id;
+	const timestamp = Date.now();
+	const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
+	const member = await Member.findOne({ where: { id: user_id, guild_id: channel.guild_id }, relations: ["roles", "user"] });
+
+	await emitEvent({
+		event: "TYPING_START",
+		channel_id: channel_id,
+		data: {
+			...(member ? { member: { ...member, roles: member?.roles?.map((x) => x.id) } } : null),
+			channel_id,
+			timestamp,
+			user_id,
+			guild_id: channel.guild_id
+		}
+	} as TypingStartEvent);
+
+	res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/channels/#channel_id/webhooks.ts b/src/api/routes/channels/#channel_id/webhooks.ts
new file mode 100644
index 00000000..b11c8eb9
--- /dev/null
+++ b/src/api/routes/channels/#channel_id/webhooks.ts
@@ -0,0 +1,34 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+import { Channel, Config, getPermission, trimSpecial, Webhook } from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { isTextChannel } from "./messages/index";
+import { DiscordApiErrors } from "@fosscord/util";
+
+const router: Router = Router();
+//TODO: implement webhooks
+router.get("/", route({}), 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" }), async (req: Request, res: Response) => {
+	const channel_id = req.params.channel_id;
+	const channel = await Channel.findOneOrFail({ where: { id: channel_id } });
+
+	isTextChannel(channel.type);
+	if (!channel.guild_id) throw new HTTPError("Not a guild channel", 400);
+
+	const webhook_count = await Webhook.count({ where: { channel_id } });
+	const { maxWebhooks } = Config.get().limits.channel;
+	if (webhook_count > maxWebhooks) throw DiscordApiErrors.MAXIMUM_WEBHOOKS.withParams(maxWebhooks);
+
+	let { avatar, name } = req.body as { name: string; avatar?: string };
+	name = trimSpecial(name);
+	if (name === "clyde") throw new HTTPError("Invalid name", 400);
+
+	// TODO: save webhook in database and send response
+	res.json(new Webhook());
+});
+
+export default router;
diff --git a/src/api/routes/discoverable-guilds.ts b/src/api/routes/discoverable-guilds.ts
new file mode 100644
index 00000000..35ecf28c
--- /dev/null
+++ b/src/api/routes/discoverable-guilds.ts
@@ -0,0 +1,39 @@
+import { Guild, Config } from "@fosscord/util";
+
+import { Router, Request, Response } from "express";
+import { route } from "..";
+import { Like } from "typeorm";
+
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { offset, limit, categories } = req.query;
+	let showAllGuilds = Config.get().guild.discovery.showAllGuilds;
+	let configLimit = Config.get().guild.discovery.limit;
+	// ! this only works using SQL querys
+	// const guilds = await Guild.find({ where: { features: "DISCOVERABLE" } }); //, take: Math.abs(Number(limit)) });
+	let guilds;
+	if (categories == undefined) {
+		guilds = showAllGuilds
+			? await Guild.find({ take: Math.abs(Number(limit || configLimit)) })
+			: await Guild.find({ where: { features: Like("%DISCOVERABLE%") }, take: Math.abs(Number(limit || configLimit)) });
+	} else {
+		guilds = showAllGuilds
+			? await Guild.find({ where: { primary_category_id: Number(categories) }, take: Math.abs(Number(limit || configLimit)) })
+			: await Guild.find({
+					where: { primary_category_id: Number(categories), features: Like("%DISCOVERABLE%") },
+					take: Math.abs(Number(limit || configLimit))
+			  });
+	}
+
+	const total = guilds ? guilds.length : undefined;
+
+	res.send({
+		total: total,
+		guilds: guilds,
+		offset: Number(offset || Config.get().guild.discovery.offset),
+		limit: Number(limit || configLimit)
+	});
+});
+
+export default router;
diff --git a/src/api/routes/discovery.ts b/src/api/routes/discovery.ts
new file mode 100644
index 00000000..30c418c6
--- /dev/null
+++ b/src/api/routes/discovery.ts
@@ -0,0 +1,18 @@
+import { Categories } from "@fosscord/util";
+import { Router, Response, Request } from "express";
+import { route } from "..";
+
+const router = Router();
+
+router.get("/categories", route({}), async (req: Request, res: Response) => {
+	// TODO:
+	// Get locale instead
+
+	const { locale, primary_only } = req.query;
+
+	const out = primary_only ? await Categories.find() : await Categories.find({ where: {is_primary: true} });
+
+	res.send(out);
+});
+
+export default router;
diff --git a/src/api/routes/downloads.ts b/src/api/routes/downloads.ts
new file mode 100644
index 00000000..44530353
--- /dev/null
+++ b/src/api/routes/downloads.ts
@@ -0,0 +1,20 @@
+import { Router, Response, Request } from "express";
+import { route } from "..";
+import { Release, Config } from "@fosscord/util";
+
+const router = Router();
+
+router.get("/:branch", route({}), async (req: Request, res: Response) => {
+	const { client } = Config.get();
+	const { branch } = req.params;
+	const { platform } = req.query;
+	//TODO
+
+	if(!platform || !["linux", "osx", "win"].includes(platform.toString())) return res.status(404)
+
+	const release = await Release.findOneOrFail({ where: { name: client.releases.upstreamVersion } });
+
+	res.redirect(release[`win_url`]);
+});
+
+export default router;
diff --git a/src/api/routes/experiments.ts b/src/api/routes/experiments.ts
new file mode 100644
index 00000000..fcbd9271
--- /dev/null
+++ b/src/api/routes/experiments.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "..";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.send({ fingerprint: "", assignments: [], guild_experiments:[] });
+});
+
+export default router;
diff --git a/src/api/routes/gateway/bot.ts b/src/api/routes/gateway/bot.ts
new file mode 100644
index 00000000..f1dbb9df
--- /dev/null
+++ b/src/api/routes/gateway/bot.ts
@@ -0,0 +1,40 @@
+import { Config } from "@fosscord/util";
+import { Router, Response, Request } from "express";
+import { route, RouteOptions } from "@fosscord/api";
+
+const router = Router();
+
+export interface GatewayBotResponse {
+	url: string;
+	shards: number;
+	session_start_limit: {
+		total: number;
+		remaining: number;
+		reset_after: number;
+		max_concurrency: number;
+	};
+}
+
+const options: RouteOptions = {
+	test: {
+		response: {
+			body: "GatewayBotResponse"
+		}
+	}
+};
+
+router.get("/", route(options), (req: Request, res: Response) => {
+	const { endpointPublic } = Config.get().gateway;
+	res.json({
+		url: endpointPublic || process.env.GATEWAY || "ws://localhost:3002",
+		shards: 1,
+		session_start_limit: {
+			total: 1000,
+			remaining: 999,
+			reset_after: 14400000,
+			max_concurrency: 1
+		}
+	});
+});
+
+export default router;
diff --git a/src/api/routes/gateway/index.ts b/src/api/routes/gateway/index.ts
new file mode 100644
index 00000000..9bad7478
--- /dev/null
+++ b/src/api/routes/gateway/index.ts
@@ -0,0 +1,24 @@
+import { Config } from "@fosscord/util";
+import { Router, Response, Request } from "express";
+import { route, RouteOptions } from "@fosscord/api";
+
+const router = Router();
+
+export interface GatewayResponse {
+	url: string;
+}
+
+const options: RouteOptions = {
+	test: {
+		response: {
+			body: "GatewayResponse"
+		}
+	}
+};
+
+router.get("/", route(options), (req: Request, res: Response) => {
+	const { endpointPublic } = Config.get().gateway;
+	res.json({ url: endpointPublic || process.env.GATEWAY || "ws://localhost:3002" });
+});
+
+export default router;
diff --git a/src/api/routes/gifs/search.ts b/src/api/routes/gifs/search.ts
new file mode 100644
index 00000000..1099dc4a
--- /dev/null
+++ b/src/api/routes/gifs/search.ts
@@ -0,0 +1,28 @@
+import { Router, Response, Request } from "express";
+import fetch from "node-fetch";
+import ProxyAgent from 'proxy-agent';
+import { route } from "@fosscord/api";
+import { getGifApiKey, parseGifResult } from "./trending";
+
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	// TODO: Custom providers
+	const { q, media_format, locale } = req.query;
+
+	const apiKey = getGifApiKey();
+	
+	const agent = new ProxyAgent();
+
+	const response = await fetch(`https://g.tenor.com/v1/search?q=${q}&media_format=${media_format}&locale=${locale}&key=${apiKey}`, {
+		agent,
+		method: "get",
+		headers: { "Content-Type": "application/json" }
+	});
+
+	const { results } = await response.json() as any;
+
+	res.json(results.map(parseGifResult)).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/gifs/trending-gifs.ts b/src/api/routes/gifs/trending-gifs.ts
new file mode 100644
index 00000000..2b28d9d2
--- /dev/null
+++ b/src/api/routes/gifs/trending-gifs.ts
@@ -0,0 +1,28 @@
+import { Router, Response, Request } from "express";
+import fetch from "node-fetch";
+import ProxyAgent from 'proxy-agent';
+import { route } from "@fosscord/api";
+import { getGifApiKey, parseGifResult } from "./trending";
+
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	// TODO: Custom providers
+	const { media_format, locale } = req.query;
+
+	const apiKey = getGifApiKey();
+	
+	const agent = new ProxyAgent();
+
+	const response = await fetch(`https://g.tenor.com/v1/trending?media_format=${media_format}&locale=${locale}&key=${apiKey}`, {
+		agent,
+		method: "get",
+		headers: { "Content-Type": "application/json" }
+	});
+
+	const { results } = await response.json() as any;
+
+	res.json(results.map(parseGifResult)).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/gifs/trending.ts b/src/api/routes/gifs/trending.ts
new file mode 100644
index 00000000..61eb76c4
--- /dev/null
+++ b/src/api/routes/gifs/trending.ts
@@ -0,0 +1,62 @@
+import { Router, Response, Request } from "express";
+import fetch from "node-fetch";
+import ProxyAgent from 'proxy-agent';
+import { route } from "@fosscord/api";
+import { Config } from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+
+const router = Router();
+
+export function parseGifResult(result: any) {
+	return {
+		id: result.id,
+		title: result.title,
+		url: result.itemurl,
+		src: result.media[0].mp4.url,
+		gif_src: result.media[0].gif.url,
+		width: result.media[0].mp4.dims[0],
+		height: result.media[0].mp4.dims[1],
+		preview: result.media[0].mp4.preview
+	};
+}
+
+export function getGifApiKey() {
+	const { enabled, provider, apiKey } = Config.get().gif;
+	if (!enabled) throw new HTTPError(`Gifs are disabled`);
+	if (provider !== "tenor" || !apiKey) throw new HTTPError(`${provider} gif provider not supported`);
+
+	return apiKey;
+}
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	// TODO: Custom providers
+	// TODO: return gifs as mp4
+	const { media_format, locale } = req.query;
+
+	const apiKey = getGifApiKey();
+	
+	const agent = new ProxyAgent();
+
+	const [responseSource, trendGifSource] = await Promise.all([
+		fetch(`https://g.tenor.com/v1/categories?locale=${locale}&key=${apiKey}`, {
+			agent,
+			method: "get",
+			headers: { "Content-Type": "application/json" }
+		}),
+		fetch(`https://g.tenor.com/v1/trending?locale=${locale}&key=${apiKey}`, {
+			agent,
+			method: "get",
+			headers: { "Content-Type": "application/json" }
+		})
+	]);
+
+	const { tags } = await responseSource.json() as any;
+	const { results } = await trendGifSource.json() as any;
+
+	res.json({
+		categories: tags.map((x: any) => ({ name: x.searchterm, src: x.image })),
+		gifs: [parseGifResult(results[0])]
+	}).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/guild-recommendations.ts b/src/api/routes/guild-recommendations.ts
new file mode 100644
index 00000000..bd0140d6
--- /dev/null
+++ b/src/api/routes/guild-recommendations.ts
@@ -0,0 +1,24 @@
+import { Guild, Config } from "@fosscord/util";
+
+import { Router, Request, Response } from "express";
+import { route } from "..";
+import {Like} from "typeorm"
+
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { limit, personalization_disabled } = req.query;
+	let showAllGuilds = Config.get().guild.discovery.showAllGuilds;
+	// ! 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 genLoadId = (size: Number) => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
+
+	const guilds = showAllGuilds
+		? await Guild.find({ take: Math.abs(Number(limit || 24)) })
+		: await Guild.find({ where: { features: Like('%DISCOVERABLE%') }, take: Math.abs(Number(limit || 24)) });
+	res.send({ recommended_guilds: guilds, load_id: `server_recs/${genLoadId(32)}`}).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/audit-logs.ts b/src/api/routes/guilds/#guild_id/audit-logs.ts
new file mode 100644
index 00000000..b54835fc
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/audit-logs.ts
@@ -0,0 +1,17 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+const router = Router();
+
+//TODO: implement audit logs
+router.get("/", route({}), async (req: Request, res: Response) => {
+	res.json({
+		audit_log_entries: [],
+		users: [],
+		integrations: [],
+		webhooks: [],
+		guild_scheduled_events: [],
+		threads: [],
+		application_commands: []
+	});
+});
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/bans.ts b/src/api/routes/guilds/#guild_id/bans.ts
new file mode 100644
index 00000000..3d405344
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/bans.ts
@@ -0,0 +1,157 @@
+import { Request, Response, Router } from "express";
+import { DiscordApiErrors, emitEvent, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, Guild, Ban, User, Member, BanRegistrySchema, BanModeratorSchema } from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { getIpAdress, route } from "@fosscord/api";
+import { OrmUtils } from "@fosscord/util";
+
+const router: Router = Router();
+
+/* TODO: Deleting the secrets is just a temporary go-around. Views should be implemented for both safety and better handling. */
+
+router.get("/", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+
+	let bans = await Ban.find({ where: { guild_id } });
+	let promisesToAwait: object[] = [];
+	const bansObj: object[] = [];
+
+	bans.filter((ban) => ban.user_id !== ban.executor_id); // pretend self-bans don't exist to prevent victim chasing
+
+	bans.forEach((ban) => {
+		promisesToAwait.push(User.getPublicUser(ban.user_id));
+	});
+
+	const bannedUsers: object[] = await Promise.all(promisesToAwait);
+
+	bans.forEach((ban, index) => {
+		const user = bannedUsers[index] as User;
+		bansObj.push({
+			reason: ban.reason,
+			user: {
+				username: user.username,
+				discriminator: user.discriminator,
+				id: user.id,
+				avatar: user.avatar,
+				public_flags: user.public_flags
+			}
+		});
+	});
+
+	return res.json(bansObj);
+});
+
+router.get("/:user", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+	const user_id = req.params.ban;
+
+	let ban = await Ban.findOneOrFail({ where: { guild_id, user_id } }) as BanRegistrySchema;
+	
+	if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN;
+	// pretend self-bans don't exist to prevent victim chasing
+	
+	/* Filter secret from registry. */
+	
+	ban = ban as BanModeratorSchema;
+
+	delete ban.ip
+
+	return res.json(ban);
+});
+
+router.put("/:user_id", route({ body: "BanCreateSchema", permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+	const banned_user_id = req.params.user_id;
+
+	if ( (req.user_id === banned_user_id) && (banned_user_id === req.permission!.cache.guild?.owner_id))
+		throw new HTTPError("You are the guild owner, hence can't ban yourself", 403);
+	
+	if (req.permission!.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400);
+	
+	const banned_user = await User.getPublicUser(banned_user_id);
+
+	const ban = OrmUtils.mergeDeep(new Ban(),{
+		user_id: banned_user_id,
+		guild_id: guild_id,
+		ip: getIpAdress(req),
+		executor_id: req.user_id,
+		reason: req.body.reason // || otherwise empty
+	});
+
+	await Promise.all([
+		Member.removeFromGuild(banned_user_id, guild_id),
+		ban.save(),
+		emitEvent({
+			event: "GUILD_BAN_ADD",
+			data: {
+				guild_id: guild_id,
+				user: banned_user
+			},
+			guild_id: guild_id
+		} as GuildBanAddEvent)
+	]);
+
+	return res.json(ban);
+});
+
+router.put("/@me", route({ body: "BanCreateSchema"}), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+
+	const banned_user = await User.getPublicUser(req.params.user_id);
+
+	if (req.permission!.cache.guild?.owner_id === req.params.user_id) 
+		throw new HTTPError("You are the guild owner, hence can't ban yourself", 403);
+	
+	const ban = OrmUtils.mergeDeep(new Ban(), {
+		user_id: req.params.user_id,
+		guild_id: guild_id,
+		ip: getIpAdress(req),
+		executor_id: req.params.user_id,
+		reason: req.body.reason // || otherwise empty
+	});
+
+	await Promise.all([
+		Member.removeFromGuild(req.user_id, guild_id),
+		ban.save(),
+		emitEvent({
+			event: "GUILD_BAN_ADD",
+			data: {
+				guild_id: guild_id,
+				user: banned_user
+			},
+			guild_id: guild_id
+		} as GuildBanAddEvent)
+	]);
+
+	return res.json(ban);
+});
+
+router.delete("/:user_id", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => {
+	const { guild_id, user_id } = req.params;
+
+	let ban = await Ban.findOneOrFail({ where: { guild_id, user_id } });
+	
+	if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN;
+	// make self-bans irreversible and hide them from view to avoid victim chasing
+	
+	const banned_user = await User.getPublicUser(user_id);
+	
+	await Promise.all([
+		Ban.delete({
+			user_id: user_id,
+			guild_id
+		}),
+
+		emitEvent({
+			event: "GUILD_BAN_REMOVE",
+			data: {
+				guild_id,
+				user: banned_user
+			},
+			guild_id
+		} as GuildBanRemoveEvent)
+	]);
+
+	return res.status(204).send();
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/channels.ts b/src/api/routes/guilds/#guild_id/channels.ts
new file mode 100644
index 00000000..8f2d3643
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/channels.ts
@@ -0,0 +1,57 @@
+import { Router, Response, Request } from "express";
+import { Channel, ChannelUpdateEvent, getPermission, emitEvent, ChannelModifySchema, ChannelReorderSchema } from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { route } from "@fosscord/api";
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+	const channels = await Channel.find({ where: { guild_id } });
+
+	res.json(channels);
+});
+
+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;
+
+	const channel = await Channel.createChannel({ ...body, guild_id }, req.user_id);
+
+	res.status(201).json(channel);
+});
+
+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 == null && !x.parent_id) throw new HTTPError(`You need to at least specify position or parent_id`, 400);
+
+			const opts: any = {};
+			if (x.position != null) 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({ where: { guild_id, id: x.id } });
+
+			await emitEvent({ event: "CHANNEL_UPDATE", data: channel, channel_id: x.id, guild_id } as ChannelUpdateEvent);
+		})
+	]);
+
+	res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/delete.ts b/src/api/routes/guilds/#guild_id/delete.ts
new file mode 100644
index 00000000..e2624651
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/delete.ts
@@ -0,0 +1,30 @@
+import { Channel, emitEvent, GuildDeleteEvent, Guild, Member, Message, Role, Invite, Emoji } from "@fosscord/util";
+import { Router, Request, Response } from "express";
+import { HTTPError } from "@fosscord/util";
+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("/", route({}), async (req: Request, res: Response) => {
+	let { guild_id } = req.params;
+
+	const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: ["owner_id"] });
+	if (guild.owner_id !== req.user_id) throw new HTTPError("You are not the owner of this guild", 401);
+
+	await Promise.all([
+		Guild.delete({ id: guild_id }), // this will also delete all guild related data
+		emitEvent({
+			event: "GUILD_DELETE",
+			data: {
+				id: guild_id
+			},
+			guild_id: guild_id
+		} as GuildDeleteEvent)
+	]);
+
+	return res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/discovery-requirements.ts b/src/api/routes/guilds/#guild_id/discovery-requirements.ts
new file mode 100644
index 00000000..ad20633f
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/discovery-requirements.ts
@@ -0,0 +1,39 @@
+import { Guild, Config } from "@fosscord/util";
+
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;	
+    // TODO:
+    // Load from database
+    // Admin control, but for now it allows anyone to be discoverable
+
+	res.send({
+		guild_id: guild_id,
+		safe_environment: true,
+        healthy: true,
+        health_score_pending: false,
+        size: true,
+        nsfw_properties: {},
+        protected: true,
+        sufficient: true,
+        sufficient_without_grace_period: true,
+        valid_rules_channel: true,
+        retention_healthy: true,
+        engagement_healthy: true,
+        age: true,
+        minimum_age: 0,
+        health_score: {
+            avg_nonnew_participators: 0,
+            avg_nonnew_communicators: 0,
+            num_intentful_joiners: 0,
+            perc_ret_w1_intentful: 0
+        },
+        minimum_size: 0
+	});
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/emojis.ts b/src/api/routes/guilds/#guild_id/emojis.ts
new file mode 100644
index 00000000..4bf4bdcd
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/emojis.ts
@@ -0,0 +1,107 @@
+import { Router, Request, Response } from "express";
+import { Config, DiscordApiErrors, emitEvent, Emoji, EmojiCreateSchema, EmojiModifySchema, GuildEmojisUpdateEvent, handleFile, Member, Snowflake, User } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { OrmUtils } from "@fosscord/util";
+
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+
+	await Member.IsInGuildOrFail(req.user_id, guild_id);
+
+	const emojis = await Emoji.find({ where: { guild_id: guild_id }, relations: ["user"] });
+
+	return res.json(emojis);
+});
+
+router.get("/:emoji_id", route({}), async (req: Request, res: Response) => {
+	const { guild_id, emoji_id } = req.params;
+
+	await Member.IsInGuildOrFail(req.user_id, guild_id);
+
+	const emoji = await Emoji.findOneOrFail({ where: { guild_id: guild_id, id: emoji_id }, relations: ["user"] });
+
+	return res.json(emoji);
+});
+
+router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+	const body = req.body as EmojiCreateSchema;
+
+	const id = Snowflake.generate();
+	const emoji_count = await Emoji.count({ where: { guild_id } });
+	const { maxEmojis } = Config.get().limits.guild;
+
+	if (emoji_count >= maxEmojis) throw DiscordApiErrors.MAXIMUM_NUMBER_OF_EMOJIS_REACHED.withParams(maxEmojis);
+	if (body.require_colons == null) body.require_colons = true;
+
+	const user = await User.findOneOrFail({ where: { id: req.user_id } });
+	body.image = (await handleFile(`/emojis/${id}`, body.image)) as string;
+
+	const emoji = await OrmUtils.mergeDeep(new Emoji(), {
+		id: id,
+		guild_id: guild_id,
+		...body,
+		user: user,
+		managed: false,
+		animated: false, // TODO: Add support animated emojis
+		available: true,
+		roles: []
+	}).save();
+
+	await emitEvent({
+		event: "GUILD_EMOJIS_UPDATE",
+		guild_id: guild_id,
+		data: {
+			guild_id: guild_id,
+			emojis: await Emoji.find({ where: { guild_id } })
+		}
+	} as GuildEmojisUpdateEvent);
+
+	return res.status(201).json(emoji);
+});
+
+router.patch(
+	"/:emoji_id",
+	route({ body: "EmojiModifySchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }),
+	async (req: Request, res: Response) => {
+		const { emoji_id, guild_id } = req.params;
+		const body = req.body as EmojiModifySchema;
+
+		const emoji = await OrmUtils.mergeDeep(new Emoji(), { ...body, id: emoji_id, guild_id: guild_id }).save();
+
+		await emitEvent({
+			event: "GUILD_EMOJIS_UPDATE",
+			guild_id: guild_id,
+			data: {
+				guild_id: guild_id,
+				emojis: await Emoji.find({ where: { guild_id } })
+			}
+		} as GuildEmojisUpdateEvent);
+
+		return res.json(emoji);
+	}
+);
+
+router.delete("/:emoji_id", route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => {
+	const { emoji_id, guild_id } = req.params;
+
+	await Emoji.delete({
+		id: emoji_id,
+		guild_id: guild_id
+	});
+
+	await emitEvent({
+		event: "GUILD_EMOJIS_UPDATE",
+		guild_id: guild_id,
+		data: {
+			guild_id: guild_id,
+			emojis: await Emoji.find({ where: { guild_id } })
+		}
+	} as GuildEmojisUpdateEvent);
+
+	res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/index.ts b/src/api/routes/guilds/#guild_id/index.ts
new file mode 100644
index 00000000..a9712c71
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/index.ts
@@ -0,0 +1,60 @@
+import { Request, Response, Router } from "express";
+import { DiscordApiErrors, emitEvent, getPermission, getRights, Guild, GuildUpdateEvent, GuildUpdateSchema, handleFile, Member } from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { OrmUtils } from "@fosscord/util";
+
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+
+	const [guild, member] = await Promise.all([
+		Guild.findOneOrFail({ where: { id: guild_id } }),
+		Member.findOne({ where: { guild_id, id: req.user_id } })
+	]);
+	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;
+
+	return res.send(guild);
+});
+
+router.patch("/", route({ body: "GuildUpdateSchema"}), async (req: Request, res: Response) => {
+	const body = req.body as GuildUpdateSchema;
+	const { guild_id } = req.params;
+	
+	
+	const rights = await getRights(req.user_id);
+	const permission = await getPermission(req.user_id, guild_id);
+	
+	if (!rights.has("MANAGE_GUILDS")||!permission.has("MANAGE_GUILD"))
+		throw DiscordApiErrors.MISSING_PERMISSIONS.withParams("MANAGE_GUILD");
+	
+	// TODO: guild update check image
+
+	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);
+
+	let guild = await Guild.findOneOrFail({
+		where: { id: guild_id },
+		relations: ["emojis", "roles", "stickers"]
+	});
+	// TODO: check if body ids are valid
+	guild = OrmUtils.mergeDeep(guild, body);
+
+	//TODO: check this, removed toJSON call
+	const data = JSON.parse(JSON.stringify(guild));
+	// TODO: guild hashes
+	// TODO: fix vanity_url_code, template_id
+	delete data.vanity_url_code;
+	delete data.template_id;
+
+	await Promise.all([guild.save(), emitEvent({ event: "GUILD_UPDATE", data, guild_id } as GuildUpdateEvent)]);
+
+	return res.json(data);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/integrations.ts b/src/api/routes/guilds/#guild_id/integrations.ts
new file mode 100644
index 00000000..90650111
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/integrations.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { route } from "@fosscord/api";
+const router = Router();
+
+//TODO: implement integrations list
+router.get("/", route({}), async (req: Request, res: Response) => {
+	res.json([]);
+});
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/invites.ts b/src/api/routes/guilds/#guild_id/invites.ts
new file mode 100644
index 00000000..b7534e31
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/invites.ts
@@ -0,0 +1,15 @@
+import { getPermission, Invite, PublicInviteRelation } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { Request, Response, Router } from "express";
+
+const router = Router();
+
+router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+
+	const invites = await Invite.find({ where: { guild_id }, relations: PublicInviteRelation });
+
+	return res.json(invites);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts
new file mode 100644
index 00000000..794369d8
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/members/#member_id/index.ts
@@ -0,0 +1,98 @@
+import { Request, Response, Router } from "express";
+import { Member, getPermission, getRights, Role, GuildMemberUpdateEvent, emitEvent, Sticker, Emoji, Rights, Guild, MemberChangeSchema } from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { OrmUtils } from "@fosscord/util";
+
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { guild_id, member_id } = req.params;
+	await Member.IsInGuildOrFail(req.user_id, guild_id);
+
+	const member = await Member.findOneOrFail({ where: { id: member_id, guild_id } });
+
+	return res.json(member);
+});
+
+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"] });
+	const permission = await getPermission(req.user_id, guild_id);
+	const everyone = await Role.findOneOrFail({ where: { guild_id: guild_id, name: "@everyone", position: 0 } });
+
+	if (body.roles) {
+		permission.hasThrow("MANAGE_ROLES");
+
+		if (body.roles.indexOf(everyone.id) === -1) body.roles.push(everyone.id);
+		member.roles = body.roles.map((x) => OrmUtils.mergeDeep(new Role(), { id: x })); // foreign key constraint will fail if role doesn't exist
+	}
+
+	await member.save();
+
+	member.roles = member.roles.filter((x) => x.id !== everyone.id);
+
+	// 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,
+		data: { ...member, roles: member.roles.map((x) => x.id) }
+	} as GuildMemberUpdateEvent);
+
+	res.json(member);
+});
+
+router.put("/", route({}), async (req: Request, res: Response) => {
+
+	// TODO: Lurker mode
+
+	const rights = await getRights(req.user_id);
+
+	let { guild_id, member_id } = req.params;
+	if (member_id === "@me") {
+		member_id = req.user_id;
+		rights.hasThrow("JOIN_GUILDS");
+	} else {
+		// TODO: join others by controller	
+	}
+
+	let guild = await Guild.findOneOrFail({
+		where: { id: guild_id }
+	});
+
+	let emoji = await Emoji.find({
+		where: { guild_id: guild_id }
+	});
+
+	let roles = await Role.find({
+		where: { guild_id: guild_id }
+	});
+
+	let stickers = await Sticker.find({
+		where: { guild_id: guild_id }
+	});
+
+	await Member.addToGuild(member_id, guild_id);
+	res.send({ ...guild, emojis: emoji, roles: roles, stickers: stickers });
+});
+
+router.delete("/", route({}), async (req: Request, res: Response) => {
+	const permission = await getPermission(req.user_id);
+	const rights = await getRights(req.user_id);
+	const { guild_id, member_id } = req.params;
+	if (member_id !== "@me" || member_id === req.user_id) {
+		// TODO: unless force-joined
+		rights.hasThrow("SELF_LEAVE_GROUPS");
+	} else {
+		rights.hasThrow("KICK_BAN_MEMBERS");
+		permission.hasThrow("KICK_MEMBERS");
+	}
+
+	await Member.removeFromGuild(member_id, guild_id);
+	res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts
new file mode 100644
index 00000000..a6c71333
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/members/#member_id/nick.ts
@@ -0,0 +1,22 @@
+import { getPermission, Member, PermissionResolvable } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { Request, Response, Router } from "express";
+
+const router = Router();
+
+router.patch("/", route({ body: "MemberNickChangeSchema" }), async (req: Request, res: Response) => {
+	let { guild_id, member_id } = req.params;
+	let permissionString: PermissionResolvable = "MANAGE_NICKNAMES";
+	if (member_id === "@me") {
+		member_id = req.user_id;
+		permissionString = "CHANGE_NICKNAME";
+	}
+
+	const perms = await getPermission(req.user_id, guild_id);
+	perms.hasThrow(permissionString);
+
+	await Member.changeNickname(member_id, guild_id, req.body.nick);
+	res.status(200).send();
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
new file mode 100644
index 00000000..8f5ca7ba
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
@@ -0,0 +1,21 @@
+import { getPermission, Member } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { Request, Response, Router } from "express";
+
+const router = Router();
+
+router.delete("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
+	const { guild_id, role_id, member_id } = req.params;
+
+	await Member.removeRole(member_id, guild_id, role_id);
+	res.sendStatus(204);
+});
+
+router.put("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
+	const { guild_id, role_id, member_id } = req.params;
+
+	await Member.addRole(member_id, guild_id, role_id);
+	res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/members/index.ts b/src/api/routes/guilds/#guild_id/members/index.ts
new file mode 100644
index 00000000..2ed28bda
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/members/index.ts
@@ -0,0 +1,31 @@
+import { Request, Response, Router } from "express";
+import { Guild, Member, PublicMemberProjection } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { MoreThan } from "typeorm";
+import { HTTPError } from "@fosscord/util";
+
+const router = Router();
+
+// TODO: send over websocket
+// TODO: check for GUILD_MEMBERS intent
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+	const limit = Number(req.query.limit) || 1;
+	if (limit > 1000 || limit < 1) throw new HTTPError("Limit must be between 1 and 1000");
+	const after = `${req.query.after}`;
+	const query = after ? { id: MoreThan(after) } : {};
+
+	await Member.IsInGuildOrFail(req.user_id, guild_id);
+
+	const members = await Member.find({
+		where: { guild_id, ...query },
+		select: PublicMemberProjection,
+		take: limit,
+		order: { id: "ASC" }
+	});
+
+	return res.json(members);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/premium.ts b/src/api/routes/guilds/#guild_id/premium.ts
new file mode 100644
index 00000000..75361ac6
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/premium.ts
@@ -0,0 +1,10 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+const router = Router();
+
+router.get("/subscriptions", route({}), async (req: Request, res: Response) => {
+	// TODO:
+	res.json([]);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/prune.ts b/src/api/routes/guilds/#guild_id/prune.ts
new file mode 100644
index 00000000..673f022f
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/prune.ts
@@ -0,0 +1,79 @@
+import { Router, Request, Response } from "express";
+import { Guild, Member, Snowflake } from "@fosscord/util";
+import { LessThan, IsNull } from "typeorm";
+import { route } from "@fosscord/api";
+const router = Router();
+
+//Returns all inactive members, respecting role hierarchy
+export const inactiveMembers = async (guild_id: string, user_id: string, days: number, roles: string[] = []) => {
+	let date = new Date();
+	date.setDate(date.getDate() - days);
+	//Snowflake should have `generateFromTime` method? Or similar?
+	let minId = BigInt(date.valueOf() - Snowflake.EPOCH) << BigInt(22);
+
+	/**
+	idea: ability to customise the cutoff variable
+	possible candidates: public read receipt, last presence, last VC leave
+	**/
+	let members = await Member.find({
+		where: [
+			{
+				guild_id,
+				last_message_id: LessThan(minId.toString())
+			},
+			{
+				last_message_id: IsNull()
+			}
+		],
+		relations: ["roles"]
+	});
+	console.log(members);
+	if (!members.length) return [];
+
+	//I'm sure I can do this in the above db query ( and it would probably be better to do so ), but oh well.
+	if (roles.length && members.length) members = members.filter((user) => user.roles?.some((role) => roles.includes(role.id)));
+
+	const me = await Member.findOneOrFail({ where: { id: user_id, guild_id }, relations: ["roles"] });
+	const myHighestRole = Math.max(...(me.roles?.map((x) => x.position) || []));
+
+	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+
+	members = members.filter(
+		(member) =>
+			member.id !== guild.owner_id && //can't kick owner
+			member.roles?.some(
+				(role) =>
+					role.position < myHighestRole || //roles higher than me can't be kicked
+					me.id === guild.owner_id //owner can kick anyone
+			)
+	);
+
+	return members;
+};
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const days = parseInt(req.query.days as string);
+
+	let roles = req.query.include_roles;
+	if (typeof roles === "string") roles = [roles]; //express will return array otherwise
+
+	const members = await inactiveMembers(req.params.guild_id, req.user_id, days, roles as string[]);
+
+	res.send({ pruned: members.length });
+});
+
+router.post("/", route({ permission: "KICK_MEMBERS", right: "KICK_BAN_MEMBERS" }), async (req: Request, res: Response) => {
+	const days = parseInt(req.body.days);
+
+	let roles = req.query.include_roles;
+	if (typeof roles === "string") roles = [roles];
+
+	const { guild_id } = req.params;
+	const members = await inactiveMembers(guild_id, req.user_id, days, roles as string[]);
+
+	await Promise.all(members.map((x) => Member.removeFromGuild(x.id, guild_id)));
+
+	res.send({ purged: members.length });
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/regions.ts b/src/api/routes/guilds/#guild_id/regions.ts
new file mode 100644
index 00000000..308d5ee5
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/regions.ts
@@ -0,0 +1,15 @@
+import { Config, Guild, Member } from "@fosscord/util";
+import { Request, Response, Router } from "express";
+import { getVoiceRegions, route } from "@fosscord/api";
+import { getIpAdress } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+	//TODO we should use an enum for guild's features and not hardcoded strings
+	return res.json(await getVoiceRegions(getIpAdress(req), guild.features.includes("VIP_REGIONS")));
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts
new file mode 100644
index 00000000..d4422a9c
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/roles/#role_id/index.ts
@@ -0,0 +1,68 @@
+import { Router, Request, Response } from "express";
+import { Role, Member, GuildRoleUpdateEvent, GuildRoleDeleteEvent, emitEvent, handleFile, RoleModifySchema } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { HTTPError } from "@fosscord/util";
+import { OrmUtils } from "@fosscord/util";
+
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { guild_id, role_id } = req.params;
+	await Member.IsInGuildOrFail(req.user_id, guild_id);
+	const role = await Role.findOneOrFail({ where: { guild_id, id: role_id } });
+	return res.json(role);
+});
+
+router.delete("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
+	const { guild_id, role_id } = req.params;
+	if (role_id === guild_id) throw new HTTPError("You can't delete the @everyone role");
+
+	await Promise.all([
+		Role.delete({
+			id: role_id,
+			guild_id: guild_id
+		}),
+		emitEvent({
+			event: "GUILD_ROLE_DELETE",
+			guild_id,
+			data: {
+				guild_id,
+				role_id
+			}
+		} as GuildRoleDeleteEvent)
+	]);
+
+	res.sendStatus(204);
+});
+
+// TODO: check role hierarchy
+
+router.patch("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => {
+	const { role_id, guild_id } = req.params;
+	const body = req.body as RoleModifySchema;
+
+	if (body.icon) body.icon = await handleFile(`/role-icons/${role_id}`, body.icon as string);
+
+	const role = OrmUtils.mergeDeep(new Role(), {
+		...body,
+		id: role_id,
+		guild_id,
+		permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0"))
+	});
+
+	await Promise.all([
+		role.save(),
+		emitEvent({
+			event: "GUILD_ROLE_UPDATE",
+			guild_id,
+			data: {
+				guild_id,
+				role
+			}
+		} as GuildRoleUpdateEvent)
+	]);
+
+	res.json(role);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/roles/index.ts b/src/api/routes/guilds/#guild_id/roles/index.ts
new file mode 100644
index 00000000..17f0b5e9
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/roles/index.ts
@@ -0,0 +1,98 @@
+import { Request, Response, Router } from "express";
+import {
+	Role,
+	getPermission,
+	Member,
+	GuildRoleCreateEvent,
+	GuildRoleUpdateEvent,
+	GuildRoleDeleteEvent,
+	emitEvent,
+	Config,
+	DiscordApiErrors,
+	handleFile,
+	RoleModifySchema,
+	RolePositionUpdateSchema
+} from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { OrmUtils } from "@fosscord/util";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const guild_id = req.params.guild_id;
+
+	await Member.IsInGuildOrFail(req.user_id, guild_id);
+
+	const roles = await Role.find({ where: { guild_id } });
+
+	return res.json(roles);
+});
+
+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 role_count = await Role.count({ where: { guild_id } });
+	const { maxRoles } = Config.get().limits.guild;
+
+	if (role_count > maxRoles) throw DiscordApiErrors.MAXIMUM_ROLES.withParams(maxRoles);
+
+	let role: Role = OrmUtils.mergeDeep(new Role(),{
+		// values before ...body are default and can be overriden
+		position: 0,
+		hoist: false,
+		color: 0,
+		mentionable: false,
+		...body,
+		guild_id: guild_id,
+		managed: false,
+		permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0")),
+		tags: undefined,
+		icon: null,
+		unicode_emoji: null
+	});
+
+	await Promise.all([
+		role.save(),
+		emitEvent({
+			event: "GUILD_ROLE_CREATE",
+			guild_id,
+			data: {
+				guild_id,
+				role: role
+			}
+		} as GuildRoleCreateEvent)
+	]);
+
+	res.json(role);
+});
+
+router.patch("/", route({ body: "RolePositionUpdateSchema" }), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+	const body = req.body as RolePositionUpdateSchema;
+
+	const perms = await getPermission(req.user_id, guild_id);
+	perms.hasThrow("MANAGE_ROLES");
+
+	await Promise.all(body.map(async (x) => Role.update({ guild_id, id: x.id }, { position: x.position })));
+
+	const roles = await Role.find({ where: body.map((x) => ({ id: x.id, guild_id })) });
+
+	await Promise.all(
+		roles.map((x) =>
+			emitEvent({
+				event: "GUILD_ROLE_UPDATE",
+				guild_id,
+				data: {
+					guild_id,
+					role: x
+				}
+			} as GuildRoleUpdateEvent)
+		)
+	);
+
+	res.json(roles);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/stickers.ts b/src/api/routes/guilds/#guild_id/stickers.ts
new file mode 100644
index 00000000..71c9dfcd
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/stickers.ts
@@ -0,0 +1,121 @@
+import {
+	emitEvent,
+	GuildStickersUpdateEvent,
+	handleFile,
+	Member,
+	ModifyGuildStickerSchema,
+	Snowflake,
+	Sticker,
+	StickerFormatType,
+	StickerType,
+	uploadFile
+} from "@fosscord/util";
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import multer from "multer";
+import { HTTPError } from "@fosscord/util";
+import { OrmUtils } from "@fosscord/util";
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+	await Member.IsInGuildOrFail(req.user_id, guild_id);
+
+	res.json(await Sticker.find({ where: { guild_id } }));
+});
+
+const bodyParser = multer({
+	limits: {
+		fileSize: 1024 * 1024 * 100,
+		fields: 10,
+		files: 1
+	},
+	storage: multer.memoryStorage()
+}).single("file");
+
+router.post(
+	"/",
+	bodyParser,
+	route({ permission: "MANAGE_EMOJIS_AND_STICKERS", body: "ModifyGuildStickerSchema" }),
+	async (req: Request, res: Response) => {
+		if (!req.file) throw new HTTPError("missing file");
+
+		const { guild_id } = req.params;
+		const body = req.body as ModifyGuildStickerSchema;
+		const id = Snowflake.generate();
+
+		const [sticker] = await Promise.all([
+			OrmUtils.mergeDeep(new Sticker(), {
+				...body,
+				guild_id,
+				id,
+				type: StickerType.GUILD,
+				format_type: getStickerFormat(req.file.mimetype),
+				available: true
+			}).save(),
+			uploadFile(`/stickers/${id}`, req.file)
+		]);
+
+		await sendStickerUpdateEvent(guild_id);
+
+		res.json(sticker);
+	}
+);
+
+export function getStickerFormat(mime_type: string) {
+	switch (mime_type) {
+		case "image/apng":
+			return StickerFormatType.APNG;
+		case "application/json":
+			return StickerFormatType.LOTTIE;
+		case "image/png":
+			return StickerFormatType.PNG;
+		case "image/gif":
+			return StickerFormatType.GIF;
+		default:
+			throw new HTTPError("invalid sticker format: must be png, apng or lottie");
+	}
+}
+
+router.get("/:sticker_id", route({}), async (req: Request, res: Response) => {
+	const { guild_id, sticker_id } = req.params;
+	await Member.IsInGuildOrFail(req.user_id, guild_id);
+
+	res.json(await Sticker.findOneOrFail({ where: { guild_id, id: sticker_id } }));
+});
+
+router.patch(
+	"/:sticker_id",
+	route({ body: "ModifyGuildStickerSchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }),
+	async (req: Request, res: Response) => {
+		const { guild_id, sticker_id } = req.params;
+		const body = req.body as ModifyGuildStickerSchema;
+
+		const sticker = await OrmUtils.mergeDeep(new Sticker(), { ...body, guild_id, id: sticker_id }).save();
+		await sendStickerUpdateEvent(guild_id);
+
+		return res.json(sticker);
+	}
+);
+
+async function sendStickerUpdateEvent(guild_id: string) {
+	return emitEvent({
+		event: "GUILD_STICKERS_UPDATE",
+		guild_id: guild_id,
+		data: {
+			guild_id: guild_id,
+			stickers: await Sticker.find({ where: { guild_id } })
+		}
+	} as GuildStickersUpdateEvent);
+}
+
+router.delete("/:sticker_id", route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => {
+	const { guild_id, sticker_id } = req.params;
+
+	await Sticker.delete({ guild_id, id: sticker_id });
+	await sendStickerUpdateEvent(guild_id);
+
+	return res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/templates.ts b/src/api/routes/guilds/#guild_id/templates.ts
new file mode 100644
index 00000000..9c79692d
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/templates.ts
@@ -0,0 +1,83 @@
+import { Request, Response, Router } from "express";
+import { Guild, Template } from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { generateCode } from "@fosscord/api";
+import { OrmUtils } from "@fosscord/util";
+
+const router: Router = Router();
+
+const TemplateGuildProjection: (keyof Guild)[] = [
+	"name",
+	"description",
+	"region",
+	"verification_level",
+	"default_message_notifications",
+	"explicit_content_filter",
+	"preferred_locale",
+	"afk_timeout",
+	"roles",
+	// "channels",
+	"afk_channel_id",
+	"system_channel_id",
+	"system_channel_flags",
+	"icon"
+];
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+
+	let templates = await Template.find({ where: { source_guild_id: guild_id } });
+
+	return res.json(templates);
+});
+
+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 exists = await Template.findOneOrFail({ where: { id: guild_id } }).catch((e) => {});
+	if (exists) throw new HTTPError("Template already exists", 400);
+
+	const template = await OrmUtils.mergeDeep(new Template(), {
+		...req.body,
+		code: generateCode(),
+		creator_id: req.user_id,
+		created_at: new Date(),
+		updated_at: new Date(),
+		source_guild_id: guild_id,
+		serialized_source_guild: guild
+	}).save();
+
+	res.json(template);
+});
+
+router.delete("/:code", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
+	const { code, guild_id } = req.params;
+
+	const template = await Template.delete({
+		code,
+		source_guild_id: guild_id
+	});
+
+	res.json(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 template = await OrmUtils.mergeDeep(new Template(), { code, serialized_source_guild: guild }).save();
+
+	res.json(template);
+});
+
+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 template = await OrmUtils.mergeDeep(new Template(), { code, name: name, description: description, source_guild_id: guild_id }).save();
+
+	res.json(template);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/vanity-url.ts b/src/api/routes/guilds/#guild_id/vanity-url.ts
new file mode 100644
index 00000000..ff92ce8d
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/vanity-url.ts
@@ -0,0 +1,59 @@
+import { Channel, ChannelType, getPermission, Guild, Invite, trimSpecial, VanityUrlSchema } from "@fosscord/util";
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { HTTPError } from "@fosscord/util";
+import { OrmUtils } from "@fosscord/util";
+
+const router = Router();
+
+const InviteRegex = /\W/g;
+
+router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+
+	if (!guild.features.includes("ALIASABLE_NAMES")) {
+		const invite = await Invite.findOne({ where: { guild_id: guild_id, vanity_url: true } });
+		if (!invite) return res.json({ code: null });
+
+		return res.json({ code: invite.code, uses: invite.uses });
+	} else {
+		const invite = await Invite.find({ where: { guild_id: guild_id, vanity_url: true } });
+		if (!invite || invite.length == 0) return res.json({ code: null });
+
+		return res.json(invite.map((x) => ({ code: x.code, uses: x.uses })));
+	}
+});
+
+router.patch("/", route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+	const body = req.body as VanityUrlSchema;
+	const code = body.code?.replace(InviteRegex, "");
+
+	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+	if (!guild.features.includes("VANITY_URL")) throw new HTTPError("Your guild doesn't support vanity urls");
+
+	if (!code || code.length === 0) throw new HTTPError("Code cannot be null or empty");
+
+	const invite = await Invite.findOne({ where: { code } });
+	if (invite) throw new HTTPError("Invite already exists");
+
+	const { id } = await Channel.findOneOrFail({ where: { guild_id, type: ChannelType.GUILD_TEXT } });
+
+	await OrmUtils.mergeDeep(new Invite(), {
+		vanity_url: true,
+		code: code,
+		temporary: false,
+		uses: 0,
+		max_uses: 0,
+		max_age: 0,
+		created_at: new Date(),
+		expires_at: new Date(),
+		guild_id: guild_id,
+		channel_id: id
+	}).save();
+
+	return res.json({ where: { code } });
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts b/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts
new file mode 100644
index 00000000..28a9e8c1
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/voice-states/#user_id/index.ts
@@ -0,0 +1,51 @@
+import { Channel, ChannelType, DiscordApiErrors, emitEvent, getPermission, VoiceState, VoiceStateUpdateEvent, VoiceStateUpdateSchema } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { Request, Response, Router } from "express";
+import { OrmUtils } from "@fosscord/util";
+
+const router = Router();
+router.patch("/", route({ body: "VoiceStateUpdateSchema" }), async (req: Request, res: Response) => {
+	const body = req.body as VoiceStateUpdateSchema;
+	let { 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");
+
+	let voice_state = await VoiceState.findOne({
+		where: {
+			guild_id,
+			channel_id: body.channel_id,
+			user_id
+		}
+	});
+	if (!voice_state) throw DiscordApiErrors.UNKNOWN_VOICE_STATE;
+
+	voice_state = OrmUtils.mergeDeep(voice_state, body) as VoiceState;
+	const channel = await Channel.findOneOrFail({ where: { 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);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/webhooks.ts b/src/api/routes/guilds/#guild_id/webhooks.ts
new file mode 100644
index 00000000..c8c1eb5c
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/webhooks.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { route } from "@fosscord/api";
+const router = Router();
+
+//TODO: implement webhooks
+router.get("/", route({}), async (req: Request, res: Response) => {
+	res.json([]);
+});
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/welcome_screen.ts b/src/api/routes/guilds/#guild_id/welcome_screen.ts
new file mode 100644
index 00000000..d08300ba
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/welcome_screen.ts
@@ -0,0 +1,31 @@
+import { Request, Response, Router } from "express";
+import { Guild, getPermission, Snowflake, Member, GuildUpdateWelcomeScreenSchema } from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const guild_id = req.params.guild_id;
+
+	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+	await Member.IsInGuildOrFail(req.user_id, guild_id);
+
+	res.json(guild.welcome_screen);
+});
+
+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({ where: { id: guild_id } });
+
+	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;
+	if (body.enabled != null) guild.welcome_screen.enabled = body.enabled;
+
+	res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/widget.json.ts b/src/api/routes/guilds/#guild_id/widget.json.ts
new file mode 100644
index 00000000..37739418
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/widget.json.ts
@@ -0,0 +1,83 @@
+import { Request, Response, Router } from "express";
+import { Config, Permissions, Guild, Invite, Channel, Member } from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { random, route } from "@fosscord/api";
+import { OrmUtils } from "@fosscord/util";
+
+const router: Router = Router();
+
+// Undocumented API notes:
+// An invite is created for the widget_channel_id on request (only if an existing one created by the widget doesn't already exist)
+// This invite created doesn't include an inviter object like user created ones and has a default expiry of 24 hours
+// Missing user object information is intentional (https://github.com/discord/discord-api-docs/issues/1287)
+// channels returns voice channel objects where @everyone has the CONNECT permission
+// members (max 100 returned) is a sample of all members, and bots par invisible status, there exists some alphabetical distribution pattern between the members returned
+
+// 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("/", route({}), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+
+	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+	if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404);
+
+	// Fetch existing widget invite for widget channel
+	let invite = await Invite.findOne({ where: { channel_id: guild.widget_channel_id } });
+
+	if (guild.widget_channel_id && !invite) {
+		// Create invite for channel if none exists
+		// TODO: Refactor invite create code to a shared function
+		const max_age = 86400; // 24 hours
+		const expires_at = new Date(max_age * 1000 + Date.now());
+		const body = {
+			code: random(),
+			temporary: false,
+			uses: 0,
+			max_uses: 0,
+			max_age: max_age,
+			expires_at,
+			created_at: new Date(),
+			guild_id,
+			channel_id: guild.widget_channel_id,
+			inviter_id: null
+		};
+
+		invite = await OrmUtils.mergeDeep(new Invite(), body).save();
+	}
+
+	// Fetch voice channels, and the @everyone permissions object
+	const channels = [] as any[];
+
+	(await Channel.find({ where: { guild_id: guild_id, type: 2 }, order: { position: "ASC" } })).filter((doc) => {
+		// Only return channels where @everyone has the CONNECT permission
+		if (
+			doc.permission_overwrites === undefined ||
+			Permissions.channelPermission(doc.permission_overwrites, Permissions.FLAGS.CONNECT) === Permissions.FLAGS.CONNECT
+		) {
+			channels.push({
+				id: doc.id,
+				name: doc.name,
+				position: doc.position
+			});
+		}
+	});
+
+	// Fetch members
+	// TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file)
+	let members = await Member.find({ where: { guild_id } });
+
+	// Construct object to respond with
+	const data = {
+		id: guild_id,
+		name: guild.name,
+		instant_invite: invite?.code,
+		channels: channels,
+		members: members,
+		presence_count: guild.presence_count
+	};
+
+	res.set("Cache-Control", "public, max-age=300");
+	return res.json(data);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/widget.png.ts b/src/api/routes/guilds/#guild_id/widget.png.ts
new file mode 100644
index 00000000..a61d938d
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/widget.png.ts
@@ -0,0 +1,111 @@
+import { Request, Response, Router } from "express";
+import { Guild } from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import fs from "fs";
+import path from "path";
+
+const router: Router = Router();
+
+// TODO: use svg templates instead of node-canvas for improved performance and to change it easily
+
+// https://discord.com/developers/docs/resources/guild#get-guild-widget-image
+// TODO: Cache the response
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+
+	const guild = await Guild.findOneOrFail({ where: { id: guild_id } });
+	if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404);
+
+	// Fetch guild information
+	const icon = guild.icon;
+	const name = guild.name;
+	const presence = guild.presence_count + " ONLINE";
+
+	// Fetch parameter
+	const style = req.query.style?.toString() || "shield";
+	if (!["shield", "banner1", "banner2", "banner3", "banner4"].includes(style)) {
+		throw new HTTPError("Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", 400);
+	}
+
+	// Setup canvas
+	const { createCanvas } = require("canvas");
+	const { loadImage } = require("canvas");
+	const sizeOf = require("image-size");
+
+	// TODO: Widget style templates need Fosscord branding
+	const source = path.join(__dirname, "..", "..", "..", "..", "..", "assets", "widget", `${style}.png`);
+	if (!fs.existsSync(source)) {
+		throw new HTTPError("Widget template does not exist.", 400);
+	}
+
+	// Create base template image for parameter
+	const { width, height } = await sizeOf(source);
+	const canvas = createCanvas(width, height);
+	const ctx = canvas.getContext("2d");
+	const template = await loadImage(source);
+	ctx.drawImage(template, 0, 0);
+
+	// Add the guild specific information to the template asset image
+	switch (style) {
+		case "shield":
+			ctx.textAlign = "center";
+			await drawText(ctx, 73, 13, "#FFFFFF", "thin 10px Verdana", presence);
+			break;
+		case "banner1":
+			if (icon) await drawIcon(ctx, 20, 27, 50, icon);
+			await drawText(ctx, 83, 51, "#FFFFFF", "12px Verdana", name, 22);
+			await drawText(ctx, 83, 66, "#C9D2F0FF", "thin 11px Verdana", presence);
+			break;
+		case "banner2":
+			if (icon) await drawIcon(ctx, 13, 19, 36, icon);
+			await drawText(ctx, 62, 34, "#FFFFFF", "12px Verdana", name, 15);
+			await drawText(ctx, 62, 49, "#C9D2F0FF", "thin 11px Verdana", presence);
+			break;
+		case "banner3":
+			if (icon) await drawIcon(ctx, 20, 20, 50, icon);
+			await drawText(ctx, 83, 44, "#FFFFFF", "12px Verdana", name, 27);
+			await drawText(ctx, 83, 58, "#C9D2F0FF", "thin 11px Verdana", presence);
+			break;
+		case "banner4":
+			if (icon) await drawIcon(ctx, 21, 136, 50, icon);
+			await drawText(ctx, 84, 156, "#FFFFFF", "13px Verdana", name, 27);
+			await drawText(ctx, 84, 171, "#C9D2F0FF", "thin 12px Verdana", presence);
+			break;
+		default:
+			throw new HTTPError("Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", 400);
+	}
+
+	// Return final image
+	const buffer = canvas.toBuffer("image/png");
+	res.set("Content-Type", "image/png");
+	res.set("Cache-Control", "public, max-age=3600");
+	return res.send(buffer);
+});
+
+async function drawIcon(canvas: any, x: number, y: number, scale: number, icon: string) {
+	// @ts-ignore
+	const img = new require("canvas").Image();
+	img.src = icon;
+
+	// Do some canvas clipping magic!
+	canvas.save();
+	canvas.beginPath();
+
+	const r = scale / 2; // use scale to determine radius
+	canvas.arc(x + r, y + r, r, 0, 2 * Math.PI, false); // start circle at x, and y coords + radius to find center
+
+	canvas.clip();
+	canvas.drawImage(img, x, y, scale, scale);
+
+	canvas.restore();
+}
+
+async function drawText(canvas: any, x: number, y: number, color: string, font: string, text: string, maxcharacters?: number) {
+	canvas.fillStyle = color;
+	canvas.font = font;
+	if (text.length > (maxcharacters || 0) && maxcharacters) text = text.slice(0, maxcharacters) + "...";
+	canvas.fillText(text, x, y);
+}
+
+export default router;
diff --git a/src/api/routes/guilds/#guild_id/widget.ts b/src/api/routes/guilds/#guild_id/widget.ts
new file mode 100644
index 00000000..dbb4cc0c
--- /dev/null
+++ b/src/api/routes/guilds/#guild_id/widget.ts
@@ -0,0 +1,27 @@
+import { Request, Response, Router } from "express";
+import { Guild, WidgetModifySchema } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+// https://discord.com/developers/docs/resources/guild#get-guild-widget-settings
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;
+
+	const guild = await Guild.findOneOrFail({ where: { 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("/", route({ body: "WidgetModifySchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => {
+	const body = req.body as WidgetModifySchema;
+	const { guild_id } = req.params;
+
+	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
+
+	return res.json(body);
+});
+
+export default router;
diff --git a/src/api/routes/guilds/index.ts b/src/api/routes/guilds/index.ts
new file mode 100644
index 00000000..e4d66192
--- /dev/null
+++ b/src/api/routes/guilds/index.ts
@@ -0,0 +1,32 @@
+import { Router, Request, Response } from "express";
+import { Role, Guild, Snowflake, Config, getRights, Member, Channel, DiscordApiErrors, handleFile, GuildCreateSchema } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+//TODO: create default channel
+
+router.post("/", route({ body: "GuildCreateSchema", right: "CREATE_GUILDS" }), async (req: Request, res: Response) => {
+	const body = req.body as GuildCreateSchema;
+
+	const { maxGuilds } = Config.get().limits.user;
+	const guild_count = await Member.count({ where: { id: req.user_id } });
+	const rights = await getRights(req.user_id);
+	if ((guild_count >= maxGuilds)&&!rights.has("MANAGE_GUILDS")) {
+		throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds);
+	}
+
+	const guild = await Guild.createGuild({ ...body, owner_id: req.user_id });
+
+	const { autoJoin } = Config.get().guild;
+	if (autoJoin.enabled && !autoJoin.guilds?.length) {
+		// @ts-ignore
+		await Config.set({ guild: { autoJoin: { guilds: [guild.id] } } });
+	}
+
+	await Member.addToGuild(req.user_id, guild.id);
+
+	res.status(201).json({ id: guild.id });
+});
+
+export default router;
diff --git a/src/api/routes/guilds/templates/index.ts b/src/api/routes/guilds/templates/index.ts
new file mode 100644
index 00000000..3a0de9e8
--- /dev/null
+++ b/src/api/routes/guilds/templates/index.ts
@@ -0,0 +1,79 @@
+import { Request, Response, Router } from "express";
+import { Template, Guild, Role, Snowflake, Config, User, Member, DiscordApiErrors, OrmUtils, GuildTemplateCreateSchema } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import fetch from "node-fetch";
+const router: Router = Router();
+
+router.get("/:code", route({}), async (req: Request, res: Response) => {
+	const { allowDiscordTemplates, allowRaws, enabled } = Config.get().templates;
+	if (!enabled) res.json({ code: 403, message: "Template creation & usage is disabled on this instance." }).sendStatus(403);
+
+	const { code } = req.params;
+	
+	if (code.startsWith("discord:")) {
+		if (!allowDiscordTemplates)	return res.json({ code: 403, message: "Discord templates cannot be used on this instance." }).sendStatus(403);
+		const discordTemplateID = code.split("discord:", 2)[1];
+
+		const discordTemplateData = await fetch(`https://discord.com/api/v9/guilds/templates/${discordTemplateID}`, {
+			method: "get",
+			headers: { "Content-Type": "application/json" }
+		});
+		return res.json(await discordTemplateData.json());
+	}
+
+	if (code.startsWith("external:")) {
+		if (!allowRaws)	return res.json({ code: 403, message: "Importing raws is disabled on this instance." }).sendStatus(403);
+
+		return res.json(code.split("external:", 2)[1]);
+	}
+
+	const template = await Template.findOneOrFail({ where: { code } });
+	res.json(template);
+});
+
+router.post("/:code", route({ body: "GuildTemplateCreateSchema" }), async (req: Request, res: Response) => {
+	const { enabled, allowTemplateCreation, allowDiscordTemplates, allowRaws } = Config.get().templates;
+	if (!enabled) return res.json({ code: 403, message: "Template creation & usage is disabled on this instance." }).sendStatus(403);
+	if (!allowTemplateCreation) return res.json({ code: 403, message: "Template creation is disabled on this instance." }).sendStatus(403);
+
+	const { code } = req.params;
+	const body = req.body as GuildTemplateCreateSchema;
+
+	const { maxGuilds } = Config.get().limits.user;
+
+	const guild_count = await Member.count({ where: { id: req.user_id } });
+	if (guild_count >= maxGuilds) {
+		throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds);
+	}
+
+	const template = await Template.findOneOrFail({ where: { code } });
+
+	const guild_id = Snowflake.generate();
+
+	const [guild, role] = await Promise.all([
+		OrmUtils.mergeDeep(new Guild(), {
+			...body,
+			...template.serialized_source_guild,
+			id: guild_id,
+			owner_id: req.user_id
+		}).save(),
+		(OrmUtils.mergeDeep(new Role(), {
+			id: guild_id,
+			guild_id: guild_id,
+			color: 0,
+			hoist: false,
+			managed: true,
+			mentionable: true,
+			name: "@everyone",
+			permissions: BigInt("2251804225"),
+			position: 0,
+			tags: null
+		}) as Role).save()
+	]);
+
+	await Member.addToGuild(req.user_id, guild_id);
+
+	res.status(201).json({ id: guild.id });
+});
+
+export default router;
diff --git a/src/api/routes/invites/index.ts b/src/api/routes/invites/index.ts
new file mode 100644
index 00000000..1b434505
--- /dev/null
+++ b/src/api/routes/invites/index.ts
@@ -0,0 +1,57 @@
+import { Router, Request, Response } from "express";
+import { emitEvent, getPermission, Guild, Invite, InviteDeleteEvent, User, PublicInviteRelation } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { HTTPError } from "@fosscord/util";
+
+const router: Router = Router();
+
+router.get("/:code", route({}), async (req: Request, res: Response) => {
+	const { code } = req.params;
+
+	const invite = await Invite.findOneOrFail({ where: { code }, relations: PublicInviteRelation });
+
+	res.status(200).send(invite);
+});
+
+router.post("/:code", route({right: "USE_MASS_INVITES"}), async (req: Request, res: Response) => {
+	const { code } = req.params;
+    const { guild_id } = await Invite.findOneOrFail({ where: { code } })
+	const { features } = await Guild.findOneOrFail({ where: { id: guild_id} });
+	const { public_flags } = await User.findOneOrFail({ where: { id: req.user_id } });
+	
+	if(features.includes("INTERNAL_EMPLOYEE_ONLY") && (public_flags & 1) !== 1) throw new HTTPError("Only intended for the staff of this server.", 401);
+	if(features.includes("INVITES_CLOSED")) throw new HTTPError("Sorry, this guild has joins closed.", 403);
+	
+	const invite = await Invite.joinGuild(req.user_id, code);
+
+	res.json(invite);
+});
+
+// * cant use permission of route() function because path doesn't have guild_id/channel_id
+router.delete("/:code", route({}), async (req: Request, res: Response) => {
+	const { code } = req.params;
+	const invite = await Invite.findOneOrFail({ where: { code } });
+	const { guild_id, channel_id } = invite;
+
+	const permission = await getPermission(req.user_id, guild_id, channel_id);
+
+	if (!permission.has("MANAGE_GUILD") && !permission.has("MANAGE_CHANNELS"))
+		throw new HTTPError("You missing the MANAGE_GUILD or MANAGE_CHANNELS permission", 401);
+
+	await Promise.all([
+		Invite.delete({ code }),
+		emitEvent({
+			event: "INVITE_DELETE",
+			guild_id: guild_id,
+			data: {
+				channel_id: channel_id,
+				guild_id: guild_id,
+				code: code
+			}
+		} as InviteDeleteEvent)
+	]);
+
+	res.json({ invite: invite });
+});
+
+export default router;
diff --git a/src/api/routes/oauth2/tokens.ts b/src/api/routes/oauth2/tokens.ts
new file mode 100644
index 00000000..bd284221
--- /dev/null
+++ b/src/api/routes/oauth2/tokens.ts
@@ -0,0 +1,10 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	//TODO
+	res.json([]);
+});
+
+export default router;
diff --git a/src/api/routes/outbound-promotions.ts b/src/api/routes/outbound-promotions.ts
new file mode 100644
index 00000000..411e95bf
--- /dev/null
+++ b/src/api/routes/outbound-promotions.ts
@@ -0,0 +1,11 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	//TODO
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/partners/#guild_id/requirements.ts b/src/api/routes/partners/#guild_id/requirements.ts
new file mode 100644
index 00000000..545c5c78
--- /dev/null
+++ b/src/api/routes/partners/#guild_id/requirements.ts
@@ -0,0 +1,40 @@
+
+import { Guild, Config } from "@fosscord/util";
+
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { guild_id } = req.params;	
+    // TODO:
+    // Load from database
+    // Admin control, but for now it allows anyone to be discoverable
+
+	res.send({
+		guild_id: guild_id,
+		safe_environment: true,
+        healthy: true,
+        health_score_pending: false,
+        size: true,
+        nsfw_properties: {},
+        protected: true,
+        sufficient: true,
+        sufficient_without_grace_period: true,
+        valid_rules_channel: true,
+        retention_healthy: true,
+        engagement_healthy: true,
+        age: true,
+        minimum_age: 0,
+        health_score: {
+            avg_nonnew_participators: 0,
+            avg_nonnew_communicators: 0,
+            num_intentful_joiners: 0,
+            perc_ret_w1_intentful: 0
+        },
+        minimum_size: 0
+	});
+});
+
+export default router;
diff --git a/src/api/routes/ping.ts b/src/api/routes/ping.ts
new file mode 100644
index 00000000..3c1da2c3
--- /dev/null
+++ b/src/api/routes/ping.ts
@@ -0,0 +1,26 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+import { Config } from "@fosscord/util";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	const { general } = Config.get();
+	res.send({
+		ping: "pong!",
+		instance: {
+			id: general.instanceId,
+			name: general.instanceName,
+			description: general.instanceDescription,
+			image: general.image,
+
+			correspondenceEmail: general.correspondenceEmail,
+			correspondenceUserID: general.correspondenceUserID,
+
+			frontPage: general.frontPage,
+			tosPage: general.tosPage,
+		},
+	});
+});
+
+export default router;
diff --git a/src/api/routes/policies/instance/domains.ts b/src/api/routes/policies/instance/domains.ts
new file mode 100644
index 00000000..20cd07ba
--- /dev/null
+++ b/src/api/routes/policies/instance/domains.ts
@@ -0,0 +1,18 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { Config } from "@fosscord/util";
+import { config } from "dotenv"
+const router = Router();
+
+router.get("/",route({}), async (req: Request, res: Response) => {
+    const { cdn, gateway } = Config.get();
+    
+    const IdentityForm = {
+        cdn: cdn.endpointPublic || process.env.CDN || "http://localhost:3001",
+        gateway: gateway.endpointPublic || process.env.GATEWAY || "ws://localhost:3002"
+    };
+
+	res.json(IdentityForm);
+});
+
+export default router;
diff --git a/src/api/routes/policies/instance/index.ts b/src/api/routes/policies/instance/index.ts
new file mode 100644
index 00000000..e3da014f
--- /dev/null
+++ b/src/api/routes/policies/instance/index.ts
@@ -0,0 +1,12 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { Config } from "@fosscord/util";
+const router = Router();
+
+
+router.get("/",route({}), async (req: Request, res: Response) => {
+	const { general } = Config.get();
+	res.json(general);
+});
+
+export default router;
diff --git a/src/api/routes/policies/instance/limits.ts b/src/api/routes/policies/instance/limits.ts
new file mode 100644
index 00000000..7de1476b
--- /dev/null
+++ b/src/api/routes/policies/instance/limits.ts
@@ -0,0 +1,11 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { Config } from "@fosscord/util";
+const router = Router();
+
+router.get("/",route({}), async (req: Request, res: Response) => {
+	const { limits } = Config.get();
+	res.json(limits);
+});
+
+export default router;
diff --git a/src/api/routes/scheduled-maintenances/upcoming_json.ts b/src/api/routes/scheduled-maintenances/upcoming_json.ts
new file mode 100644
index 00000000..83092e44
--- /dev/null
+++ b/src/api/routes/scheduled-maintenances/upcoming_json.ts
@@ -0,0 +1,12 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+const router = Router();
+
+router.get("/scheduled-maintenances/upcoming.json",route({}), async (req: Request, res: Response) => {
+	res.json({
+  "page": {},
+  "scheduled_maintenances": {}
+  });
+});
+
+export default router;
diff --git a/src/api/routes/science.ts b/src/api/routes/science.ts
new file mode 100644
index 00000000..8556a3ad
--- /dev/null
+++ b/src/api/routes/science.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.post("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/stage-instances.ts b/src/api/routes/stage-instances.ts
new file mode 100644
index 00000000..411e95bf
--- /dev/null
+++ b/src/api/routes/stage-instances.ts
@@ -0,0 +1,11 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	//TODO
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/sticker-packs/index.ts b/src/api/routes/sticker-packs/index.ts
new file mode 100644
index 00000000..e6560d12
--- /dev/null
+++ b/src/api/routes/sticker-packs/index.ts
@@ -0,0 +1,13 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+import { StickerPack } from "@fosscord/util";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const sticker_packs = await StickerPack.find({ relations: ["stickers"] });
+
+	res.json({ sticker_packs });
+});
+
+export default router;
diff --git a/src/api/routes/stickers/#sticker_id/index.ts b/src/api/routes/stickers/#sticker_id/index.ts
new file mode 100644
index 00000000..b484a7a1
--- /dev/null
+++ b/src/api/routes/stickers/#sticker_id/index.ts
@@ -0,0 +1,12 @@
+import { Sticker } from "@fosscord/util";
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { sticker_id } = req.params;
+
+	res.json(await Sticker.find({ where: { id: sticker_id } }));
+});
+
+export default router;
diff --git a/src/api/routes/stop.ts b/src/api/routes/stop.ts
new file mode 100644
index 00000000..7f8b78ba
--- /dev/null
+++ b/src/api/routes/stop.ts
@@ -0,0 +1,26 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { User } from "@fosscord/util";
+
+const router: Router = Router();
+
+router.post("/", route({}), async (req: Request, res: Response) => {
+	//EXPERIMENTAL: have an "OPERATOR" platform permission implemented for this API route
+	const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["rights"] });
+	if((Number(user.rights) << Number(0))%Number(2)==Number(1)) {
+		console.log("user that POSTed to the API was ALLOWED");
+		console.log(user.rights);
+		res.sendStatus(200)
+		process.kill(process.pid, 'SIGTERM')
+	}
+	else {
+		console.log("operation failed");
+		console.log(user.rights);
+		res.sendStatus(403)
+	}
+});
+
+export default router;
+
+//THIS API CAN ONLY BE USED BY USERS WITH THE 'OPERATOR' RIGHT (which is the value of 1) ONLY IF ANY OTHER RIGHTS ARE ADDED OR IF THE USER DOESNT HAVE PERMISSION,
+//THE REQUEST WILL RETURN 403 'FORBIDDEN'
diff --git a/src/api/routes/store/published-listings/applications.ts b/src/api/routes/store/published-listings/applications.ts
new file mode 100644
index 00000000..060a4c3d
--- /dev/null
+++ b/src/api/routes/store/published-listings/applications.ts
@@ -0,0 +1,79 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/:id", route({}), async (req: Request, res: Response) => {
+	//TODO
+	const id = req.params.id;
+	res.json({
+		id: "",
+		summary: "",
+		sku: {
+			id: "",
+			type: 1,
+			dependent_sku_id: null,
+			application_id: "",
+			manifets_labels: [],
+			access_type: 2,
+			name: "",
+			features: [],
+			release_date: "",
+			premium: false,
+			slug: "",
+			flags: 4,
+			genres: [],
+			legal_notice: "",
+			application: {
+				id: "",
+				name: "",
+				icon: "",
+				description: "",
+				summary: "",
+				cover_image: "",
+				primary_sku_id: "",
+				hook: true,
+				slug: "",
+				guild_id: "",
+				bot_public: "",
+				bot_require_code_grant: false,
+				verify_key: "",
+				publishers: [
+					{
+						id: "",
+						name: ""
+					}
+				],
+				developers: [
+					{
+						id: "",
+						name: ""
+					}
+				],
+				system_requirements: {},
+				show_age_gate: false,
+				price: {
+					amount: 0,
+					currency: "EUR"
+				},
+				locales: []
+			},
+			tagline: "",
+			description: "",
+			carousel_items: [
+				{
+					asset_id: ""
+				}
+			],
+			header_logo_dark_theme: {}, //{id: "", size: 4665, mime_type: "image/gif", width 160, height: 160}
+			header_logo_light_theme: {},
+			box_art: {},
+			thumbnail: {},
+			header_background: {},
+			hero_background: {},
+			assets: []
+		}
+	}).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/store/published-listings/applications/#id/subscription-plans.ts b/src/api/routes/store/published-listings/applications/#id/subscription-plans.ts
new file mode 100644
index 00000000..54151ae5
--- /dev/null
+++ b/src/api/routes/store/published-listings/applications/#id/subscription-plans.ts
@@ -0,0 +1,25 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	//TODO
+	res.json([
+		{
+			id: "",
+			name: "",
+			interval: 1,
+			interval_count: 1,
+			tax_inclusive: true,
+			sku_id: "",
+			fallback_price: 499,
+			fallback_currency: "eur",
+			currency: "eur",
+			price: 4199,
+			price_tier: null
+		}
+	]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/store/published-listings/skus.ts b/src/api/routes/store/published-listings/skus.ts
new file mode 100644
index 00000000..060a4c3d
--- /dev/null
+++ b/src/api/routes/store/published-listings/skus.ts
@@ -0,0 +1,79 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/:id", route({}), async (req: Request, res: Response) => {
+	//TODO
+	const id = req.params.id;
+	res.json({
+		id: "",
+		summary: "",
+		sku: {
+			id: "",
+			type: 1,
+			dependent_sku_id: null,
+			application_id: "",
+			manifets_labels: [],
+			access_type: 2,
+			name: "",
+			features: [],
+			release_date: "",
+			premium: false,
+			slug: "",
+			flags: 4,
+			genres: [],
+			legal_notice: "",
+			application: {
+				id: "",
+				name: "",
+				icon: "",
+				description: "",
+				summary: "",
+				cover_image: "",
+				primary_sku_id: "",
+				hook: true,
+				slug: "",
+				guild_id: "",
+				bot_public: "",
+				bot_require_code_grant: false,
+				verify_key: "",
+				publishers: [
+					{
+						id: "",
+						name: ""
+					}
+				],
+				developers: [
+					{
+						id: "",
+						name: ""
+					}
+				],
+				system_requirements: {},
+				show_age_gate: false,
+				price: {
+					amount: 0,
+					currency: "EUR"
+				},
+				locales: []
+			},
+			tagline: "",
+			description: "",
+			carousel_items: [
+				{
+					asset_id: ""
+				}
+			],
+			header_logo_dark_theme: {}, //{id: "", size: 4665, mime_type: "image/gif", width 160, height: 160}
+			header_logo_light_theme: {},
+			box_art: {},
+			thumbnail: {},
+			header_background: {},
+			hero_background: {},
+			assets: []
+		}
+	}).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts b/src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts
new file mode 100644
index 00000000..723a5160
--- /dev/null
+++ b/src/api/routes/store/published-listings/skus/#sku_id/subscription-plans.ts
@@ -0,0 +1,142 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+const skus = new Map([
+	[
+		"521842865731534868",
+		[
+			{
+				id: "511651856145973248",
+				name: "Premium Monthly (Legacy)",
+				interval: 1,
+				interval_count: 1,
+				tax_inclusive: true,
+				sku_id: "521842865731534868",
+				currency: "usd",
+				price: 0,
+				price_tier: null
+			},
+			{
+				id: "511651860671627264",
+				name: "Premium Yearly (Legacy)",
+				interval: 2,
+				interval_count: 1,
+				tax_inclusive: true,
+				sku_id: "521842865731534868",
+				currency: "usd",
+				price: 0,
+				price_tier: null
+			}
+		]
+	],
+	[
+		"521846918637420545",
+		[
+			{
+				id: "511651871736201216",
+				name: "Premium Classic Monthly",
+				interval: 1,
+				interval_count: 1,
+				tax_inclusive: true,
+				sku_id: "521846918637420545",
+				currency: "usd",
+				price: 0,
+				price_tier: null
+			},
+			{
+				id: "511651876987469824",
+				name: "Premium Classic Yearly",
+				interval: 2,
+				interval_count: 1,
+				tax_inclusive: true,
+				sku_id: "521846918637420545",
+				currency: "usd",
+				price: 0,
+				price_tier: null
+			}
+		]
+	],
+	[
+		"521847234246082599",
+		[
+			{
+				id: "642251038925127690",
+				name: "Premium Quarterly",
+				interval: 1,
+				interval_count: 3,
+				tax_inclusive: true,
+				sku_id: "521847234246082599",
+				currency: "usd",
+				price: 0,
+				price_tier: null
+			},
+			{
+				id: "511651880837840896",
+				name: "Premium Monthly",
+				interval: 1,
+				interval_count: 1,
+				tax_inclusive: true,
+				sku_id: "521847234246082599",
+				currency: "usd",
+				price: 0,
+				price_tier: null
+			},
+			{
+				id: "511651885459963904",
+				name: "Premium Yearly",
+				interval: 2,
+				interval_count: 1,
+				tax_inclusive: true,
+				sku_id: "521847234246082599",
+				currency: "usd",
+				price: 0,
+				price_tier: null
+			}
+		]
+	],
+	[
+		"590663762298667008",
+		[
+			{
+				id: "590665532894740483",
+				name: "Server Boost Monthly",
+				interval: 1,
+				interval_count: 1,
+				tax_inclusive: true,
+				sku_id: "590663762298667008",
+				discount_price: 0,
+				currency: "usd",
+				price: 0,
+				price_tier: null
+			},
+			{
+				id: "590665538238152709",
+				name: "Server Boost Yearly",
+				interval: 2,
+				interval_count: 1,
+				tax_inclusive: true,
+				sku_id: "590663762298667008",
+				discount_price: 0,
+				currency: "usd",
+				price: 0,
+				price_tier: null
+			}
+		]
+	]
+]);
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	// TODO: add the ability to add custom
+	const { sku_id } = req.params;
+
+	if (!skus.has(sku_id)) {
+		console.log(`Request for invalid SKU ${sku_id}! Please report this!`);
+		res.sendStatus(404);
+	} else {
+		res.json(skus.get(sku_id)).status(200);
+	}
+});
+
+export default router;
diff --git a/src/api/routes/teams.ts b/src/api/routes/teams.ts
new file mode 100644
index 00000000..7ce3abcb
--- /dev/null
+++ b/src/api/routes/teams.ts
@@ -0,0 +1,11 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	//TODO
+	res.send([]);
+});
+
+export default router;
diff --git a/src/api/routes/template.ts.disabled b/src/api/routes/template.ts.disabled
new file mode 100644
index 00000000..fcc59ef4
--- /dev/null
+++ b/src/api/routes/template.ts.disabled
@@ -0,0 +1,11 @@
+//TODO: this is a template for a generic route
+
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+const router = Router();
+
+router.get("/",route({}), async (req: Request, res: Response) => {
+	res.json({});
+});
+
+export default router;
diff --git a/src/api/routes/track.ts b/src/api/routes/track.ts
new file mode 100644
index 00000000..8556a3ad
--- /dev/null
+++ b/src/api/routes/track.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.post("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/updates.ts b/src/api/routes/updates.ts
new file mode 100644
index 00000000..a24e94c1
--- /dev/null
+++ b/src/api/routes/updates.ts
@@ -0,0 +1,20 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+import { Config, Release } from "@fosscord/util";
+
+const router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { client } = Config.get();
+
+    const release = await Release.findOneOrFail({ where: { name: client.releases.upstreamVersion } })
+
+	res.json({
+        name: release.name,
+        pub_date: release.pub_date,
+        url: release.url,
+        notes: release.notes
+    });
+});
+
+export default router;
diff --git a/src/api/routes/users/#id/index.ts b/src/api/routes/users/#id/index.ts
new file mode 100644
index 00000000..bdb1060f
--- /dev/null
+++ b/src/api/routes/users/#id/index.ts
@@ -0,0 +1,13 @@
+import { Router, Request, Response } from "express";
+import { User } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const { id } = req.params;
+
+	res.json(await User.getPublicUser(id));
+});
+
+export default router;
diff --git a/src/api/routes/users/#id/profile.ts b/src/api/routes/users/#id/profile.ts
new file mode 100644
index 00000000..7a995a8c
--- /dev/null
+++ b/src/api/routes/users/#id/profile.ts
@@ -0,0 +1,58 @@
+import { Router, Request, Response } from "express";
+import { PublicConnectedAccount, PublicUser, User, UserPublic, Member } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+export interface UserProfileResponse {
+	user: UserPublic;
+	connected_accounts: PublicConnectedAccount;
+	premium_guild_since?: Date;
+	premium_since?: Date;
+}
+
+router.get("/", route({ test: { response: { body: "UserProfileResponse" } } }), async (req: Request, res: Response) => {
+	if (req.params.id === "@me") req.params.id = req.user_id;
+	const user = await User.getPublicUser(req.params.id, { relations: ["connected_accounts"] });
+
+	let mutual_guilds: object[] = [];
+	let premium_guild_since;
+	const requested_member = await Member.find( { where: { id: req.params.id, } })
+	const self_member = await Member.find( { where: { id: req.user_id, } })
+
+	for(const rmem of requested_member) {
+		if(rmem.premium_since) {
+			if(premium_guild_since){
+				if(premium_guild_since > rmem.premium_since) {
+					premium_guild_since = rmem.premium_since;
+				}
+			} else {
+				premium_guild_since = rmem.premium_since;
+			}
+		}
+		for(const smem of self_member) {
+			if (smem.guild_id === rmem.guild_id) {
+				mutual_guilds.push({id: rmem.guild_id, nick: rmem.nick})
+			}
+		}
+	}
+	res.json({
+		connected_accounts: user.connected_accounts,
+		premium_guild_since: premium_guild_since, // TODO
+		premium_since: user.premium_since, // TODO
+		mutual_guilds: mutual_guilds, // TODO {id: "", nick: null} when ?with_mutual_guilds=true
+		user: {
+			username: user.username,
+			discriminator: user.discriminator,
+			id: user.id,
+			public_flags: user.public_flags,
+			avatar: user.avatar,
+			accent_color: user.accent_color,
+			banner: user.banner,
+			bio: req.user_bot ? null : user.bio,
+			bot: user.bot
+		}
+	});
+});
+
+export default router;
diff --git a/src/api/routes/users/#id/relationships.ts b/src/api/routes/users/#id/relationships.ts
new file mode 100644
index 00000000..61655c25
--- /dev/null
+++ b/src/api/routes/users/#id/relationships.ts
@@ -0,0 +1,41 @@
+import { Router, Request, Response } from "express";
+import { User } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+export interface UserRelationsResponse {
+	object: {
+		id?: string,
+		username?: string,
+		avatar?: string, 
+		discriminator?: string, 
+		public_flags?: number
+	}
+}
+
+
+router.get("/", route({ test: { response: { body: "UserRelationsResponse" } } }), async (req: Request, res: Response) => {
+	let mutual_relations: object[] = [];
+    const requested_relations = await User.findOneOrFail({
+		where: { id: req.params.id },
+		relations: ["relationships"]
+	});
+    const self_relations = await User.findOneOrFail({
+		where: { id: req.user_id },
+		relations: ["relationships"]
+	});
+	
+    for(const rmem of requested_relations.relationships) {
+		for(const smem of self_relations.relationships)
+		if (rmem.to_id === smem.to_id && rmem.type === 1 && rmem.to_id !== req.user_id) {
+			let relation_user = await User.getPublicUser(rmem.to_id)
+
+			mutual_relations.push({id: relation_user.id, username: relation_user.username, avatar: relation_user.avatar, discriminator: relation_user.discriminator, public_flags: relation_user.public_flags})
+		}
+	}
+
+	res.json(mutual_relations)
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/activities/statistics/applications.ts b/src/api/routes/users/@me/activities/statistics/applications.ts
new file mode 100644
index 00000000..014df8af
--- /dev/null
+++ b/src/api/routes/users/@me/activities/statistics/applications.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/affinities/guilds.ts b/src/api/routes/users/@me/affinities/guilds.ts
new file mode 100644
index 00000000..8d744744
--- /dev/null
+++ b/src/api/routes/users/@me/affinities/guilds.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.status(200).send({ guild_affinities: [] });
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/affinities/users.ts b/src/api/routes/users/@me/affinities/users.ts
new file mode 100644
index 00000000..6d4e4991
--- /dev/null
+++ b/src/api/routes/users/@me/affinities/users.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.status(200).send({ user_affinities: [], inverse_user_affinities: [] });
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/applications/#app_id/entitlements.ts b/src/api/routes/users/@me/applications/#app_id/entitlements.ts
new file mode 100644
index 00000000..411e95bf
--- /dev/null
+++ b/src/api/routes/users/@me/applications/#app_id/entitlements.ts
@@ -0,0 +1,11 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	//TODO
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/billing/country-code.ts b/src/api/routes/users/@me/billing/country-code.ts
new file mode 100644
index 00000000..33d40796
--- /dev/null
+++ b/src/api/routes/users/@me/billing/country-code.ts
@@ -0,0 +1,11 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	//TODO
+	res.json({ country_code: "US" }).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/billing/payment-sources.ts b/src/api/routes/users/@me/billing/payment-sources.ts
new file mode 100644
index 00000000..014df8af
--- /dev/null
+++ b/src/api/routes/users/@me/billing/payment-sources.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/billing/subscriptions.ts b/src/api/routes/users/@me/billing/subscriptions.ts
new file mode 100644
index 00000000..411e95bf
--- /dev/null
+++ b/src/api/routes/users/@me/billing/subscriptions.ts
@@ -0,0 +1,11 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	//TODO
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/channels.ts b/src/api/routes/users/@me/channels.ts
new file mode 100644
index 00000000..ad483529
--- /dev/null
+++ b/src/api/routes/users/@me/channels.ts
@@ -0,0 +1,20 @@
+import { Request, Response, Router } from "express";
+import { Recipient, DmChannelDTO, Channel, DmChannelCreateSchema } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const recipients = await Recipient.find({
+		where: { user_id: req.user_id, closed: false },
+		relations: ["channel", "channel.recipients"]
+	});
+	res.json(await Promise.all(recipients.map((r) => DmChannelDTO.from(r.channel, [req.user_id]))));
+});
+
+router.post("/", route({ body: "DmChannelCreateSchema" }), async (req: Request, res: Response) => {
+	const body = req.body as DmChannelCreateSchema;
+	res.json(await Channel.createDMChannel(body.recipients, req.user_id, body.name));
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/connections.ts b/src/api/routes/users/@me/connections.ts
new file mode 100644
index 00000000..411e95bf
--- /dev/null
+++ b/src/api/routes/users/@me/connections.ts
@@ -0,0 +1,11 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	//TODO
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/delete.ts b/src/api/routes/users/@me/delete.ts
new file mode 100644
index 00000000..1d81c2b9
--- /dev/null
+++ b/src/api/routes/users/@me/delete.ts
@@ -0,0 +1,32 @@
+import { Router, Request, Response } from "express";
+import { Guild, Member, User } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import bcrypt from "bcrypt";
+import { HTTPError } from "@fosscord/util";
+
+const router = Router();
+
+router.post("/", route({}), async (req: Request, res: Response) => {
+	const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data"] }); //User object
+	let correctpass = true;
+
+	if (user.data.hash) {
+		// guest accounts can delete accounts without password
+		correctpass = await bcrypt.compare(req.body.password, user.data.hash);
+		if (!correctpass) {
+			throw new HTTPError(req.t("auth:login.INVALID_PASSWORD"));
+		}
+	}
+
+	// TODO: decrement guild member count
+
+	if (correctpass) {
+		await Promise.all([User.delete({ id: req.user_id }), Member.delete({ id: req.user_id })]);
+
+		res.sendStatus(204);
+	} else {
+		res.sendStatus(401);
+	}
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/devices.ts b/src/api/routes/users/@me/devices.ts
new file mode 100644
index 00000000..8556a3ad
--- /dev/null
+++ b/src/api/routes/users/@me/devices.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.post("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/disable.ts b/src/api/routes/users/@me/disable.ts
new file mode 100644
index 00000000..4aff3774
--- /dev/null
+++ b/src/api/routes/users/@me/disable.ts
@@ -0,0 +1,26 @@
+import { User } from "@fosscord/util";
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+import bcrypt from "bcrypt";
+
+const router = Router();
+
+router.post("/", route({}), async (req: Request, res: Response) => {
+	const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data"] }); //User object
+	let correctpass = true;
+
+	if (user.data.hash) {
+		// guest accounts can delete accounts without password
+		correctpass = await bcrypt.compare(req.body.password, user.data.hash); //Not sure if user typed right password :/
+	}
+
+	if (correctpass) {
+		await User.update({ id: req.user_id }, { disabled: true });
+
+		res.sendStatus(204);
+	} else {
+		res.status(400).json({ message: "Password does not match", code: 50018 });
+	}
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/email-settings.ts b/src/api/routes/users/@me/email-settings.ts
new file mode 100644
index 00000000..3114984e
--- /dev/null
+++ b/src/api/routes/users/@me/email-settings.ts
@@ -0,0 +1,20 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.json({
+		categories: {
+			social: true,
+			communication: true,
+			tips: false,
+			updates_and_announcements: false,
+			recommendations_and_events: false
+		},
+		initialized: false
+	}).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/entitlements.ts b/src/api/routes/users/@me/entitlements.ts
new file mode 100644
index 00000000..341e2b4c
--- /dev/null
+++ b/src/api/routes/users/@me/entitlements.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/gifts", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/guilds.ts b/src/api/routes/users/@me/guilds.ts
new file mode 100644
index 00000000..4d4fccd4
--- /dev/null
+++ b/src/api/routes/users/@me/guilds.ts
@@ -0,0 +1,57 @@
+import { Router, Request, Response } from "express";
+import { Guild, Member, User, GuildDeleteEvent, GuildMemberRemoveEvent, emitEvent, Config } from "@fosscord/util";
+import { HTTPError } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const members = await Member.find({ relations: ["guild"], where: { id: req.user_id } });
+
+	let guild = members.map((x) => x.guild);
+
+	if ("with_counts" in req.query && req.query.with_counts == "true") {
+		guild = []; // TODO: Load guilds with user role permissions number
+	}
+
+	res.json(guild);
+});
+
+// user send to leave a certain guild
+router.delete("/:guild_id", route({}), async (req: Request, res: Response) => {
+	const { autoJoin } = Config.get().guild;
+	const { guild_id } = req.params;
+	const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: ["owner_id"] });
+
+	if (!guild) throw new HTTPError("Guild doesn't exist", 404);
+	if (guild.owner_id === req.user_id) throw new HTTPError("You can't leave your own guild", 400);
+	if (autoJoin.enabled && autoJoin.guilds.includes(guild_id) && !autoJoin.canLeave) {
+		throw new HTTPError("You can't leave instance auto join guilds", 400);
+	}
+
+	await Promise.all([
+		Member.delete({ id: req.user_id, guild_id: guild_id }),
+		emitEvent({
+			event: "GUILD_DELETE",
+			data: {
+				id: guild_id
+			},
+			user_id: req.user_id
+		} as GuildDeleteEvent)
+	]);
+
+	const user = await User.getPublicUser(req.user_id);
+
+	await emitEvent({
+		event: "GUILD_MEMBER_REMOVE",
+		data: {
+			guild_id: guild_id,
+			user: user
+		},
+		guild_id: guild_id
+	} as GuildMemberRemoveEvent);
+
+	return res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/guilds/premium/subscription-slots.ts b/src/api/routes/users/@me/guilds/premium/subscription-slots.ts
new file mode 100644
index 00000000..014df8af
--- /dev/null
+++ b/src/api/routes/users/@me/guilds/premium/subscription-slots.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.json([]).status(200);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/index.ts b/src/api/routes/users/@me/index.ts
new file mode 100644
index 00000000..7d095451
--- /dev/null
+++ b/src/api/routes/users/@me/index.ts
@@ -0,0 +1,72 @@
+import { Router, Request, Response } from "express";
+import { User, PrivateUserProjection, emitEvent, UserUpdateEvent, handleFile, FieldErrors, UserModifySchema } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import bcrypt from "bcrypt";
+import { OrmUtils, generateToken } from "@fosscord/util";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	res.json(await User.findOne({ select: PrivateUserProjection, where: { id: req.user_id } }));
+});
+
+router.patch("/", route({ body: "UserModifySchema" }), async (req: Request, res: Response) => {
+	var token = null as any;
+	const body = req.body as UserModifySchema;
+
+	if (body.avatar) body.avatar = await handleFile(`/avatars/${req.user_id}`, body.avatar as string);
+	if (body.banner) body.banner = await handleFile(`/banners/${req.user_id}`, body.banner as string);
+	let user = await User.findOneOrFail({ where: { id: req.user_id }, select: [...PrivateUserProjection, "data"] });
+
+	if (body.password) {
+		if (user.data?.hash) {
+			const same_password = await bcrypt.compare(body.password, user.data.hash || "");
+			if (!same_password) {
+				throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } });
+			}
+		} else {
+			user.data.hash = await bcrypt.hash(body.password, 12);
+		}
+	}
+
+	if (body.new_password) {
+		if (!body.password && !user.email) {
+			throw FieldErrors({
+				password: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }
+			});
+		}
+		user.data.hash = await bcrypt.hash(body.new_password, 12);
+		user.data.valid_tokens_since = new Date();
+		token = await generateToken(user.id) as string;
+	}
+
+    if(body.username){
+        let check_username = body?.username?.replace(/\s/g, '');
+        if(!check_username) {
+            throw FieldErrors({
+                username: { code: "BASE_TYPE_REQUIRED", message: req.t("common:field.BASE_TYPE_REQUIRED") }
+            });
+        }
+    }
+
+	user = OrmUtils.mergeDeep(user, body);
+	await user.save();
+
+	// @ts-ignore
+	delete user.data;
+
+	// TODO: send update member list event in gateway
+	await emitEvent({
+		event: "USER_UPDATE",
+		user_id: req.user_id,
+		data: user
+	} as UserUpdateEvent);
+	
+	res.json({
+		...user,
+		token
+	});
+});
+
+export default router;
+// {"message": "Invalid two-factor code", "code": 60008}
diff --git a/src/api/routes/users/@me/library.ts b/src/api/routes/users/@me/library.ts
new file mode 100644
index 00000000..7ac13bae
--- /dev/null
+++ b/src/api/routes/users/@me/library.ts
@@ -0,0 +1,11 @@
+import { Router, Response, Request } from "express";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.get("/", route({}), (req: Request, res: Response) => {
+	// TODO:
+	res.status(200).send([]);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/mfa/codes.ts b/src/api/routes/users/@me/mfa/codes.ts
new file mode 100644
index 00000000..4224a1c0
--- /dev/null
+++ b/src/api/routes/users/@me/mfa/codes.ts
@@ -0,0 +1,45 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { BackupCode, Config, FieldErrors, generateMfaBackupCodes, MfaCodesSchema, User } from "@fosscord/util";
+import bcrypt from "bcrypt";
+
+const router = Router();
+
+// TODO: This route is replaced with users/@me/mfa/codes-verification in newer clients
+
+router.post("/", route({ body: "MfaCodesSchema" }), async (req: Request, res: Response) => {
+	const { password, regenerate } = req.body as MfaCodesSchema;
+
+	const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data"] });
+
+	if (!await bcrypt.compare(password, user.data.hash || "")) {
+		throw FieldErrors({ password: { message: req.t("auth:login.INVALID_PASSWORD"), code: "INVALID_PASSWORD" } });
+	}
+
+	var codes: BackupCode[];
+	if (regenerate && Config.get().security.twoFactor.generateBackupCodes) {
+		await BackupCode.update(
+			{ user: { id: req.user_id } },
+			{ expired: true }
+		);
+
+		codes = generateMfaBackupCodes(req.user_id);
+		await Promise.all(codes.map(x => x.save()));
+	}
+	else {
+		codes = await BackupCode.find({
+			where: {
+				user: {
+					id: req.user_id,
+				},
+				expired: false
+			}
+		});
+	}
+
+	return res.json({
+		backup_codes: codes.map(x => ({ ...x, expired: undefined })),
+	})
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/mfa/totp/disable.ts b/src/api/routes/users/@me/mfa/totp/disable.ts
new file mode 100644
index 00000000..2fe9355c
--- /dev/null
+++ b/src/api/routes/users/@me/mfa/totp/disable.ts
@@ -0,0 +1,41 @@
+import { Router, Request, Response } from "express";
+import { route } from "@fosscord/api";
+import { verifyToken } from 'node-2fa';
+import { HTTPError } from "lambert-server";
+import { User, generateToken, BackupCode, TotpDisableSchema } from "@fosscord/util";
+
+const router = Router();
+
+router.post("/", route({ body: "TotpDisableSchema" }), async (req: Request, res: Response) => {
+	const body = req.body as TotpDisableSchema;
+
+	const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["totp_secret"] });
+
+	const backup = await BackupCode.findOne({ where: { code: body.code } });
+	if (!backup) {
+		const ret = verifyToken(user.totp_secret!, body.code);
+		if (!ret || ret.delta != 0)
+			throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+	}
+
+	await User.update(
+		{ id: req.user_id },
+		{
+			mfa_enabled: false,
+			totp_secret: "",
+		},
+	);
+
+	await BackupCode.update(
+		{ user: { id: req.user_id } },
+		{
+			expired: true,
+		}
+	);
+
+	return res.json({
+		token: await generateToken(user.id),
+	});
+});
+
+export default router;
\ No newline at end of file
diff --git a/src/api/routes/users/@me/mfa/totp/enable.ts b/src/api/routes/users/@me/mfa/totp/enable.ts
new file mode 100644
index 00000000..ac668d1d
--- /dev/null
+++ b/src/api/routes/users/@me/mfa/totp/enable.ts
@@ -0,0 +1,48 @@
+import { Router, Request, Response } from "express";
+import { User, generateToken, BackupCode, generateMfaBackupCodes, Config, TotpEnableSchema } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import bcrypt from "bcrypt";
+import { HTTPError } from "lambert-server";
+import { verifyToken } from 'node-2fa';
+
+const router = Router();
+
+router.post("/", route({ body: "TotpEnableSchema" }), async (req: Request, res: Response) => {
+	const body = req.body as TotpEnableSchema;
+
+	const user = await User.findOneOrFail({ where: { id: req.user_id }, select: ["data"] });
+
+	// TODO: Are guests allowed to enable 2fa?
+	if (user.data.hash) {
+		if (!await bcrypt.compare(body.password, user.data.hash)) {
+			throw new HTTPError(req.t("auth:login.INVALID_PASSWORD"));
+		}
+	}
+
+	if (!body.secret)
+		throw new HTTPError(req.t("auth:login.INVALID_TOTP_SECRET"), 60005);
+
+	if (!body.code)
+		throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+
+	if (verifyToken(body.secret, body.code)?.delta != 0)
+		throw new HTTPError(req.t("auth:login.INVALID_TOTP_CODE"), 60008);
+
+	let backup_codes: BackupCode[] = [];
+	if (Config.get().security.twoFactor.generateBackupCodes) {
+		backup_codes = generateMfaBackupCodes(req.user_id);
+		await Promise.all(backup_codes.map(x => x.save()));
+	}
+
+	await User.update(
+		{ id: req.user_id },
+		{ mfa_enabled: true, totp_secret: body.secret }
+	);
+
+	res.send({
+		token: await generateToken(user.id),
+		backup_codes: backup_codes.map(x => ({ ...x, expired: undefined })),
+	});
+});
+
+export default router;
\ No newline at end of file
diff --git a/src/api/routes/users/@me/notes.ts b/src/api/routes/users/@me/notes.ts
new file mode 100644
index 00000000..f938f088
--- /dev/null
+++ b/src/api/routes/users/@me/notes.ts
@@ -0,0 +1,60 @@
+import { Request, Response, Router } from "express";
+import { route } from "@fosscord/api";
+import { User, Note, emitEvent, Snowflake } from "@fosscord/util";
+
+const router: Router = Router();
+
+router.get("/:id", route({}), async (req: Request, res: Response) => {
+	const { id } = req.params;
+
+	const note = await Note.findOneOrFail({
+		where: {
+			owner: { id: req.user_id },
+			target: { id: id },
+		}
+	});
+
+	return res.json({
+		note: note?.content,
+		note_user_id: id,
+		user_id: req.user_id,
+	});
+});
+
+router.put("/:id", route({}), async (req: Request, res: Response) => {
+	const { id } = req.params;
+	const owner = await User.findOneOrFail({ where: { id: req.user_id } });
+	const target = await User.findOneOrFail({ where: { id: id } });		//if noted user does not exist throw
+	const { note } = req.body;
+
+	if (note && note.length) {
+		// upsert a note
+		if (await Note.findOne({ where: { owner: { id: owner.id }, target: { id: target.id } } })) {
+			Note.update(
+				{ owner: { id: owner.id }, target: { id: target.id } },
+				{ owner, target, content: note }
+			);
+		}
+		else {
+			Note.insert(
+				{ id: Snowflake.generate(), owner, target, content: note }
+			);
+		}
+	}
+	else {
+		await Note.delete({ owner: { id: owner.id }, target: { id: target.id } });
+	}
+
+	await emitEvent({
+		event: "USER_NOTE_UPDATE",
+		data: {
+			note: note,
+			id: target.id
+		},
+		user_id: owner.id,
+	});
+
+	return res.status(204);
+});
+
+export default router;
diff --git a/src/api/routes/users/@me/relationships.ts b/src/api/routes/users/@me/relationships.ts
new file mode 100644
index 00000000..f7464b99
--- /dev/null
+++ b/src/api/routes/users/@me/relationships.ts
@@ -0,0 +1,204 @@
+import {
+	RelationshipAddEvent,
+	User,
+	PublicUserProjection,
+	RelationshipType,
+	RelationshipRemoveEvent,
+	emitEvent,
+	Relationship,
+	Config
+} from "@fosscord/util";
+import { Router, Response, Request } from "express";
+import { HTTPError } from "@fosscord/util";
+import { DiscordApiErrors } from "@fosscord/util";
+import { route } from "@fosscord/api";
+import { OrmUtils } from "@fosscord/util";
+
+const router = Router();
+
+const userProjection: (keyof User)[] = ["relationships", ...PublicUserProjection];
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	const user = await User.findOneOrFail({
+		where: { id: req.user_id },
+		relations: ["relationships", "relationships.to"],
+		select: ["relationships"]
+	});
+
+	//TODO DTO
+	const related_users = user.relationships.map((r) => {
+		return {
+			id: r.to.id,
+			type: r.type,
+			nickname: null,
+			user: r.to.toPublicUser()
+		};
+	});
+
+	return res.json(related_users);
+});
+
+router.put("/:id", route({ body: "RelationshipPutSchema" }), async (req: Request, res: Response) => {
+	return await updateRelationship(
+		req,
+		res,
+		await User.findOneOrFail({ where: { id: req.params.id }, relations: ["relationships", "relationships.to"], select: userProjection }),
+		req.body.type ?? RelationshipType.friends
+	);
+});
+
+router.post("/", route({ body: "RelationshipPostSchema" }), async (req: Request, res: Response) => {
+	return await updateRelationship(
+		req,
+		res,
+		await User.findOneOrFail({
+			relations: ["relationships", "relationships.to"],
+			select: userProjection,
+			where: {
+				discriminator: String(req.body.discriminator).padStart(4, "0"), //Discord send the discriminator as integer, we need to add leading zeroes
+				username: req.body.username
+			}
+		}),
+		req.body.type
+	);
+});
+
+router.delete("/:id", route({}), async (req: Request, res: Response) => {
+	const { id } = req.params;
+	if (id === req.user_id) throw new HTTPError("You can't remove yourself as a friend");
+
+	const user = await User.findOneOrFail({ where: { id: req.user_id }, select: userProjection, relations: ["relationships"] });
+	const friend = await User.findOneOrFail({ where: { id: id }, select: userProjection, relations: ["relationships"] });
+
+	const relationship = user.relationships.find((x) => x.to_id === id);
+	const friendRequest = friend.relationships.find((x) => x.to_id === req.user_id);
+
+	if (!relationship) throw new HTTPError("You are not friends with the user", 404);
+	if (relationship?.type === RelationshipType.blocked) {
+		// unblock user
+
+		await Promise.all([
+			Relationship.delete({ id: relationship.id }),
+			emitEvent({
+				event: "RELATIONSHIP_REMOVE",
+				user_id: req.user_id,
+				data: relationship.toPublicRelationship()
+			} as RelationshipRemoveEvent)
+		]);
+		return res.sendStatus(204);
+	}
+	if (friendRequest && friendRequest.type !== RelationshipType.blocked) {
+		await Promise.all([
+			Relationship.delete({ id: friendRequest.id }),
+			await emitEvent({
+				event: "RELATIONSHIP_REMOVE",
+				data: friendRequest.toPublicRelationship(),
+				user_id: id
+			} as RelationshipRemoveEvent)
+		]);
+	}
+
+	await Promise.all([
+		Relationship.delete({ id: relationship.id }),
+		emitEvent({
+			event: "RELATIONSHIP_REMOVE",
+			data: relationship.toPublicRelationship(),
+			user_id: req.user_id
+		} as RelationshipRemoveEvent)
+	]);
+
+	return res.sendStatus(204);
+});
+
+export default router;
+
+async function updateRelationship(req: Request, res: Response, friend: User, type: RelationshipType) {
+	const id = friend.id;
+	if (id === req.user_id) throw new HTTPError("You can't add yourself as a friend");
+
+	const user = await User.findOneOrFail({
+		where: { id: req.user_id },
+		relations: ["relationships", "relationships.to"],
+		select: userProjection
+	});
+
+	let relationship = user.relationships.find((x) => x.to_id === id);
+	const friendRequest = friend.relationships.find((x) => x.to_id === req.user_id);
+
+	// TODO: you can add infinitely many blocked users (should this be prevented?)
+	if (type === RelationshipType.blocked) {
+		if (relationship) {
+			if (relationship.type === RelationshipType.blocked) throw new HTTPError("You already blocked the user");
+			relationship.type = RelationshipType.blocked;
+			await relationship.save();
+		} else {
+			relationship = await (OrmUtils.mergeDeep(new Relationship(), { to_id: id, type: RelationshipType.blocked, from_id: req.user_id }) as Relationship).save();
+		}
+
+		if (friendRequest && friendRequest.type !== RelationshipType.blocked) {
+			await Promise.all([
+				Relationship.delete({ id: friendRequest.id }),
+				emitEvent({
+					event: "RELATIONSHIP_REMOVE",
+					data: friendRequest.toPublicRelationship(),
+					user_id: id
+				} as RelationshipRemoveEvent)
+			]);
+		}
+
+		await emitEvent({
+			event: "RELATIONSHIP_ADD",
+			data: relationship.toPublicRelationship(),
+			user_id: req.user_id
+		} as RelationshipAddEvent);
+
+		return res.sendStatus(204);
+	}
+
+	const { maxFriends } = Config.get().limits.user;
+	if (user.relationships.length >= maxFriends) throw DiscordApiErrors.MAXIMUM_FRIENDS.withParams(maxFriends);
+
+	let incoming_relationship = OrmUtils.mergeDeep(new Relationship(), { nickname: undefined, type: RelationshipType.incoming, to: user, from: friend });
+	let outgoing_relationship = OrmUtils.mergeDeep(new Relationship(), {
+		nickname: undefined,
+		type: RelationshipType.outgoing,
+		to: friend,
+		from: user
+	});
+
+	if (friendRequest) {
+		if (friendRequest.type === RelationshipType.blocked) throw new HTTPError("The user blocked you");
+		if (friendRequest.type === RelationshipType.friends) throw new HTTPError("You are already friends with the user");
+		// accept friend request
+		incoming_relationship = friendRequest as any; //TODO: checkme, any cast
+		incoming_relationship.type = RelationshipType.friends;
+	}
+
+	if (relationship) {
+		if (relationship.type === RelationshipType.outgoing) throw new HTTPError("You already sent a friend request");
+		if (relationship.type === RelationshipType.blocked) throw new HTTPError("Unblock the user before sending a friend request");
+		if (relationship.type === RelationshipType.friends) throw new HTTPError("You are already friends with the user");
+		outgoing_relationship = relationship as any; //TODO: checkme, any cast
+		outgoing_relationship.type = RelationshipType.friends;
+	}
+
+	await Promise.all([
+		incoming_relationship.save(),
+		outgoing_relationship.save(),
+		emitEvent({
+			event: "RELATIONSHIP_ADD",
+			data: outgoing_relationship.toPublicRelationship(),
+			user_id: req.user_id
+		} as RelationshipAddEvent),
+		emitEvent({
+			event: "RELATIONSHIP_ADD",
+			data: {
+				...incoming_relationship.toPublicRelationship(),
+				should_notify: true
+			},
+			user_id: id
+		} as RelationshipAddEvent)
+	]);
+
+	return res.sendStatus(204);
+}
diff --git a/src/api/routes/users/@me/settings.ts b/src/api/routes/users/@me/settings.ts
new file mode 100644
index 00000000..7578d36e
--- /dev/null
+++ b/src/api/routes/users/@me/settings.ts
@@ -0,0 +1,18 @@
+import { Router, Response, Request } from "express";
+import { User, UserSettings } from "@fosscord/util";
+import { route } from "@fosscord/api";
+
+const router = Router();
+
+router.patch("/", route({ body: "UserSettingsSchema" }), async (req: Request, res: Response) => {
+	const body = req.body as UserSettings;
+	if (body.locale === "en") body.locale = "en-US"; // fix discord client crash on unkown locale
+
+	const user = await User.findOneOrFail({ where: { id: req.user_id, bot: false }, relations: ["settings"] });
+	user.settings = { ...user.settings, ...body } as UserSettings;
+	await user.save();
+
+	res.sendStatus(204);
+});
+
+export default router;
diff --git a/src/api/routes/voice/regions.ts b/src/api/routes/voice/regions.ts
new file mode 100644
index 00000000..4de304ee
--- /dev/null
+++ b/src/api/routes/voice/regions.ts
@@ -0,0 +1,11 @@
+import { Router, Request, Response } from "express";
+import { getIpAdress, route } from "@fosscord/api";
+import { getVoiceRegions } from "@fosscord/api";
+
+const router: Router = Router();
+
+router.get("/", route({}), async (req: Request, res: Response) => {
+	res.json(await getVoiceRegions(getIpAdress(req), true)); //vip true?
+});
+
+export default router;