diff --git a/api/assets/schemas.json b/api/assets/schemas.json index f066f71..26ed589 100644 --- a/api/assets/schemas.json +++ b/api/assets/schemas.json @@ -2,12 +2,18 @@ "MessageCreateSchema": { "type": "object", "properties": { + "type": { + "type": "integer" + }, "content": { "type": "string" }, "nonce": { "type": "string" }, + "channel_id": { + "type": "string" + }, "tts": { "type": "boolean" }, diff --git a/api/src/middlewares/RateLimit.ts b/api/src/middlewares/RateLimit.ts index ca6de98..13f1602 100644 --- a/api/src/middlewares/RateLimit.ts +++ b/api/src/middlewares/RateLimit.ts @@ -46,12 +46,14 @@ }): any { return async (req: Request, res: Response, next: NextFunction): Promise => { // exempt user? if so, immediately short circuit - const rights = await getRights(req.user_id); - if (rights.has("BYPASS_RATE_LIMITS")) return; - + if (req.user_id) { + const rights = await getRights(req.user_id); + if (rights.has("BYPASS_RATE_LIMITS")) return; + } + const bucket_id = opts.bucket || req.originalUrl.replace(API_PREFIX_TRAILING_SLASH, ""); var executor_id = getIpAdress(req); - if (!opts.onlyIp && req.user_id) executor_id = req.user_id; + if (!opts.onlyIp && req.user_id) executor_id = req.user_id; var max_hits = opts.count; if (opts.bot && req.user_bot) max_hits = opts.bot; @@ -161,7 +163,7 @@ app.use("/auth/register", rateLimit({ onlyIp: true, success: true, ...routes.auth.register })); } -async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number }) { +async function hitRoute(opts: { executor_id: string; bucket_id: string; max_hits: number; window: number; }) { const id = opts.executor_id + opts.bucket_id; var limit = Cache.get(id); if (!limit) { diff --git "a/api/src/routes/channels/\043channel_id/messages/index.ts" "b/api/src/routes/channels/\043channel_id/messages/index.ts" index 2d6a297..fc2e457 100644 --- "a/api/src/routes/channels/\043channel_id/messages/index.ts" +++ "b/api/src/routes/channels/\043channel_id/messages/index.ts" @@ -17,7 +17,7 @@ } from "@fosscord/util"; import { HTTPError } from "lambert-server"; import { handleMessage, postHandleMessage, route } from "@fosscord/api"; -import multer from "multer"; +import multer, { Multer } from "multer"; import { FindManyOptions, LessThan, MoreThan } from "typeorm"; import { URL } from "url"; @@ -50,8 +50,10 @@ } export interface MessageCreateSchema { + type?: number; content?: string; nonce?: string; + channel_id?: string; tts?: boolean; flags?: string; embeds?: Embed[]; @@ -161,7 +163,7 @@ limits: { fileSize: 1024 * 1024 * 100, fields: 10, - files: 1 + // files: 1 }, storage: multer.memoryStorage() }); // max upload 50 mb @@ -176,7 +178,7 @@ // Send message router.post( "/", - messageUpload.single("file"), + messageUpload.any(), async (req, res, next) => { if (req.body.payload_json) { req.body = JSON.parse(req.body.payload_json); @@ -190,19 +192,22 @@ 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 (!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({ diff --git "a/api/src/routes/guilds/\043guild_id/roles.ts" "b/api/src/routes/guilds/\043guild_id/roles.ts" deleted file mode 100644 index b6894e3..0000000 --- "a/api/src/routes/guilds/\043guild_id/roles.ts" +++ /dev/null @@ -1,164 +0,0 @@ -import { Request, Response, Router } from "express"; -import { - Role, - getPermission, - Member, - GuildRoleCreateEvent, - GuildRoleUpdateEvent, - GuildRoleDeleteEvent, - emitEvent, - Config, - DiscordApiErrors, - handleFile -} from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { route } from "@fosscord/api"; - -const router: Router = Router(); - -export interface RoleModifySchema { - name?: string; - permissions?: string; - color?: number; - hoist?: boolean; // whether the role should be displayed separately in the sidebar - mentionable?: boolean; // whether the role should be mentionable - position?: number; - icon?: string; - unicode_emoji?: string; -} - -export type RolePositionUpdateSchema = { - id: string; - position: number; -}[]; - -router.get("/", route({}), async (req: Request, res: Response) => { - const guild_id = req.params.guild_id; - - await Member.IsInGuildOrFail(req.user_id, guild_id); - - const roles = await Role.find({ guild_id: guild_id }); - - return res.json(roles); -}); - -router.post("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { - const guild_id = req.params.guild_id; - const body = req.body as RoleModifySchema; - - const role_count = await Role.count({ guild_id }); - const { maxRoles } = Config.get().limits.guild; - - if (role_count > maxRoles) throw DiscordApiErrors.MAXIMUM_ROLES.withParams(maxRoles); - - const role = new Role({ - // values before ...body are default and can be overriden - position: 0, - hoist: false, - color: 0, - mentionable: false, - ...body, - guild_id: guild_id, - managed: false, - permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0")), - tags: undefined, - icon: null, - unicode_emoji: null - }); - - await Promise.all([ - role.save(), - emitEvent({ - event: "GUILD_ROLE_CREATE", - guild_id, - data: { - guild_id, - role: role - } - } as GuildRoleCreateEvent) - ]); - - res.json(role); -}); - -router.delete("/:role_id", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { - const guild_id = req.params.guild_id; - const { role_id } = req.params; - if (role_id === guild_id) throw new HTTPError("You can't delete the @everyone role"); - - await Promise.all([ - Role.delete({ - id: role_id, - guild_id: guild_id - }), - emitEvent({ - event: "GUILD_ROLE_DELETE", - guild_id, - data: { - guild_id, - role_id - } - } as GuildRoleDeleteEvent) - ]); - - res.sendStatus(204); -}); - -// TODO: check role hierarchy - -router.patch("/:role_id", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { - const { role_id, guild_id } = req.params; - const body = req.body as RoleModifySchema; - - if (body.icon) body.icon = await handleFile(`/role-icons/${role_id}`, body.icon as string); - - const role = new Role({ - ...body, - id: role_id, - guild_id, - permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0")) - }); - - await Promise.all([ - role.save(), - emitEvent({ - event: "GUILD_ROLE_UPDATE", - guild_id, - data: { - guild_id, - role - } - } as GuildRoleUpdateEvent) - ]); - - res.json(role); -}); - -router.patch("/", route({ body: "RolePositionUpdateSchema" }), async (req: Request, res: Response) => { - const { guild_id } = req.params; - const body = req.body as RolePositionUpdateSchema; - - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_ROLES"); - - await Promise.all(body.map(async (x) => Role.update({ guild_id, id: x.id }, { position: x.position }))); - - const roles = await Role.find({ where: body.map((x) => ({ id: x.id, guild_id })) }); - - await Promise.all( - roles.map((x) => - emitEvent({ - event: "GUILD_ROLE_UPDATE", - guild_id, - data: { - guild_id, - role: x - } - } as GuildRoleUpdateEvent) - ) - ); - - res.json(roles); -}); - -export default router; diff --git "a/api/src/routes/guilds/\043guild_id/roles/\043role_id/index.ts" "b/api/src/routes/guilds/\043guild_id/roles/\043role_id/index.ts" new file mode 100644 index 0000000..2ad0168 --- /dev/null +++ "b/api/src/routes/guilds/\043guild_id/roles/\043role_id/index.ts" @@ -0,0 +1,68 @@ +import { Router, Request, Response } from "express"; +import { Role, Member, GuildRoleUpdateEvent, GuildRoleDeleteEvent, emitEvent, handleFile } from "@fosscord/util"; +import { route } from "@fosscord/api"; +import { HTTPError } from "lambert-server"; +import { RoleModifySchema } from "../"; + +const router = Router(); + +router.get("/", route({}), async (req: Request, res: Response) => { + const { guild_id, role_id } = req.params; + await Member.IsInGuildOrFail(req.user_id, guild_id); + const role = await Role.findOneOrFail({ guild_id, id: role_id }); + return res.json(role); +}); + +router.delete("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { + const { guild_id, role_id } = req.params; + if (role_id === guild_id) throw new HTTPError("You can't delete the @everyone role"); + + await Promise.all([ + Role.delete({ + id: role_id, + guild_id: guild_id + }), + emitEvent({ + event: "GUILD_ROLE_DELETE", + guild_id, + data: { + guild_id, + role_id + } + } as GuildRoleDeleteEvent) + ]); + + res.sendStatus(204); +}); + +// TODO: check role hierarchy + +router.patch("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { + const { role_id, guild_id } = req.params; + const body = req.body as RoleModifySchema; + + if (body.icon) body.icon = await handleFile(`/role-icons/${role_id}`, body.icon as string); + + const role = new Role({ + ...body, + id: role_id, + guild_id, + permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0")) + }); + + await Promise.all([ + role.save(), + emitEvent({ + event: "GUILD_ROLE_UPDATE", + guild_id, + data: { + guild_id, + role + } + } as GuildRoleUpdateEvent) + ]); + + res.json(role); +}); + +export default router; diff --git "a/api/src/routes/guilds/\043guild_id/roles/index.ts" "b/api/src/routes/guilds/\043guild_id/roles/index.ts" new file mode 100644 index 0000000..5346510 --- /dev/null +++ "b/api/src/routes/guilds/\043guild_id/roles/index.ts" @@ -0,0 +1,111 @@ +import { Request, Response, Router } from "express"; +import { + Role, + getPermission, + Member, + GuildRoleCreateEvent, + GuildRoleUpdateEvent, + GuildRoleDeleteEvent, + emitEvent, + Config, + DiscordApiErrors, + handleFile +} from "@fosscord/util"; +import { HTTPError } from "lambert-server"; +import { route } from "@fosscord/api"; + +const router: Router = Router(); + +export interface RoleModifySchema { + name?: string; + permissions?: string; + color?: number; + hoist?: boolean; // whether the role should be displayed separately in the sidebar + mentionable?: boolean; // whether the role should be mentionable + position?: number; + icon?: string; + unicode_emoji?: string; +} + +export type RolePositionUpdateSchema = { + id: string; + position: number; +}[]; + +router.get("/", route({}), async (req: Request, res: Response) => { + const guild_id = req.params.guild_id; + + await Member.IsInGuildOrFail(req.user_id, guild_id); + + const roles = await Role.find({ guild_id: guild_id }); + + return res.json(roles); +}); + +router.post("/", route({ body: "RoleModifySchema", permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { + const guild_id = req.params.guild_id; + const body = req.body as RoleModifySchema; + + const role_count = await Role.count({ guild_id }); + const { maxRoles } = Config.get().limits.guild; + + if (role_count > maxRoles) throw DiscordApiErrors.MAXIMUM_ROLES.withParams(maxRoles); + + const role = new Role({ + // values before ...body are default and can be overriden + position: 0, + hoist: false, + color: 0, + mentionable: false, + ...body, + guild_id: guild_id, + managed: false, + permissions: String(req.permission!.bitfield & BigInt(body.permissions || "0")), + tags: undefined, + icon: null, + unicode_emoji: null + }); + + await Promise.all([ + role.save(), + emitEvent({ + event: "GUILD_ROLE_CREATE", + guild_id, + data: { + guild_id, + role: role + } + } as GuildRoleCreateEvent) + ]); + + res.json(role); +}); + +router.patch("/", route({ body: "RolePositionUpdateSchema" }), async (req: Request, res: Response) => { + const { guild_id } = req.params; + const body = req.body as RolePositionUpdateSchema; + + const perms = await getPermission(req.user_id, guild_id); + perms.hasThrow("MANAGE_ROLES"); + + await Promise.all(body.map(async (x) => Role.update({ guild_id, id: x.id }, { position: x.position }))); + + const roles = await Role.find({ where: body.map((x) => ({ id: x.id, guild_id })) }); + + await Promise.all( + roles.map((x) => + emitEvent({ + event: "GUILD_ROLE_UPDATE", + guild_id, + data: { + guild_id, + role: x + } + } as GuildRoleUpdateEvent) + ) + ); + + res.json(roles); +}); + +export default router;