From 6f25022ae0e69ac82ff45e61a1b40baa9dc93154 Mon Sep 17 00:00:00 2001 From: AlTech98 Date: Sat, 4 Sep 2021 11:41:41 +0200 Subject: [PATCH 1/4] VOICE_SERVER_UPDATE now has the endpoint of the guild's region --- gateway/src/opcodes/VoiceStateUpdate.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/gateway/src/opcodes/VoiceStateUpdate.ts b/gateway/src/opcodes/VoiceStateUpdate.ts index fba0db1f..95a01608 100644 --- a/gateway/src/opcodes/VoiceStateUpdate.ts +++ b/gateway/src/opcodes/VoiceStateUpdate.ts @@ -2,7 +2,7 @@ import { VoiceStateUpdateSchema } from "../schema/VoiceStateUpdateSchema"; import { Payload } from "../util/Constants"; import WebSocket from "../util/WebSocket"; import { check } from "./instanceOf"; -import { Config, emitEvent, Member, VoiceServerUpdateEvent, VoiceState, VoiceStateUpdateEvent } from "@fosscord/util"; +import { Config, emitEvent, Guild, Member, Region, VoiceServerUpdateEvent, VoiceState, VoiceStateUpdateEvent } from "@fosscord/util"; import { genVoiceToken } from "../util/SessionUtils"; // TODO: check if a voice server is setup // Notice: Bot users respect the voice channel's user limit, if set. When the voice channel is full, you will not receive the Voice State Update or Voice Server Update events in response to your own Voice State Update. Having MANAGE_CHANNELS permission bypasses this limit and allows you to join regardless of the channel being full or not. @@ -11,7 +11,7 @@ export async function onVoiceStateUpdate(this: WebSocket, data: Payload) { check.call(this, VoiceStateUpdateSchema, data.d); const body = data.d as VoiceStateUpdateSchema; - let voiceState; + let voiceState: VoiceState; try { voiceState = await VoiceState.findOneOrFail({ where: { user_id: this.user_id } @@ -69,14 +69,21 @@ export async function onVoiceStateUpdate(this: WebSocket, data: Payload) { //If it's null it means that we are leaving the channel and this event is not needed if (voiceState.channel_id !== null) { + const guild = await Guild.findOne({ id: voiceState.guild_id }) const regions = Config.get().regions; + let guildRegion: Region; + if (guild && guild.region) { + guildRegion = regions.available.filter(r => (r.id === guild.region))[0] + } else { + guildRegion = regions.available.filter(r => (r.id === regions.default))[0] + } await emitEvent({ event: "VOICE_SERVER_UPDATE", data: { token: voiceState.token, guild_id: voiceState.guild_id, - endpoint: regions.available[0].endpoint, //TODO return best endpoint or default + endpoint: guildRegion.endpoint, }, guild_id: voiceState.guild_id, } as VoiceServerUpdateEvent); From aafdc5d0eeab36d04d0a0463a2c7d10727954f88 Mon Sep 17 00:00:00 2001 From: AlTech98 Date: Sat, 4 Sep 2021 11:43:09 +0200 Subject: [PATCH 2/4] Added missing permissions and channel types --- .../routes/channels/#channel_id/messages/index.ts | 4 ++++ util/src/entities/Channel.ts | 4 ++++ util/src/util/Permissions.ts | 15 ++++++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/api/src/routes/channels/#channel_id/messages/index.ts b/api/src/routes/channels/#channel_id/messages/index.ts index 1a3150cf..ad590d05 100644 --- a/api/src/routes/channels/#channel_id/messages/index.ts +++ b/api/src/routes/channels/#channel_id/messages/index.ts @@ -17,11 +17,15 @@ export function isTextChannel(type: ChannelType): boolean { switch (type) { case ChannelType.GUILD_STORE: case ChannelType.GUILD_VOICE: + case ChannelType.GUILD_STAGE_VOICE: case ChannelType.GUILD_CATEGORY: throw new HTTPError("not a text channel", 400); case ChannelType.DM: case ChannelType.GROUP_DM: case ChannelType.GUILD_NEWS: + case ChannelType.GUILD_NEWS_THREAD: + case ChannelType.GUILD_PUBLIC_THREAD: + case ChannelType.GUILD_PRIVATE_THREAD: case ChannelType.GUILD_TEXT: return true; } diff --git a/util/src/entities/Channel.ts b/util/src/entities/Channel.ts index 0be1c5ec..486b5c44 100644 --- a/util/src/entities/Channel.ts +++ b/util/src/entities/Channel.ts @@ -16,6 +16,10 @@ export enum ChannelType { GUILD_CATEGORY = 4, // an organizational category that contains up to 50 channels GUILD_NEWS = 5, // a channel that users can follow and crosspost into their own server GUILD_STORE = 6, // a channel in which game developers can sell their game on Discord + GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel + GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel + GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission + GUILD_STAGE_VOICE = 13, // a voice channel for hosting events with an audience } @Entity("channels") diff --git a/util/src/util/Permissions.ts b/util/src/util/Permissions.ts index ab8dd9b1..9cac0226 100644 --- a/util/src/util/Permissions.ts +++ b/util/src/util/Permissions.ts @@ -46,7 +46,13 @@ type PermissionString = | "MANAGE_NICKNAMES" | "MANAGE_ROLES" | "MANAGE_WEBHOOKS" - | "MANAGE_EMOJIS_AND_STICKERS"; + | "MANAGE_EMOJIS_AND_STICKERS" + | "USE_APPLICATION_COMMANDS" + | "REQUEST_TO_SPEAK" + | "MANAGE_THREADS" + | "USE_PUBLIC_THREADS" + | "USE_PRIVATE_THREADS" + | "USE_EXTERNAL_STICKERS"; const CUSTOM_PERMISSION_OFFSET = BigInt(1) << BigInt(48); // 16 free custom permission bits, and 16 for discord to add new ones @@ -85,6 +91,13 @@ export class Permissions extends BitField { MANAGE_ROLES: BigInt(1) << BigInt(28), MANAGE_WEBHOOKS: BigInt(1) << BigInt(29), MANAGE_EMOJIS_AND_STICKERS: BigInt(1) << BigInt(30), + USE_APPLICATION_COMMANDS: BigInt(1) << BigInt(31), + REQUEST_TO_SPEAK: BigInt(1) << BigInt(32), + MANAGE_THREADS: BigInt(1) << BigInt(34), + USE_PUBLIC_THREADS: BigInt(1) << BigInt(35), + USE_PRIVATE_THREADS: BigInt(1) << BigInt(36), + USE_EXTERNAL_STICKERS: BigInt(1) << BigInt(37), + /** * CUSTOM PERMISSIONS ideas: * - allow user to dm members From 58fb2d8a9187fb9f43a0df53fd5f36a75d02cc83 Mon Sep 17 00:00:00 2001 From: AlTech98 Date: Sat, 4 Sep 2021 11:46:46 +0200 Subject: [PATCH 3/4] Added /guilds/:id/voice-states apis --- .../#guild_id/voice-states/#user_id/index.ts | 15 ++++++ .../#guild_id/voice-states/@me/index.ts | 15 ++++++ api/src/schema/Guild.ts | 12 +++++ api/src/util/VoiceState.ts | 54 +++++++++++++++++++ 4 files changed, 96 insertions(+) create mode 100644 api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts create mode 100644 api/src/routes/guilds/#guild_id/voice-states/@me/index.ts create mode 100644 api/src/util/VoiceState.ts 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 new file mode 100644 index 00000000..02951f81 --- /dev/null +++ b/api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts @@ -0,0 +1,15 @@ +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, user_id } = req.params; + await updateVoiceState(body, guild_id, req.user_id, user_id) + return res.sendStatus(204); +}); + +export default router; \ No newline at end of file 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 new file mode 100644 index 00000000..42ba543e --- /dev/null +++ b/api/src/routes/guilds/#guild_id/voice-states/@me/index.ts @@ -0,0 +1,15 @@ +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/schema/Guild.ts b/api/src/schema/Guild.ts index 7c96905e..29c78ab0 100644 --- a/api/src/schema/Guild.ts +++ b/api/src/schema/Guild.ts @@ -92,3 +92,15 @@ export interface GuildUpdateWelcomeScreenSchema { enabled?: boolean; description?: string; } + +export const VoiceStateUpdateSchema = { + channel_id: String, // Snowflake + $suppress: Boolean, + $request_to_speak_timestamp: String // ISO8601 timestamp +}; + +export interface VoiceStateUpdateSchema { + channel_id: string; // Snowflake + suppress?: boolean; + request_to_speak_timestamp?: string // ISO8601 timestamp +} diff --git a/api/src/util/VoiceState.ts b/api/src/util/VoiceState.ts new file mode 100644 index 00000000..07022ec9 --- /dev/null +++ b/api/src/util/VoiceState.ts @@ -0,0 +1,54 @@ +import { Channel, ChannelType, DiscordApiErrors, emitEvent, getPermission, VoiceState, VoiceStateUpdateEvent } from "@fosscord/util"; +import { VoiceStateUpdateSchema } from "../schema"; + + +//TODO need more testing when community guild and voice stage channel are working +export async function updateVoiceState(vsuSchema: VoiceStateUpdateSchema, guildId: string, userId: string, targetUserId?: string) { + const perms = await getPermission(userId, guildId, vsuSchema.channel_id); + + /* + From https://discord.com/developers/docs/resources/guild#modify-current-user-voice-state + You must have the MUTE_MEMBERS permission to unsuppress yourself. 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 (targetUserId !== undefined || (vsuSchema.suppress !== undefined && !vsuSchema.suppress)) { + perms.hasThrow("MUTE_MEMBERS"); + } + if (vsuSchema.request_to_speak_timestamp !== undefined && vsuSchema.request_to_speak_timestamp !== "") { + perms.hasThrow("REQUEST_TO_SPEAK") + } + + if (!targetUserId) { + targetUserId = userId; + } else { + if (vsuSchema.suppress !== undefined && vsuSchema.suppress) + vsuSchema.request_to_speak_timestamp = "" //Need to check if empty string is the right value + } + + //TODO assumed that empty string means clean, need to test if it's right + let voiceState + try { + voiceState = await VoiceState.findOneOrFail({ + guild_id: guildId, + channel_id: vsuSchema.channel_id, + user_id: targetUserId + }); + } catch (error) { + throw DiscordApiErrors.UNKNOWN_VOICE_STATE; + } + + voiceState.assign(vsuSchema); + const channel = await Channel.findOneOrFail({ guild_id: guildId, id: vsuSchema.channel_id }) + if (channel.type !== ChannelType.GUILD_STAGE_VOICE) { + throw DiscordApiErrors.CANNOT_EXECUTE_ON_THIS_CHANNEL_TYPE; + } + + await Promise.all([ + voiceState.save(), + emitEvent({ + event: "VOICE_STATE_UPDATE", + data: voiceState, + guild_id: guildId + } as VoiceStateUpdateEvent)]); + return; +} \ No newline at end of file From 8f002d14d56c6bb628aa45f0af00a21d1da397f0 Mon Sep 17 00:00:00 2001 From: Flam3rboy <34555296+Flam3rboy@users.noreply.github.com> Date: Sat, 4 Sep 2021 12:17:26 +0200 Subject: [PATCH 4/4] :pencil: added comments and updated type --- util/src/entities/Channel.ts | 1 + util/src/util/Permissions.ts | 42 +++--------------------------------- 2 files changed, 4 insertions(+), 39 deletions(-) diff --git a/util/src/entities/Channel.ts b/util/src/entities/Channel.ts index 486b5c44..fce85e3f 100644 --- a/util/src/entities/Channel.ts +++ b/util/src/entities/Channel.ts @@ -16,6 +16,7 @@ export enum ChannelType { GUILD_CATEGORY = 4, // an organizational category that contains up to 50 channels GUILD_NEWS = 5, // a channel that users can follow and crosspost into their own server GUILD_STORE = 6, // a channel in which game developers can sell their game on Discord + // TODO: what are channel types between 7-9? GUILD_NEWS_THREAD = 10, // a temporary sub-channel within a GUILD_NEWS channel GUILD_PUBLIC_THREAD = 11, // a temporary sub-channel within a GUILD_TEXT channel GUILD_PRIVATE_THREAD = 12, // a temporary sub-channel within a GUILD_TEXT channel that is only viewable by those invited and those with the MANAGE_THREADS permission diff --git a/util/src/util/Permissions.ts b/util/src/util/Permissions.ts index 9cac0226..628a495d 100644 --- a/util/src/util/Permissions.ts +++ b/util/src/util/Permissions.ts @@ -15,46 +15,9 @@ try { export type PermissionResolvable = bigint | number | Permissions | PermissionResolvable[] | PermissionString; -type PermissionString = - | "CREATE_INSTANT_INVITE" - | "KICK_MEMBERS" - | "BAN_MEMBERS" - | "ADMINISTRATOR" - | "MANAGE_CHANNELS" - | "MANAGE_GUILD" - | "ADD_REACTIONS" - | "VIEW_AUDIT_LOG" - | "PRIORITY_SPEAKER" - | "STREAM" - | "VIEW_CHANNEL" - | "SEND_MESSAGES" - | "SEND_TTS_MESSAGES" - | "MANAGE_MESSAGES" - | "EMBED_LINKS" - | "ATTACH_FILES" - | "READ_MESSAGE_HISTORY" - | "MENTION_EVERYONE" - | "USE_EXTERNAL_EMOJIS" - | "VIEW_GUILD_INSIGHTS" - | "CONNECT" - | "SPEAK" - | "MUTE_MEMBERS" - | "DEAFEN_MEMBERS" - | "MOVE_MEMBERS" - | "USE_VAD" - | "CHANGE_NICKNAME" - | "MANAGE_NICKNAMES" - | "MANAGE_ROLES" - | "MANAGE_WEBHOOKS" - | "MANAGE_EMOJIS_AND_STICKERS" - | "USE_APPLICATION_COMMANDS" - | "REQUEST_TO_SPEAK" - | "MANAGE_THREADS" - | "USE_PUBLIC_THREADS" - | "USE_PRIVATE_THREADS" - | "USE_EXTERNAL_STICKERS"; +type PermissionString = keyof typeof Permissions.FLAGS; -const CUSTOM_PERMISSION_OFFSET = BigInt(1) << BigInt(48); // 16 free custom permission bits, and 16 for discord to add new ones +const CUSTOM_PERMISSION_OFFSET = BigInt(1) << BigInt(48); // 16 free custom permission bits, and 11 for discord to add new ones export class Permissions extends BitField { cache: PermissionCache = {}; @@ -93,6 +56,7 @@ export class Permissions extends BitField { MANAGE_EMOJIS_AND_STICKERS: BigInt(1) << BigInt(30), USE_APPLICATION_COMMANDS: BigInt(1) << BigInt(31), REQUEST_TO_SPEAK: BigInt(1) << BigInt(32), + // TODO: what is permission 33? MANAGE_THREADS: BigInt(1) << BigInt(34), USE_PUBLIC_THREADS: BigInt(1) << BigInt(35), USE_PRIVATE_THREADS: BigInt(1) << BigInt(36),