summary refs log tree commit diff
path: root/api/src/routes/channels
diff options
context:
space:
mode:
authorFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-09-24 10:43:43 +0200
committerFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-09-24 10:43:43 +0200
commit9abb04d2e6fcc2567ddafc2dc753cbb0c79ca930 (patch)
treea9ce1cd54d5dd50023e1a1c5d0a494802f37b5e6 /api/src/routes/channels
parent:sparkles: added User flags (diff)
parent:art: remove start from setup script (diff)
downloadserver-ts-9abb04d2e6fcc2567ddafc2dc753cbb0c79ca930.tar.xz
Merge branch 'master' of https://github.com/fosscord/fosscord-server
Diffstat (limited to 'api/src/routes/channels')
-rw-r--r--api/src/routes/channels/#channel_id/index.ts84
-rw-r--r--api/src/routes/channels/#channel_id/invites.ts42
-rw-r--r--api/src/routes/channels/#channel_id/messages/#message_id/ack.ts12
-rw-r--r--api/src/routes/channels/#channel_id/messages/#message_id/index.ts22
-rw-r--r--api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts33
-rw-r--r--api/src/routes/channels/#channel_id/messages/bulk-delete.ts9
-rw-r--r--api/src/routes/channels/#channel_id/messages/index.ts157
-rw-r--r--api/src/routes/channels/#channel_id/permissions.ts82
-rw-r--r--api/src/routes/channels/#channel_id/pins.ts33
-rw-r--r--api/src/routes/channels/#channel_id/recipients.ts57
-rw-r--r--api/src/routes/channels/#channel_id/typing.ts10
-rw-r--r--api/src/routes/channels/#channel_id/webhooks.ts16
12 files changed, 353 insertions, 204 deletions
diff --git a/api/src/routes/channels/#channel_id/index.ts b/api/src/routes/channels/#channel_id/index.ts

index 4aa5a5b9..61c851e8 100644 --- a/api/src/routes/channels/#channel_id/index.ts +++ b/api/src/routes/channels/#channel_id/index.ts
@@ -1,47 +1,81 @@ -import { ChannelDeleteEvent, Channel, ChannelUpdateEvent, emitEvent, getPermission } from "@fosscord/util"; -import { Router, Response, Request } from "express"; -import { HTTPError } from "lambert-server"; -import { ChannelModifySchema } from "../../../schema/Channel"; -import { check } from "../../../util/instanceOf"; +import { + Channel, + ChannelDeleteEvent, + ChannelPermissionOverwriteType, + ChannelType, + ChannelUpdateEvent, + emitEvent, + Recipient, + handleFile +} from "@fosscord/util"; +import { Request, Response, Router } from "express"; +import { route } from "@fosscord/api"; + const router: Router = Router(); // TODO: delete channel // TODO: Get channel -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { const { channel_id } = req.params; const channel = await Channel.findOneOrFail({ id: channel_id }); - const permission = await getPermission(req.user_id, channel.guild_id, channel_id); - permission.hasThrow("VIEW_CHANNEL"); - return res.send(channel); }); -router.delete("/", async (req: Request, res: Response) => { +router.delete("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { const { channel_id } = req.params; - const channel = await Channel.findOneOrFail({ id: channel_id }); - - const permission = await getPermission(req.user_id, channel?.guild_id, channel_id); - permission.hasThrow("MANAGE_CHANNELS"); - - // TODO: Dm channel "close" not delete - const data = channel; + const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] }); - await emitEvent({ event: "CHANNEL_DELETE", data, channel_id } as ChannelDeleteEvent); + if (channel.type === ChannelType.DM) { + const recipient = await Recipient.findOneOrFail({ where: { channel_id: channel_id, user_id: req.user_id } }); + recipient.closed = true; + await Promise.all([ + recipient.save(), + emitEvent({ event: "CHANNEL_DELETE", data: channel, user_id: req.user_id } as ChannelDeleteEvent) + ]); + } else if (channel.type === ChannelType.GROUP_DM) { + await Channel.removeRecipientFromChannel(channel, req.user_id); + } else { + await Promise.all([ + Channel.delete({ id: channel_id }), + emitEvent({ event: "CHANNEL_DELETE", data: channel, channel_id } as ChannelDeleteEvent) + ]); + } - await Channel.delete({ id: channel_id }); - - res.send(data); + res.send(channel); }); -router.patch("/", check(ChannelModifySchema), async (req: Request, res: Response) => { +export interface ChannelModifySchema { + /** + * @maxLength 100 + */ + name?: string; + type?: ChannelType; + topic?: string; + icon?: string | null; + bitrate?: number; + user_limit?: number; + rate_limit_per_user?: number; + position?: number; + permission_overwrites?: { + id: string; + type: ChannelPermissionOverwriteType; + allow: bigint; + deny: bigint; + }[]; + parent_id?: string; + id?: string; // is not used (only for guild create) + nsfw?: boolean; + rtc_region?: string; + default_auto_archive_duration?: number; +} + +router.patch("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), 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"); + if (payload.icon) payload.icon = await handleFile(`/channel-icons/${channel_id}`, payload.icon); const channel = await Channel.findOneOrFail({ id: channel_id }); channel.assign(payload); diff --git a/api/src/routes/channels/#channel_id/invites.ts b/api/src/routes/channels/#channel_id/invites.ts
index fe22d3bc..22420983 100644 --- a/api/src/routes/channels/#channel_id/invites.ts +++ b/api/src/routes/channels/#channel_id/invites.ts
@@ -1,14 +1,25 @@ import { Router, Request, Response } from "express"; import { HTTPError } from "lambert-server"; -import { check } from "../../../util/instanceOf"; -import { random } from "../../../util/RandomInviteID"; -import { InviteCreateSchema } from "../../../schema/Invite"; +import { route } from "@fosscord/api"; +import { random } from "@fosscord/api"; import { getPermission, Channel, Invite, InviteCreateEvent, emitEvent, User, Guild, PublicInviteRelation } from "@fosscord/util"; import { isTextChannel } from "./messages"; const router: Router = Router(); -router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) => { +export interface InviteCreateSchema { + target_user_id?: string; + target_type?: string; + validate?: string; // ? what is this + max_age?: number; + max_uses?: number; + temporary?: boolean; + unique?: boolean; + target_user?: string; + target_user_type?: number; +} + +router.post("/", route({ body: "InviteCreateSchema", permission: "CREATE_INSTANT_INVITE" }), async (req: Request, res: Response) => { const { user_id } = req; const { channel_id } = req.params; const channel = await Channel.findOneOrFail({ where: { id: channel_id }, select: ["id", "name", "type", "guild_id"] }); @@ -19,23 +30,6 @@ router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) } const { guild_id } = channel; - const permission = await getPermission(user_id, guild_id, undefined, { - guild_select: [ - "banner", - "description", - "features", - "icon", - "id", - "name", - "nsfw", - "nsfw_level", - "splash", - "vanity_url_code", - "verification_level" - ] as (keyof Guild)[] - }); - permission.hasThrow("CREATE_INSTANT_INVITE"); - const expires_at = new Date(req.body.max_age * 1000 + Date.now()); const invite = await new Invite({ @@ -52,14 +46,14 @@ router.post("/", check(InviteCreateSchema), async (req: Request, res: Response) }).save(); const data = invite.toJSON(); data.inviter = await User.getPublicUser(req.user_id); - data.guild = permission.cache.guild; + data.guild = await Guild.findOne({ id: guild_id }); data.channel = channel; await emitEvent({ event: "INVITE_CREATE", data, guild_id } as InviteCreateEvent); res.status(201).send(data); }); -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { const { user_id } = req; const { channel_id } = req.params; const channel = await Channel.findOneOrFail({ id: channel_id }); @@ -68,8 +62,6 @@ router.get("/", async (req: Request, res: Response) => { 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 Invite.find({ where: { guild_id }, relations: PublicInviteRelation }); 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
index 0fd5f2be..786e4581 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts
@@ -1,14 +1,18 @@ import { emitEvent, getPermission, MessageAckEvent, ReadState } from "@fosscord/util"; import { Request, Response, Router } from "express"; - -import { check } from "../../../../../util/instanceOf"; +import { route } from "@fosscord/api"; 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) => { +export interface MessageAcknowledgeSchema { + manual?: boolean; + mention_count?: number; +} + +router.post("/", route({ body: "MessageAcknowledgeSchema" }), async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; const permission = await getPermission(req.user_id, undefined, channel_id); @@ -22,7 +26,7 @@ router.post("/", check({ $manual: Boolean, $mention_count: Number }), async (req data: { channel_id, message_id, - version: 496 + version: 3763 } } as MessageAckEvent); 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
index 7a00de43..7f7de264 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/index.ts
@@ -1,16 +1,17 @@ import { Channel, emitEvent, getPermission, MessageDeleteEvent, Message, MessageUpdateEvent } from "@fosscord/util"; import { Router, Response, Request } from "express"; -import { MessageCreateSchema } from "../../../../../schema/Message"; -import { check } from "../../../../../util/instanceOf"; -import { handleMessage, postHandleMessage } from "../../../../../util/Message"; +import { route } from "@fosscord/api"; +import { handleMessage, postHandleMessage } from "@fosscord/api"; +import { MessageCreateSchema } from "../index"; const router = Router(); +// TODO: message content/embed string length limit -router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response) => { +router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES" }), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; var body = req.body as MessageCreateSchema; - const message = await Message.findOneOrFail({ id: message_id, channel_id }); + const message = await Message.findOneOrFail({ where: { id: message_id, channel_id }, relations: ["attachments"] }); const permissions = await getPermission(req.user_id, undefined, channel_id); @@ -45,16 +46,17 @@ router.patch("/", check(MessageCreateSchema), async (req: Request, res: Response return res.json(message); }); -// TODO: delete attachments in message - -router.delete("/", async (req: Request, res: Response) => { +// permission check only if deletes messagr from other user +router.delete("/", route({}), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const channel = await Channel.findOneOrFail({ id: channel_id }); const message = await Message.findOneOrFail({ id: message_id }); - const permission = await getPermission(req.user_id, channel.guild_id, channel_id); - if (message.author_id !== req.user_id) permission.hasThrow("MANAGE_MESSAGES"); + if (message.author_id !== req.user_id) { + const permission = await getPermission(req.user_id, channel.guild_id, channel_id); + permission.hasThrow("MANAGE_MESSAGES"); + } await Message.delete({ id: message_id }); 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
index f60484b5..f2b83d40 100644 --- a/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts +++ b/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts
@@ -13,6 +13,7 @@ import { PublicUserProjection, User } from "@fosscord/util"; +import { route } from "@fosscord/api"; import { Router, Response, Request } from "express"; import { HTTPError } from "lambert-server"; import { In } from "typeorm"; @@ -35,14 +36,11 @@ function getEmoji(emoji: string): PartialEmoji { }; } -router.delete("/", async (req: Request, res: Response) => { +router.delete("/", route({ permission: "MANAGE_MESSAGES" }), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const channel = await Channel.findOneOrFail({ id: channel_id }); - const permissions = await getPermission(req.user_id, undefined, channel_id); - permissions.hasThrow("MANAGE_MESSAGES"); - await Message.update({ id: message_id, channel_id }, { reactions: [] }); await emitEvent({ @@ -58,13 +56,10 @@ router.delete("/", async (req: Request, res: Response) => { res.sendStatus(204); }); -router.delete("/:emoji", async (req: Request, res: Response) => { +router.delete("/:emoji", route({ permission: "MANAGE_MESSAGES" }), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const emoji = getEmoji(req.params.emoji); - const permissions = await getPermission(req.user_id, undefined, channel_id); - permissions.hasThrow("MANAGE_MESSAGES"); - const message = await Message.findOneOrFail({ id: message_id, channel_id }); const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); @@ -88,7 +83,7 @@ router.delete("/:emoji", async (req: Request, res: Response) => { res.sendStatus(204); }); -router.get("/:emoji", async (req: Request, res: Response) => { +router.get("/:emoji", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { const { message_id, channel_id } = req.params; const emoji = getEmoji(req.params.emoji); @@ -96,9 +91,6 @@ router.get("/:emoji", async (req: Request, res: Response) => { 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 User.find({ where: { id: In(reaction.user_ids) @@ -109,7 +101,7 @@ router.get("/:emoji", async (req: Request, res: Response) => { res.json(users); }); -router.put("/:emoji/:user_id", async (req: Request, res: Response) => { +router.put("/:emoji/:user_id", route({ permission: "READ_MESSAGE_HISTORY" }), 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); @@ -118,13 +110,11 @@ router.put("/:emoji/:user_id", async (req: Request, res: Response) => { const message = await Message.findOneOrFail({ id: message_id, channel_id }); 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 (!already_added) req.permission!.hasThrow("ADD_REACTIONS"); if (emoji.id) { const external_emoji = await Emoji.findOneOrFail({ id: emoji.id }); - if (!already_added) permissions.hasThrow("USE_EXTERNAL_EMOJIS"); + if (!already_added) req.permission!.hasThrow("USE_EXTERNAL_EMOJIS"); emoji.animated = external_emoji.animated; emoji.name = external_emoji.name; } @@ -154,7 +144,7 @@ router.put("/:emoji/:user_id", async (req: Request, res: Response) => { res.sendStatus(204); }); -router.delete("/:emoji/:user_id", async (req: Request, res: Response) => { +router.delete("/:emoji/:user_id", route({}), async (req: Request, res: Response) => { var { message_id, channel_id, user_id } = req.params; const emoji = getEmoji(req.params.emoji); @@ -162,10 +152,11 @@ router.delete("/:emoji/:user_id", async (req: Request, res: Response) => { const channel = await Channel.findOneOrFail({ id: channel_id }); const message = await Message.findOneOrFail({ id: message_id, channel_id }); - const permissions = await getPermission(req.user_id, undefined, channel_id); - if (user_id === "@me") user_id = req.user_id; - else permissions.hasThrow("MANAGE_MESSAGES"); + else { + const permissions = await getPermission(req.user_id, undefined, channel_id); + permissions.hasThrow("MANAGE_MESSAGES"); + } const already_added = message.reactions.find((x) => (x.emoji.id === emoji.id && emoji.id) || x.emoji.name === emoji.name); if (!already_added || !already_added.user_ids.includes(user_id)) throw new HTTPError("Reaction not found", 404); diff --git a/api/src/routes/channels/#channel_id/messages/bulk-delete.ts b/api/src/routes/channels/#channel_id/messages/bulk-delete.ts
index 5c486676..a0fe7cc0 100644 --- a/api/src/routes/channels/#channel_id/messages/bulk-delete.ts +++ b/api/src/routes/channels/#channel_id/messages/bulk-delete.ts
@@ -1,18 +1,21 @@ import { Router, Response, Request } from "express"; import { Channel, Config, emitEvent, getPermission, MessageDeleteBulkEvent, Message } from "@fosscord/util"; import { HTTPError } from "lambert-server"; - -import { check } from "../../../../util/instanceOf"; +import { route } from "@fosscord/api"; import { In } from "typeorm"; const router: Router = Router(); export default router; +export interface BulkDeleteSchema { + messages: string[]; +} + // 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) => { +router.post("/", route({ body: "BulkDeleteSchema" }), async (req: Request, res: Response) => { const { channel_id } = req.params; const channel = await Channel.findOneOrFail({ id: channel_id }); if (!channel.guild_id) throw new HTTPError("Can't bulk delete dm channel messages", 400); diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts
index ad590d05..fab20977 100644 --- a/api/src/routes/channels/#channel_id/messages/index.ts +++ b/api/src/routes/channels/#channel_id/messages/index.ts
@@ -1,12 +1,19 @@ import { Router, Response, Request } from "express"; -import { Attachment, Channel, ChannelType, getPermission, Message } from "@fosscord/util"; +import { + Attachment, + Channel, + ChannelType, + DmChannelDTO, + Embed, + emitEvent, + getPermission, + Message, + MessageCreateEvent, + uploadFile +} from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { MessageCreateSchema } from "../../../../schema/Message"; -import { check, instanceOf, Length } from "../../../../util/instanceOf"; +import { handleMessage, postHandleMessage, route } from "@fosscord/api"; import multer from "multer"; -import { Query } from "mongoose"; -import { sendMessage } from "../../../../util/Message"; -import { uploadFile } from "../../../../util/cdn"; import { FindManyOptions, LessThan, MoreThan } from "typeorm"; const router: Router = Router(); @@ -31,6 +38,31 @@ export function isTextChannel(type: ChannelType): boolean { } } +export interface MessageCreateSchema { + content?: string; + nonce?: string; + tts?: boolean; + flags?: string; + embeds?: Embed[]; + embed?: Embed; + // TODO: ^ embed is deprecated in favor of embeds (https://discord.com/developers/docs/resources/channel#message-object) + allowed_mentions?: { + parse?: string[]; + roles?: string[]; + users?: string[]; + replied_user?: boolean; + }; + message_reference?: { + message_id: string; + channel_id: string; + guild_id?: string; + fail_if_not_exists?: boolean; + }; + payload_json?: string; + file?: any; + attachments?: any[]; //TODO we should create an interface for attachments +} + // https://discord.com/developers/docs/resources/channel#create-message // get messages router.get("/", async (req: Request, res: Response) => { @@ -39,17 +71,12 @@ router.get("/", async (req: Request, res: Response) => { if (!channel) throw new HTTPError("Channel not found", 404); isTextChannel(channel.type); + const around = req.query.around ? `${req.query.around}` : undefined; + const before = req.query.before ? `${req.query.before}` : undefined; + const after = req.query.after ? `${req.query.after}` : undefined; + const limit = Number(req.query.limit) || 50; + if (limit < 1 || limit > 100) throw new HTTPError("limit must be between 1 and 100"); - 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); const permissions = await getPermission(req.user_id, channel.guild_id, channel_id); @@ -109,39 +136,77 @@ const messageUpload = multer({ // 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[] = []; +router.post( + "/", + messageUpload.single("file"), + async (req, res, next) => { + if (req.body.payload_json) { + req.body = JSON.parse(req.body.payload_json); + } - 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); + next(); + }, + route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES" }), + 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/${req.params.channel_id}`, req.file); + attachments.push({ ...file, proxy_url: file.url }); + } catch (error) { + return res.status(400).json(error); + } } - } + const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] }); - if (body.payload_json) { - body = JSON.parse(body.payload_json); - } + const embeds = []; + if (body.embed) embeds.push(body.embed); + let message = await handleMessage({ + ...body, + type: 0, + pinned: false, + author_id: req.user_id, + embeds, + channel_id, + attachments, + edited_timestamp: undefined, + timestamp: new Date() + }); - const errors = instanceOf(MessageCreateSchema, body, { req }); - if (errors !== true) throw errors; + message = await message.save(); - 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: undefined - }); + await channel.assign({ last_message_id: message.id }).save(); - return res.json(data); -}); + if (channel.isDm()) { + const channel_dto = await DmChannelDTO.from(channel); + + for (let recipient of channel.recipients!) { + if (recipient.closed) { + await emitEvent({ + event: "CHANNEL_CREATE", + data: channel_dto.excludedRecipients([recipient.user_id]), + user_id: recipient.user_id + }); + } + } + + //Only one recipients should be closed here, since in group DMs the recipient is deleted not closed + await Promise.all( + channel + .recipients!.filter((r) => r.closed) + .map(async (r) => { + r.closed = false; + return await r.save(); + }) + ); + } + + await emitEvent({ event: "MESSAGE_CREATE", channel_id: channel_id, data: message } as MessageCreateEvent); + postHandleMessage(message).catch((e) => {}); // no await as it shouldnt block the message send function and silently catch error + + return res.json(message); + } +); diff --git a/api/src/routes/channels/#channel_id/permissions.ts b/api/src/routes/channels/#channel_id/permissions.ts
index 9c49542b..bc7ad5b8 100644 --- a/api/src/routes/channels/#channel_id/permissions.ts +++ b/api/src/routes/channels/#channel_id/permissions.ts
@@ -2,65 +2,65 @@ import { Channel, ChannelPermissionOverwrite, ChannelUpdateEvent, emitEvent, get import { Router, Response, Request } from "express"; import { HTTPError } from "lambert-server"; -import { check } from "../../../util/instanceOf"; +import { route } from "@fosscord/api"; 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 }; +export interface ChannelPermissionOverwriteSchema extends ChannelPermissionOverwrite {} - var channel = await Channel.findOneOrFail({ id: channel_id }); - if (!channel.guild_id) throw new HTTPError("Channel not found", 404); +router.put( + "/:overwrite_id", + route({ body: "ChannelPermissionOverwriteSchema", permission: "MANAGE_ROLES" }), + async (req: Request, res: Response) => { + const { channel_id, overwrite_id } = req.params; + const body = req.body as { allow: bigint; deny: bigint; type: number; id: string }; - const permissions = await getPermission(req.user_id, channel.guild_id, channel_id); - permissions.hasThrow("MANAGE_ROLES"); + var channel = await Channel.findOneOrFail({ id: channel_id }); + if (!channel.guild_id) throw new HTTPError("Channel not found", 404); - if (body.type === 0) { - if (!(await Role.count({ id: overwrite_id }))) throw new HTTPError("role not found", 404); - } else if (body.type === 1) { - if (!(await Member.count({ id: overwrite_id }))) throw new HTTPError("user not found", 404); - } else throw new HTTPError("type not supported", 501); + if (body.type === 0) { + if (!(await Role.count({ id: overwrite_id }))) throw new HTTPError("role not found", 404); + } else if (body.type === 1) { + if (!(await Member.count({ 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 Channel.findOneOrFailAndUpdate({ id: channel_id }, channel, { new: true }); + 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; - await emitEvent({ - event: "CHANNEL_UPDATE", - channel_id, - data: channel - } as ChannelUpdateEvent); + await Promise.all([ + channel.save(), + emitEvent({ + event: "CHANNEL_UPDATE", + channel_id, + data: channel + } as ChannelUpdateEvent) + ]); - return res.sendStatus(204); -}); + return res.sendStatus(204); + } +); // TODO: check permission hierarchy -router.delete("/:overwrite_id", async (req: Request, res: Response) => { +router.delete("/:overwrite_id", route({ permission: "MANAGE_ROLES" }), 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 Channel.findOneOrFail({ id: channel_id }); if (!channel.guild_id) throw new HTTPError("Channel not found", 404); - channel.permission_overwrites = channel.permission_overwrites.filter((x) => x.id === overwrite_id); + channel.permission_overwrites = channel.permission_overwrites!.filter((x) => x.id === overwrite_id); await Promise.all([ channel.save(), diff --git a/api/src/routes/channels/#channel_id/pins.ts b/api/src/routes/channels/#channel_id/pins.ts
index 33309c86..e71e659f 100644 --- a/api/src/routes/channels/#channel_id/pins.ts +++ b/api/src/routes/channels/#channel_id/pins.ts
@@ -1,19 +1,26 @@ -import { Channel, ChannelPinsUpdateEvent, Config, emitEvent, getPermission, Message, MessageUpdateEvent } from "@fosscord/util"; +import { + Channel, + ChannelPinsUpdateEvent, + Config, + emitEvent, + getPermission, + Message, + MessageUpdateEvent, + DiscordApiErrors +} from "@fosscord/util"; import { Router, Request, Response } from "express"; import { HTTPError } from "lambert-server"; -import { DiscordApiErrors } from "@fosscord/util"; +import { route } from "@fosscord/api"; const router: Router = Router(); -router.put("/:message_id", async (req: Request, res: Response) => { +router.put("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; const message = await Message.findOneOrFail({ id: message_id }); - const permission = await getPermission(req.user_id, message.guild_id, channel_id); - permission.hasThrow("VIEW_CHANNEL"); // * in dm channels anyone can pin messages -> only check for guilds - if (message.guild_id) permission.hasThrow("MANAGE_MESSAGES"); + if (message.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES"); const pinned_count = await Message.count({ channel: { id: channel_id }, pinned: true }); const { maxPins } = Config.get().limits.channel; @@ -26,7 +33,6 @@ router.put("/:message_id", async (req: Request, res: Response) => { channel_id, data: message } as MessageUpdateEvent), - emitEvent({ event: "CHANNEL_PINS_UPDATE", channel_id, @@ -41,14 +47,11 @@ router.put("/:message_id", async (req: Request, res: Response) => { res.sendStatus(204); }); -router.delete("/:message_id", async (req: Request, res: Response) => { +router.delete("/:message_id", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { const { channel_id, message_id } = req.params; const channel = await Channel.findOneOrFail({ id: channel_id }); - - const permission = await getPermission(req.user_id, channel.guild_id, channel_id); - permission.hasThrow("VIEW_CHANNEL"); - if (channel.guild_id) permission.hasThrow("MANAGE_MESSAGES"); + if (channel.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES"); const message = await Message.findOneOrFail({ id: message_id }); message.pinned = false; @@ -76,13 +79,9 @@ router.delete("/:message_id", async (req: Request, res: Response) => { res.sendStatus(204); }); -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({ permission: ["READ_MESSAGE_HISTORY"] }), async (req: Request, res: Response) => { const { channel_id } = req.params; - const channel = await Channel.findOneOrFail({ id: channel_id }); - const permission = await getPermission(req.user_id, channel.guild_id, channel_id); - permission.hasThrow("VIEW_CHANNEL"); - let pins = await Message.find({ channel_id: channel_id, pinned: true }); res.send(pins); diff --git a/api/src/routes/channels/#channel_id/recipients.ts b/api/src/routes/channels/#channel_id/recipients.ts
index ea6bc563..83b62049 100644 --- a/api/src/routes/channels/#channel_id/recipients.ts +++ b/api/src/routes/channels/#channel_id/recipients.ts
@@ -1,5 +1,58 @@ -import { Router, Response, Request } from "express"; +import { Request, Response, Router } from "express"; +import { Channel, ChannelRecipientAddEvent, ChannelType, DiscordApiErrors, DmChannelDTO, emitEvent, PublicUserProjection, Recipient, User } from "@fosscord/util"; +import { route } from "@fosscord/api" + const router: Router = Router(); -// TODO: + +router.put("/:user_id", route({}), async (req: Request, res: Response) => { + const { channel_id, user_id } = req.params; + const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] }); + + if (channel.type !== ChannelType.GROUP_DM) { + const recipients = [ + ...channel.recipients!.map(r => r.user_id), + user_id + ].unique() + + const new_channel = await Channel.createDMChannel(recipients, req.user_id) + return res.status(201).json(new_channel); + } else { + if (channel.recipients!.map(r => r.user_id).includes(user_id)) { + throw DiscordApiErrors.INVALID_RECIPIENT //TODO is this the right error? + } + + channel.recipients!.push(new Recipient({ channel_id: channel_id, user_id: user_id })); + await channel.save() + + await emitEvent({ + event: "CHANNEL_CREATE", + data: await DmChannelDTO.from(channel, [user_id]), + user_id: user_id + }); + + await emitEvent({ + event: "CHANNEL_RECIPIENT_ADD", data: { + channel_id: channel_id, + user: await User.findOneOrFail({ where: { id: user_id }, select: PublicUserProjection }) + }, channel_id: channel_id + } as ChannelRecipientAddEvent); + return res.sendStatus(204); + } +}); + +router.delete("/:user_id", route({}), async (req: Request, res: Response) => { + const { channel_id, user_id } = req.params; + const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] }); + if (!(channel.type === ChannelType.GROUP_DM && (channel.owner_id === req.user_id || user_id === req.user_id))) + throw DiscordApiErrors.MISSING_PERMISSIONS + + if (!channel.recipients!.map(r => r.user_id).includes(user_id)) { + throw DiscordApiErrors.INVALID_RECIPIENT //TODO is this the right error? + } + + await Channel.removeRecipientFromChannel(channel, user_id) + + return res.sendStatus(204); +}); export default router; diff --git a/api/src/routes/channels/#channel_id/typing.ts b/api/src/routes/channels/#channel_id/typing.ts
index f1fb3c86..a9dcb315 100644 --- a/api/src/routes/channels/#channel_id/typing.ts +++ b/api/src/routes/channels/#channel_id/typing.ts
@@ -1,29 +1,29 @@ import { Channel, emitEvent, Member, TypingStartEvent } from "@fosscord/util"; +import { route } from "@fosscord/api"; import { Router, Request, Response } from "express"; -import { HTTPError } from "lambert-server"; - const router: Router = Router(); -router.post("/", async (req: Request, res: Response) => { +router.post("/", route({ permission: "SEND_MESSAGES" }), async (req: Request, res: Response) => { const { channel_id } = req.params; const user_id = req.user_id; const timestamp = Date.now(); const channel = await Channel.findOneOrFail({ id: channel_id }); - const member = await Member.findOneOrFail({ id: user_id }); + const member = await Member.findOneOrFail({ where: { id: user_id }, relations: ["roles"] }); await emitEvent({ event: "TYPING_START", channel_id: channel_id, data: { // this is the paylod - member: { ...member, roles: member.roles.map((x) => x.id) }, + member: { ...member, roles: member.roles?.map((x) => x.id) }, channel_id, timestamp, user_id, guild_id: channel.guild_id } } as TypingStartEvent); + res.sendStatus(204); }); diff --git a/api/src/routes/channels/#channel_id/webhooks.ts b/api/src/routes/channels/#channel_id/webhooks.ts
index e4125879..7b894455 100644 --- a/api/src/routes/channels/#channel_id/webhooks.ts +++ b/api/src/routes/channels/#channel_id/webhooks.ts
@@ -1,5 +1,5 @@ import { Router, Response, Request } from "express"; -import { check, Length } from "../../../util/instanceOf"; +import { route } from "@fosscord/api"; import { Channel, Config, getPermission, trimSpecial, Webhook } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { isTextChannel } from "./messages/index"; @@ -7,9 +7,16 @@ import { DiscordApiErrors } from "@fosscord/util"; const router: Router = Router(); // TODO: webhooks +export interface WebhookCreateSchema { + /** + * @maxLength 80 + */ + name: string; + avatar: string; +} // 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) => { +router.post("/", route({ body: "WebhookCreateSchema", permission: "MANAGE_WEBHOOKS" }), async (req: Request, res: Response) => { const channel_id = req.params.channel_id; const channel = await Channel.findOneOrFail({ id: channel_id }); @@ -20,12 +27,11 @@ router.post("/", check({ name: new Length(String, 1, 80), $avatar: String }), as const { maxWebhooks } = Config.get().limits.channel; if (webhook_count > maxWebhooks) throw DiscordApiErrors.MAXIMUM_WEBHOOKS.withParams(maxWebhooks); - 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); + + // TODO: save webhook in database and send response }); export default router;