summary refs log tree commit diff
path: root/api/src/routes/channels
diff options
context:
space:
mode:
Diffstat (limited to 'api/src/routes/channels')
-rw-r--r--api/src/routes/channels/#channel_id/followers.ts14
-rw-r--r--api/src/routes/channels/#channel_id/index.ts60
-rw-r--r--api/src/routes/channels/#channel_id/invites.ts65
-rw-r--r--api/src/routes/channels/#channel_id/messages/#message_id/ack.ts35
-rw-r--r--api/src/routes/channels/#channel_id/messages/#message_id/crosspost.ts8
-rw-r--r--api/src/routes/channels/#channel_id/messages/#message_id/index.ts72
-rw-r--r--api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts191
-rw-r--r--api/src/routes/channels/#channel_id/messages/bulk-delete.ts37
-rw-r--r--api/src/routes/channels/#channel_id/messages/index.ts146
-rw-r--r--api/src/routes/channels/#channel_id/permissions.ts72
-rw-r--r--api/src/routes/channels/#channel_id/pins.ts93
-rw-r--r--api/src/routes/channels/#channel_id/recipients.ts5
-rw-r--r--api/src/routes/channels/#channel_id/typing.ts31
-rw-r--r--api/src/routes/channels/#channel_id/webhooks.ts26
14 files changed, 855 insertions, 0 deletions
diff --git a/api/src/routes/channels/#channel_id/followers.ts b/api/src/routes/channels/#channel_id/followers.ts
new file mode 100644
index 00000000..641af4f8
--- /dev/null
+++ b/api/src/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/api/src/routes/channels/#channel_id/index.ts b/api/src/routes/channels/#channel_id/index.ts
new file mode 100644
index 00000000..81e5054e
--- /dev/null
+++ b/api/src/routes/channels/#channel_id/index.ts
@@ -0,0 +1,60 @@
+import { ChannelDeleteEvent, ChannelModel, ChannelUpdateEvent, getPermission, GuildUpdateEvent, toObject } from "@fosscord/server-util";
+import { Router, Response, Request } from "express";
+import { HTTPError } from "lambert-server";
+import { ChannelModifySchema } from "../../../schema/Channel";
+import { emitEvent } from "../../../util/Event";
+import { check } from "../../../util/instanceOf";
+const router: Router = Router();
+// TODO: delete channel
+// TODO: Get channel
+
+router.get("/", async (req: Request, res: Response) => {
+	const { channel_id } = req.params;
+
+	const channel = await ChannelModel.findOne({ id: channel_id }).exec();
+
+	const permission = await getPermission(req.user_id, channel.guild_id, channel_id);
+	permission.hasThrow("VIEW_CHANNEL");
+
+	return res.send(toObject(channel));
+});
+
+router.delete("/", async (req: Request, res: Response) => {
+	const { channel_id } = req.params;
+
+	const channel = await ChannelModel.findOne({ id: channel_id }).exec();
+
+	const permission = await getPermission(req.user_id, channel?.guild_id, channel_id, { channel });
+	permission.hasThrow("MANAGE_CHANNELS");
+
+	// TODO: Dm channel "close" not delete
+	const data = toObject(channel);
+
+	await emitEvent({ event: "CHANNEL_DELETE", data, channel_id } as ChannelDeleteEvent);
+
+	await ChannelModel.deleteOne({ id: channel_id });
+
+	res.send(data);
+});
+
+router.patch("/", check(ChannelModifySchema), async (req: Request, res: Response) => {
+	var payload = req.body as ChannelModifySchema;
+	const { channel_id } = req.params;
+
+	const permission = await getPermission(req.user_id, undefined, channel_id);
+	permission.hasThrow("MANAGE_CHANNELS");
+
+	const channel = await ChannelModel.findOneAndUpdate({ id: channel_id }, payload).exec();
+
+	const data = toObject(channel);
+
+	await emitEvent({
+		event: "CHANNEL_UPDATE",
+		data,
+		channel_id
+	} as ChannelUpdateEvent);
+
+	res.send(data);
+});
+
+export default router;
diff --git a/api/src/routes/channels/#channel_id/invites.ts b/api/src/routes/channels/#channel_id/invites.ts
new file mode 100644
index 00000000..c9db4dd2
--- /dev/null
+++ b/api/src/routes/channels/#channel_id/invites.ts
@@ -0,0 +1,65 @@
+import { Router, Request, Response } from "express";
+import { HTTPError } from "lambert-server";
+
+import { check } from "../../../util/instanceOf";
+import { random } from "../../../util/RandomInviteID";
+import { emitEvent } from "../../../util/Event";
+
+import { InviteCreateSchema } from "../../../schema/Invite";
+
+import { getPermission, ChannelModel, InviteModel, InviteCreateEvent, toObject } from "@fosscord/server-util";
+
+const router: Router = Router();
+
+router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) => {
+	const { user_id } = req;
+	const { channel_id } = req.params;
+	const channel = await ChannelModel.findOne({ id: channel_id }).exec();
+
+	if (!channel.guild_id) {
+		throw new HTTPError("This channel doesn't exist", 404);
+	}
+	const { guild_id } = channel;
+
+	const permission = await getPermission(user_id, guild_id);
+	permission.hasThrow("CREATE_INSTANT_INVITE");
+
+	const expires_at = new Date(req.body.max_age * 1000 + Date.now());
+
+	const invite = {
+		code: random(),
+		temporary: req.body.temporary,
+		uses: 0,
+		max_uses: req.body.max_uses,
+		max_age: req.body.max_age,
+		expires_at,
+		created_at: new Date(),
+		guild_id,
+		channel_id: channel_id,
+		inviter_id: user_id
+	};
+
+	await new InviteModel(invite).save();
+
+	await emitEvent({ event: "INVITE_CREATE", data: invite, guild_id } as InviteCreateEvent);
+	res.status(201).send(invite);
+});
+
+router.get("/", async (req: Request, res: Response) => {
+	const { user_id } = req;
+	const { channel_id } = req.params;
+	const channel = await ChannelModel.findOne({ id: channel_id }).exec();
+
+	if (!channel.guild_id) {
+		throw new HTTPError("This channel doesn't exist", 404);
+	}
+	const { guild_id } = channel;
+	const permission = await getPermission(user_id, guild_id);
+	permission.hasThrow("MANAGE_CHANNELS");
+
+	const invites = await InviteModel.find({ guild_id }).exec();
+
+	res.status(200).send(toObject(invites));
+});
+
+export default router;
diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts b/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts
new file mode 100644
index 00000000..f4d9e696
--- /dev/null
+++ b/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts
@@ -0,0 +1,35 @@
+import { getPermission, MessageAckEvent, ReadStateModel } from "@fosscord/server-util";
+import { Request, Response, Router } from "express";
+import { emitEvent } from "../../../../../util/Event";
+import { check } from "../../../../../util/instanceOf";
+
+const router = Router();
+
+// TODO: check if message exists
+// TODO: send read state event to all channel members
+
+router.post("/", check({ $manual: Boolean, $mention_count: Number }), 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");
+
+	await ReadStateModel.updateOne(
+		{ user_id: req.user_id, channel_id, message_id },
+		{ user_id: req.user_id, channel_id, message_id }
+	).exec();
+
+	await emitEvent({
+		event: "MESSAGE_ACK",
+		user_id: req.user_id,
+		data: {
+			channel_id,
+			message_id,
+			version: 496
+		}
+	} as MessageAckEvent);
+
+	res.sendStatus(204);
+});
+
+export default router;
diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/crosspost.ts b/api/src/routes/channels/#channel_id/messages/#message_id/crosspost.ts
new file mode 100644
index 00000000..6753e832
--- /dev/null
+++ b/api/src/routes/channels/#channel_id/messages/#message_id/crosspost.ts
@@ -0,0 +1,8 @@
+import { Router, Response, Request } from "express";
+
+const router = Router();
+
+// TODO:
+// router.post("/", (req: Request, res: Response) => {});
+
+export default router;
diff --git a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts
new file mode 100644
index 00000000..a7c23d2f
--- /dev/null
+++ b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts
@@ -0,0 +1,72 @@
+import { ChannelModel, getPermission, MessageDeleteEvent, MessageModel, MessageUpdateEvent, toObject } from "@fosscord/server-util";
+import { Router, Response, Request } from "express";
+import { HTTPError } from "lambert-server";
+import { MessageCreateSchema } from "../../../../../schema/Message";
+import { emitEvent } from "../../../../../util/Event";
+import { check } from "../../../../../util/instanceOf";
+import { handleMessage, postHandleMessage } from "../../../../../util/Message";
+
+const router = Router();
+
+router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response) => {
+	const { message_id, channel_id } = req.params;
+	var body = req.body as MessageCreateSchema;
+
+	var message = await MessageModel.findOne({ id: message_id, channel_id }, { author_id: true }).exec();
+
+	const permissions = await getPermission(req.user_id, undefined, channel_id);
+
+	if (req.user_id !== message.author_id) {
+		permissions.hasThrow("MANAGE_MESSAGES");
+		body = { flags: body.flags };
+	}
+
+	const opts = await handleMessage({
+		...body,
+		author_id: message.author_id,
+		channel_id,
+		id: message_id,
+		edited_timestamp: new Date()
+	});
+
+	// @ts-ignore
+	message = await MessageModel.findOneAndUpdate({ id: message_id }, opts).populate("author").exec();
+
+	await emitEvent({
+		event: "MESSAGE_UPDATE",
+		channel_id,
+		data: { ...toObject(message), nonce: undefined }
+	} as MessageUpdateEvent);
+
+	postHandleMessage(message);
+
+	return res.json(toObject(message));
+});
+
+// TODO: delete attachments in message
+
+router.delete("/", async (req: Request, res: Response) => {
+	const { message_id, channel_id } = req.params;
+
+	const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true });
+	const message = await MessageModel.findOne({ id: message_id }, { author_id: true }).exec();
+
+	const permission = await getPermission(req.user_id, channel.guild_id, channel_id);
+	if (message.author_id !== req.user_id) permission.hasThrow("MANAGE_MESSAGES");
+
+	await MessageModel.deleteOne({ id: message_id }).exec();
+
+	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/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts b/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts
new file mode 100644
index 00000000..168a870f
--- /dev/null
+++ b/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts
@@ -0,0 +1,191 @@
+import {
+	ChannelModel,
+	EmojiModel,
+	getPermission,
+	MemberModel,
+	MessageModel,
+	MessageReactionAddEvent,
+	MessageReactionRemoveAllEvent,
+	MessageReactionRemoveEmojiEvent,
+	MessageReactionRemoveEvent,
+	PartialEmoji,
+	PublicUserProjection,
+	toObject,
+	UserModel
+} from "@fosscord/server-util";
+import { Router, Response, Request } from "express";
+import { HTTPError } from "lambert-server";
+import { emitEvent } from "../../../../../util/Event";
+
+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("/", async (req: Request, res: Response) => {
+	const { message_id, channel_id } = req.params;
+
+	const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec();
+
+	const permissions = await getPermission(req.user_id, undefined, channel_id);
+	permissions.hasThrow("MANAGE_MESSAGES");
+
+	await MessageModel.findOneAndUpdate({ id: message_id, channel_id }, { reactions: [] }).exec();
+
+	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", async (req: Request, res: Response) => {
+	const { message_id, channel_id } = req.params;
+	const emoji = getEmoji(req.params.emoji);
+
+	const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec();
+
+	const permissions = await getPermission(req.user_id, undefined, channel_id);
+	permissions.hasThrow("MANAGE_MESSAGES");
+
+	const message = await MessageModel.findOne({ id: message_id, channel_id }).exec();
+
+	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 MessageModel.updateOne({ id: message_id, channel_id }, message).exec();
+
+	await emitEvent({
+		event: "MESSAGE_REACTION_REMOVE_EMOJI",
+		channel_id,
+		data: {
+			channel_id,
+			message_id,
+			guild_id: channel.guild_id,
+			emoji
+		}
+	} as MessageReactionRemoveEmojiEvent);
+
+	res.sendStatus(204);
+});
+
+router.get("/:emoji", async (req: Request, res: Response) => {
+	const { message_id, channel_id } = req.params;
+	const emoji = getEmoji(req.params.emoji);
+
+	const message = await MessageModel.findOne({ id: message_id, channel_id }).exec();
+	if (!message) throw new HTTPError("Message not found", 404);
+	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 permissions = await getPermission(req.user_id, undefined, channel_id);
+	permissions.hasThrow("VIEW_CHANNEL");
+
+	const users = await UserModel.find({ id: { $in: reaction.user_ids } }, PublicUserProjection).exec();
+
+	res.json(toObject(users));
+});
+
+router.put("/:emoji/:user_id", 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 ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec();
+	const message = await MessageModel.findOne({ id: message_id, channel_id }).exec();
+	const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name);
+
+	const permissions = await getPermission(req.user_id, undefined, channel_id);
+	permissions.hasThrow("READ_MESSAGE_HISTORY");
+	if (!already_added) permissions.hasThrow("ADD_REACTIONS");
+
+	if (emoji.id) {
+		const external_emoji = await EmojiModel.findOne({ id: emoji.id }).exec();
+		if (!already_added) permissions.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 MessageModel.updateOne({ id: message_id, channel_id }, message).exec();
+
+	const member = channel.guild_id && (await MemberModel.findOne({ id: req.user_id }).exec());
+
+	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", async (req: Request, res: Response) => {
+	var { message_id, channel_id, user_id } = req.params;
+
+	const emoji = getEmoji(req.params.emoji);
+
+	const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true }).exec();
+	const message = await MessageModel.findOne({ id: message_id, channel_id }).exec();
+
+	const permissions = await getPermission(req.user_id, undefined, channel_id);
+
+	if (user_id === "@me") user_id = req.user_id;
+	else 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 MessageModel.updateOne({ id: message_id, channel_id }, message).exec();
+
+	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/api/src/routes/channels/#channel_id/messages/bulk-delete.ts b/api/src/routes/channels/#channel_id/messages/bulk-delete.ts
new file mode 100644
index 00000000..e53cd597
--- /dev/null
+++ b/api/src/routes/channels/#channel_id/messages/bulk-delete.ts
@@ -0,0 +1,37 @@
+import { Router, Response, Request } from "express";
+import { ChannelModel, Config, getPermission, MessageDeleteBulkEvent, MessageModel } from "@fosscord/server-util";
+import { HTTPError } from "lambert-server";
+import { emitEvent } from "../../../../util/Event";
+import { check } from "../../../../util/instanceOf";
+
+const router: Router = Router();
+
+export default router;
+
+// TODO: should users be able to bulk delete messages or only bots?
+// TODO: should this request fail, if you provide messages older than 14 days/invalid ids?
+// https://discord.com/developers/docs/resources/channel#bulk-delete-messages
+router.post("/", check({ messages: [String] }), async (req: Request, res: Response) => {
+	const { channel_id } = req.params;
+	const channel = await ChannelModel.findOne({ id: channel_id }, { permission_overwrites: true, guild_id: true }).exec();
+	if (!channel.guild_id) throw new HTTPError("Can't bulk delete dm channel messages", 400);
+
+	const permission = await getPermission(req.user_id, channel?.guild_id, channel_id, { channel });
+	permission.hasThrow("MANAGE_MESSAGES");
+
+	const { maxBulkDelete } = Config.get().limits.message;
+
+	const { messages } = req.body as { messages: string[] };
+	if (messages.length < 2) throw new HTTPError("You must at least specify 2 messages to bulk delete");
+	if (messages.length > maxBulkDelete) throw new HTTPError(`You cannot delete more than ${maxBulkDelete} messages`);
+
+	await MessageModel.deleteMany({ id: { $in: messages } }).exec();
+
+	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/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts
new file mode 100644
index 00000000..fea4d6a4
--- /dev/null
+++ b/api/src/routes/channels/#channel_id/messages/index.ts
@@ -0,0 +1,146 @@
+import { Router, Response, Request } from "express";
+import { Attachment, ChannelModel, ChannelType, getPermission, MessageDocument, MessageModel, toObject } from "@fosscord/server-util";
+import { HTTPError } from "lambert-server";
+import { MessageCreateSchema } from "../../../../schema/Message";
+import { check, instanceOf, Length } from "../../../../util/instanceOf";
+import multer from "multer";
+import { Query } from "mongoose";
+import { sendMessage } from "../../../../util/Message";
+import { uploadFile } from "../../../../util/cdn";
+
+const router: Router = Router();
+
+export default router;
+
+export function isTextChannel(type: ChannelType): boolean {
+	switch (type) {
+		case ChannelType.GUILD_VOICE:
+		case ChannelType.GUILD_CATEGORY:
+			throw new HTTPError("not a text channel", 400);
+		case ChannelType.DM:
+		case ChannelType.GROUP_DM:
+		case ChannelType.GUILD_NEWS:
+		case ChannelType.GUILD_STORE:
+		case ChannelType.GUILD_TEXT:
+			return true;
+	}
+}
+
+// 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 ChannelModel.findOne(
+		{ id: channel_id },
+		{ guild_id: true, type: true, permission_overwrites: true, recipient_ids: true, owner_id: true }
+	)
+		.lean() // lean is needed, because we don't want to populate .recipients that also auto deletes .recipient_ids
+		.exec();
+	if (!channel) throw new HTTPError("Channel not found", 404);
+
+	isTextChannel(channel.type);
+
+	try {
+		instanceOf({ $around: String, $after: String, $before: String, $limit: new Length(Number, 1, 100) }, req.query, {
+			path: "query",
+			req
+		});
+	} catch (error) {
+		return res.status(400).json({ code: 50035, message: "Invalid Query", success: false, errors: error });
+	}
+	var { around, after, before, limit }: { around?: string; after?: string; before?: string; limit?: number } = req.query;
+	if (!limit) limit = 50;
+	var halfLimit = Math.floor(limit / 2);
+
+	// @ts-ignore
+	const permissions = await getPermission(req.user_id, channel.guild_id, channel_id, { channel });
+	permissions.hasThrow("VIEW_CHANNEL");
+	if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
+
+	var query: Query<MessageDocument[], MessageDocument>;
+	if (after) query = MessageModel.find({ channel_id, id: { $gt: after } });
+	else if (before) query = MessageModel.find({ channel_id, id: { $lt: before } });
+	else if (around)
+		query = MessageModel.find({
+			channel_id,
+			id: { $gt: (BigInt(around) - BigInt(halfLimit)).toString(), $lt: (BigInt(around) + BigInt(halfLimit)).toString() }
+		});
+	else {
+		query = MessageModel.find({ channel_id });
+	}
+
+	query = query.sort({ id: -1 });
+
+	const messages = await query.limit(limit).exec();
+
+	return res.json(
+		toObject(messages).map((x) => {
+			(x.reactions || []).forEach((x) => {
+				// @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 = { discriminator: "0000", username: "Deleted User", public_flags: 0n, avatar: null };
+
+			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
+// TODO: check: sum of all characters in an embed structure must not exceed 6000 characters
+
+// https://discord.com/developers/docs/resources/channel#create-message
+// TODO: text channel slowdown
+// TODO: trim and replace message content and every embed field
+// TODO: check allowed_mentions
+
+// Send message
+router.post("/", messageUpload.single("file"), async (req: Request, res: Response) => {
+	const { channel_id } = req.params;
+	var body = req.body as MessageCreateSchema;
+	const attachments: Attachment[] = [];
+
+	if (req.file) {
+		try {
+			const file = await uploadFile(`/attachments/${channel_id}`, req.file);
+			attachments.push({ ...file, proxy_url: file.url });
+		} catch (error) {
+			return res.status(400).json(error);
+		}
+	}
+
+	if (body.payload_json) {
+		body = JSON.parse(body.payload_json);
+	}
+
+	const errors = instanceOf(MessageCreateSchema, body, { req });
+	if (errors !== true) throw errors;
+
+	const embeds = [];
+	if (body.embed) embeds.push(body.embed);
+	const data = await sendMessage({
+		...body,
+		type: 0,
+		pinned: false,
+		author_id: req.user_id,
+		embeds,
+		channel_id,
+		attachments,
+		edited_timestamp: null
+	});
+
+	return res.send(data);
+});
diff --git a/api/src/routes/channels/#channel_id/permissions.ts b/api/src/routes/channels/#channel_id/permissions.ts
new file mode 100644
index 00000000..12364293
--- /dev/null
+++ b/api/src/routes/channels/#channel_id/permissions.ts
@@ -0,0 +1,72 @@
+import { ChannelModel, ChannelPermissionOverwrite, ChannelUpdateEvent, getPermission, MemberModel, RoleModel } from "@fosscord/server-util";
+import { Router, Response, Request } from "express";
+import { HTTPError } from "lambert-server";
+import { emitEvent } from "../../../util/Event";
+import { check } from "../../../util/instanceOf";
+const router: Router = Router();
+
+// TODO: Only permissions your bot has in the guild or channel can be allowed/denied (unless your bot has a MANAGE_ROLES overwrite in the channel)
+
+router.put("/:overwrite_id", check({ allow: String, deny: String, type: Number, id: String }), async (req: Request, res: Response) => {
+	const { channel_id, overwrite_id } = req.params;
+	const body = req.body as { allow: bigint; deny: bigint; type: number; id: string };
+
+	var channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true, permission_overwrites: true }).exec();
+	if (!channel.guild_id) throw new HTTPError("Channel not found", 404);
+
+	const permissions = await getPermission(req.user_id, channel.guild_id, channel_id);
+	permissions.hasThrow("MANAGE_ROLES");
+
+	if (body.type === 0) {
+		if (!(await RoleModel.exists({ id: overwrite_id }))) throw new HTTPError("role not found", 404);
+	} else if (body.type === 1) {
+		if (!(await MemberModel.exists({ id: overwrite_id }))) throw new HTTPError("user not found", 404);
+	} else throw new HTTPError("type not supported", 501);
+
+	// @ts-ignore
+	var overwrite: ChannelPermissionOverwrite = channel.permission_overwrites.find((x) => x.id === overwrite_id);
+	if (!overwrite) {
+		// @ts-ignore
+		overwrite = {
+			id: overwrite_id,
+			type: body.type,
+			allow: body.allow,
+			deny: body.deny
+		};
+		channel.permission_overwrites.push(overwrite);
+	}
+	overwrite.allow = body.allow;
+	overwrite.deny = body.deny;
+
+	// @ts-ignore
+	channel = await ChannelModel.findOneAndUpdate({ id: channel_id }, channel).exec();
+
+	await emitEvent({
+		event: "CHANNEL_UPDATE",
+		channel_id,
+		data: channel
+	} as ChannelUpdateEvent);
+
+	return res.sendStatus(204);
+});
+
+// TODO: check permission hierarchy
+router.delete("/:overwrite_id", async (req: Request, res: Response) => {
+	const { channel_id, overwrite_id } = req.params;
+
+	const permissions = await getPermission(req.user_id, undefined, channel_id);
+	permissions.hasThrow("MANAGE_ROLES");
+
+	const channel = await ChannelModel.findOneAndUpdate({ id: channel_id }, { $pull: { permission_overwrites: { id: overwrite_id } } });
+	if (!channel.guild_id) throw new HTTPError("Channel not found", 404);
+
+	await emitEvent({
+		event: "CHANNEL_UPDATE",
+		channel_id,
+		data: channel
+	} as ChannelUpdateEvent);
+
+	return res.sendStatus(204);
+});
+
+export default router;
diff --git a/api/src/routes/channels/#channel_id/pins.ts b/api/src/routes/channels/#channel_id/pins.ts
new file mode 100644
index 00000000..65d6b975
--- /dev/null
+++ b/api/src/routes/channels/#channel_id/pins.ts
@@ -0,0 +1,93 @@
+import {
+	ChannelModel,
+	ChannelPinsUpdateEvent,
+	Config,
+	getPermission,
+	MessageModel,
+	MessageUpdateEvent,
+	toObject
+} from "@fosscord/server-util";
+import { Router, Request, Response } from "express";
+import { HTTPError } from "lambert-server";
+import { emitEvent } from "../../../util/Event";
+
+const router: Router = Router();
+
+router.put("/:message_id", async (req: Request, res: Response) => {
+	const { channel_id, message_id } = req.params;
+	const channel = await ChannelModel.findOne({ id: channel_id }).exec();
+	const permission = await getPermission(req.user_id, channel.guild_id, channel_id);
+	permission.hasThrow("VIEW_CHANNEL");
+
+	// * in dm channels anyone can pin messages -> only check for guilds
+	if (channel.guild_id) permission.hasThrow("MANAGE_MESSAGES");
+
+	const pinned_count = await MessageModel.count({ channel_id, pinned: true }).exec();
+	const { maxPins } = Config.get().limits.channel;
+	if (pinned_count >= maxPins) throw new HTTPError("Max pin count reached: " + maxPins);
+
+	await MessageModel.updateOne({ id: message_id }, { pinned: true }).exec();
+	const message = toObject(await MessageModel.findOne({ id: message_id }).exec());
+
+	await emitEvent({
+		event: "MESSAGE_UPDATE",
+		channel_id,
+		data: message
+	} as MessageUpdateEvent);
+
+	await 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.delete("/:message_id", async (req: Request, res: Response) => {
+	const { channel_id, message_id } = req.params;
+
+	const channel = await ChannelModel.findOne({ id: channel_id }).exec();
+
+	const permission = await getPermission(req.user_id, channel.guild_id, channel_id);
+	permission.hasThrow("VIEW_CHANNEL");
+	if (channel.guild_id) permission.hasThrow("MANAGE_MESSAGES");
+
+	const message = toObject(await MessageModel.findOneAndUpdate({ id: message_id }, { pinned: false }).exec());
+
+	await emitEvent({
+		event: "MESSAGE_UPDATE",
+		channel_id,
+		data: message
+	} as MessageUpdateEvent);
+
+	await 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("/", async (req: Request, res: Response) => {
+	const { channel_id } = req.params;
+
+	const channel = await ChannelModel.findOne({ id: channel_id }).exec();
+	const permission = await getPermission(req.user_id, channel.guild_id, channel_id);
+	permission.hasThrow("VIEW_CHANNEL");
+
+	let pins = await MessageModel.find({ channel_id: channel_id, pinned: true }).exec();
+
+	res.send(toObject(pins));
+});
+
+export default router;
diff --git a/api/src/routes/channels/#channel_id/recipients.ts b/api/src/routes/channels/#channel_id/recipients.ts
new file mode 100644
index 00000000..ea6bc563
--- /dev/null
+++ b/api/src/routes/channels/#channel_id/recipients.ts
@@ -0,0 +1,5 @@
+import { Router, Response, Request } from "express";
+const router: Router = Router();
+// TODO:
+
+export default router;
diff --git a/api/src/routes/channels/#channel_id/typing.ts b/api/src/routes/channels/#channel_id/typing.ts
new file mode 100644
index 00000000..de549883
--- /dev/null
+++ b/api/src/routes/channels/#channel_id/typing.ts
@@ -0,0 +1,31 @@
+import { ChannelModel, MemberModel, toObject, TypingStartEvent } from "@fosscord/server-util";
+import { Router, Request, Response } from "express";
+
+import { HTTPError } from "lambert-server";
+import { emitEvent } from "../../../util/Event";
+
+const router: Router = Router();
+
+router.post("/", async (req: Request, res: Response) => {
+	const { channel_id } = req.params;
+	const user_id = req.user_id;
+	const timestamp = Date.now();
+	const channel = await ChannelModel.findOne({ id: channel_id });
+	const member = await MemberModel.findOne({ id: user_id }).exec();
+
+	await emitEvent({
+		event: "TYPING_START",
+		channel_id: channel_id,
+		data: {
+			// this is the paylod
+			member: toObject(member),
+			channel_id,
+			timestamp,
+			user_id,
+			guild_id: channel.guild_id
+		}
+	} as TypingStartEvent);
+	res.sendStatus(204);
+});
+
+export default router;
diff --git a/api/src/routes/channels/#channel_id/webhooks.ts b/api/src/routes/channels/#channel_id/webhooks.ts
new file mode 100644
index 00000000..6c1aea2a
--- /dev/null
+++ b/api/src/routes/channels/#channel_id/webhooks.ts
@@ -0,0 +1,26 @@
+import { Router, Response, Request } from "express";
+import { check, Length } from "../../../util/instanceOf";
+import { ChannelModel, getPermission, trimSpecial } from "@fosscord/server-util";
+import { HTTPError } from "lambert-server";
+import { isTextChannel } from "./messages/index";
+
+const router: Router = Router();
+// TODO:
+
+// TODO: use Image Data Type for avatar instead of String
+router.post("/", check({ name: new Length(String, 1, 80), $avatar: String }), async (req: Request, res: Response) => {
+	const channel_id = req.params.channel_id;
+	const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true, type: true }).exec();
+
+	isTextChannel(channel.type);
+	if (!channel.guild_id) throw new HTTPError("Not a guild channel", 400);
+
+	const permission = await getPermission(req.user_id, channel.guild_id);
+	permission.hasThrow("MANAGE_WEBHOOKS");
+
+	var { avatar, name } = req.body as { name: string; avatar?: string };
+	name = trimSpecial(name);
+	if (name === "clyde") throw new HTTPError("Invalid name", 400);
+});
+
+export default router;