feat(voice): add audio bitrate guild features and 96 kbps cap (#2703)

This commit is contained in:
Hampus
2026-09-11 22:00:15 +02:00
committed by GitHub
parent cfa20b7093
commit 258fe6f742
24 changed files with 358 additions and 61 deletions
@@ -7,7 +7,7 @@ import {
GUILD_TEXT_BASED_CHANNEL_TYPES,
Permissions,
} from '@fluxer/constants/src/ChannelConstants';
import {ContentWarningLevel, GuildFeatures} from '@fluxer/constants/src/GuildConstants';
import {ContentWarningLevel, clampVoiceChannelBitrate, GuildFeatures} from '@fluxer/constants/src/GuildConstants';
import {MAX_CHANNELS_PER_CATEGORY} from '@fluxer/constants/src/LimitConstants';
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
import {InvalidChannelTypeError} from '@fluxer/errors/src/domains/channel/InvalidChannelTypeError';
@@ -260,13 +260,17 @@ export class ChannelOperationsService {
validateCapacity: requestedParentId !== null && requestedParentId !== (channel.parentId ?? null),
});
}
let nextBitrate = channel.bitrate;
if (data.bitrate !== undefined && channel.type === ChannelTypes.GUILD_VOICE) {
nextBitrate = data.bitrate === null ? null : clampVoiceChannelBitrate(data.bitrate, guild.features ?? []);
}
const updatedChannelData = {
...channel.toRow(),
name: channelName,
topic: data.topic !== undefined ? data.topic : channel.topic,
url: data.url !== undefined && channel.type === ChannelTypes.GUILD_LINK ? data.url : channel.url,
parent_id: requestedParentId,
bitrate: data.bitrate !== undefined && channel.type === ChannelTypes.GUILD_VOICE ? data.bitrate : channel.bitrate,
bitrate: nextBitrate,
user_limit:
data.user_limit !== undefined && channel.type === ChannelTypes.GUILD_VOICE
? data.user_limit
@@ -2,7 +2,7 @@
import {AuditLogActionType} from '@fluxer/constants/src/AuditLogActionType';
import {ALL_PERMISSIONS, ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants';
import {ContentWarningLevel, GuildFeatures} from '@fluxer/constants/src/GuildConstants';
import {ContentWarningLevel, GuildFeatures, resolveVoiceChannelBitrate} from '@fluxer/constants/src/GuildConstants';
import {
MAX_CHANNELS_PER_CATEGORY,
MAX_GUILD_CHANNELS,
@@ -119,12 +119,16 @@ export class ChannelOperationsService {
);
}
let channelName = params.data.name;
if (params.data.type === ChannelTypes.GUILD_TEXT) {
let guildFeatures: Array<string> | null = null;
if (params.data.type === ChannelTypes.GUILD_TEXT || params.data.type === ChannelTypes.GUILD_VOICE) {
const guildData = await this.gatewayService.getGuildData({
guildId: params.guildId,
userId: params.userId,
});
const hasFlexibleNamesEnabled = guildData.features.includes(GuildFeatures.TEXT_CHANNEL_FLEXIBLE_NAMES);
guildFeatures = guildData.features;
}
if (params.data.type === ChannelTypes.GUILD_TEXT) {
const hasFlexibleNamesEnabled = (guildFeatures ?? []).includes(GuildFeatures.TEXT_CHANNEL_FLEXIBLE_NAMES);
if (!hasFlexibleNamesEnabled) {
channelName = ChannelNameType.parse(channelName);
}
@@ -156,7 +160,10 @@ export class ChannelOperationsService {
content_warning_level: requestedContentWarningLevel,
content_warning_text: requestedContentWarningText,
rate_limit_per_user: params.data.rate_limit_per_user ?? 0,
bitrate: params.data.type === ChannelTypes.GUILD_VOICE ? (params.data.bitrate ?? 64000) : null,
bitrate:
params.data.type === ChannelTypes.GUILD_VOICE
? resolveVoiceChannelBitrate(params.data.bitrate, guildFeatures)
: null,
user_limit: params.data.type === ChannelTypes.GUILD_VOICE ? (params.data.user_limit ?? 0) : null,
voice_connection_limit:
params.data.type === ChannelTypes.GUILD_VOICE
@@ -10,11 +10,13 @@ import {
GuildSplashCardAlignment,
GuildVerificationLevel,
JoinSourceTypes,
resolveVoiceChannelBitrate,
SystemChannelFlags,
} from '@fluxer/constants/src/GuildConstants';
import {
MAX_GUILD_CHANNELS,
MAX_GUILD_ROLES,
VOICE_CHANNEL_BITRATE_DEFAULT,
VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT,
} from '@fluxer/constants/src/LimitConstants';
import {DEFAULT_GUILD_FOLDER_ICON} from '@fluxer/constants/src/UserConstants';
@@ -934,7 +936,14 @@ export class GuildOperationsService {
addChannel(textCategoryId, ChannelTypes.GUILD_CATEGORY, DEFAULT_TEXT_CATEGORY_NAME, null, 0);
addChannel(voiceCategoryId, ChannelTypes.GUILD_CATEGORY, DEFAULT_VOICE_CATEGORY_NAME, null, 1);
addChannel(generalChannelId, ChannelTypes.GUILD_TEXT, DEFAULT_TEXT_CHANNEL_NAME, textCategoryId, 0);
addChannel(generalVoiceId, ChannelTypes.GUILD_VOICE, DEFAULT_VOICE_CHANNEL_NAME, voiceCategoryId, 0, 64000);
addChannel(
generalVoiceId,
ChannelTypes.GUILD_VOICE,
DEFAULT_VOICE_CHANNEL_NAME,
voiceCategoryId,
0,
VOICE_CHANNEL_BITRATE_DEFAULT,
);
batch.addPrepared(
GuildRoles.insert({
guild_id: guildId,
@@ -1105,7 +1114,7 @@ export class GuildOperationsService {
content_warning_level: null,
content_warning_text: null,
rate_limit_per_user: channel.rate_limit_per_user ?? 0,
bitrate: isVoice ? (channel.bitrate ?? 64000) : null,
bitrate: isVoice ? resolveVoiceChannelBitrate(channel.bitrate, null) : null,
user_limit: isVoice ? (channel.user_limit ?? 0) : null,
voice_connection_limit: isVoice
? (channel.voice_connection_limit ?? VOICE_CHANNEL_CONNECTION_LIMIT_DEFAULT)
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants';
import {GuildFeatures} from '@fluxer/constants/src/GuildConstants';
import type {ChannelResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
import {createTestAccount} from '../../auth/tests/AuthTestUtils';
@@ -25,6 +26,14 @@ describe('Guild Channel Management', () => {
afterEach(async () => {
await harness?.shutdown();
});
async function addGuildFeaturesForTesting(guildId: string, features: Array<string>): Promise<void> {
await createBuilder<{
success: boolean;
}>(harness, '')
.post(`/test/guilds/${guildId}/features`)
.body({add_features: features})
.execute();
}
describe('Channel Name Updates', () => {
test('should normalize channel name with spaces to hyphens', async () => {
const account = await createTestAccount(harness);
@@ -582,7 +591,7 @@ describe('Guild Channel Management', () => {
.expect(HTTP_STATUS.BAD_REQUEST)
.execute();
});
test('should reject bitrate above maximum (320000)', async () => {
test('should reject bitrate above maximum (384000)', async () => {
const account = await createTestAccount(harness);
const guild = await createGuild(harness, account.token, 'Test Guild');
const voiceChannel = await createChannel(
@@ -594,7 +603,7 @@ describe('Guild Channel Management', () => {
);
await createBuilder(harness, account.token)
.patch(`/channels/${voiceChannel.id}`)
.body({type: ChannelTypes.GUILD_VOICE, bitrate: 320001})
.body({type: ChannelTypes.GUILD_VOICE, bitrate: 384001})
.expect(HTTP_STATUS.BAD_REQUEST)
.execute();
});
@@ -678,7 +687,7 @@ describe('Guild Channel Management', () => {
.execute();
expect(data.bitrate).toBe(8000);
});
test('should accept maximum bitrate (320000)', async () => {
test('should clamp bitrate to 96000 without an audio bitrate feature', async () => {
const account = await createTestAccount(harness);
const guild = await createGuild(harness, account.token, 'Test Guild');
const voiceChannel = await createChannel(
@@ -690,9 +699,74 @@ describe('Guild Channel Management', () => {
);
const data = await createBuilder<ChannelResponse>(harness, account.token)
.patch(`/channels/${voiceChannel.id}`)
.body({type: ChannelTypes.GUILD_VOICE, bitrate: 320000})
.body({type: ChannelTypes.GUILD_VOICE, bitrate: 384000})
.execute();
expect(data.bitrate).toBe(320000);
expect(data.bitrate).toBe(96000);
});
test('should clamp bitrate to the feature the guild holds', async () => {
const account = await createTestAccount(harness);
const guild = await createGuild(harness, account.token, 'Test Guild');
await addGuildFeaturesForTesting(guild.id, [GuildFeatures.AUDIO_BITRATE_256_KBPS]);
const voiceChannel = await createChannel(
harness,
account.token,
guild.id,
'voice-channel',
ChannelTypes.GUILD_VOICE,
);
const data = await createBuilder<ChannelResponse>(harness, account.token)
.patch(`/channels/${voiceChannel.id}`)
.body({type: ChannelTypes.GUILD_VOICE, bitrate: 384000})
.execute();
expect(data.bitrate).toBe(256000);
});
test('should accept maximum bitrate (384000) with the 384 kbps feature', async () => {
const account = await createTestAccount(harness);
const guild = await createGuild(harness, account.token, 'Test Guild');
await addGuildFeaturesForTesting(guild.id, [GuildFeatures.AUDIO_BITRATE_384_KBPS]);
const voiceChannel = await createChannel(
harness,
account.token,
guild.id,
'voice-channel',
ChannelTypes.GUILD_VOICE,
);
const data = await createBuilder<ChannelResponse>(harness, account.token)
.patch(`/channels/${voiceChannel.id}`)
.body({type: ChannelTypes.GUILD_VOICE, bitrate: 384000})
.execute();
expect(data.bitrate).toBe(384000);
});
test('should store the default bitrate on a new voice channel', async () => {
const account = await createTestAccount(harness);
const guild = await createGuild(harness, account.token, 'Test Guild');
const voiceChannel = await createChannel(
harness,
account.token,
guild.id,
'voice-channel',
ChannelTypes.GUILD_VOICE,
);
expect(voiceChannel.bitrate).toBe(64000);
});
test('should clamp bitrate on create without an audio bitrate feature', async () => {
const account = await createTestAccount(harness);
const guild = await createGuild(harness, account.token, 'Test Guild');
const data = await createBuilder<ChannelResponse>(harness, account.token)
.post(`/guilds/${guild.id}/channels`)
.body({type: ChannelTypes.GUILD_VOICE, name: 'loud-channel', bitrate: 384000})
.execute();
expect(data.bitrate).toBe(96000);
});
test('should keep bitrate on create with the 128 kbps feature', async () => {
const account = await createTestAccount(harness);
const guild = await createGuild(harness, account.token, 'Test Guild');
await addGuildFeaturesForTesting(guild.id, [GuildFeatures.AUDIO_BITRATE_128_KBPS]);
const data = await createBuilder<ChannelResponse>(harness, account.token)
.post(`/guilds/${guild.id}/channels`)
.body({type: ChannelTypes.GUILD_VOICE, name: 'loud-channel', bitrate: 128000})
.execute();
expect(data.bitrate).toBe(128000);
});
test('should accept maximum user limit (99)', async () => {
const account = await createTestAccount(harness);
@@ -734,6 +808,7 @@ describe('Guild Channel Management', () => {
test('should update both bitrate and user limit together', async () => {
const account = await createTestAccount(harness);
const guild = await createGuild(harness, account.token, 'Test Guild');
await addGuildFeaturesForTesting(guild.id, [GuildFeatures.AUDIO_BITRATE_128_KBPS]);
const voiceChannel = await createChannel(
harness,
account.token,
+23 -17
View File
@@ -27348,8 +27348,8 @@
"description": "ID of the parent category for this channel"
},
"bitrate": {
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 320000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-320000)"
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 384000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-384000), clamped to 96000 unless the guild holds an AUDIO_BITRATE feature"
},
"user_limit": {
"anyOf": [{"type": "integer", "minimum": 0, "maximum": 99, "format": "int32"}, {"type": "null"}],
@@ -27442,8 +27442,8 @@
"description": "ID of the parent category for this channel"
},
"bitrate": {
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 320000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-320000)"
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 384000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-384000), clamped to 96000 unless the guild holds an AUDIO_BITRATE feature"
},
"user_limit": {
"anyOf": [{"type": "integer", "minimum": 0, "maximum": 99, "format": "int32"}, {"type": "null"}],
@@ -27526,8 +27526,8 @@
"description": "ID of the parent category for this channel"
},
"bitrate": {
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 320000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-320000)"
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 384000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-384000), clamped to 96000 unless the guild holds an AUDIO_BITRATE feature"
},
"user_limit": {
"anyOf": [{"type": "integer", "minimum": 0, "maximum": 99, "format": "int32"}, {"type": "null"}],
@@ -27610,8 +27610,8 @@
"description": "ID of the parent category for this channel"
},
"bitrate": {
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 320000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-320000)"
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 384000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-384000), clamped to 96000 unless the guild holds an AUDIO_BITRATE feature"
},
"user_limit": {
"anyOf": [{"type": "integer", "minimum": 0, "maximum": 99, "format": "int32"}, {"type": "null"}],
@@ -28041,6 +28041,9 @@
"x-enumNames": [
"ANIMATED_ICON",
"ANIMATED_BANNER",
"AUDIO_BITRATE_128_KBPS",
"AUDIO_BITRATE_256_KBPS",
"AUDIO_BITRATE_384_KBPS",
"BANNER",
"CLONE_EMOJI_DISABLED",
"CLONE_STICKER_DISABLED",
@@ -28071,6 +28074,9 @@
"x-enumDescriptions": [
"Guild can have an animated icon",
"Guild can have an animated banner",
"Guild can set a voice channel bitrate of up to 128 kbps",
"Guild can set a voice channel bitrate of up to 256 kbps",
"Guild can set a voice channel bitrate of up to 384 kbps",
"Guild can have a banner",
"Guild has the in-app one-click emoji clone shortcut disabled for non-members",
"Guild has the in-app one-click sticker clone shortcut disabled for non-members",
@@ -28098,7 +28104,7 @@
"Guild has large guild overrides enabled",
"Guild has increased member capacity enabled"
],
"description": "A guild feature flag Known values: ANIMATED_ICON, ANIMATED_BANNER, BANNER, CLONE_EMOJI_DISABLED, CLONE_STICKER_DISABLED, DETACHED_BANNER, INVITE_SPLASH, INVITES_DISABLED, RAID_DETECTED, TEXT_CHANNEL_FLEXIBLE_NAMES, HIDE_OWNER_CROWN, MORE_EMOJI, MORE_STICKERS, UNLIMITED_EMOJI, UNLIMITED_STICKERS, EXPRESSION_PURGE_ALLOWED, VANITY_URL, DISCOVERABLE, PARTNERED, VERIFIED, VIP_VOICE, VOICE_E2EE, UNAVAILABLE_FOR_EVERYONE, UNAVAILABLE_FOR_EVERYONE_BUT_STAFF, UNAVAILABLE_HIDDEN, VISIONARY, LARGE_GUILD_OVERRIDE, VERY_LARGE_GUILD (other values allowed)"
"description": "A guild feature flag Known values: ANIMATED_ICON, ANIMATED_BANNER, AUDIO_BITRATE_128_KBPS, AUDIO_BITRATE_256_KBPS, AUDIO_BITRATE_384_KBPS, BANNER, CLONE_EMOJI_DISABLED, CLONE_STICKER_DISABLED, DETACHED_BANNER, INVITE_SPLASH, INVITES_DISABLED, RAID_DETECTED, TEXT_CHANNEL_FLEXIBLE_NAMES, HIDE_OWNER_CROWN, MORE_EMOJI, MORE_STICKERS, UNLIMITED_EMOJI, UNLIMITED_STICKERS, EXPRESSION_PURGE_ALLOWED, VANITY_URL, DISCOVERABLE, PARTNERED, VERIFIED, VIP_VOICE, VOICE_E2EE, UNAVAILABLE_FOR_EVERYONE, UNAVAILABLE_FOR_EVERYONE_BUT_STAFF, UNAVAILABLE_HIDDEN, VISIONARY, LARGE_GUILD_OVERRIDE, VERY_LARGE_GUILD (other values allowed)"
},
"ChannelPartialResponse": {
"type": "object",
@@ -30329,8 +30335,8 @@
"description": "ID of the parent category for this channel"
},
"bitrate": {
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 320000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-320000)"
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 384000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-384000), clamped to 96000 unless the guild holds an AUDIO_BITRATE feature"
},
"user_limit": {
"anyOf": [{"type": "integer", "minimum": 0, "maximum": 99, "format": "int32"}, {"type": "null"}],
@@ -30397,8 +30403,8 @@
"description": "ID of the parent category for this channel"
},
"bitrate": {
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 320000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-320000)"
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 384000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-384000), clamped to 96000 unless the guild holds an AUDIO_BITRATE feature"
},
"user_limit": {
"anyOf": [{"type": "integer", "minimum": 0, "maximum": 99, "format": "int32"}, {"type": "null"}],
@@ -30465,8 +30471,8 @@
"description": "ID of the parent category for this channel"
},
"bitrate": {
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 320000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-320000)"
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 384000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-384000), clamped to 96000 unless the guild holds an AUDIO_BITRATE feature"
},
"user_limit": {
"anyOf": [{"type": "integer", "minimum": 0, "maximum": 99, "format": "int32"}, {"type": "null"}],
@@ -30533,8 +30539,8 @@
"description": "ID of the parent category for this channel"
},
"bitrate": {
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 320000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-320000)"
"anyOf": [{"type": "integer", "minimum": 8000, "maximum": 384000, "format": "int32"}, {"type": "null"}],
"description": "Voice channel bitrate in bits per second (8000-384000), clamped to 96000 unless the guild holds an AUDIO_BITRATE feature"
},
"user_limit": {
"anyOf": [{"type": "integer", "minimum": 0, "maximum": 99, "format": "int32"}, {"type": "null"}],