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;
|