diff options
author | Madeline <46743919+MaddyUnderStars@users.noreply.github.com> | 2022-08-30 15:05:23 +1000 |
---|---|---|
committer | Madeline <46743919+MaddyUnderStars@users.noreply.github.com> | 2022-08-30 15:08:18 +1000 |
commit | 16315a3170ec018a834e68360e06b506415446d2 (patch) | |
tree | 90cfe456040fce35b904e88462886e3c73a2f3f2 /api/src/routes/guilds | |
parent | Start listening after database and config has been loaded (diff) | |
parent | Oop, deprecated typeorm call (diff) | |
download | server-16315a3170ec018a834e68360e06b506415446d2.tar.xz |
Merge branch 'staging' into dev/Maddy/fix/listeningAfterDb
Diffstat (limited to 'api/src/routes/guilds')
29 files changed, 0 insertions, 1779 deletions
diff --git a/api/src/routes/guilds/#guild_id/audit-logs.ts b/api/src/routes/guilds/#guild_id/audit-logs.ts deleted file mode 100644 index a4f2f800..00000000 --- a/api/src/routes/guilds/#guild_id/audit-logs.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Router, Response, Request } from "express"; -import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { route } from "@fosscord/api"; -import { ChannelModifySchema } from "../../channels/#channel_id"; -const router = Router(); - -//TODO: implement audit logs -router.get("/", route({}), async (req: Request, res: Response) => { - res.json({ - audit_log_entries: [], - users: [], - integrations: [], - webhooks: [], - guild_scheduled_events: [], - threads: [], - application_commands: [] - }); -}); -export default router; diff --git a/api/src/routes/guilds/#guild_id/bans.ts b/api/src/routes/guilds/#guild_id/bans.ts deleted file mode 100644 index 1ce41936..00000000 --- a/api/src/routes/guilds/#guild_id/bans.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { Request, Response, Router } from "express"; -import { DiscordApiErrors, emitEvent, getPermission, GuildBanAddEvent, GuildBanRemoveEvent, Guild, Ban, User, Member } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { getIpAdress, route } from "@fosscord/api"; - -export interface BanCreateSchema { - delete_message_days?: string; - reason?: string; -}; - -export interface BanRegistrySchema { - id: string; - user_id: string; - guild_id: string; - executor_id: string; - ip?: string; - reason?: string | undefined; -}; - -export interface BanModeratorSchema { - id: string; - user_id: string; - guild_id: string; - executor_id: string; - reason?: string | undefined; -}; - -const router: Router = Router(); - -/* TODO: Deleting the secrets is just a temporary go-around. Views should be implemented for both safety and better handling. */ - -router.get("/", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { - const { guild_id } = req.params; - - let bans = await Ban.find({ guild_id: guild_id }); - let promisesToAwait: object[] = []; - const bansObj: object[] = []; - - bans.filter((ban) => ban.user_id !== ban.executor_id); // pretend self-bans don't exist to prevent victim chasing - - bans.forEach((ban) => { - promisesToAwait.push(User.getPublicUser(ban.user_id)); - }); - - const bannedUsers: object[] = await Promise.all(promisesToAwait); - - bans.forEach((ban, index) => { - const user = bannedUsers[index] as User; - bansObj.push({ - reason: ban.reason, - user: { - username: user.username, - discriminator: user.discriminator, - id: user.id, - avatar: user.avatar, - public_flags: user.public_flags - } - }); - }); - - return res.json(bansObj); -}); - -router.get("/:user", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { - const { guild_id } = req.params; - const user_id = req.params.ban; - - let ban = await Ban.findOneOrFail({ guild_id: guild_id, user_id: user_id }) as BanRegistrySchema; - - if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN; - // pretend self-bans don't exist to prevent victim chasing - - /* Filter secret from registry. */ - - ban = ban as BanModeratorSchema; - - delete ban.ip - - return res.json(ban); -}); - -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; - - if ( (req.user_id === banned_user_id) && (banned_user_id === req.permission!.cache.guild?.owner_id)) - throw new HTTPError("You are the guild owner, hence can't ban yourself", 403); - - if (req.permission!.cache.guild?.owner_id === banned_user_id) throw new HTTPError("You can't ban the owner", 400); - - const banned_user = await User.getPublicUser(banned_user_id); - - const ban = new Ban({ - user_id: banned_user_id, - guild_id: guild_id, - ip: getIpAdress(req), - executor_id: req.user_id, - reason: req.body.reason // || otherwise empty - }); - - await Promise.all([ - Member.removeFromGuild(banned_user_id, guild_id), - ban.save(), - emitEvent({ - event: "GUILD_BAN_ADD", - data: { - guild_id: guild_id, - user: banned_user - }, - guild_id: guild_id - } as GuildBanAddEvent) - ]); - - return res.json(ban); -}); - -router.put("/@me", route({ body: "BanCreateSchema"}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - - const banned_user = await User.getPublicUser(req.params.user_id); - - if (req.permission!.cache.guild?.owner_id === req.params.user_id) - throw new HTTPError("You are the guild owner, hence can't ban yourself", 403); - - const ban = new Ban({ - user_id: req.params.user_id, - guild_id: guild_id, - ip: getIpAdress(req), - executor_id: req.params.user_id, - reason: req.body.reason // || otherwise empty - }); - - await Promise.all([ - Member.removeFromGuild(req.user_id, guild_id), - ban.save(), - emitEvent({ - event: "GUILD_BAN_ADD", - data: { - guild_id: guild_id, - user: banned_user - }, - guild_id: guild_id - } as GuildBanAddEvent) - ]); - - return res.json(ban); -}); - -router.delete("/:user_id", route({ permission: "BAN_MEMBERS" }), async (req: Request, res: Response) => { - const { guild_id, user_id } = req.params; - - let ban = await Ban.findOneOrFail({ guild_id: guild_id, user_id: user_id }); - - if (ban.user_id === ban.executor_id) throw DiscordApiErrors.UNKNOWN_BAN; - // make self-bans irreversible and hide them from view to avoid victim chasing - - const banned_user = await User.getPublicUser(user_id); - - await Promise.all([ - Ban.delete({ - user_id: user_id, - guild_id - }), - - emitEvent({ - event: "GUILD_BAN_REMOVE", - data: { - guild_id, - user: banned_user - }, - guild_id - } as GuildBanRemoveEvent) - ]); - - return res.status(204).send(); -}); - -export default router; diff --git a/api/src/routes/guilds/#guild_id/channels.ts b/api/src/routes/guilds/#guild_id/channels.ts deleted file mode 100644 index a921fa21..00000000 --- a/api/src/routes/guilds/#guild_id/channels.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Router, Response, Request } from "express"; -import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { route } from "@fosscord/api"; -import { ChannelModifySchema } from "../../channels/#channel_id"; -const router = Router(); - -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - const channels = await Channel.find({ guild_id }); - - res.json(channels); -}); - -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; - - const channel = await Channel.createChannel({ ...body, guild_id }, req.user_id); - - res.status(201).json(channel); -}); - -export type ChannelReorderSchema = { id: string; position?: number; lock_permissions?: boolean; parent_id?: string }[]; - -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 == null && !x.parent_id) throw new HTTPError(`You need to at least specify position or parent_id`, 400); - - const opts: any = {}; - if (x.position != null) 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; - } - } - - 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); - }) - ]); - - 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 deleted file mode 100644 index bd158c56..00000000 --- a/api/src/routes/guilds/#guild_id/delete.ts +++ /dev/null @@ -1,30 +0,0 @@ -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("/", 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); - - await Promise.all([ - Guild.delete({ id: guild_id }), // this will also delete all guild related data - emitEvent({ - event: "GUILD_DELETE", - data: { - id: guild_id - }, - guild_id: guild_id - } as GuildDeleteEvent) - ]); - - return res.sendStatus(204); -}); - -export default router; diff --git a/api/src/routes/guilds/#guild_id/discovery-requirements.ts b/api/src/routes/guilds/#guild_id/discovery-requirements.ts deleted file mode 100644 index ad20633f..00000000 --- a/api/src/routes/guilds/#guild_id/discovery-requirements.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Guild, Config } from "@fosscord/util"; - -import { Router, Request, Response } from "express"; -import { route } from "@fosscord/api"; - -const router = Router(); - -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - // TODO: - // Load from database - // Admin control, but for now it allows anyone to be discoverable - - res.send({ - guild_id: guild_id, - safe_environment: true, - healthy: true, - health_score_pending: false, - size: true, - nsfw_properties: {}, - protected: true, - sufficient: true, - sufficient_without_grace_period: true, - valid_rules_channel: true, - retention_healthy: true, - engagement_healthy: true, - age: true, - minimum_age: 0, - health_score: { - avg_nonnew_participators: 0, - avg_nonnew_communicators: 0, - num_intentful_joiners: 0, - perc_ret_w1_intentful: 0 - }, - minimum_size: 0 - }); -}); - -export default router; diff --git a/api/src/routes/guilds/#guild_id/emojis.ts b/api/src/routes/guilds/#guild_id/emojis.ts deleted file mode 100644 index 85d7ac05..00000000 --- a/api/src/routes/guilds/#guild_id/emojis.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Router, Request, Response } from "express"; -import { Config, DiscordApiErrors, emitEvent, Emoji, GuildEmojisUpdateEvent, handleFile, Member, Snowflake, User } from "@fosscord/util"; -import { route } from "@fosscord/api"; - -const router = Router(); - -export interface EmojiCreateSchema { - name?: string; - image: string; - require_colons?: boolean | null; - roles?: string[]; -} - -export interface EmojiModifySchema { - name?: string; - roles?: string[]; -} - -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - - await Member.IsInGuildOrFail(req.user_id, guild_id); - - const emojis = await Emoji.find({ where: { guild_id: guild_id }, relations: ["user"] }); - - return res.json(emojis); -}); - -router.get("/:emoji_id", route({}), async (req: Request, res: Response) => { - const { guild_id, emoji_id } = req.params; - - await Member.IsInGuildOrFail(req.user_id, guild_id); - - const emoji = await Emoji.findOneOrFail({ where: { guild_id: guild_id, id: emoji_id }, relations: ["user"] }); - - return res.json(emoji); -}); - -router.post("/", route({ body: "EmojiCreateSchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { - const { guild_id } = req.params; - const body = req.body as EmojiCreateSchema; - - const id = Snowflake.generate(); - const emoji_count = await Emoji.count({ guild_id: guild_id }); - const { maxEmojis } = Config.get().limits.guild; - - if (emoji_count >= maxEmojis) throw DiscordApiErrors.MAXIMUM_NUMBER_OF_EMOJIS_REACHED.withParams(maxEmojis); - if (body.require_colons == null) body.require_colons = true; - - const user = await User.findOneOrFail({ id: req.user_id }); - body.image = (await handleFile(`/emojis/${id}`, body.image)) as string; - - const emoji = await new Emoji({ - id: id, - guild_id: guild_id, - ...body, - user: user, - managed: false, - animated: false, // TODO: Add support animated emojis - available: true, - roles: [] - }).save(); - - await emitEvent({ - event: "GUILD_EMOJIS_UPDATE", - guild_id: guild_id, - data: { - guild_id: guild_id, - emojis: await Emoji.find({ guild_id: guild_id }) - } - } as GuildEmojisUpdateEvent); - - return res.status(201).json(emoji); -}); - -router.patch( - "/:emoji_id", - route({ body: "EmojiModifySchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), - async (req: Request, res: Response) => { - const { emoji_id, guild_id } = req.params; - const body = req.body as EmojiModifySchema; - - const emoji = await new Emoji({ ...body, id: emoji_id, guild_id: guild_id }).save(); - - await emitEvent({ - event: "GUILD_EMOJIS_UPDATE", - guild_id: guild_id, - data: { - guild_id: guild_id, - emojis: await Emoji.find({ guild_id: guild_id }) - } - } as GuildEmojisUpdateEvent); - - return res.json(emoji); - } -); - -router.delete("/:emoji_id", route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { - const { emoji_id, guild_id } = req.params; - - await Emoji.delete({ - id: emoji_id, - guild_id: guild_id - }); - - await emitEvent({ - event: "GUILD_EMOJIS_UPDATE", - guild_id: guild_id, - data: { - guild_id: guild_id, - emojis: await Emoji.find({ guild_id: guild_id }) - } - } as GuildEmojisUpdateEvent); - - res.sendStatus(204); -}); - -export default router; diff --git a/api/src/routes/guilds/#guild_id/index.ts b/api/src/routes/guilds/#guild_id/index.ts deleted file mode 100644 index 4ec3df72..00000000 --- a/api/src/routes/guilds/#guild_id/index.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Request, Response, Router } from "express"; -import { DiscordApiErrors, emitEvent, getPermission, getRights, Guild, GuildUpdateEvent, handleFile, Member } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { route } from "@fosscord/api"; -import "missing-native-js-functions"; -import { GuildCreateSchema } from "../index"; - -const router = Router(); - -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] = await Promise.all([ - Guild.findOneOrFail({ id: guild_id }), - Member.findOne({ guild_id: guild_id, id: req.user_id }) - ]); - 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; - - return res.send(guild); -}); - -router.patch("/", route({ body: "GuildUpdateSchema"}), async (req: Request, res: Response) => { - const body = req.body as GuildUpdateSchema; - const { guild_id } = req.params; - - - const rights = await getRights(req.user_id); - const permission = await getPermission(req.user_id, guild_id); - - if (!rights.has("MANAGE_GUILDS")||!permission.has("MANAGE_GUILD")) - throw DiscordApiErrors.MISSING_PERMISSIONS.withParams("MANAGE_GUILD"); - - // TODO: guild update check image - - 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); - - var guild = await Guild.findOneOrFail({ - where: { id: guild_id }, - relations: ["emojis", "roles", "stickers"] - }); - // TODO: check if body ids are valid - guild.assign(body); - - const data = guild.toJSON(); - // TODO: guild hashes - // TODO: fix vanity_url_code, template_id - delete data.vanity_url_code; - delete data.template_id; - - await Promise.all([guild.save(), emitEvent({ event: "GUILD_UPDATE", data, guild_id } as GuildUpdateEvent)]); - - return res.json(data); -}); - -export default router; diff --git a/api/src/routes/guilds/#guild_id/integrations.ts b/api/src/routes/guilds/#guild_id/integrations.ts deleted file mode 100644 index abf997c9..00000000 --- a/api/src/routes/guilds/#guild_id/integrations.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Router, Response, Request } from "express"; -import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { route } from "@fosscord/api"; -import { ChannelModifySchema } from "../../channels/#channel_id"; -const router = Router(); - -//TODO: implement integrations list -router.get("/", route({}), async (req: Request, res: Response) => { - res.json([]); -}); -export default router; diff --git a/api/src/routes/guilds/#guild_id/invites.ts b/api/src/routes/guilds/#guild_id/invites.ts deleted file mode 100644 index b7534e31..00000000 --- a/api/src/routes/guilds/#guild_id/invites.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { getPermission, Invite, PublicInviteRelation } from "@fosscord/util"; -import { route } from "@fosscord/api"; -import { Request, Response, Router } from "express"; - -const router = Router(); - -router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { - const { guild_id } = req.params; - - const invites = await Invite.find({ where: { guild_id }, relations: PublicInviteRelation }); - - return res.json(invites); -}); - -export default router; 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 deleted file mode 100644 index c285abb3..00000000 --- a/api/src/routes/guilds/#guild_id/members/#member_id/index.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Request, Response, Router } from "express"; -import { Member, getPermission, getRights, Role, GuildMemberUpdateEvent, emitEvent, Sticker, Emoji, Rights, Guild } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { route } from "@fosscord/api"; - -const router = Router(); - -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); - - const member = await Member.findOneOrFail({ id: member_id, guild_id }); - - return res.json(member); -}); - -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"] }); - const permission = await getPermission(req.user_id, guild_id); - const everyone = await Role.findOneOrFail({ guild_id: guild_id, name: "@everyone", position: 0 }); - - if (body.roles) { - permission.hasThrow("MANAGE_ROLES"); - - if (body.roles.indexOf(everyone.id) === -1) body.roles.push(everyone.id); - member.roles = body.roles.map((x) => new Role({ id: x })); // foreign key constraint will fail if role doesn't exist - } - - await member.save(); - - member.roles = member.roles.filter((x) => x.id !== everyone.id); - - // 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, - data: { ...member, roles: member.roles.map((x) => x.id) } - } as GuildMemberUpdateEvent); - - res.json(member); -}); - -router.put("/", route({}), async (req: Request, res: Response) => { - - // TODO: Lurker mode - - const rights = await getRights(req.user_id); - - let { guild_id, member_id } = req.params; - if (member_id === "@me") { - member_id = req.user_id; - rights.hasThrow("JOIN_GUILDS"); - } else { - // TODO: join others by controller - } - - var guild = await Guild.findOneOrFail({ - where: { id: guild_id } - }); - - var emoji = await Emoji.find({ - where: { guild_id: guild_id } - }); - - var roles = await Role.find({ - where: { guild_id: guild_id } - }); - - var stickers = await Sticker.find({ - where: { guild_id: guild_id } - }); - - await Member.addToGuild(member_id, guild_id); - res.send({ ...guild, emojis: emoji, roles: roles, stickers: stickers }); -}); - -router.delete("/", route({}), async (req: Request, res: Response) => { - const permission = await getPermission(req.user_id); - const rights = await getRights(req.user_id); - const { guild_id, member_id } = req.params; - if (member_id !== "@me" || member_id === req.user_id) { - // TODO: unless force-joined - rights.hasThrow("SELF_LEAVE_GROUPS"); - } else { - rights.hasThrow("KICK_BAN_MEMBERS"); - permission.hasThrow("KICK_MEMBERS"); - } - - await Member.removeFromGuild(member_id, guild_id); - res.sendStatus(204); -}); - -export default router; 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 deleted file mode 100644 index 27f7f65d..00000000 --- a/api/src/routes/guilds/#guild_id/members/#member_id/nick.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { getPermission, Member, PermissionResolvable } from "@fosscord/util"; -import { route } from "@fosscord/api"; -import { Request, Response, Router } from "express"; - -const router = Router(); - -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") { - member_id = req.user_id; - permissionString = "CHANGE_NICKNAME"; - } - - const perms = await getPermission(req.user_id, guild_id); - perms.hasThrow(permissionString); - - await Member.changeNickname(member_id, guild_id, req.body.nick); - res.status(200).send(); -}); - -export default router; 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 deleted file mode 100644 index 8f5ca7ba..00000000 --- a/api/src/routes/guilds/#guild_id/members/#member_id/roles/#role_id/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { getPermission, Member } from "@fosscord/util"; -import { route } from "@fosscord/api"; -import { Request, Response, Router } from "express"; - -const router = Router(); - -router.delete("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { - const { guild_id, role_id, member_id } = req.params; - - await Member.removeRole(member_id, guild_id, role_id); - res.sendStatus(204); -}); - -router.put("/", route({ permission: "MANAGE_ROLES" }), async (req: Request, res: Response) => { - const { guild_id, role_id, member_id } = req.params; - - await Member.addRole(member_id, guild_id, role_id); - res.sendStatus(204); -}); - -export default router; diff --git a/api/src/routes/guilds/#guild_id/members/index.ts b/api/src/routes/guilds/#guild_id/members/index.ts deleted file mode 100644 index b730a4e7..00000000 --- a/api/src/routes/guilds/#guild_id/members/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Request, Response, Router } from "express"; -import { Guild, Member, PublicMemberProjection } from "@fosscord/util"; -import { route } from "@fosscord/api"; -import { MoreThan } from "typeorm"; -import { HTTPError } from "lambert-server"; - -const router = Router(); - -// TODO: send over websocket -// TODO: check for GUILD_MEMBERS intent - -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, - order: { id: "ASC" } - }); - - return res.json(members); -}); - -export default router; diff --git a/api/src/routes/guilds/#guild_id/premium.ts b/api/src/routes/guilds/#guild_id/premium.ts deleted file mode 100644 index 75361ac6..00000000 --- a/api/src/routes/guilds/#guild_id/premium.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Router, Request, Response } from "express"; -import { route } from "@fosscord/api"; -const router = Router(); - -router.get("/subscriptions", route({}), async (req: Request, res: Response) => { - // TODO: - res.json([]); -}); - -export default router; diff --git a/api/src/routes/guilds/#guild_id/prune.ts b/api/src/routes/guilds/#guild_id/prune.ts deleted file mode 100644 index 0e587d22..00000000 --- a/api/src/routes/guilds/#guild_id/prune.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Router, Request, Response } from "express"; -import { Guild, Member, Snowflake } from "@fosscord/util"; -import { LessThan, IsNull } from "typeorm"; -import { route } from "@fosscord/api"; -const router = Router(); - -//Returns all inactive members, respecting role hierarchy -export const inactiveMembers = async (guild_id: string, user_id: string, days: number, roles: string[] = []) => { - var date = new Date(); - date.setDate(date.getDate() - days); - //Snowflake should have `generateFromTime` method? Or similar? - var minId = BigInt(date.valueOf() - Snowflake.EPOCH) << BigInt(22); - - /** - idea: ability to customise the cutoff variable - possible candidates: public read receipt, last presence, last VC leave - **/ - var members = await Member.find({ - where: [ - { - guild_id, - last_message_id: LessThan(minId.toString()) - }, - { - last_message_id: IsNull() - } - ], - relations: ["roles"] - }); - console.log(members); - if (!members.length) return []; - - //I'm sure I can do this in the above db query ( and it would probably be better to do so ), but oh well. - if (roles.length && members.length) members = members.filter((user) => user.roles?.some((role) => roles.includes(role.id))); - - const me = await Member.findOneOrFail({ id: user_id, guild_id }, { relations: ["roles"] }); - const myHighestRole = Math.max(...(me.roles?.map((x) => x.position) || [])); - - const guild = await Guild.findOneOrFail({ where: { id: guild_id } }); - - members = members.filter( - (member) => - member.id !== guild.owner_id && //can't kick owner - member.roles?.some( - (role) => - role.position < myHighestRole || //roles higher than me can't be kicked - me.id === guild.owner_id //owner can kick anyone - ) - ); - - return members; -}; - -router.get("/", route({}), async (req: Request, res: Response) => { - const days = parseInt(req.query.days as string); - - var roles = req.query.include_roles; - if (typeof roles === "string") roles = [roles]; //express will return array otherwise - - const members = await inactiveMembers(req.params.guild_id, req.user_id, days, roles as string[]); - - res.send({ pruned: members.length }); -}); - -export interface PruneSchema { - /** - * @min 0 - */ - days: number; -} - -router.post("/", route({ permission: "KICK_MEMBERS", right: "KICK_BAN_MEMBERS" }), async (req: Request, res: Response) => { - const days = parseInt(req.body.days); - - var roles = req.query.include_roles; - if (typeof roles === "string") roles = [roles]; - - const { guild_id } = req.params; - const members = await inactiveMembers(guild_id, req.user_id, days, roles as string[]); - - await Promise.all(members.map((x) => Member.removeFromGuild(x.id, guild_id))); - - res.send({ purged: members.length }); -}); - -export default router; diff --git a/api/src/routes/guilds/#guild_id/regions.ts b/api/src/routes/guilds/#guild_id/regions.ts deleted file mode 100644 index 75d24fd1..00000000 --- a/api/src/routes/guilds/#guild_id/regions.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Config, Guild, Member } from "@fosscord/util"; -import { Request, Response, Router } from "express"; -import { getVoiceRegions, route } from "@fosscord/api"; -import { getIpAdress } from "@fosscord/api"; - -const router = Router(); - -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 - return res.json(await getVoiceRegions(getIpAdress(req), guild.features.includes("VIP_REGIONS"))); -}); - -export default router; diff --git a/api/src/routes/guilds/#guild_id/roles/#role_id/index.ts b/api/src/routes/guilds/#guild_id/roles/#role_id/index.ts deleted file mode 100644 index 2ad01682..00000000 --- a/api/src/routes/guilds/#guild_id/roles/#role_id/index.ts +++ /dev/null @@ -1,68 +0,0 @@ -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/#guild_id/roles/index.ts b/api/src/routes/guilds/#guild_id/roles/index.ts deleted file mode 100644 index 53465105..00000000 --- a/api/src/routes/guilds/#guild_id/roles/index.ts +++ /dev/null @@ -1,111 +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.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/#guild_id/stickers.ts b/api/src/routes/guilds/#guild_id/stickers.ts deleted file mode 100644 index 4ea1dce1..00000000 --- a/api/src/routes/guilds/#guild_id/stickers.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { - emitEvent, - GuildStickersUpdateEvent, - handleFile, - Member, - Snowflake, - Sticker, - StickerFormatType, - StickerType, - uploadFile -} from "@fosscord/util"; -import { Router, Request, Response } from "express"; -import { route } from "@fosscord/api"; -import multer from "multer"; -import { HTTPError } from "lambert-server"; -const router = Router(); - -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - await Member.IsInGuildOrFail(req.user_id, guild_id); - - res.json(await Sticker.find({ guild_id })); -}); - -const bodyParser = multer({ - limits: { - fileSize: 1024 * 1024 * 100, - fields: 10, - files: 1 - }, - storage: multer.memoryStorage() -}).single("file"); - -router.post( - "/", - bodyParser, - route({ permission: "MANAGE_EMOJIS_AND_STICKERS", body: "ModifyGuildStickerSchema" }), - async (req: Request, res: Response) => { - if (!req.file) throw new HTTPError("missing file"); - - const { guild_id } = req.params; - const body = req.body as ModifyGuildStickerSchema; - const id = Snowflake.generate(); - - const [sticker] = await Promise.all([ - new Sticker({ - ...body, - guild_id, - id, - type: StickerType.GUILD, - format_type: getStickerFormat(req.file.mimetype), - available: true - }).save(), - uploadFile(`/stickers/${id}`, req.file) - ]); - - await sendStickerUpdateEvent(guild_id); - - res.json(sticker); - } -); - -export function getStickerFormat(mime_type: string) { - switch (mime_type) { - case "image/apng": - return StickerFormatType.APNG; - case "application/json": - return StickerFormatType.LOTTIE; - case "image/png": - return StickerFormatType.PNG; - case "image/gif": - return StickerFormatType.GIF; - default: - throw new HTTPError("invalid sticker format: must be png, apng or lottie"); - } -} - -router.get("/:sticker_id", route({}), async (req: Request, res: Response) => { - const { guild_id, sticker_id } = req.params; - await Member.IsInGuildOrFail(req.user_id, guild_id); - - res.json(await Sticker.findOneOrFail({ guild_id, id: sticker_id })); -}); - -export interface ModifyGuildStickerSchema { - /** - * @minLength 2 - * @maxLength 30 - */ - name: string; - /** - * @maxLength 100 - */ - description?: string; - /** - * @maxLength 200 - */ - tags: string; -} - -router.patch( - "/:sticker_id", - route({ body: "ModifyGuildStickerSchema", permission: "MANAGE_EMOJIS_AND_STICKERS" }), - async (req: Request, res: Response) => { - const { guild_id, sticker_id } = req.params; - const body = req.body as ModifyGuildStickerSchema; - - const sticker = await new Sticker({ ...body, guild_id, id: sticker_id }).save(); - await sendStickerUpdateEvent(guild_id); - - return res.json(sticker); - } -); - -async function sendStickerUpdateEvent(guild_id: string) { - return emitEvent({ - event: "GUILD_STICKERS_UPDATE", - guild_id: guild_id, - data: { - guild_id: guild_id, - stickers: await Sticker.find({ guild_id: guild_id }) - } - } as GuildStickersUpdateEvent); -} - -router.delete("/:sticker_id", route({ permission: "MANAGE_EMOJIS_AND_STICKERS" }), async (req: Request, res: Response) => { - const { guild_id, sticker_id } = req.params; - - await Sticker.delete({ guild_id, id: sticker_id }); - await sendStickerUpdateEvent(guild_id); - - return res.sendStatus(204); -}); - -export default router; diff --git a/api/src/routes/guilds/#guild_id/templates.ts b/api/src/routes/guilds/#guild_id/templates.ts deleted file mode 100644 index 5179e761..00000000 --- a/api/src/routes/guilds/#guild_id/templates.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Request, Response, Router } from "express"; -import { Guild, Template } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { route } from "@fosscord/api"; -import { generateCode } from "@fosscord/api"; - -const router: Router = Router(); - -const TemplateGuildProjection: (keyof Guild)[] = [ - "name", - "description", - "region", - "verification_level", - "default_message_notifications", - "explicit_content_filter", - "preferred_locale", - "afk_timeout", - "roles", - // "channels", - "afk_channel_id", - "system_channel_id", - "system_channel_flags", - "icon" -]; - -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 }); - - return res.json(templates); -}); - -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 exists = await Template.findOneOrFail({ id: guild_id }).catch((e) => {}); - if (exists) throw new HTTPError("Template already exists", 400); - - const template = await new Template({ - ...req.body, - code: generateCode(), - creator_id: req.user_id, - created_at: new Date(), - updated_at: new Date(), - source_guild_id: guild_id, - serialized_source_guild: guild - }).save(); - - res.json(template); -}); - -router.delete("/:code", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { - const { code, guild_id } = req.params; - - const template = await Template.delete({ - code, - source_guild_id: guild_id - }); - - res.json(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 template = await new Template({ code, serialized_source_guild: guild }).save(); - - res.json(template); -}); - -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 template = await new Template({ code, name: name, description: description, source_guild_id: guild_id }).save(); - - res.json(template); -}); - -export default router; diff --git a/api/src/routes/guilds/#guild_id/vanity-url.ts b/api/src/routes/guilds/#guild_id/vanity-url.ts deleted file mode 100644 index 29cd25e2..00000000 --- a/api/src/routes/guilds/#guild_id/vanity-url.ts +++ /dev/null @@ -1,66 +0,0 @@ -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"; - -const router = Router(); - -const InviteRegex = /\W/g; - -router.get("/", route({ permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { - const { guild_id } = req.params; - const guild = await Guild.findOneOrFail({ id: guild_id }); - - if (!guild.features.includes("ALIASABLE_NAMES")) { - const invite = await Invite.findOne({ where: { guild_id: guild_id, vanity_url: true } }); - if (!invite) return res.json({ code: null }); - - return res.json({ code: invite.code, uses: invite.uses }); - } else { - const invite = await Invite.find({ where: { guild_id: guild_id, vanity_url: true } }); - if (!invite || invite.length == 0) return res.json({ code: null }); - - return res.json(invite.map((x) => ({ code: x.code, uses: x.uses }))); - } -}); - -export interface VanityUrlSchema { - /** - * @minLength 1 - * @maxLength 20 - */ - code?: string; -} - -router.patch("/", route({ body: "VanityUrlSchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { - const { guild_id } = req.params; - const body = req.body as VanityUrlSchema; - const code = body.code?.replace(InviteRegex, ""); - - const guild = await Guild.findOneOrFail({ id: guild_id }); - if (!guild.features.includes("VANITY_URL")) throw new HTTPError("Your guild doesn't support vanity urls"); - - if (!code || code.length === 0) throw new HTTPError("Code cannot be null or empty"); - - const invite = await Invite.findOne({ code }); - if (invite) throw new HTTPError("Invite already exists"); - - const { id } = await Channel.findOneOrFail({ guild_id, type: ChannelType.GUILD_TEXT }); - - await new Invite({ - vanity_url: true, - code: code, - temporary: false, - uses: 0, - max_uses: 0, - max_age: 0, - created_at: new Date(), - expires_at: new Date(), - guild_id: guild_id, - channel_id: id - }).save(); - - return res.json({ code: code }); -}); - -export default router; 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 deleted file mode 100644 index f9fbea54..00000000 --- a/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Channel, ChannelType, DiscordApiErrors, emitEvent, getPermission, VoiceState, VoiceStateUpdateEvent } from "@fosscord/util"; -import { route } from "@fosscord/api"; -import { Request, Response, Router } from "express"; - -const router = Router(); -//TODO need more testing when community guild and voice stage channel are working - -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; - 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; diff --git a/api/src/routes/guilds/#guild_id/webhooks.ts b/api/src/routes/guilds/#guild_id/webhooks.ts deleted file mode 100644 index 8b2febea..00000000 --- a/api/src/routes/guilds/#guild_id/webhooks.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Router, Response, Request } from "express"; -import { Channel, ChannelUpdateEvent, getPermission, emitEvent } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { route } from "@fosscord/api"; -import { ChannelModifySchema } from "../../channels/#channel_id"; -const router = Router(); - -//TODO: implement webhooks -router.get("/", route({}), async (req: Request, res: Response) => { - res.json([]); -}); -export default router; diff --git a/api/src/routes/guilds/#guild_id/welcome_screen.ts b/api/src/routes/guilds/#guild_id/welcome_screen.ts deleted file mode 100644 index 7141f17e..00000000 --- a/api/src/routes/guilds/#guild_id/welcome_screen.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Request, Response, Router } from "express"; -import { Guild, getPermission, Snowflake, Member } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { route } from "@fosscord/api"; - -const router: Router = Router(); - -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("/", 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 }); - - 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; - if (body.enabled != null) guild.welcome_screen.enabled = body.enabled; - - res.sendStatus(204); -}); - -export default router; diff --git a/api/src/routes/guilds/#guild_id/widget.json.ts b/api/src/routes/guilds/#guild_id/widget.json.ts deleted file mode 100644 index c31519fa..00000000 --- a/api/src/routes/guilds/#guild_id/widget.json.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Request, Response, Router } from "express"; -import { Config, Permissions, Guild, Invite, Channel, Member } from "@fosscord/util"; -import { HTTPError } from "lambert-server"; -import { random, route } from "@fosscord/api"; - -const router: Router = Router(); - -// Undocumented API notes: -// An invite is created for the widget_channel_id on request (only if an existing one created by the widget doesn't already exist) -// This invite created doesn't include an inviter object like user created ones and has a default expiry of 24 hours -// Missing user object information is intentional (https://github.com/discord/discord-api-docs/issues/1287) -// channels returns voice channel objects where @everyone has the CONNECT permission -// members (max 100 returned) is a sample of all members, and bots par invisible status, there exists some alphabetical distribution pattern between the members returned - -// 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("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - - const guild = await Guild.findOneOrFail({ id: guild_id }); - if (!guild.widget_enabled) throw new HTTPError("Widget Disabled", 404); - - // Fetch existing widget invite for widget channel - var invite = await Invite.findOne({ channel_id: guild.widget_channel_id }); - - if (guild.widget_channel_id && !invite) { - // Create invite for channel if none exists - // TODO: Refactor invite create code to a shared function - const max_age = 86400; // 24 hours - const expires_at = new Date(max_age * 1000 + Date.now()); - const body = { - code: random(), - temporary: false, - uses: 0, - max_uses: 0, - max_age: max_age, - expires_at, - created_at: new Date(), - guild_id, - channel_id: guild.widget_channel_id, - inviter_id: null - }; - - invite = await new Invite(body).save(); - } - - // Fetch voice channels, and the @everyone permissions object - const channels = [] as any[]; - - (await Channel.find({ where: { guild_id: guild_id, type: 2 }, order: { position: "ASC" } })).filter((doc) => { - // Only return channels where @everyone has the CONNECT permission - if ( - doc.permission_overwrites === undefined || - Permissions.channelPermission(doc.permission_overwrites, Permissions.FLAGS.CONNECT) === Permissions.FLAGS.CONNECT - ) { - channels.push({ - id: doc.id, - name: doc.name, - position: doc.position - }); - } - }); - - // Fetch members - // TODO: Understand how Discord's max 100 random member sample works, and apply to here (see top of this file) - let members = await Member.find({ guild_id: guild_id }); - - // Construct object to respond with - const data = { - id: guild_id, - name: guild.name, - instant_invite: invite?.code, - channels: channels, - members: members, - presence_count: guild.presence_count - }; - - res.set("Cache-Control", "public, max-age=300"); - return res.json(data); -}); - -export default router; diff --git a/api/src/routes/guilds/#guild_id/widget.png.ts b/api/src/routes/guilds/#guild_id/widget.png.ts deleted file mode 100644 index 4c82b740..00000000 --- a/api/src/routes/guilds/#guild_id/widget.png.ts +++ /dev/null @@ -1,111 +0,0 @@ -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"; - -const router: Router = Router(); - -// TODO: use svg templates instead of node-canvas for improved performance and to change it easily - -// https://discord.com/developers/docs/resources/guild#get-guild-widget-image -// TODO: Cache the response -router.get("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - - const guild = await Guild.findOneOrFail({ id: guild_id }); - if (!guild.widget_enabled) throw new HTTPError("Unknown Guild", 404); - - // Fetch guild information - const icon = guild.icon; - const name = guild.name; - const presence = guild.presence_count + " ONLINE"; - - // Fetch parameter - const style = req.query.style?.toString() || "shield"; - if (!["shield", "banner1", "banner2", "banner3", "banner4"].includes(style)) { - throw new HTTPError("Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", 400); - } - - // Setup canvas - const { createCanvas } = require("canvas"); - const { loadImage } = require("canvas"); - const sizeOf = require("image-size"); - - // TODO: Widget style templates need Fosscord branding - const source = path.join(__dirname, "..", "..", "..", "..", "assets", "widget", `${style}.png`); - if (!fs.existsSync(source)) { - throw new HTTPError("Widget template does not exist.", 400); - } - - // Create base template image for parameter - const { width, height } = await sizeOf(source); - const canvas = createCanvas(width, height); - const ctx = canvas.getContext("2d"); - const template = await loadImage(source); - ctx.drawImage(template, 0, 0); - - // Add the guild specific information to the template asset image - switch (style) { - case "shield": - ctx.textAlign = "center"; - await drawText(ctx, 73, 13, "#FFFFFF", "thin 10px Verdana", presence); - break; - case "banner1": - if (icon) await drawIcon(ctx, 20, 27, 50, icon); - await drawText(ctx, 83, 51, "#FFFFFF", "12px Verdana", name, 22); - await drawText(ctx, 83, 66, "#C9D2F0FF", "thin 11px Verdana", presence); - break; - case "banner2": - if (icon) await drawIcon(ctx, 13, 19, 36, icon); - await drawText(ctx, 62, 34, "#FFFFFF", "12px Verdana", name, 15); - await drawText(ctx, 62, 49, "#C9D2F0FF", "thin 11px Verdana", presence); - break; - case "banner3": - if (icon) await drawIcon(ctx, 20, 20, 50, icon); - await drawText(ctx, 83, 44, "#FFFFFF", "12px Verdana", name, 27); - await drawText(ctx, 83, 58, "#C9D2F0FF", "thin 11px Verdana", presence); - break; - case "banner4": - if (icon) await drawIcon(ctx, 21, 136, 50, icon); - await drawText(ctx, 84, 156, "#FFFFFF", "13px Verdana", name, 27); - await drawText(ctx, 84, 171, "#C9D2F0FF", "thin 12px Verdana", presence); - break; - default: - throw new HTTPError("Value must be one of ('shield', 'banner1', 'banner2', 'banner3', 'banner4').", 400); - } - - // Return final image - const buffer = canvas.toBuffer("image/png"); - res.set("Content-Type", "image/png"); - res.set("Cache-Control", "public, max-age=3600"); - return res.send(buffer); -}); - -async function drawIcon(canvas: any, x: number, y: number, scale: number, icon: string) { - // @ts-ignore - const img = new require("canvas").Image(); - img.src = icon; - - // Do some canvas clipping magic! - canvas.save(); - canvas.beginPath(); - - const r = scale / 2; // use scale to determine radius - canvas.arc(x + r, y + r, r, 0, 2 * Math.PI, false); // start circle at x, and y coords + radius to find center - - canvas.clip(); - canvas.drawImage(img, x, y, scale, scale); - - canvas.restore(); -} - -async function drawText(canvas: any, x: number, y: number, color: string, font: string, text: string, maxcharacters?: number) { - canvas.fillStyle = color; - canvas.font = font; - if (text.length > (maxcharacters || 0) && maxcharacters) text = text.slice(0, maxcharacters) + "..."; - canvas.fillText(text, x, y); -} - -export default router; diff --git a/api/src/routes/guilds/#guild_id/widget.ts b/api/src/routes/guilds/#guild_id/widget.ts deleted file mode 100644 index 2640618d..00000000 --- a/api/src/routes/guilds/#guild_id/widget.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Request, Response, Router } from "express"; -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("/", route({}), async (req: Request, res: Response) => { - const { guild_id } = req.params; - - 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("/", route({ body: "WidgetModifySchema", permission: "MANAGE_GUILD" }), async (req: Request, res: Response) => { - const body = req.body as WidgetModifySchema; - const { guild_id } = req.params; - - 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 - - return res.json(body); -}); - -export default router; diff --git a/api/src/routes/guilds/index.ts b/api/src/routes/guilds/index.ts deleted file mode 100644 index 10721413..00000000 --- a/api/src/routes/guilds/index.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Router, Request, Response } from "express"; -import { Role, Guild, Snowflake, Config, getRights, 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("/", route({ body: "GuildCreateSchema", right: "CREATE_GUILDS" }), async (req: Request, res: Response) => { - const body = req.body as GuildCreateSchema; - - const { maxGuilds } = Config.get().limits.user; - const guild_count = await Member.count({ id: req.user_id }); - const rights = await getRights(req.user_id); - if ((guild_count >= maxGuilds)&&!rights.has("MANAGE_GUILDS")) { - throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds); - } - - const guild = await Guild.createGuild({ ...body, owner_id: req.user_id }); - - const { autoJoin } = Config.get().guild; - if (autoJoin.enabled && !autoJoin.guilds?.length) { - // @ts-ignore - await Config.set({ guild: { autoJoin: { guilds: [guild.id] } } }); - } - - await Member.addToGuild(req.user_id, guild.id); - - res.status(201).json({ id: guild.id }); -}); - -export default router; diff --git a/api/src/routes/guilds/templates/index.ts b/api/src/routes/guilds/templates/index.ts deleted file mode 100644 index 3d922e85..00000000 --- a/api/src/routes/guilds/templates/index.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Request, Response, Router } from "express"; -import { Template, Guild, Role, Snowflake, Config, User, Member } from "@fosscord/util"; -import { route } from "@fosscord/api"; -import { DiscordApiErrors } from "@fosscord/util"; -import fetch from "node-fetch"; -const router: Router = Router(); - -export interface GuildTemplateCreateSchema { - name: string; - avatar?: string | null; -} - -router.get("/:code", route({}), async (req: Request, res: Response) => { - const { allowDiscordTemplates, allowRaws, enabled } = Config.get().templates; - if (!enabled) res.json({ code: 403, message: "Template creation & usage is disabled on this instance." }).sendStatus(403); - - const { code } = req.params; - - if (code.startsWith("discord:")) { - if (!allowDiscordTemplates) return res.json({ code: 403, message: "Discord templates cannot be used on this instance." }).sendStatus(403); - const discordTemplateID = code.split("discord:", 2)[1]; - - const discordTemplateData = await fetch(`https://discord.com/api/v9/guilds/templates/${discordTemplateID}`, { - method: "get", - headers: { "Content-Type": "application/json" } - }); - return res.json(await discordTemplateData.json()); - } - - if (code.startsWith("external:")) { - if (!allowRaws) return res.json({ code: 403, message: "Importing raws is disabled on this instance." }).sendStatus(403); - - return res.json(code.split("external:", 2)[1]); - } - - const template = await Template.findOneOrFail({ code: code }); - res.json(template); -}); - -router.post("/:code", route({ body: "GuildTemplateCreateSchema" }), async (req: Request, res: Response) => { - const { enabled, allowTemplateCreation, allowDiscordTemplates, allowRaws } = Config.get().templates; - if (!enabled) return res.json({ code: 403, message: "Template creation & usage is disabled on this instance." }).sendStatus(403); - if (!allowTemplateCreation) return res.json({ code: 403, message: "Template creation is disabled on this instance." }).sendStatus(403); - - const { code } = req.params; - const body = req.body as GuildTemplateCreateSchema; - - const { maxGuilds } = Config.get().limits.user; - - const guild_count = await Member.count({ id: req.user_id }); - if (guild_count >= maxGuilds) { - throw DiscordApiErrors.MAXIMUM_GUILDS.withParams(maxGuilds); - } - - const template = await Template.findOneOrFail({ code: code }); - - const guild_id = Snowflake.generate(); - - const [guild, role] = await Promise.all([ - new Guild({ - ...body, - ...template.serialized_source_guild, - id: guild_id, - owner_id: req.user_id - }).save(), - new Role({ - id: guild_id, - guild_id: guild_id, - color: 0, - hoist: false, - managed: true, - mentionable: true, - name: "@everyone", - permissions: BigInt("2251804225"), - position: 0, - tags: null - }).save() - ]); - - await Member.addToGuild(req.user_id, guild_id); - - res.status(201).json({ id: guild.id }); -}); - -export default router; |