diff --git a/src/routes/channels/#channel_id/followers.ts b/src/routes/channels/#channel_id/followers.ts
new file mode 100644
index 00000000..9a4e81fa
--- /dev/null
+++ b/src/routes/channels/#channel_id/followers.ts
@@ -0,0 +1,4 @@
+import { Router } from "express";
+const router: Router = Router();
+
+export default router;
diff --git a/src/routes/channels/#channel_id/index.ts b/src/routes/channels/#channel_id/index.ts
new file mode 100644
index 00000000..9a4e81fa
--- /dev/null
+++ b/src/routes/channels/#channel_id/index.ts
@@ -0,0 +1,4 @@
+import { Router } from "express";
+const router: Router = Router();
+
+export default router;
diff --git a/src/routes/channels/#channel_id/invites.ts b/src/routes/channels/#channel_id/invites.ts
new file mode 100644
index 00000000..4c21e7d4
--- /dev/null
+++ b/src/routes/channels/#channel_id/invites.ts
@@ -0,0 +1,67 @@
+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 } from "fosscord-server-util";
+
+const router: Router = Router();
+
+router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) => {
+ const usID = req.user_id;
+ const chID = BigInt(req.params.channel_id);
+ const channel = await ChannelModel.findOne({ id: chID }).exec();
+
+ if (!channel || !channel.guild_id) {
+ throw new HTTPError("This channel doesn't exist", 404);
+ }
+ const { guild_id: guID } = channel;
+
+ const permission = await getPermission(usID, guID);
+
+ if (!permission.has("CREATE_INSTANT_INVITE")) {
+ throw new HTTPError("You aren't authorised to access this endpoint", 401);
+ }
+
+ const invite = {
+ code: random(),
+ temporary: req.body.temporary,
+ uses: 0,
+ max_uses: req.body.max_uses,
+ max_age: req.body.max_age,
+ created_at: new Date(),
+ guild_id: guID,
+ channel_id: chID,
+ inviter_id: usID,
+ };
+
+ await new InviteModel(invite).save();
+
+ await emitEvent({ event: "INVITE_CREATE", data: invite } as InviteCreateEvent);
+ res.status(201).send(invite);
+});
+
+router.get("/", async (req: Request, res: Response) => {
+ const usID = req.user_id;
+ const chID = BigInt(req.params.channel_id);
+ const channel = await ChannelModel.findOne({ id: chID }).exec();
+
+ if (!channel || !channel.guild_id) {
+ throw new HTTPError("This channel doesn't exist", 404);
+ }
+ const { guild_id: guID } = channel;
+ const permission = await getPermission(usID, guID);
+
+ if (!permission.has("MANAGE_CHANNELS")) {
+ throw new HTTPError("You aren't authorised to access this endpoint", 401);
+ }
+
+ const invites = await InviteModel.find({ guild_id: guID }).exec();
+ res.status(200).send(invites);
+});
+
+export default router;
diff --git a/src/routes/channels/#channel_id/messages/bulk-delete.ts b/src/routes/channels/#channel_id/messages/bulk-delete.ts
new file mode 100644
index 00000000..c805cf08
--- /dev/null
+++ b/src/routes/channels/#channel_id/messages/bulk-delete.ts
@@ -0,0 +1,37 @@
+import { Router } from "express";
+import { ChannelModel, getPermission, MessageDeleteBulkEvent, MessageModel } from "fosscord-server-util";
+import { HTTPError } from "lambert-server";
+import Config from "../../../../../../util/Config";
+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: [BigInt] }), async (req, res) => {
+ const channel_id = BigInt(req.params.channel_id);
+ 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 });
+ if (!permission.has("MANAGE_MESSAGES")) throw new HTTPError("You are missing the MANAGE_MESSAGES permissions");
+
+ const { maxBulkDelete } = Config.get().limits.message;
+
+ const { messages } = req.body as { messages: bigint[] };
+ 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.status(204).send();
+});
diff --git a/src/routes/channels/#channel_id/messages/index.ts b/src/routes/channels/#channel_id/messages/index.ts
new file mode 100644
index 00000000..ade048a0
--- /dev/null
+++ b/src/routes/channels/#channel_id/messages/index.ts
@@ -0,0 +1,136 @@
+import { Router } from "express";
+import { ChannelModel, ChannelType, getPermission, Message, MessageCreateEvent, MessageModel, Snowflake } from "fosscord-server-util";
+import { HTTPError } from "lambert-server";
+import { MessageCreateSchema } from "../../../../../../schema/Message";
+import { check, instanceOf, Length } from "../../../../../../util/instanceOf";
+import { PublicUserProjection } from "../../../../../../util/User";
+import multer from "multer";
+import { emitEvent } from "../../../../../../util/Event";
+const router: Router = Router();
+
+export default router;
+
+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, res) => {
+ const channel_id = BigInt(req.params.channel_id);
+ const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true, type: true, permission_overwrites: true }).exec();
+ if (!channel) throw new HTTPError("Channel not found", 404);
+
+ isTextChannel(channel.type);
+
+ try {
+ instanceOf({ $around: BigInt, $after: BigInt, $before: BigInt, $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?: bigint; after?: bigint; before?: bigint; limit?: number } = req.query;
+ if (!limit) limit = 50;
+ var halfLimit = BigInt(Math.floor(limit / 2));
+
+ if ([ChannelType.GUILD_VOICE, ChannelType.GUILD_CATEGORY, ChannelType.GUILD_STORE].includes(channel.type))
+ throw new HTTPError("Not a text channel");
+
+ if (channel.guild_id) {
+ const permissions = await getPermission(req.user_id, channel.guild_id, channel_id, { channel });
+ if (!permissions.has("VIEW_CHANNEL")) throw new HTTPError("You don't have permission to view this channel", 401);
+ if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]);
+ } else if (channel.recipients) {
+ // group/dm channel
+ if (!channel.recipients.includes(req.user_id)) throw new HTTPError("You don't have permission to view this channel", 401);
+ }
+
+ var query: any;
+ 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: around - halfLimit, $lt: around + halfLimit } });
+ else {
+ query = MessageModel.find({ channel_id }).sort({ id: -1 });
+ }
+
+ const messages = await query
+ .limit(limit)
+ .populate({ path: "author", select: PublicUserProjection })
+ .populate({ path: "mentions", select: PublicUserProjection })
+ .populate({ path: "mention_channels", select: { id: true, guild_id: true, type: true, name: true } })
+ .populate("mention_roles")
+ // .populate({ path: "member", select: PublicMemberProjection })
+ .exec();
+
+ return res.json(messages);
+});
+
+// TODO: config max upload size
+const messageUpload = multer({ limits: { fieldSize: 1024 * 1024 * 1024 * 50 } }); // 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
+// Send message
+router.post("/", check(MessageCreateSchema), async (req, res) => {
+ const channel_id = BigInt(req.params.channel_id);
+ const body = req.body as MessageCreateSchema;
+
+ const channel = await ChannelModel.findOne({ id: channel_id }, { guild_id: true, type: true, permission_overwrites: true }).exec();
+ if (!channel) throw new HTTPError("Channel not found", 404);
+
+ if (channel.guild_id) {
+ const permissions = await getPermission(req.user_id, channel.guild_id, channel_id, { channel });
+ if (!permissions.has("SEND_MESSAGES")) throw new HTTPError("You don't have the SEND_MESSAGES permission");
+ if (body.tts && !permissions.has("SEND_TTS_MESSAGES")) throw new HTTPError("You are missing the SEND_TTS_MESSAGES permission");
+ if (body.message_reference) {
+ if (!permissions.has("READ_MESSAGE_HISTORY"))
+ throw new HTTPError("You are missing the READ_MESSAGE_HISTORY permission to reply");
+ if (body.message_reference.guild_id !== channel.guild_id)
+ throw new HTTPError("You can only reference messages from this guild");
+ }
+ }
+
+ if (body.message_reference) {
+ if (body.message_reference.channel_id !== channel_id) throw new HTTPError("You can only reference messages from this channel");
+ // TODO: should it be checked if the message exists?
+ }
+
+ const embeds = [];
+ if (body.embed) embeds.push(body.embed);
+
+ const message: Message = {
+ id: Snowflake.generate(),
+ channel_id,
+ guild_id: channel.guild_id,
+ author_id: req.user_id,
+ content: req.body,
+ timestamp: new Date(),
+ mention_channels_ids: [],
+ mention_role_ids: [],
+ mention_user_ids: [],
+ attachments: [],
+ embeds: [],
+ reactions: [],
+ type: 0,
+ };
+
+ await new MessageModel(message).save();
+
+ await emitEvent({ event: "MESSAGE_CREATE", channel_id, data: {} } as MessageCreateEvent);
+});
diff --git a/src/routes/channels/#channel_id/permissions.ts b/src/routes/channels/#channel_id/permissions.ts
new file mode 100644
index 00000000..9a4e81fa
--- /dev/null
+++ b/src/routes/channels/#channel_id/permissions.ts
@@ -0,0 +1,4 @@
+import { Router } from "express";
+const router: Router = Router();
+
+export default router;
diff --git a/src/routes/channels/#channel_id/pins.ts b/src/routes/channels/#channel_id/pins.ts
new file mode 100644
index 00000000..9a4e81fa
--- /dev/null
+++ b/src/routes/channels/#channel_id/pins.ts
@@ -0,0 +1,4 @@
+import { Router } from "express";
+const router: Router = Router();
+
+export default router;
diff --git a/src/routes/channels/#channel_id/recipients.ts b/src/routes/channels/#channel_id/recipients.ts
new file mode 100644
index 00000000..9a4e81fa
--- /dev/null
+++ b/src/routes/channels/#channel_id/recipients.ts
@@ -0,0 +1,4 @@
+import { Router } from "express";
+const router: Router = Router();
+
+export default router;
diff --git a/src/routes/channels/#channel_id/typing.ts b/src/routes/channels/#channel_id/typing.ts
new file mode 100644
index 00000000..9a4e81fa
--- /dev/null
+++ b/src/routes/channels/#channel_id/typing.ts
@@ -0,0 +1,4 @@
+import { Router } from "express";
+const router: Router = Router();
+
+export default router;
diff --git a/src/routes/channels/#channel_id/webhooks.ts b/src/routes/channels/#channel_id/webhooks.ts
new file mode 100644
index 00000000..9a4e81fa
--- /dev/null
+++ b/src/routes/channels/#channel_id/webhooks.ts
@@ -0,0 +1,4 @@
+import { Router } from "express";
+const router: Router = Router();
+
+export default router;
|