summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlam3rboy <34555296+Flam3rboy@users.noreply.github.com>2021-09-04 12:18:49 +0200
committerGitHub <noreply@github.com>2021-09-04 12:18:49 +0200
commitbfdd9c10d82760f0cadc2d59aa2b467cdf14b02a (patch)
tree5477dcf75a9abf3d5d5124b319e9251728744fd1
parent:bug: fix ReadyEventData (diff)
parent:pencil: added comments and updated type (diff)
downloadserver-bfdd9c10d82760f0cadc2d59aa2b467cdf14b02a.tar.xz
Merge pull request #320 from AlTech98/master
Added /guilds/:id/voice-states/ apis, VOICE_SERVER_UPDATE fix
-rw-r--r--api/src/routes/channels/#channel_id/messages/index.ts4
-rw-r--r--api/src/routes/guilds/#guild_id/voice-states/#user_id/index.ts15
-rw-r--r--api/src/routes/guilds/#guild_id/voice-states/@me/index.ts15
-rw-r--r--api/src/schema/Guild.ts12
-rw-r--r--api/src/util/VoiceState.ts54
-rw-r--r--gateway/src/opcodes/VoiceStateUpdate.ts13
-rw-r--r--util/src/entities/Channel.ts5
-rw-r--r--util/src/util/Permissions.ts43
8 files changed, 125 insertions, 36 deletions
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/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
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);
diff --git a/util/src/entities/Channel.ts b/util/src/entities/Channel.ts
index 0be1c5ec..fce85e3f 100644
--- a/util/src/entities/Channel.ts
+++ b/util/src/entities/Channel.ts
@@ -16,6 +16,11 @@ 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
+	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..628a495d 100644
--- a/util/src/util/Permissions.ts
+++ b/util/src/util/Permissions.ts
@@ -15,40 +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";
+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 = {};
@@ -85,6 +54,14 @@ 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),
+		// 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),
+		USE_EXTERNAL_STICKERS: BigInt(1) << BigInt(37),
+
 		/**
 		 * CUSTOM PERMISSIONS ideas:
 		 * - allow user to dm members