diff --git a/fluxer_admin/openapi-admin.json b/fluxer_admin/openapi-admin.json index 2aa79f80b..6b93cc9c4 100644 --- a/fluxer_admin/openapi-admin.json +++ b/fluxer_admin/openapi-admin.json @@ -11107,7 +11107,7 @@ "category": {"type": "string"}, "scope": {"type": "string"}, "isToggle": {"type": "boolean"}, - "unit": {"type": "string", "enum": ["bytes", "count"]}, + "unit": {"type": "string", "enum": ["bytes", "count", "bps"]}, "min": {"type": "number"}, "max": {"type": "number"} }, @@ -11154,6 +11154,7 @@ "max_private_channels_per_user", "max_reactions_per_message", "max_relationships", + "max_screen_share_bitrate", "max_users_per_message_reaction", "max_voice_message_duration", "max_webhooks_per_channel", diff --git a/fluxer_admin/src/admin_hints.rs b/fluxer_admin/src/admin_hints.rs index d851630ea..c086502bb 100644 --- a/fluxer_admin/src/admin_hints.rs +++ b/fluxer_admin/src/admin_hints.rs @@ -12,6 +12,11 @@ pub fn limit_key_hint(key: &str) -> Option> { "Community creation policy", )), }), + "max_screen_share_bitrate" => Some(Hint { + name: Some("Max Screen Share Bitrate"), + body: "In bits per second, so 9000000 is 9 Mbps. Raising it also raises the automatic bitrate of every screen share quality. Accounts with Higher Video Quality can pick their own bitrate up to this value.", + link: None, + }), _ => None, } } diff --git a/fluxer_admin/src/templates/pages/limit_config.rs b/fluxer_admin/src/templates/pages/limit_config.rs index a449bf484..1f130ea66 100644 --- a/fluxer_admin/src/templates/pages/limit_config.rs +++ b/fluxer_admin/src/templates/pages/limit_config.rs @@ -28,6 +28,7 @@ const CATEGORY_ORDER: &[&str] = &[ "expressions", "files", "social", + "media", ]; pub fn limit_config_page( diff --git a/fluxer_api/src/api/admin/tests/LimitConfigAllowedRange.test.ts b/fluxer_api/src/api/admin/tests/LimitConfigAllowedRange.test.ts index 74b3e5f27..9e337007c 100644 --- a/fluxer_api/src/api/admin/tests/LimitConfigAllowedRange.test.ts +++ b/fluxer_api/src/api/admin/tests/LimitConfigAllowedRange.test.ts @@ -8,7 +8,11 @@ import {HTTP_STATUS} from '@app/api/test/TestConstants'; import {createBuilder} from '@app/api/test/TestRequestBuilder'; import {AdminACLs} from '@fluxer/constants/src/AdminACLs'; import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes'; -import {MAX_BIO_LENGTH} from '@fluxer/constants/src/LimitConstants'; +import { + MAX_BIO_LENGTH, + MAX_SCREEN_SHARE_BITRATE_CEILING, + MIN_SCREEN_SHARE_BITRATE_CEILING, +} from '@fluxer/constants/src/LimitConstants'; import type {LimitConfigGetResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas'; import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest'; import type {z} from 'zod'; @@ -59,6 +63,30 @@ describe('limit config allowed ranges', () => { expect(saved.metadata.max_bio_length).toMatchObject({min: 0, max: MAX_BIO_LENGTH}); }); + it('keeps a screen share bitrate ceiling inside its allowed range', async () => { + const admin = await createAdmin(); + const rule = (bitrate: number) => ({ + id: 'vip', + filters: {traits: ['vip']}, + limits: {max_screen_share_bitrate: bitrate}, + }); + for (const bitrate of [MIN_SCREEN_SHARE_BITRATE_CEILING - 1, MAX_SCREEN_SHARE_BITRATE_CEILING + 1]) { + await createBuilder(harness, admin.token) + .put('/admin/limit-config') + .body({limit_config: {traitDefinitions: ['vip'], rules: [rule(bitrate)]}}) + .expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_FORM_BODY) + .execute(); + } + const saved = await createBuilder(harness, admin.token) + .put('/admin/limit-config') + .body({limit_config: {traitDefinitions: ['vip'], rules: [rule(25_000_000)]}}) + .execute(); + expect(saved.limit_config.rules.find((entry) => entry.id === 'vip')?.limits).toEqual({ + max_screen_share_bitrate: 25_000_000, + }); + expect(saved.defaults.default?.max_screen_share_bitrate).toBe(9_000_000); + }); + it('reads a stored bio length above the profile maximum as the maximum', async () => { const admin = await createAdmin(); await getInstanceConfigRepository().setLimitConfig({traitDefinitions: ['vip'], rules: [vipRule(1000)]}); diff --git a/fluxer_docs/src/content/docs/http-api/instance.mdx b/fluxer_docs/src/content/docs/http-api/instance.mdx index dcbd961cf..dcf0a005d 100644 --- a/fluxer_docs/src/content/docs/http-api/instance.mdx +++ b/fluxer_docs/src/content/docs/http-api/instance.mdx @@ -385,6 +385,7 @@ Each key names one limit. A key whose name begins with `feature_` is a feature g | max_private_channels_per_user | Maximum direct message channels per user, in user scope | | max_reactions_per_message | Maximum distinct reactions per message, in guild scope | | max_relationships | Maximum friend and blocked relationships, in user scope | +| max_screen_share_bitrate | Highest screen share video bitrate in bits per second, from 1000000 to 50000000, in user scope | | max_users_per_message_reaction | Maximum users who can use the same reaction on a message, in guild scope | | max_voice_message_duration | Maximum voice message duration in seconds, in user and guild scope | | max_webhooks_per_channel | Maximum webhooks per channel, in guild scope | diff --git a/packages/constants/src/LimitConfigMetadata.ts b/packages/constants/src/LimitConfigMetadata.ts index fbcc264b9..1f028aabe 100644 --- a/packages/constants/src/LimitConfigMetadata.ts +++ b/packages/constants/src/LimitConfigMetadata.ts @@ -1,6 +1,11 @@ // SPDX-License-Identifier: AGPL-3.0-or-later -import {BASE64_IMAGE_MAX_SIZE, MAX_BIO_LENGTH} from '@fluxer/constants/src/LimitConstants'; +import { + BASE64_IMAGE_MAX_SIZE, + MAX_BIO_LENGTH, + MAX_SCREEN_SHARE_BITRATE_CEILING, + MIN_SCREEN_SHARE_BITRATE_CEILING, +} from '@fluxer/constants/src/LimitConstants'; export const LIMIT_KEYS = [ 'avatar_max_size', @@ -42,6 +47,7 @@ export const LIMIT_KEYS = [ 'max_private_channels_per_user', 'max_reactions_per_message', 'max_relationships', + 'max_screen_share_bitrate', 'max_users_per_message_reaction', 'max_voice_message_duration', 'max_webhooks_per_channel', @@ -92,6 +98,7 @@ export const LIMIT_KEY_SCOPES: Record = { max_private_channels_per_user: 'user', max_reactions_per_message: 'guild', max_relationships: 'user', + max_screen_share_bitrate: 'user', max_users_per_message_reaction: 'guild', max_voice_message_duration: 'both', max_webhooks_per_channel: 'guild', @@ -99,7 +106,7 @@ export const LIMIT_KEY_SCOPES: Record = { sticker_max_size: 'guild', }; -type LimitCategory = 'messages' | 'guilds' | 'channels' | 'expressions' | 'files' | 'social' | 'features'; +type LimitCategory = 'messages' | 'guilds' | 'channels' | 'expressions' | 'files' | 'social' | 'media' | 'features'; interface LimitKeyMetadata { key: LimitKey; @@ -108,7 +115,7 @@ interface LimitKeyMetadata { category: LimitCategory; scope: LimitScope; isToggle: boolean; - unit?: 'bytes' | 'count'; + unit?: 'bytes' | 'count' | 'bps'; min?: number; max?: number; } @@ -120,6 +127,7 @@ export const LIMIT_CATEGORY_LABELS: Record = { expressions: 'Expressions & Stickers', files: 'Files & Attachments', social: 'Social & Private', + media: 'Voice & Video', features: 'Feature Toggles', }; export const LIMIT_KEY_METADATA: Record = { @@ -437,6 +445,17 @@ export const LIMIT_KEY_METADATA: Record = { isToggle: false, unit: 'count', }, + max_screen_share_bitrate: { + key: 'max_screen_share_bitrate', + label: 'Max Screen Share Bitrate', + description: 'Highest video bitrate a screen share can send, in bits per second', + category: 'media', + scope: 'user', + isToggle: false, + unit: 'bps', + min: MIN_SCREEN_SHARE_BITRATE_CEILING, + max: MAX_SCREEN_SHARE_BITRATE_CEILING, + }, feature_animated_avatar: { key: 'feature_animated_avatar', label: 'Animated Avatar', diff --git a/packages/constants/src/LimitConstants.ts b/packages/constants/src/LimitConstants.ts index 2dd6a66f6..3a117ec06 100644 --- a/packages/constants/src/LimitConstants.ts +++ b/packages/constants/src/LimitConstants.ts @@ -72,6 +72,9 @@ export const MAX_FAVORITE_MEMES_NON_PREMIUM = 50; export const MAX_FAVORITE_MEME_TAGS = 10; export const MAX_FAVORITE_GIFS = 10000; export const MAX_VOICE_MESSAGE_DURATION = 1200; +export const MAX_SCREEN_SHARE_BITRATE = 9_000_000; +export const MIN_SCREEN_SHARE_BITRATE_CEILING = 1_000_000; +export const MAX_SCREEN_SHARE_BITRATE_CEILING = 50_000_000; export const MAX_MEDIA_DURATION_SECONDS = 86400; export const EMOJI_MAX_SIZE = ASSET_FORMAT_POLICY.emoji.maxBytes; export const STICKER_MAX_SIZE = ASSET_FORMAT_POLICY.sticker.maxBytes; diff --git a/packages/limits/src/LimitDefaults.ts b/packages/limits/src/LimitDefaults.ts index f8c246ddd..ec07d513d 100644 --- a/packages/limits/src/LimitDefaults.ts +++ b/packages/limits/src/LimitDefaults.ts @@ -33,6 +33,7 @@ import { MAX_PRIVATE_CHANNELS_PER_USER, MAX_REACTIONS_PER_MESSAGE, MAX_RELATIONSHIPS, + MAX_SCREEN_SHARE_BITRATE, MAX_USERS_PER_MESSAGE_REACTION, MAX_VOICE_MESSAGE_DURATION, MAX_WEBHOOKS_PER_CHANNEL, @@ -80,6 +81,7 @@ export const DEFAULT_RESTRICTED_LIMITS: Record = { max_private_channels_per_user: MAX_PRIVATE_CHANNELS_PER_USER, max_reactions_per_message: MAX_REACTIONS_PER_MESSAGE, max_relationships: MAX_RELATIONSHIPS, + max_screen_share_bitrate: MAX_SCREEN_SHARE_BITRATE, max_users_per_message_reaction: MAX_USERS_PER_MESSAGE_REACTION, max_voice_message_duration: MAX_VOICE_MESSAGE_DURATION, max_webhooks_per_channel: MAX_WEBHOOKS_PER_CHANNEL, diff --git a/packages/schema/src/domains/admin/AdminSchemas.ts b/packages/schema/src/domains/admin/AdminSchemas.ts index 849be041e..1f9607a6a 100644 --- a/packages/schema/src/domains/admin/AdminSchemas.ts +++ b/packages/schema/src/domains/admin/AdminSchemas.ts @@ -1683,7 +1683,7 @@ const LimitKeyMetadataSchema = z.object({ category: z.string(), scope: z.string(), isToggle: z.boolean(), - unit: z.enum(['bytes', 'count']).optional(), + unit: z.enum(['bytes', 'count', 'bps']).optional(), min: z.number().optional(), max: z.number().optional(), });