summary refs log tree commit diff
path: root/api/src/routes/guilds
diff options
context:
space:
mode:
Diffstat (limited to 'api/src/routes/guilds')
-rw-r--r--api/src/routes/guilds/#guild_id/bans.ts32
-rw-r--r--api/src/routes/guilds/#guild_id/channels.ts70
-rw-r--r--api/src/routes/guilds/#guild_id/delete.ts15
-rw-r--r--api/src/routes/guilds/#guild_id/index.ts37
-rw-r--r--api/src/routes/guilds/#guild_id/invites.ts6
-rw-r--r--api/src/routes/guilds/#guild_id/members/#member_id/index.ts36
-rw-r--r--api/src/routes/guilds/#guild_id/members/#member_id/nick.ts9
-rw-r--r--api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts11
-rw-r--r--api/src/routes/guilds/#guild_id/members/index.ts28
-rw-r--r--api/src/routes/guilds/#guild_id/regions.ts8
-rw-r--r--api/src/routes/guilds/#guild_id/roles.ts49
-rw-r--r--api/src/routes/guilds/#guild_id/templates.ts53
-rw-r--r--api/src/routes/guilds/#guild_id/vanity-url.ts29
-rw-r--r--api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts59
-rw-r--r--api/src/routes/guilds/#guild_id/voice-states/@me/index.ts15
-rw-r--r--api/src/routes/guilds/#guild_id/welcome_screen.ts23
-rw-r--r--api/src/routes/guilds/#guild_id/widget.json.ts4
-rw-r--r--api/src/routes/guilds/#guild_id/widget.png.ts3
-rw-r--r--api/src/routes/guilds/#guild_id/widget.ts21
-rw-r--r--api/src/routes/guilds/index.ts24
-rw-r--r--api/src/routes/guilds/templates/index.ts13
21 files changed, 279 insertions, 266 deletions
diff --git a/api/src/routes/guilds/#guild_id/bans.ts b/api/src/routes/guilds/#guild_id/bans.ts

index 31aa2385..e7d46898 100644 --- a/api/src/routes/guilds/#guild_id/bans.ts +++ b/api/src/routes/guilds/#guild_id/bans.ts
@@ -1,20 +1,22 @@ import { Request, Response, Router } from "express"; import { emitEvent, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, Guild, Ban, User, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { getIpAdress } from "../../../util/ipAddress"; -import { BanCreateSchema } from "../../../schema/Ban"; -import { check } from "../../../util/instanceOf"; +import { getIpAdress, route } from "@fosscord/api"; -const router: Router = Router(); +export interface BanCreateSchema { + delete_message_days?: string; + reason?: string; +} -router.get("/", async (req: Request, res: Response) => { +const router: Router = Router(); +router.get("/", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { const { guild_id } = req.params; var bans = await Ban.find({ guild_id: guild_id }); return res.json(bans); }); -router.get("/:user", async (req: Request, res: Response) => { +router.get("/:user", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { const { guild_id } = req.params; const user_id = req.params.ban; @@ -22,15 +24,14 @@ router.get("/:user", async (req: Request, res: Response) => { return res.json(ban); }); -router.put("/:user_id", check(BanCreateSchema), async (req: Request, res: Response) => { +router.put("/:user_id", route({ body: "BanCreateSchema", permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { const { guild_id } = req.params; const banned_user_id = req.params.user_id; const banned_user = await User.getPublicUser(banned_user_id); - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("BAN_MEMBERS"); + if (req.user_id === banned_user_id) throw new HTTPError("You can't ban yourself", 400); - if (perms.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400); + if (req.permission!.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400); const ban = new Ban({ user_id: banned_user_id, @@ -56,17 +57,14 @@ router.put("/:user_id", check(BanCreateSchema), async (req: Request, res: Respon return res.json(ban); }); -router.delete("/:user_id", async (req: Request, res: Response) => { - var { guild_id } = req.params; - var banned_user_id = req.params.user_id; +router.delete("/:user_id", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { + const { guild_id, user_id } = req.params; - const banned_user = await User.getPublicUser(banned_user_id); - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("BAN_MEMBERS"); + const banned_user = await User.getPublicUser(user_id); await Promise.all([ Ban.delete({ - user_id: banned_user_id, + user_id: user_id, guild_id }), diff --git a/api/src/routes/guilds/#guild_id/channels.ts b/api/src/routes/guilds/#guild_id/channels.ts
index 5aa1d33d..a36e5448 100644 --- a/api/src/routes/guilds/#guild_id/channels.ts +++ b/api/src/routes/guilds/#guild_id/channels.ts
@@ -1,22 +1,18 @@ import { Router, Response, Request } from "express"; import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { ChannelModifySchema } from "../../../schema/Channel"; - -import { check } from "../../../util/instanceOf"; +import { route } from "@fosscord/api"; +import { ChannelModifySchema } from "../../channels/#channel_id"; const router = Router(); -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; const channels = await Channel.find({ guild_id }); res.json(channels); }); -// TODO: check if channel type is permitted -// TODO: check if parent_id exists - -router.post("/", check(ChannelModifySchema), async (req: Request, res: Response) => { +router.post("/", route({ body: "ChannelModifySchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { // creates a new guild channel https://discord.com/developers/docs/resources/guild#create-guild-channel const { guild_id } = req.params; const body = req.body as ChannelModifySchema; @@ -26,45 +22,39 @@ router.post("/", check(ChannelModifySchema), async (req: Request, res: Response) res.status(201).json(channel); }); -// TODO: check if parent_id exists -router.patch( - "/", - check([{ id: String, $position: Number, $lock_permissions: Boolean, $parent_id: String }]), - async (req: Request, res: Response) => { - // changes guild channel position - const { guild_id } = req.params; - const body = req.body as { id: string; position?: number; lock_permissions?: boolean; parent_id?: string }[]; +export type ChannelReorderSchema = { id: string; position?: number; lock_permissions?: boolean; parent_id?: string }[]; - const permission = await getPermission(req.user_id, guild_id); - permission.hasThrow("MANAGE_CHANNELS"); +router.patch("/", route({ body: "ChannelReorderSchema", permission: "MANAGE_CHANNELS" }), async (req: Request, res: Response) => { + // changes guild channel position + const { guild_id } = req.params; + const body = req.body as ChannelReorderSchema; - await Promise.all([ - body.map(async (x) => { - if (!x.position && !x.parent_id) throw new HTTPError(`You need to at least specify position or parent_id`, 400); + await Promise.all([ + body.map(async (x) => { + if (!x.position && !x.parent_id) throw new HTTPError(`You need to at least specify position or parent_id`, 400); - const opts: any = {}; - if (x.position) opts.position = x.position; + const opts: any = {}; + if (x.position) opts.position = x.position; - if (x.parent_id) { - opts.parent_id = x.parent_id; - const parent_channel = await Channel.findOneOrFail({ - where: { id: x.parent_id, guild_id }, - select: ["permission_overwrites"] - }); - if (x.lock_permissions) { - opts.permission_overwrites = parent_channel.permission_overwrites; - } + if (x.parent_id) { + opts.parent_id = x.parent_id; + const parent_channel = await Channel.findOneOrFail({ + where: { id: x.parent_id, guild_id }, + select: ["permission_overwrites"] + }); + if (x.lock_permissions) { + opts.permission_overwrites = parent_channel.permission_overwrites; } + } - await Channel.update({ guild_id, id: x.id }, opts); - const channel = await Channel.findOneOrFail({ guild_id, id: x.id }); + await Channel.update({ guild_id, id: x.id }, opts); + const channel = await Channel.findOneOrFail({ guild_id, id: x.id }); - await emitEvent({ event: "CHANNEL_UPDATE", data: channel, channel_id: x.id, guild_id } as ChannelUpdateEvent); - }) - ]); + await emitEvent({ event: "CHANNEL_UPDATE", data: channel, channel_id: x.id, guild_id } as ChannelUpdateEvent); + }) + ]); - res.sendStatus(204); - } -); + res.sendStatus(204); +}); export default router; diff --git a/api/src/routes/guilds/#guild_id/delete.ts b/api/src/routes/guilds/#guild_id/delete.ts
index bbbd1fa4..bd158c56 100644 --- a/api/src/routes/guilds/#guild_id/delete.ts +++ b/api/src/routes/guilds/#guild_id/delete.ts
@@ -1,26 +1,20 @@ import { Channel, emitEvent, GuildDeleteEvent, Guild, Member, Message, Role, Invite, Emoji } from "@fosscord/util"; import { Router, Request, Response } from "express"; import { HTTPError } from "lambert-server"; +import { route } from "@fosscord/api"; const router = Router(); // discord prefixes this route with /delete instead of using the delete method // docs are wrong https://discord.com/developers/docs/resources/guild#delete-guild -router.post("/", async (req: Request, res: Response) => { +router.post("/", route({}), async (req: Request, res: Response) => { var { guild_id } = req.params; const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: ["owner_id"] }); if (guild.owner_id !== req.user_id) throw new HTTPError("You are not the owner of this guild", 401); - // do not put everything into promise all, because of "QueryFailedError: SQLITE_CONSTRAINT: FOREIGN KEY constraint failed" - - await Message.delete({ guild_id }); // messages must be deleted before channel - await Promise.all([ - Role.delete({ guild_id }), - Channel.delete({ guild_id }), - Emoji.delete({ guild_id }), - Member.delete({ guild_id }), + Guild.delete({ id: guild_id }), // this will also delete all guild related data emitEvent({ event: "GUILD_DELETE", data: { @@ -30,9 +24,6 @@ router.post("/", async (req: Request, res: Response) => { } as GuildDeleteEvent) ]); - await Invite.delete({ guild_id }); // invite must be deleted after channel - await Guild.delete({ id: guild_id }); // guild must be deleted after everything else - return res.sendStatus(204); }); diff --git a/api/src/routes/guilds/#guild_id/index.ts b/api/src/routes/guilds/#guild_id/index.ts
index 9d302a48..d8ee86ff 100644 --- a/api/src/routes/guilds/#guild_id/index.ts +++ b/api/src/routes/guilds/#guild_id/index.ts
@@ -1,23 +1,35 @@ import { Request, Response, Router } from "express"; -import { emitEvent, getPermission, Guild, GuildUpdateEvent, Member } from "@fosscord/util"; +import { emitEvent, getPermission, Guild, GuildUpdateEvent, handleFile, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { GuildUpdateSchema } from "../../../schema/Guild"; - -import { check } from "../../../util/instanceOf"; -import { handleFile } from "../../../util/cdn"; +import { route } from "@fosscord/api"; import "missing-native-js-functions"; +import { GuildCreateSchema } from "../index"; const router = Router(); -router.get("/", async (req: Request, res: Response) => { +export interface GuildUpdateSchema extends Omit<GuildCreateSchema, "channels"> { + banner?: string | null; + splash?: string | null; + description?: string; + features?: string[]; + verification_level?: number; + default_message_notifications?: number; + system_channel_flags?: number; + explicit_content_filter?: number; + public_updates_channel_id?: string; + afk_timeout?: number; + afk_channel_id?: string; + preferred_locale?: string; +} + +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; - const [guild, member_count, member] = await Promise.all([ + const [guild, member] = await Promise.all([ Guild.findOneOrFail({ id: guild_id }), - Member.count({ guild_id: guild_id, id: req.user_id }), - Member.findOneOrFail({ id: req.user_id }) + Member.findOne({ guild_id: guild_id, id: req.user_id }) ]); - if (!member_count) throw new HTTPError("You are not a member of the guild you are trying to access", 401); + if (!member) throw new HTTPError("You are not a member of the guild you are trying to access", 401); // @ts-ignore guild.joined_at = member?.joined_at; @@ -25,14 +37,11 @@ router.get("/", async (req: Request, res: Response) => { return res.json(guild); }); -router.patch("/", check(GuildUpdateSchema), async (req: Request, res: Response) => { +router.patch("/", route({ body: "GuildUpdateSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const body = req.body as GuildUpdateSchema; const { guild_id } = req.params; // TODO: guild update check image - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); - if (body.icon) body.icon = await handleFile(`/icons/${guild_id}`, body.icon); if (body.banner) body.banner = await handleFile(`/banners/${guild_id}`, body.banner); if (body.splash) body.splash = await handleFile(`/splashes/${guild_id}`, body.splash); diff --git a/api/src/routes/guilds/#guild_id/invites.ts b/api/src/routes/guilds/#guild_id/invites.ts
index 39a934ee..b7534e31 100644 --- a/api/src/routes/guilds/#guild_id/invites.ts +++ b/api/src/routes/guilds/#guild_id/invites.ts
@@ -1,14 +1,12 @@ import { getPermission, Invite, PublicInviteRelation } from "@fosscord/util"; +import { route } from "@fosscord/api"; import { Request, Response, Router } from "express"; const router = Router(); -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const { guild_id } = req.params; - const permissions = await getPermission(req.user_id, guild_id); - permissions.hasThrow("MANAGE_GUILD"); - const invites = await Invite.find({ where: { guild_id }, relations: PublicInviteRelation }); return res.json(invites); diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts
index 0d62e555..ab489743 100644 --- a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts +++ b/api/src/routes/guilds/#guild_id/members/#member_id/index.ts
@@ -1,23 +1,15 @@ import { Request, Response, Router } from "express"; -import { - Guild, - Member, - User, - GuildMemberAddEvent, - getPermission, - PermissionResolvable, - Role, - GuildMemberUpdateEvent, - emitEvent -} from "@fosscord/util"; +import { Member, getPermission, Role, GuildMemberUpdateEvent, emitEvent } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { check } from "../../../../../util/instanceOf"; -import { MemberChangeSchema } from "../../../../../schema/Member"; -import { In } from "typeorm"; +import { route } from "@fosscord/api"; const router = Router(); -router.get("/", async (req: Request, res: Response) => { +export interface MemberChangeSchema { + roles?: string[]; +} + +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id, member_id } = req.params; await Member.IsInGuildOrFail(req.user_id, guild_id); @@ -26,8 +18,9 @@ router.get("/", async (req: Request, res: Response) => { return res.json(member); }); -router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response) => { - const { guild_id, member_id } = req.params; +router.patch("/", route({ body: "MemberChangeSchema" }), async (req: Request, res: Response) => { + let { guild_id, member_id } = req.params; + if (member_id === "@me") member_id = req.user_id; const body = req.body as MemberChangeSchema; const member = await Member.findOneOrFail({ where: { id: member_id, guild_id }, relations: ["roles", "user"] }); @@ -39,7 +32,7 @@ router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response) } await member.save(); - // do not use promise.all as we have to first write to db before emitting the event + // do not use promise.all as we have to first write to db before emitting the event to catch errors await emitEvent({ event: "GUILD_MEMBER_UPDATE", guild_id, @@ -49,7 +42,7 @@ router.patch("/", check(MemberChangeSchema), async (req: Request, res: Response) res.json(member); }); -router.put("/", async (req: Request, res: Response) => { +router.put("/", route({}), async (req: Request, res: Response) => { let { guild_id, member_id } = req.params; if (member_id === "@me") member_id = req.user_id; @@ -59,12 +52,9 @@ router.put("/", async (req: Request, res: Response) => { res.sendStatus(204); }); -router.delete("/", async (req: Request, res: Response) => { +router.delete("/", route({ permission: "KICK_MEMBERS" }), async (req: Request, res: Response) => { const { guild_id, member_id } = req.params; - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("KICK_MEMBERS"); - await Member.removeFromGuild(member_id, guild_id); res.sendStatus(204); }); diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts b/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts
index 3f2975e6..27f7f65d 100644 --- a/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts +++ b/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts
@@ -1,11 +1,14 @@ import { getPermission, Member, PermissionResolvable } from "@fosscord/util"; +import { route } from "@fosscord/api"; import { Request, Response, Router } from "express"; -import { check } from "lambert-server"; -import { MemberNickChangeSchema } from "../../../../../schema/Member"; const router = Router(); -router.patch("/", check(MemberNickChangeSchema), async (req: Request, res: Response) => { +export interface MemberNickChangeSchema { + nick: string; +} + +router.patch("/", route({ body: "MemberNickChangeSchema" }), async (req: Request, res: Response) => { var { guild_id, member_id } = req.params; var permissionString: PermissionResolvable = "MANAGE_NICKNAMES"; if (member_id === "@me") { diff --git a/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts b/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
index cb9bad9a..8f5ca7ba 100644 --- a/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts +++ b/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts
@@ -1,24 +1,19 @@ import { getPermission, Member } from "@fosscord/util"; +import { route } from "@fosscord/api"; import { Request, Response, Router } from "express"; const router = Router(); -router.delete("/:member_id/roles/:role_id", async (req: Request, res: Response) => { +router.delete("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { const { guild_id, role_id, member_id } = req.params; - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_ROLES"); - await Member.removeRole(member_id, guild_id, role_id); res.sendStatus(204); }); -router.put("/:member_id/roles/:role_id", async (req: Request, res: Response) => { +router.put("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { const { guild_id, role_id, member_id } = req.params; - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_ROLES"); - await Member.addRole(member_id, guild_id, role_id); res.sendStatus(204); }); diff --git a/api/src/routes/guilds/#guild_id/members/index.ts b/api/src/routes/guilds/#guild_id/members/index.ts
index 0bfd71cb..386276c8 100644 --- a/api/src/routes/guilds/#guild_id/members/index.ts +++ b/api/src/routes/guilds/#guild_id/members/index.ts
@@ -1,34 +1,28 @@ import { Request, Response, Router } from "express"; import { Guild, Member, PublicMemberProjection } from "@fosscord/util"; -import { instanceOf, Length } from "../../../../util/instanceOf"; +import { route } from "@fosscord/api"; import { MoreThan } from "typeorm"; +import { HTTPError } from "lambert-server"; const router = Router(); // TODO: not allowed for user -> only allowed for bots with privileged intents // TODO: send over websocket -router.get("/", async (req: Request, res: Response) => { - const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ id: guild_id }); - await Member.IsInGuildOrFail(req.user_id, guild_id); - - try { - instanceOf({ $limit: new Length(Number, 1, 1000), $after: String }, req.query, { - path: "query", - req, - ref: { obj: null, key: "" } - }); - } catch (error) { - return res.status(400).json({ code: 50035, message: "Invalid Query", success: false, errors: error }); - } +// TODO: check for GUILD_MEMBERS intent - const { limit, after } = (<unknown>req.query) as { limit?: number; after?: string }; +router.get("/", route({}), async (req: Request, res: Response) => { + const { guild_id } = req.params; + const limit = Number(req.query.limit) || 1; + if (limit > 1000 || limit < 1) throw new HTTPError("Limit must be between 1 and 1000"); + const after = `${req.query.after}`; const query = after ? { id: MoreThan(after) } : {}; + await Member.IsInGuildOrFail(req.user_id, guild_id); + const members = await Member.find({ where: { guild_id, ...query }, select: PublicMemberProjection, - take: limit || 1, + take: limit, order: { id: "ASC" } }); diff --git a/api/src/routes/guilds/#guild_id/regions.ts b/api/src/routes/guilds/#guild_id/regions.ts
index 212c9bcd..75d24fd1 100644 --- a/api/src/routes/guilds/#guild_id/regions.ts +++ b/api/src/routes/guilds/#guild_id/regions.ts
@@ -1,11 +1,11 @@ -import {Config, Guild, Member} from "@fosscord/util"; +import { Config, Guild, Member } from "@fosscord/util"; import { Request, Response, Router } from "express"; -import {getVoiceRegions} from "../../../util/Voice"; -import {getIpAdress} from "../../../util/ipAddress"; +import { getVoiceRegions, route } from "@fosscord/api"; +import { getIpAdress } from "@fosscord/api"; const router = Router(); -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; const guild = await Guild.findOneOrFail({ id: guild_id }); //TODO we should use an enum for guild's features and not hardcoded strings diff --git a/api/src/routes/guilds/#guild_id/roles.ts b/api/src/routes/guilds/#guild_id/roles.ts
index 6a318688..d1d60906 100644 --- a/api/src/routes/guilds/#guild_id/roles.ts +++ b/api/src/routes/guilds/#guild_id/roles.ts
@@ -2,24 +2,34 @@ import { Request, Response, Router } from "express"; import { Role, getPermission, - Snowflake, Member, GuildRoleCreateEvent, GuildRoleUpdateEvent, GuildRoleDeleteEvent, emitEvent, - Config + Config, + DiscordApiErrors } from "@fosscord/util"; import { HTTPError } from "lambert-server"; - -import { check } from "../../../util/instanceOf"; -import { RoleModifySchema, RolePositionUpdateSchema } from "../../../schema/Roles"; -import { DiscordApiErrors } from "@fosscord/util"; -import { In } from "typeorm"; +import { route } from "@fosscord/api"; const router: Router = Router(); -router.get("/", async (req: Request, res: Response) => { +export interface RoleModifySchema { + name?: string; + permissions?: bigint; + color?: number; + hoist?: boolean; // whether the role should be displayed separately in the sidebar + mentionable?: boolean; // whether the role should be mentionable + position?: number; +} + +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); @@ -29,13 +39,10 @@ router.get("/", async (req: Request, res: Response) => { return res.json(roles); }); -router.post("/", check(RoleModifySchema), async (req: Request, res: Response) => { +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 perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_ROLES"); - const role_count = await Role.count({ guild_id }); const { maxRoles } = Config.get().limits.guild; @@ -50,7 +57,7 @@ router.post("/", check(RoleModifySchema), async (req: Request, res: Response) => ...body, guild_id: guild_id, managed: false, - permissions: String(perms.bitfield & (body.permissions || 0n)), + permissions: String(req.permission!.bitfield & (body.permissions || 0n)), tags: undefined }); @@ -69,14 +76,11 @@ router.post("/", check(RoleModifySchema), async (req: Request, res: Response) => res.json(role); }); -router.delete("/:role_id", async (req: Request, res: Response) => { +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"); - const permissions = await getPermission(req.user_id, guild_id); - permissions.hasThrow("MANAGE_ROLES"); - await Promise.all([ Role.delete({ id: role_id, @@ -97,14 +101,11 @@ router.delete("/:role_id", async (req: Request, res: Response) => { // TODO: check role hierarchy -router.patch("/:role_id", check(RoleModifySchema), async (req: Request, res: Response) => { +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; - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_ROLES"); - - const role = new Role({ ...body, id: role_id, guild_id, permissions: String(perms.bitfield & (body.permissions || 0n)) }); + const role = new Role({ ...body, id: role_id, guild_id, permissions: String(req.permission!.bitfield & (body.permissions || 0n)) }); await Promise.all([ role.save(), @@ -121,7 +122,7 @@ router.patch("/:role_id", check(RoleModifySchema), async (req: Request, res: Res res.json(role); }); -router.patch("/", check(RolePositionUpdateSchema), async (req: Request, res: Response) => { +router.patch("/", route({ body: "RolePositionUpdateSchema" }), async (req: Request, res: Response) => { const { guild_id } = req.params; const body = req.body as RolePositionUpdateSchema; @@ -130,7 +131,7 @@ router.patch("/", check(RolePositionUpdateSchema), async (req: Request, res: Res await Promise.all(body.map(async (x) => Role.update({ guild_id, id: x.id }, { position: x.position }))); - const roles = await Role.find({ guild_id, id: In(body.map((x) => x.id)) }); + const roles = await Role.find({ where: body.map((x) => ({ id: x.id, guild_id })) }); await Promise.all( roles.map((x) => diff --git a/api/src/routes/guilds/#guild_id/templates.ts b/api/src/routes/guilds/#guild_id/templates.ts
index a7613abf..5179e761 100644 --- a/api/src/routes/guilds/#guild_id/templates.ts +++ b/api/src/routes/guilds/#guild_id/templates.ts
@@ -1,9 +1,8 @@ import { Request, Response, Router } from "express"; -import { Guild, getPermission, Template } from "@fosscord/util"; +import { Guild, Template } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { TemplateCreateSchema, TemplateModifySchema } from "../../../schema/Template"; -import { check } from "../../../util/instanceOf"; -import { generateCode } from "../../../util/String"; +import { route } from "@fosscord/api"; +import { generateCode } from "@fosscord/api"; const router: Router = Router(); @@ -24,7 +23,17 @@ const TemplateGuildProjection: (keyof Guild)[] = [ "icon" ]; -router.get("/", async (req: Request, res: Response) => { +export interface TemplateCreateSchema { + name: string; + description?: string; +} + +export interface TemplateModifySchema { + name: string; + description?: string; +} + +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; var templates = await Template.find({ source_guild_id: guild_id }); @@ -32,12 +41,9 @@ router.get("/", async (req: Request, res: Response) => { return res.json(templates); }); -router.post("/", check(TemplateCreateSchema), async (req: Request, res: Response) => { +router.post("/", route({ body: "TemplateCreateSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const { guild_id } = req.params; const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: TemplateGuildProjection }); - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); - const exists = await Template.findOneOrFail({ id: guild_id }).catch((e) => {}); if (exists) throw new HTTPError("Template already exists", 400); @@ -54,44 +60,31 @@ router.post("/", check(TemplateCreateSchema), async (req: Request, res: Response res.json(template); }); -router.delete("/:code", async (req: Request, res: Response) => { - const guild_id = req.params.guild_id; - const { code } = req.params; - - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); +router.delete("/:code", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { + const { code, guild_id } = req.params; const template = await Template.delete({ - code + code, + source_guild_id: guild_id }); res.json(template); }); -router.put("/:code", async (req: Request, res: Response) => { - // synchronizes the template +router.put("/:code", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const { code, guild_id } = req.params; - const guild = await Guild.findOneOrFail({ where: { id: guild_id }, select: TemplateGuildProjection }); - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); - const template = await new Template({ code, serialized_source_guild: guild }).save(); res.json(template); }); -router.patch("/:code", check(TemplateModifySchema), async (req: Request, res: Response) => { - // updates the template description - const { guild_id } = req.params; - const { code } = req.params; +router.patch("/:code", route({ body: "TemplateModifySchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { + const { code, guild_id } = req.params; const { name, description } = req.body; - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); - - const template = await new Template({ code, name: name, description: description }).save(); + const template = await new Template({ code, name: name, description: description, source_guild_id: guild_id }).save(); res.json(template); }); diff --git a/api/src/routes/guilds/#guild_id/vanity-url.ts b/api/src/routes/guilds/#guild_id/vanity-url.ts
index 58940b42..7f2cea9e 100644 --- a/api/src/routes/guilds/#guild_id/vanity-url.ts +++ b/api/src/routes/guilds/#guild_id/vanity-url.ts
@@ -1,40 +1,43 @@ import { Channel, ChannelType, getPermission, Guild, Invite, trimSpecial } from "@fosscord/util"; import { Router, Request, Response } from "express"; +import { route } from "@fosscord/api"; import { HTTPError } from "lambert-server"; -import { check, Length } from "../../../util/instanceOf"; const router = Router(); const InviteRegex = /\W/g; -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const { guild_id } = req.params; - const permission = await getPermission(req.user_id, guild_id); - permission.hasThrow("MANAGE_GUILD"); - const guild = await Guild.findOneOrFail({ where: { id: guild_id }, relations: ["vanity_url"] }); if (!guild.vanity_url) return res.json({ code: null }); return res.json({ code: guild.vanity_url_code, uses: guild.vanity_url.uses }); }); +export interface VanityUrlSchema { + /** + * @minLength 1 + * @maxLength 20 + */ + code?: string; +} + // TODO: check if guild is elgible for vanity url -router.patch("/", check({ code: new Length(String, 0, 20) }), async (req: Request, res: Response) => { +router.patch("/", route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const { guild_id } = req.params; - const code = req.body.code.replace(InviteRegex); + const body = req.body as VanityUrlSchema; + const code = body.code?.replace(InviteRegex, ""); - await Invite.findOneOrFail({ code }); + const invite = await Invite.findOne({ code }); + if (invite) throw new HTTPError("Invite already exists"); const guild = await Guild.findOneOrFail({ id: guild_id }); - const permission = await getPermission(req.user_id, guild_id); - permission.hasThrow("MANAGE_GUILD"); - const { id } = await Channel.findOneOrFail({ guild_id, type: ChannelType.GUILD_TEXT }); - guild.vanity_url_code = code; Promise.all([ - guild.save(), + Guild.update({ id: guild_id }, { vanity_url_code: code }), Invite.delete({ code: guild.vanity_url_code }), new Invite({ code: code, diff --git a/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts b/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts
index 02951f81..f9fbea54 100644 --- a/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts +++ b/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts
@@ -1,15 +1,60 @@ -import { check } from "../../../../../util/instanceOf"; -import { VoiceStateUpdateSchema } from "../../../../../schema"; +import { Channel, ChannelType, DiscordApiErrors, emitEvent, getPermission, VoiceState, VoiceStateUpdateEvent } from "@fosscord/util"; +import { route } from "@fosscord/api"; import { Request, Response, Router } from "express"; -import { updateVoiceState } from "../../../../../util/VoiceState"; const router = Router(); +//TODO need more testing when community guild and voice stage channel are working -router.patch("/", check(VoiceStateUpdateSchema), async (req: Request, res: Response) => { +export interface VoiceStateUpdateSchema { + channel_id: string; + guild_id?: string; + suppress?: boolean; + request_to_speak_timestamp?: Date; + self_mute?: boolean; + self_deaf?: boolean; + self_video?: boolean; +} + +router.patch("/", route({ body: "VoiceStateUpdateSchema" }), async (req: Request, res: Response) => { const body = req.body as VoiceStateUpdateSchema; - const { guild_id, user_id } = req.params; - await updateVoiceState(body, guild_id, req.user_id, user_id) + var { guild_id, user_id } = req.params; + if (user_id === "@me") user_id = req.user_id; + + const perms = await getPermission(req.user_id, guild_id, body.channel_id); + + /* + From https://discord.com/developers/docs/resources/guild#modify-current-user-voice-state + You must have the MUTE_MEMBERS permission to unsuppress others. You can always suppress yourself. + You must have the REQUEST_TO_SPEAK permission to request to speak. You can always clear your own request to speak. + */ + if (body.suppress && user_id !== req.user_id) { + perms.hasThrow("MUTE_MEMBERS"); + } + if (!body.suppress) body.request_to_speak_timestamp = new Date(); + if (body.request_to_speak_timestamp) perms.hasThrow("REQUEST_TO_SPEAK"); + + const voice_state = await VoiceState.findOne({ + guild_id, + channel_id: body.channel_id, + user_id + }); + if (!voice_state) throw DiscordApiErrors.UNKNOWN_VOICE_STATE; + + voice_state.assign(body); + const channel = await Channel.findOneOrFail({ guild_id, id: body.channel_id }); + if (channel.type !== ChannelType.GUILD_STAGE_VOICE) { + throw DiscordApiErrors.CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE; + } + + await Promise.all([ + voice_state.save(), + emitEvent({ + event: "VOICE_STATE_UPDATE", + data: voice_state, + guild_id + } as VoiceStateUpdateEvent) + ]); return res.sendStatus(204); }); -export default router; \ No newline at end of file +export default router; diff --git a/api/src/routes/guilds/#guild_id/voice-states/@me/index.ts b/api/src/routes/guilds/#guild_id/voice-states/@me/index.ts deleted file mode 100644
index 42ba543e..00000000 --- a/api/src/routes/guilds/#guild_id/voice-states/@me/index.ts +++ /dev/null
@@ -1,15 +0,0 @@ -import { check } from "../../../../../util/instanceOf"; -import { VoiceStateUpdateSchema } from "../../../../../schema"; -import { Request, Response, Router } from "express"; -import { updateVoiceState } from "../../../../../util/VoiceState"; - -const router = Router(); - -router.patch("/", check(VoiceStateUpdateSchema), async (req: Request, res: Response) => { - const body = req.body as VoiceStateUpdateSchema; - const { guild_id } = req.params; - await updateVoiceState(body, guild_id, req.user_id) - return res.sendStatus(204); -}); - -export default router; \ No newline at end of file diff --git a/api/src/routes/guilds/#guild_id/welcome_screen.ts b/api/src/routes/guilds/#guild_id/welcome_screen.ts
index defbcd40..7141f17e 100644 --- a/api/src/routes/guilds/#guild_id/welcome_screen.ts +++ b/api/src/routes/guilds/#guild_id/welcome_screen.ts
@@ -1,31 +1,36 @@ import { Request, Response, Router } from "express"; import { Guild, getPermission, Snowflake, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; - -import { check } from "../../../util/instanceOf"; -import { GuildUpdateWelcomeScreenSchema } from "../../../schema/Guild"; +import { route } from "@fosscord/api"; const router: Router = Router(); -router.get("/", async (req: Request, res: Response) => { +export interface GuildUpdateWelcomeScreenSchema { + welcome_channels?: { + channel_id: string; + description: string; + emoji_id?: string; + emoji_name: string; + }[]; + enabled?: boolean; + description?: string; +} + +router.get("/", route({}), async (req: Request, res: Response) => { const guild_id = req.params.guild_id; const guild = await Guild.findOneOrFail({ id: guild_id }); - await Member.IsInGuildOrFail(req.user_id, guild_id); res.json(guild.welcome_screen); }); -router.patch("/", check(GuildUpdateWelcomeScreenSchema), async (req: Request, res: Response) => { +router.patch("/", route({ body: "GuildUpdateWelcomeScreenSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const guild_id = req.params.guild_id; const body = req.body as GuildUpdateWelcomeScreenSchema; const guild = await Guild.findOneOrFail({ id: guild_id }); - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); - if (!guild.welcome_screen.enabled) throw new HTTPError("Welcome screen disabled", 400); if (body.welcome_channels) guild.welcome_screen.welcome_channels = body.welcome_channels; // TODO: check if they exist and are valid if (body.description) guild.welcome_screen.description = body.description; diff --git a/api/src/routes/guilds/#guild_id/widget.json.ts b/api/src/routes/guilds/#guild_id/widget.json.ts
index 193ed095..c31519fa 100644 --- a/api/src/routes/guilds/#guild_id/widget.json.ts +++ b/api/src/routes/guilds/#guild_id/widget.json.ts
@@ -1,7 +1,7 @@ import { Request, Response, Router } from "express"; import { Config, Permissions, Guild, Invite, Channel, Member } from "@fosscord/util"; import { HTTPError } from "lambert-server"; -import { random } from "../../../util/RandomInviteID"; +import { random, route } from "@fosscord/api"; const router: Router = Router(); @@ -14,7 +14,7 @@ const router: Router = Router(); // https://discord.com/developers/docs/resources/guild#get-guild-widget // TODO: Cache the response for a guild for 5 minutes regardless of response -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; const guild = await Guild.findOneOrFail({ id: guild_id }); diff --git a/api/src/routes/guilds/#guild_id/widget.png.ts b/api/src/routes/guilds/#guild_id/widget.png.ts
index 89b31153..4c82b740 100644 --- a/api/src/routes/guilds/#guild_id/widget.png.ts +++ b/api/src/routes/guilds/#guild_id/widget.png.ts
@@ -1,6 +1,7 @@ import { Request, Response, Router } from "express"; import { Guild } from "@fosscord/util"; import { HTTPError } from "lambert-server"; +import { route } from "@fosscord/api"; import fs from "fs"; import path from "path"; @@ -10,7 +11,7 @@ const router: Router = Router(); // https://discord.com/developers/docs/resources/guild#get-guild-widget-image // TODO: Cache the response -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; const guild = await Guild.findOneOrFail({ id: guild_id }); diff --git a/api/src/routes/guilds/#guild_id/widget.ts b/api/src/routes/guilds/#guild_id/widget.ts
index fcf71402..2640618d 100644 --- a/api/src/routes/guilds/#guild_id/widget.ts +++ b/api/src/routes/guilds/#guild_id/widget.ts
@@ -1,31 +1,28 @@ import { Request, Response, Router } from "express"; -import { getPermission, Guild } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { check } from "../../../util/instanceOf"; -import { WidgetModifySchema } from "../../../schema/Widget"; +import { Guild } from "@fosscord/util"; +import { route } from "@fosscord/api"; + +export interface WidgetModifySchema { + enabled: boolean; // whether the widget is enabled + channel_id: string; // the widget channel id +} const router: Router = Router(); // https://discord.com/developers/docs/resources/guild#get-guild-widget-settings -router.get("/", async (req: Request, res: Response) => { +router.get("/", route({}), async (req: Request, res: Response) => { const { guild_id } = req.params; - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); - const guild = await Guild.findOneOrFail({ id: guild_id }); return res.json({ enabled: guild.widget_enabled || false, channel_id: guild.widget_channel_id || null }); }); // https://discord.com/developers/docs/resources/guild#modify-guild-widget -router.patch("/", check(WidgetModifySchema), async (req: Request, res: Response) => { +router.patch("/", route({ body: "WidgetModifySchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { const body = req.body as WidgetModifySchema; const { guild_id } = req.params; - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow("MANAGE_GUILD"); - await Guild.update({ id: guild_id }, { widget_enabled: body.enabled, widget_channel_id: body.channel_id }); // Widget invite for the widget_channel_id gets created as part of the /guilds/{guild.id}/widget.json request diff --git a/api/src/routes/guilds/index.ts b/api/src/routes/guilds/index.ts
index e5830647..abde147d 100644 --- a/api/src/routes/guilds/index.ts +++ b/api/src/routes/guilds/index.ts
@@ -1,15 +1,26 @@ import { Router, Request, Response } from "express"; -import { Role, Guild, Snowflake, Config, User, Member, Channel } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { check } from "./../../util/instanceOf"; -import { GuildCreateSchema } from "../../schema/Guild"; -import { DiscordApiErrors } from "@fosscord/util"; +import { Role, Guild, Snowflake, Config, Member, Channel, DiscordApiErrors, handleFile } from "@fosscord/util"; +import { route } from "@fosscord/api"; +import { ChannelModifySchema } from "../channels/#channel_id"; const router: Router = Router(); +export interface GuildCreateSchema { + /** + * @maxLength 100 + */ + name: string; + region?: string; + icon?: string | null; + channels?: ChannelModifySchema[]; + guild_template_code?: string; + system_channel_id?: string; + rules_channel_id?: string; +} + //TODO: create default channel -router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) => { +router.post("/", route({ body: "GuildCreateSchema" }), async (req: Request, res: Response) => { const body = req.body as GuildCreateSchema; const { maxGuilds } = Config.get().limits.user; @@ -22,6 +33,7 @@ router.post("/", check(GuildCreateSchema), async (req: Request, res: Response) = await Guild.insert({ name: body.name, + icon: await handleFile(`/icons/${guild_id}`, body.icon as string), region: Config.get().regions.default, owner_id: req.user_id, afk_timeout: 300, diff --git a/api/src/routes/guilds/templates/index.ts b/api/src/routes/guilds/templates/index.ts
index 58201f65..b5e243e9 100644 --- a/api/src/routes/guilds/templates/index.ts +++ b/api/src/routes/guilds/templates/index.ts
@@ -1,12 +1,15 @@ import { Request, Response, Router } from "express"; const router: Router = Router(); import { Template, Guild, Role, Snowflake, Config, User, Member } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { GuildTemplateCreateSchema } from "../../../schema/Guild"; -import { check } from "../../../util/instanceOf"; +import { route } from "@fosscord/api"; import { DiscordApiErrors } from "@fosscord/util"; -router.get("/:code", async (req: Request, res: Response) => { +export interface GuildTemplateCreateSchema { + name: string; + avatar?: string | null; +} + +router.get("/:code", route({}), async (req: Request, res: Response) => { const { code } = req.params; const template = await Template.findOneOrFail({ code: code }); @@ -14,7 +17,7 @@ router.get("/:code", async (req: Request, res: Response) => { res.json(template); }); -router.post("/:code", check(GuildTemplateCreateSchema), async (req: Request, res: Response) => { +router.post("/:code", route({ body: "GuildTemplateCreateSchema" }), async (req: Request, res: Response) => { const { code } = req.params; const body = req.body as GuildTemplateCreateSchema;