diff options
Diffstat (limited to 'api/src/routes/channels/#channel_id')
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; |