diff options
Diffstat (limited to 'api/src/routes/channels')
15 files changed, 0 insertions, 1339 deletions
diff --git a/api/src/routes/channels/#channel_id/followers.ts b/api/src/routes/channels/#channel_id/followers.ts deleted file mode 100644 index 641af4f8..00000000 --- a/api/src/routes/channels/#channel_id/followers.ts +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 2fca4fdf..00000000 --- a/api/src/routes/channels/#channel_id/index.ts +++ /dev/null @@ -1,95 +0,0 @@ -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("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { - const { channel_id } = req.params; - - const channel = await Channel.findOneOrFail({ id: channel_id }); - - return res.send(channel); -}); - -router.delete("/", route({ permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { - const { channel_id } = req.params; - - const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients"] }); - - 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) - ]); - } - - res.send(channel); -}); - -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: string; - deny: string; - }[]; - 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; - if (payload.icon) payload.icon = await handleFile(`/channel-icons/${channel_id}`, payload.icon); - - const channel = await Channel.findOneOrFail({ id: channel_id }); - channel.assign(payload); - - await Promise.all([ - channel.save(), - emitEvent({ - event: "CHANNEL_UPDATE", - data: channel, - channel_id - } as ChannelUpdateEvent) - ]); - - res.send(channel); -}); - -export default router; diff --git a/api/src/routes/channels/#channel_id/invites.ts b/api/src/routes/channels/#channel_id/invites.ts deleted file mode 100644 index 9c361164..00000000 --- a/api/src/routes/channels/#channel_id/invites.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Router, Request, Response } from "express"; -import { HTTPError } from "lambert-server"; -import { route } from "@fosscord/api"; -import { random } from "@fosscord/api"; -import { Channel, Invite, InviteCreateEvent, emitEvent, User, Guild, PublicInviteRelation } from "@fosscord/util"; -import { isTextChannel } from "./messages"; - -const router: Router = Router(); - -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", right: "CREATE_INVITES" }), - 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"] }); - isTextChannel(channel.type); - - if (!channel.guild_id) { - throw new HTTPError("This channel doesn't exist", 404); - } - const { guild_id } = channel; - - const expires_at = new Date(req.body.max_age * 1000 + Date.now()); - - const invite = await new 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 - }).save(); - const data = invite.toJSON(); - data.inviter = await User.getPublicUser(req.user_id); - 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("/", 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 }); - - if (!channel.guild_id) { - throw new HTTPError("This channel doesn't exist", 404); - } - const { guild_id } = channel; - - const invites = await Invite.find({ where: { guild_id }, relations: PublicInviteRelation }); - - res.status(200).send(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 deleted file mode 100644 index 885c5eca..00000000 --- a/api/src/routes/channels/#channel_id/messages/#message_id/ack.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { emitEvent, getPermission, MessageAckEvent, ReadState, Snowflake } from "@fosscord/util"; -import { Request, Response, Router } from "express"; -import { route } from "@fosscord/api"; - -const router = Router(); - -// TODO: public read receipts & privacy scoping -// TODO: send read state event to all channel members -// TODO: advance-only notification cursor - -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); - permission.hasThrow("VIEW_CHANNEL"); - - let read_state = await ReadState.findOne({ user_id: req.user_id, channel_id }); - if (!read_state) read_state = new ReadState({ user_id: req.user_id, channel_id }); - read_state.last_message_id = message_id; - - await read_state.save(); - - await emitEvent({ - event: "MESSAGE_ACK", - user_id: req.user_id, - data: { - channel_id, - message_id, - version: 3763 - } - } 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 deleted file mode 100644 index b2cb6763..00000000 --- a/api/src/routes/channels/#channel_id/messages/#message_id/crosspost.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Router, Response, Request } from "express"; -import { route } from "@fosscord/api"; - -const router = Router(); - -router.post("/", route({ permission: "MANAGE_MESSAGES" }), (req: Request, res: Response) => { - // TODO: - res.json({ - id: "", - type: 0, - content: "", - channel_id: "", - author: { id: "", username: "", avatar: "", discriminator: "", public_flags: 64 }, - attachments: [], - embeds: [], - mentions: [], - mention_roles: [], - pinned: false, - mention_everyone: false, - tts: false, - timestamp: "", - edited_timestamp: null, - flags: 1, - components: [] - }).status(200); -}); - -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 deleted file mode 100644 index 63fee9b9..00000000 --- a/api/src/routes/channels/#channel_id/messages/#message_id/index.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { - Attachment, - Channel, - Embed, - DiscordApiErrors, - emitEvent, - FosscordApiErrors, - getPermission, - getRights, - Message, - MessageCreateEvent, - MessageDeleteEvent, - MessageUpdateEvent, - Snowflake, - uploadFile -} from "@fosscord/util"; -import { Router, Response, Request } from "express"; -import multer from "multer"; -import { route } from "@fosscord/api"; -import { handleMessage, postHandleMessage } from "@fosscord/api"; -import { MessageCreateSchema } from "../index"; -import { HTTPError } from "lambert-server"; - -const router = Router(); -// TODO: message content/embed string length limit - -const messageUpload = multer({ - limits: { - fileSize: 1024 * 1024 * 100, - fields: 10, - files: 1 - }, - storage: multer.memoryStorage() -}); // max upload 50 mb - -router.patch("/", route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "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({ where: { id: message_id, channel_id }, relations: ["attachments"] }); - - const permissions = await getPermission(req.user_id, undefined, channel_id); - - const rights = await getRights(req.user_id); - - if ((req.user_id !== message.author_id)) { - if (!rights.has("MANAGE_MESSAGES")) { - permissions.hasThrow("MANAGE_MESSAGES"); - body = { flags: body.flags }; -// guild admins can only suppress embeds of other messages, no such restriction imposed to instance-wide admins - } - } else rights.hasThrow("SELF_EDIT_MESSAGES"); - - const new_message = await handleMessage({ - ...message, - // TODO: should message_reference be overridable? - // @ts-ignore - message_reference: message.message_reference, - ...body, - author_id: message.author_id, - channel_id, - id: message_id, - edited_timestamp: new Date() - }); - - await Promise.all([ - new_message!.save(), - await emitEvent({ - event: "MESSAGE_UPDATE", - channel_id, - data: { ...new_message, nonce: undefined } - } as MessageUpdateEvent) - ]); - - postHandleMessage(message); - - return res.json(message); -}); - - -// Backfill message with specific timestamp -router.put( - "/", - messageUpload.single("file"), - async (req, res, next) => { - if (req.body.payload_json) { - req.body = JSON.parse(req.body.payload_json); - } - - next(); - }, - route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_BACKDATED_EVENTS" }), - async (req: Request, res: Response) => { - const { channel_id, message_id } = req.params; - var body = req.body as MessageCreateSchema; - const attachments: Attachment[] = []; - - const rights = await getRights(req.user_id); - rights.hasThrow("SEND_MESSAGES"); - - // regex to check if message contains anything other than numerals ( also no decimals ) - if (!message_id.match(/^\+?\d+$/)) { - throw new HTTPError("Message IDs must be positive integers", 400); - } - - const snowflake = Snowflake.deconstruct(message_id) - if (Date.now() < snowflake.timestamp) { - // message is in the future - throw FosscordApiErrors.CANNOT_BACKFILL_TO_THE_FUTURE; - } - - const exists = await Message.findOne({ where: { id: message_id, channel_id: channel_id }}); - if (exists) { - throw FosscordApiErrors.CANNOT_REPLACE_BY_BACKFILL; - } - - 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"] }); - - const embeds = body.embeds || []; - if (body.embed) embeds.push(body.embed); - let message = await handleMessage({ - ...body, - type: 0, - pinned: false, - author_id: req.user_id, - id: message_id, - embeds, - channel_id, - attachments, - edited_timestamp: undefined, - timestamp: new Date(snowflake.timestamp), - }); - - //Fix for the client bug - delete message.member - - await Promise.all([ - message.save(), - emitEvent({ event: "MESSAGE_CREATE", channel_id: channel_id, data: message } as MessageCreateEvent), - channel.save() - ]); - - postHandleMessage(message).catch((e) => { }); // no await as it shouldnt block the message send function and silently catch error - - return res.json(message); - } -); - -router.get("/", route({ permission: "VIEW_CHANNEL" }), async (req: Request, res: Response) => { - const { message_id, channel_id } = req.params; - - const message = await Message.findOneOrFail({ where: { id: message_id, channel_id }, relations: ["attachments"] }); - - const permissions = await getPermission(req.user_id, undefined, channel_id); - - if (message.author_id !== req.user_id) permissions.hasThrow("READ_MESSAGE_HISTORY"); - - return res.json(message); -}); - -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 rights = await getRights(req.user_id); - - if ((message.author_id !== req.user_id)) { - if (!rights.has("MANAGE_MESSAGES")) { - const permission = await getPermission(req.user_id, channel.guild_id, channel_id); - permission.hasThrow("MANAGE_MESSAGES"); - } - } else rights.hasThrow("SELF_DELETE_MESSAGES"); - - await Message.delete({ id: message_id }); - - 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 deleted file mode 100644 index d93cf70f..00000000 --- a/api/src/routes/channels/#channel_id/messages/#message_id/reactions.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { - Channel, - emitEvent, - Emoji, - getPermission, - Member, - Message, - MessageReactionAddEvent, - MessageReactionRemoveAllEvent, - MessageReactionRemoveEmojiEvent, - MessageReactionRemoveEvent, - PartialEmoji, - 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"; - -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("/", route({ permission: "MANAGE_MESSAGES" }), async (req: Request, res: Response) => { - const { message_id, channel_id } = req.params; - - const channel = await Channel.findOneOrFail({ id: channel_id }); - - await Message.update({ id: message_id, channel_id }, { reactions: [] }); - - 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", route({ permission: "MANAGE_MESSAGES" }), async (req: Request, res: Response) => { - const { message_id, channel_id } = req.params; - const emoji = getEmoji(req.params.emoji); - - 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); - if (!already_added) throw new HTTPError("Reaction not found", 404); - message.reactions.remove(already_added); - - await Promise.all([ - message.save(), - emitEvent({ - event: "MESSAGE_REACTION_REMOVE_EMOJI", - channel_id, - data: { - channel_id, - message_id, - guild_id: message.guild_id, - emoji - } - } as MessageReactionRemoveEmojiEvent) - ]); - - res.sendStatus(204); -}); - -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); - - const message = await Message.findOneOrFail({ id: message_id, channel_id }); - 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 users = await User.find({ - where: { - id: In(reaction.user_ids) - }, - select: PublicUserProjection - }); - - res.json(users); -}); - -router.put("/:emoji/:user_id", route({ permission: "READ_MESSAGE_HISTORY", right: "SELF_ADD_REACTIONS" }), 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 Channel.findOneOrFail({ id: channel_id }); - 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); - - if (!already_added) req.permission!.hasThrow("ADD_REACTIONS"); - - if (emoji.id) { - const external_emoji = await Emoji.findOneOrFail({ id: emoji.id }); - if (!already_added) req.permission!.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 message.save(); - - const member = channel.guild_id && (await Member.findOneOrFail({ id: req.user_id })); - - 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", route({}), async (req: Request, res: Response) => { - var { message_id, channel_id, user_id } = req.params; - - const emoji = getEmoji(req.params.emoji); - - const channel = await Channel.findOneOrFail({ id: channel_id }); - const message = await Message.findOneOrFail({ id: message_id, channel_id }); - - if (user_id === "@me") user_id = req.user_id; - 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); - - already_added.count--; - - if (already_added.count <= 0) message.reactions.remove(already_added); - - await message.save(); - - 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 deleted file mode 100644 index 6eacf249..00000000 --- a/api/src/routes/channels/#channel_id/messages/bulk-delete.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Router, Response, Request } from "express"; -import { Channel, Config, emitEvent, getPermission, getRights, MessageDeleteBulkEvent, Message } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { route } from "@fosscord/api"; -import { In } from "typeorm"; - -const router: Router = Router(); - -export default router; - -export interface BulkDeleteSchema { - messages: string[]; -} - -// should users be able to bulk delete messages or only bots? ANSWER: all users -// should this request fail, if you provide messages older than 14 days/invalid ids? ANSWER: NO -// https://discord.com/developers/docs/resources/channel#bulk-delete-messages -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); - - const rights = await getRights(req.user_id); - rights.hasThrow("SELF_DELETE_MESSAGES"); - - let superuser = rights.has("MANAGE_MESSAGES"); - const permission = await getPermission(req.user_id, channel?.guild_id, channel_id); - - const { maxBulkDelete } = Config.get().limits.message; - - const { messages } = req.body as { messages: string[] }; - if (messages.length === 0) throw new HTTPError("You must specify messages to bulk delete"); - if (!superuser) { - permission.hasThrow("MANAGE_MESSAGES"); - if (messages.length > maxBulkDelete) throw new HTTPError(`You cannot delete more than ${maxBulkDelete} messages`); - } - - await Message.delete(messages.map((x) => ({ id: x }))); - - 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 deleted file mode 100644 index 54e6edcc..00000000 --- a/api/src/routes/channels/#channel_id/messages/index.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { Router, Response, Request } from "express"; -import { - Attachment, - Channel, - ChannelType, - Config, - DmChannelDTO, - Embed, - emitEvent, - getPermission, - getRights, - Message, - MessageCreateEvent, - Snowflake, - uploadFile, - Member -} from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { handleMessage, postHandleMessage, route } from "@fosscord/api"; -import multer from "multer"; -import { FindManyOptions, LessThan, MoreThan } from "typeorm"; -import { URL } from "url"; - -const router: Router = Router(); - -export default router; - -export function isTextChannel(type: ChannelType): boolean { - switch (type) { - case ChannelType.GUILD_STORE: - case ChannelType.GUILD_VOICE: - case ChannelType.GUILD_STAGE_VOICE: - case ChannelType.GUILD_CATEGORY: - case ChannelType.GUILD_FORUM: - case ChannelType.DIRECTORY: - throw new HTTPError("not a text channel", 400); - case ChannelType.DM: - case ChannelType.GROUP_DM: - case ChannelType.GUILD_NEWS: - case ChannelType.GUILD_NEWS_THREAD: - case ChannelType.GUILD_PUBLIC_THREAD: - case ChannelType.GUILD_PRIVATE_THREAD: - case ChannelType.GUILD_TEXT: - case ChannelType.ENCRYPTED: - case ChannelType.ENCRYPTED_THREAD: - return true; - default: - throw new HTTPError("unimplemented", 400); - } -} - -export interface MessageCreateSchema { - type?: number; - content?: string; - nonce?: string; - channel_id?: 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; - /** - TODO: we should create an interface for attachments - TODO: OpenWAAO<-->attachment-style metadata conversion - **/ - attachments?: any[]; - sticker_ids?: string[]; -} - -// 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 Channel.findOneOrFail({ id: channel_id }); - 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", 422); - - var halfLimit = Math.floor(limit / 2); - - const permissions = await getPermission(req.user_id, channel.guild_id, channel_id); - permissions.hasThrow("VIEW_CHANNEL"); - if (!permissions.has("READ_MESSAGE_HISTORY")) return res.json([]); - - var query: FindManyOptions<Message> & { where: { id?: any; }; } = { - order: { id: "DESC" }, - take: limit, - where: { channel_id }, - relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"] - }; - - - if (after) { - if (after > new Snowflake()) return res.status(422); - query.where.id = MoreThan(after); - } - else if (before) { - if (before < req.params.channel_id) return res.status(422); - query.where.id = LessThan(before); - } - else if (around) { - query.where.id = [ - MoreThan((BigInt(around) - BigInt(halfLimit)).toString()), - LessThan((BigInt(around) + BigInt(halfLimit)).toString()) - ]; - } - - const messages = await Message.find(query); - const endpoint = Config.get().cdn.endpointPublic; - - return res.json( - messages.map((x: any) => { - (x.reactions || []).forEach((x: any) => { - // @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 = { id: "4", discriminator: "0000", username: "Fosscord Ghost", public_flags: "0", avatar: null }; - x.attachments?.forEach((y: any) => { - // dynamically set attachment proxy_url in case the endpoint changed - const uri = y.proxy_url.startsWith("http") ? y.proxy_url : `https://example.org${y.proxy_url}`; - y.proxy_url = `${endpoint == null ? "" : endpoint}${new URL(uri).pathname}`; - }); - - /** - Some clients ( discord.js ) only check if a property exists within the response, - which causes erorrs when, say, the `application` property is `null`. - **/ - - for (var curr in x) { - if (x[curr] === null) - delete x[curr]; - } - - 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 - - https://discord.com/developers/docs/resources/channel#create-message - TODO: text channel slowdown (per-user and across-users) - Q: trim and replace message content and every embed field A: NO, given this cannot be implemented in E2EE channels - TODO: only dispatch notifications for mentions denoted in allowed_mentions -**/ -// Send message -router.post( - "/", - messageUpload.any(), - async (req, res, next) => { - if (req.body.payload_json) { - req.body = JSON.parse(req.body.payload_json); - } - - next(); - }, - route({ body: "MessageCreateSchema", permission: "SEND_MESSAGES", right: "SEND_MESSAGES" }), - async (req: Request, res: Response) => { - const { channel_id } = req.params; - var body = req.body as MessageCreateSchema; - const attachments: Attachment[] = []; - - const channel = await Channel.findOneOrFail({ where: { id: channel_id }, relations: ["recipients", "recipients.user"] }); - if (!channel.isWritable()) { - throw new HTTPError(`Cannot send messages to channel of type ${channel.type}`, 400) - } - - const files = req.files as Express.Multer.File[] ?? []; - for (var currFile of files) { - try { - const file = await uploadFile(`/attachments/${channel.id}`, currFile); - attachments.push({ ...file, proxy_url: file.url }); - } - catch (error) { - return res.status(400).json(error); - } - } - - const embeds = body.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() - }); - - channel.last_message_id = message.id; - - if (channel.isDm()) { - const channel_dto = await DmChannelDTO.from(channel); - - // Only one recipients should be closed here, since in group DMs the recipient is deleted not closed - Promise.all( - channel.recipients!.map((recipient) => { - if (recipient.closed) { - recipient.closed = false; - return Promise.all([ - recipient.save(), - emitEvent({ - event: "CHANNEL_CREATE", - data: channel_dto.excludedRecipients([recipient.user_id]), - user_id: recipient.user_id - }) - ]); - } - }) - ); - } - - //Fix for the client bug - delete message.member - - await Promise.all([ - message.save(), - emitEvent({ event: "MESSAGE_CREATE", channel_id: channel_id, data: message } as MessageCreateEvent), - message.guild_id ? Member.update({ id: req.user_id, guild_id: message.guild_id }, { last_message_id: message.id }) : null, - channel.save() - ]); - - 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 deleted file mode 100644 index 2eded853..00000000 --- a/api/src/routes/channels/#channel_id/permissions.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - Channel, - ChannelPermissionOverwrite, - ChannelPermissionOverwriteType, - ChannelUpdateEvent, - emitEvent, - getPermission, - Member, - Role -} from "@fosscord/util"; -import { Router, Response, Request } from "express"; -import { HTTPError } from "lambert-server"; - -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) - -export interface ChannelPermissionOverwriteSchema extends ChannelPermissionOverwrite {} - -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 ChannelPermissionOverwriteSchema; - - 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); - - // @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 - }; - channel.permission_overwrites!.push(overwrite); - } - overwrite.allow = String(req.permission!.bitfield & (BigInt(body.allow) || BigInt("0"))); - overwrite.deny = String(req.permission!.bitfield & (BigInt(body.deny) || BigInt("0"))); - - await Promise.all([ - channel.save(), - emitEvent({ - event: "CHANNEL_UPDATE", - channel_id, - data: channel - } as ChannelUpdateEvent) - ]); - - return res.sendStatus(204); - } -); - -// TODO: check permission hierarchy -router.delete("/:overwrite_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { - const { channel_id, overwrite_id } = req.params; - - 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); - - await Promise.all([ - channel.save(), - 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 deleted file mode 100644 index e71e659f..00000000 --- a/api/src/routes/channels/#channel_id/pins.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { - Channel, - ChannelPinsUpdateEvent, - Config, - emitEvent, - getPermission, - Message, - MessageUpdateEvent, - DiscordApiErrors -} from "@fosscord/util"; -import { Router, Request, Response } from "express"; -import { HTTPError } from "lambert-server"; -import { route } from "@fosscord/api"; - -const router: Router = Router(); - -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 }); - - // * in dm channels anyone can pin messages -> only check for guilds - 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; - if (pinned_count >= maxPins) throw DiscordApiErrors.MAXIMUM_PINS.withParams(maxPins); - - await Promise.all([ - Message.update({ id: message_id }, { pinned: true }), - emitEvent({ - event: "MESSAGE_UPDATE", - channel_id, - data: message - } as MessageUpdateEvent), - emitEvent({ - event: "CHANNEL_PINS_UPDATE", - channel_id, - data: { - channel_id, - guild_id: message.guild_id, - last_pin_timestamp: undefined - } - } as ChannelPinsUpdateEvent) - ]); - - res.sendStatus(204); -}); - -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 }); - if (channel.guild_id) req.permission!.hasThrow("MANAGE_MESSAGES"); - - const message = await Message.findOneOrFail({ id: message_id }); - message.pinned = false; - - await Promise.all([ - message.save(), - - emitEvent({ - event: "MESSAGE_UPDATE", - channel_id, - data: message - } as MessageUpdateEvent), - - 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("/", route({ permission: ["READ_MESSAGE_HISTORY"] }), async (req: Request, res: Response) => { - const { channel_id } = req.params; - - let pins = await Message.find({ channel_id: channel_id, pinned: true }); - - res.send(pins); -}); - -export default router; diff --git a/api/src/routes/channels/#channel_id/purge.ts b/api/src/routes/channels/#channel_id/purge.ts deleted file mode 100644 index 28b52b50..00000000 --- a/api/src/routes/channels/#channel_id/purge.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { HTTPError } from "lambert-server"; -import { route } from "@fosscord/api"; -import { isTextChannel } from "./messages"; -import { FindManyOptions, Between, Not } from "typeorm"; -import { - Attachment, - Channel, - Config, - Embed, - DiscordApiErrors, - emitEvent, - FosscordApiErrors, - getPermission, - getRights, - Message, - MessageDeleteBulkEvent, - Snowflake, - uploadFile -} from "@fosscord/util"; -import { Router, Response, Request } from "express"; -import multer from "multer"; -import { handleMessage, postHandleMessage } from "@fosscord/api"; - -const router: Router = Router(); - -export default router; - -export interface PurgeSchema { - before: string; - after: string -} - -/** -TODO: apply the delete bit by bit to prevent client and database stress -**/ -router.post("/", route({ /*body: "PurgeSchema",*/ }), 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 purge dm channels", 400); - isTextChannel(channel.type); - - const rights = await getRights(req.user_id); - if (!rights.has("MANAGE_MESSAGES")) { - const permissions = await getPermission(req.user_id, channel.guild_id, channel_id); - permissions.hasThrow("MANAGE_MESSAGES"); - permissions.hasThrow("MANAGE_CHANNELS"); - } - - const { before, after } = req.body as PurgeSchema; - - // TODO: send the deletion event bite-by-bite to prevent client stress - - var query: FindManyOptions<Message> & { where: { id?: any; }; } = { - order: { id: "ASC" }, - // take: limit, - where: { - channel_id, - id: Between(after, before), // the right way around - author_id: rights.has("SELF_DELETE_MESSAGES") ? undefined : Not(req.user_id) - // if you lack the right of self-deletion, you can't delete your own messages, even in purges - }, - relations: ["author", "webhook", "application", "mentions", "mention_roles", "mention_channels", "sticker_items", "attachments"] - }; - - - const messages = await Message.find(query); - const endpoint = Config.get().cdn.endpointPublic; - - if (messages.length == 0) { - res.sendStatus(304); - return; - } - - await Message.delete(messages.map((x) => ({ id: x }))); - - await emitEvent({ - event: "MESSAGE_DELETE_BULK", - channel_id, - data: { ids: messages.map(x => x.id), channel_id, guild_id: channel.guild_id } - } as MessageDeleteBulkEvent); - - res.sendStatus(204); -}); diff --git a/api/src/routes/channels/#channel_id/recipients.ts b/api/src/routes/channels/#channel_id/recipients.ts deleted file mode 100644 index e6466211..00000000 --- a/api/src/routes/channels/#channel_id/recipients.ts +++ /dev/null @@ -1,67 +0,0 @@ -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(); - -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 deleted file mode 100644 index 56652368..00000000 --- a/api/src/routes/channels/#channel_id/typing.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Channel, emitEvent, Member, TypingStartEvent } from "@fosscord/util"; -import { route } from "@fosscord/api"; -import { Router, Request, Response } from "express"; - -const router: Router = Router(); - -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.findOne({ where: { id: user_id, guild_id: channel.guild_id }, relations: ["roles", "user"] }); - - await emitEvent({ - event: "TYPING_START", - channel_id: channel_id, - data: { - ...(member ? { member: { ...member, roles: member?.roles?.map((x) => x.id) } } : null), - 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 deleted file mode 100644 index 92895da6..00000000 --- a/api/src/routes/channels/#channel_id/webhooks.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Router, Response, Request } from "express"; -import { route } from "@fosscord/api"; -import { Channel, Config, getPermission, trimSpecial, Webhook } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { isTextChannel } from "./messages/index"; -import { DiscordApiErrors } from "@fosscord/util"; - -const router: Router = Router(); -// TODO: webhooks -export interface WebhookCreateSchema { - /** - * @maxLength 80 - */ - name: string; - avatar: string; -} -//TODO: implement webhooks -router.get("/", route({}), async (req: Request, res: Response) => { - res.json([]); -}); - -// TODO: use Image Data Type for avatar instead of String -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 }); - - isTextChannel(channel.type); - if (!channel.guild_id) throw new HTTPError("Not a guild channel", 400); - - const webhook_count = await Webhook.count({ channel_id }); - const { maxWebhooks } = Config.get().limits.channel; - if (webhook_count > maxWebhooks) throw DiscordApiErrors.MAXIMUM_WEBHOOKS.withParams(maxWebhooks); - - 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; |