diff --git a/fluxer_api/src/api/user/entrance_sound/EntranceSoundPlayService.ts b/fluxer_api/src/api/user/entrance_sound/EntranceSoundPlayService.ts index b63414cee..2167d40b2 100644 --- a/fluxer_api/src/api/user/entrance_sound/EntranceSoundPlayService.ts +++ b/fluxer_api/src/api/user/entrance_sound/EntranceSoundPlayService.ts @@ -1,7 +1,6 @@ // SPDX-License-Identifier: AGPL-3.0-or-later import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; -import {UnknownChannelError} from '@fluxer/errors/src/domains/channel/UnknownChannelError'; import {InputValidationError} from '@fluxer/errors/src/domains/core/InputValidationError'; import {type ChannelID, createUserID, type EntranceSoundID, type UserID} from '../../BrandedTypes'; import type {IChannelRepository} from '../../channel/IChannelRepository'; @@ -25,14 +24,10 @@ export class EntranceSoundPlayService { async play(params: PlayEntranceSoundParams): Promise { const {userId, channelId, soundId} = params; const channel = await this.channelRepository.findUnique(channelId); - if (!channel) { - throw new UnknownChannelError(); - } - const guildId = channel.guildId ?? undefined; - const {voiceStates} = await this.gatewayService.getVoiceStatesForChannel({ - guildId, - channelId, - }); + const guildId = channel?.guildId ?? undefined; + const voiceStates = channel + ? (await this.gatewayService.getVoiceStatesForChannel({guildId, channelId})).voiceStates + : []; const senderInChannel = voiceStates.some((state) => state.userId === userId.toString()); if (!senderInChannel) { throw InputValidationError.fromCode('channel_id', ValidationErrorCodes.ENTRANCE_SOUND_INVALID_SCOPE); diff --git a/fluxer_docs/src/content/docs/http-api/entrance-sounds.mdx b/fluxer_docs/src/content/docs/http-api/entrance-sounds.mdx index fe8b6004d..659c9a391 100644 --- a/fluxer_docs/src/content/docs/http-api/entrance-sounds.mdx +++ b/fluxer_docs/src/content/docs/http-api/entrance-sounds.mdx @@ -337,18 +337,13 @@ Nothing is published to the media servers. Each recipient receives a Dispatch na | Status | Body | Condition | | --- | --- | --- | | 204 | empty | Fan-out was attempted for every other connected account | -| 400 | [error response](/http-api/#error-response) | Caller holds no voice state in the channel and the request returns `ENTRANCE_SOUND_INVALID_SCOPE` at the `channel_id` path, or the account owns no such clip and the request returns `ENTRANCE_SOUND_NOT_FOUND` at the `sound_id` path | -| 404 | [error response](/http-api/#error-response) | Channel does not exist and the request returns `UNKNOWN_CHANNEL` | +| 400 | [error response](/http-api/#error-response) | Caller holds no voice state in the channel, including when the channel does not exist, and the request returns `ENTRANCE_SOUND_INVALID_SCOPE` at the `channel_id` path, or the account owns no such clip and the request returns `ENTRANCE_SOUND_NOT_FOUND` at the `sound_id` path | :::caution[Connection is the only authorisation] Fluxer evaluates no channel permission and checks no guild membership. ::: -:::caution[Any account can probe channel existence] -Fluxer resolves the channel before the connection check. A channel the caller is not connected to returns 400, and an ID naming no channel returns 404. -::: - -The connection check runs before the clip is looked up, so a caller who is not connected never learns whether the named clip exists. +An ID naming no channel is refused exactly like a channel the caller is not connected to, so the response tells the two apart in neither direction. The connection check runs before the clip is looked up, so a caller who is not connected never learns whether the named clip exists. ### Side effects diff --git a/fluxer_docs/src/content/docs/voice/index.md b/fluxer_docs/src/content/docs/voice/index.md index 803e15df1..d24cc66d5 100644 --- a/fluxer_docs/src/content/docs/voice/index.md +++ b/fluxer_docs/src/content/docs/voice/index.md @@ -179,7 +179,7 @@ An entrance sound is a short clip an account plays for everyone already connecte Fluxer records which clip belongs to which scope and nothing more. [Play entrance sound](/http-api/entrance-sounds/#play-entrance-sound) names the clip explicitly, so the client decides which selection applies to a given channel. -Playback requires a voice state in the target channel and no channel permission. A caller that holds none is refused at the `channel_id` path with the validation code `ENTRANCE_SOUND_INVALID_SCOPE`. A successful call fans one [ENTRANCE_SOUND_PLAY](/gateway/events/#entrance-sound-play) Dispatch out to every other account with a voice state in the channel, at most once per account and never back to the caller. That Dispatch has the clip's CDN URL, and each recipient fetches and plays it locally, so no audio track is published for it. +Playback requires a voice state in the target channel and no channel permission. A caller that holds none is refused at the `channel_id` path with the validation code `ENTRANCE_SOUND_INVALID_SCOPE`, and a channel ID naming no channel is refused the same way. A successful call fans one [ENTRANCE_SOUND_PLAY](/gateway/events/#entrance-sound-play) Dispatch out to every other account with a voice state in the channel, at most once per account and never back to the caller. That Dispatch has the clip's CDN URL, and each recipient fetches and plays it locally, so no audio track is published for it. Every session the account holds receives the Dispatch, including sessions that are not in the channel. A client filters on `channel_id`.