diff --git a/packages/bot/src/functions/music/commands/play/handlers/playHandler.ts b/packages/bot/src/functions/music/commands/play/handlers/playHandler.ts index 13aa59043..85650e771 100644 --- a/packages/bot/src/functions/music/commands/play/handlers/playHandler.ts +++ b/packages/bot/src/functions/music/commands/play/handlers/playHandler.ts @@ -3,6 +3,7 @@ import type { PlayerNodeInitializationResult } from 'discord-player' import type { CommandExecuteParams } from '../../../../../types/CommandData' import { ENVIRONMENT_CONFIG } from '@lucky/shared/config' import { errorLog, debugLog, warnLog } from '@lucky/shared/utils' +import { assertDefined } from '@lucky/shared/utils/guards' import { createErrorEmbed } from '../../../../../utils/general/embeds' import { interactionReply } from '../../../../../utils/general/interactionReply' import { createUserFriendlyError } from '@lucky/shared/utils/general/errorSanitizer' @@ -44,7 +45,7 @@ export async function executePlayHandler({ } const member = interaction.member as GuildMember - const voiceChannel = member.voice.channel! + const voiceChannel = assertDefined(member.voice.channel, 'Voice channel guaranteed by requireVoiceChannel check') try { await interaction.deferReply() @@ -88,7 +89,7 @@ export async function executePlayHandler({ try { const deferredMsg = await interaction.fetchReply() registerNowPlayingMessage( - interaction.guildId!, + assertDefined(interaction.guildId, 'Guild ID guaranteed by requireGuild check'), deferredMsg.id, interaction.channelId, ) @@ -213,7 +214,7 @@ export async function executePlayHandler({ // single failure never silently skips the others (#1085). void runPostPlayBackgroundOps({ queue, - guildId: interaction.guildId!, + guildId: assertDefined(interaction.guildId, 'Guild ID guaranteed by requireGuild check'), track, hadQueueBeforePlay, isPlaylist, diff --git a/packages/bot/src/functions/music/commands/stop.spec.ts b/packages/bot/src/functions/music/commands/stop.spec.ts index 15ecc4aa0..bd45d0f27 100644 --- a/packages/bot/src/functions/music/commands/stop.spec.ts +++ b/packages/bot/src/functions/music/commands/stop.spec.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, jest } from '@jest/globals' import stopCommand from './stop' +const requireGuildMock = jest.fn() const requireQueueMock = jest.fn() const requireDJRoleMock = jest.fn() const interactionReplyMock = jest.fn() @@ -11,6 +12,7 @@ const deleteSnapshotMock = jest.fn() const clearSessionMoodCacheMock = jest.fn() jest.mock('../../../utils/command/commandValidations', () => ({ + requireGuild: (...args: unknown[]) => requireGuildMock(...args), requireQueue: (...args: unknown[]) => requireQueueMock(...args), requireDJRole: (...args: unknown[]) => requireDJRoleMock(...args), })) @@ -63,6 +65,7 @@ function createQueue(guildId = 'guild-1') { describe('stop command', () => { beforeEach(() => { jest.clearAllMocks() + requireGuildMock.mockResolvedValue(true) requireQueueMock.mockResolvedValue(true) requireDJRoleMock.mockResolvedValue(true) interactionReplyMock.mockResolvedValue(undefined) @@ -145,6 +148,18 @@ describe('stop command', () => { expect(queue.delete).toHaveBeenCalled() }) + it('returns early if guild check fails', async () => { + requireGuildMock.mockResolvedValue(false) + + await stopCommand.execute({ + interaction: createInteraction('guild-1'), + client: {} as any, + }) + + expect(requireQueueMock).not.toHaveBeenCalled() + expect(clearSessionMoodCacheMock).not.toHaveBeenCalled() + }) + it('returns early if queue check fails', async () => { resolveGuildQueueMock.mockReturnValue({ queue: null }) requireQueueMock.mockResolvedValue(false) diff --git a/packages/bot/src/functions/music/commands/stop.ts b/packages/bot/src/functions/music/commands/stop.ts index 52c726dca..f46f54a64 100644 --- a/packages/bot/src/functions/music/commands/stop.ts +++ b/packages/bot/src/functions/music/commands/stop.ts @@ -4,6 +4,7 @@ import { interactionReply } from '../../../utils/general/interactionReply' import { clearSessionMoodCache } from '../../../utils/music/autoplay/replenisher' import type { CommandExecuteParams } from '../../../types/CommandData' import { + requireGuild, requireQueue, requireDJRole, } from '../../../utils/command/commandValidations' @@ -11,6 +12,7 @@ import { resolveGuildQueue } from '../../../utils/music/queueResolver' import { createSuccessEmbed } from '../../../utils/general/embeds' import { musicWatchdogService } from '../../../utils/music/watchdog' import { musicSessionSnapshotService } from '../../../utils/music/sessionSnapshots' +import { assertDefined } from '@lucky/shared/utils/guards' export default new Command({ data: new SlashCommandBuilder() @@ -18,10 +20,12 @@ export default new Command({ .setDescription('⏹️ Stop playback and clear the queue.'), category: 'music', execute: async ({ client, interaction }: CommandExecuteParams) => { + if (!(await requireGuild(interaction))) return + const { queue } = resolveGuildQueue(client, interaction.guildId ?? '') if (!(await requireQueue(queue, interaction))) return - if (!(await requireDJRole(interaction, interaction.guildId!))) return + if (!(await requireDJRole(interaction, assertDefined(interaction.guildId, 'Guild ID required after requireGuild check')))) return if (queue) { musicWatchdogService.markIntentionalStop(queue.guild.id) diff --git a/packages/bot/src/functions/music/commands/volume.ts b/packages/bot/src/functions/music/commands/volume.ts index 79292e83b..127158058 100644 --- a/packages/bot/src/functions/music/commands/volume.ts +++ b/packages/bot/src/functions/music/commands/volume.ts @@ -13,6 +13,7 @@ import { requireDJRole } from "../../../utils/command/commandValidations" import { resolveGuildQueue } from '../../../utils/music/queueResolver' +import { assertDefined } from '@lucky/shared/utils/guards' /** * Validate volume value @@ -78,7 +79,7 @@ export default new Command({ category: 'music', execute: async ({ client, interaction }: CommandExecuteParams) => { if (!(await requireGuild(interaction))) return - if (!(await requireDJRole(interaction, interaction.guildId!))) return + if (!(await requireDJRole(interaction, assertDefined(interaction.guildId, 'Guild ID required after requireGuild check')))) return const { queue } = resolveGuildQueue(client, interaction.guildId ?? '') if (!(await requireQueue(queue, interaction))) return diff --git a/packages/bot/src/functions/music/commands/voteskip.ts b/packages/bot/src/functions/music/commands/voteskip.ts index 34a5ee964..32c78e4ae 100644 --- a/packages/bot/src/functions/music/commands/voteskip.ts +++ b/packages/bot/src/functions/music/commands/voteskip.ts @@ -20,6 +20,7 @@ import { } from '../../../utils/music/voteSkipStore' import { guildSettingsService } from '@lucky/shared/services' import { debugLog } from '@lucky/shared/utils' +import { assertDefined } from '@lucky/shared/utils/guards' import type { GuildMember } from 'discord.js' const DEFAULT_THRESHOLD = 50 @@ -35,7 +36,7 @@ export default new Command({ }: CommandExecuteParams): Promise => { if (!(await requireGuild(interaction))) return - const guildId = interaction.guildId! + const guildId = assertDefined(interaction.guildId, 'Guild ID required after requireGuild check') const { queue } = resolveGuildQueue(client, guildId) if (!(await requireQueue(queue, interaction))) return @@ -111,7 +112,7 @@ export default new Command({ if (voteCount >= required) { clearVotes(guildId) - queue!.node.skip() + assertDefined(queue, 'Queue required after requireQueue check').node.skip() await interactionReply({ interaction, content: { diff --git a/packages/bot/src/handlers/message/autoModHandler.ts b/packages/bot/src/handlers/message/autoModHandler.ts index dd76e3edb..d94ed97e5 100644 --- a/packages/bot/src/handlers/message/autoModHandler.ts +++ b/packages/bot/src/handlers/message/autoModHandler.ts @@ -1,6 +1,7 @@ import type { Message } from 'discord.js' import { autoModService, moderationService } from '@lucky/shared/services' import { errorLog } from '@lucky/shared/utils' +import { assertDefined } from '@lucky/shared/utils/guards' import type { MessageContext, MessageHandler, @@ -112,12 +113,13 @@ export const autoModHandler: MessageHandler = { await message.delete().catch(() => {}) + const clientUser = assertDefined(message.client.user, 'Client user guaranteed when bot is ready') const caseInput = { guildId, userId, username: message.author.tag, - moderatorId: message.client.user!.id, - moderatorName: message.client.user!.tag, + moderatorId: clientUser.id, + moderatorName: clientUser.tag, reason: `[AutoMod] ${violations[0].reason}`, channelId: message.channelId, } diff --git a/packages/bot/src/handlers/player/streamBridge.ts b/packages/bot/src/handlers/player/streamBridge.ts index d560a6e02..0ec09cdc0 100644 --- a/packages/bot/src/handlers/player/streamBridge.ts +++ b/packages/bot/src/handlers/player/streamBridge.ts @@ -3,6 +3,7 @@ import { PassThrough } from 'stream' import type { Readable } from 'stream' import type { Track } from 'discord-player' import { errorLog, infoLog, warnLog, debugLog } from '@lucky/shared/utils' +import { assertDefined } from '@lucky/shared/utils/guards' import { cleanTitle, cleanAuthor, @@ -68,17 +69,17 @@ export function streamViaYtDlp(url: string): Promise { }, 15_000) const stderrChunks: Buffer[] = [] - proc.stderr!.on('data', (chunk: Buffer) => stderrChunks.push(chunk)) + assertDefined(proc.stderr, 'stderr guaranteed by stdio config').on('data', (chunk: Buffer) => stderrChunks.push(chunk)) let settled = false - proc.stdout!.once('data', (firstChunk: Buffer) => { + assertDefined(proc.stdout, 'stdout guaranteed by stdio config').once('data', (firstChunk: Buffer) => { if (settled) return settled = true clearTimeout(timeout) const through = new PassThrough() through.write(firstChunk) - proc.stdout!.pipe(through) + assertDefined(proc.stdout, 'stdout guaranteed by stdio config').pipe(through) resolve(through) }) diff --git a/packages/bot/src/spotify/spotifyApi.ts b/packages/bot/src/spotify/spotifyApi.ts index e73670d47..90b552c7e 100644 --- a/packages/bot/src/spotify/spotifyApi.ts +++ b/packages/bot/src/spotify/spotifyApi.ts @@ -1,5 +1,6 @@ import { logAndSwallow } from '@lucky/shared/utils/error' import { debugLog, warnLog } from '@lucky/shared/utils/general/log' +import { assertDefined } from '@lucky/shared/utils/guards' export interface SpotifyRecommendationTrack { id: string @@ -218,8 +219,8 @@ export async function getSpotifyRecommendations( return (data?.tracks ?? []) .filter((t) => t.id && t.name) .map((t) => ({ - id: t.id!, - name: t.name!, + id: assertDefined(t.id, 'Track ID guaranteed by filter'), + name: assertDefined(t.name, 'Track name guaranteed by filter'), artists: (t.artists ?? []).map((a) => ({ name: a.name ?? '', })), @@ -591,16 +592,16 @@ export async function getUserTopArtistsAndTracks( const artists: SpotifyTopArtist[] = (artistsData.items ?? []) .filter((a) => a.id && a.name) .map((a) => ({ - id: a.id!, - name: a.name!, + id: assertDefined(a.id, 'Artist ID guaranteed by filter'), + name: assertDefined(a.name, 'Artist name guaranteed by filter'), genres: a.genres ?? [], })) const tracks: SpotifyTopTrack[] = (tracksData.items ?? []) .filter((t) => t.id && t.name) .map((t) => ({ - id: t.id!, - name: t.name!, + id: assertDefined(t.id, 'Track ID guaranteed by filter'), + name: assertDefined(t.name, 'Track name guaranteed by filter'), artist: t.artists?.[0]?.name ?? 'Unknown', })) diff --git a/packages/bot/src/utils/music/queueEditOps.ts b/packages/bot/src/utils/music/queueEditOps.ts index 6d3226862..b8beb2b90 100644 --- a/packages/bot/src/utils/music/queueEditOps.ts +++ b/packages/bot/src/utils/music/queueEditOps.ts @@ -1,6 +1,7 @@ import { type Track, type GuildQueue } from 'discord-player' import { randomInt } from 'node:crypto' import { debugLog, errorLog } from '@lucky/shared/utils' +import { assertDefined } from '@lucky/shared/utils/guards' import { replenishQueue } from './autoplay/replenisher' import { markAsAutoplayTrack } from './autoplay/queueMarkers' @@ -174,7 +175,7 @@ export function moveUserTrackToPriority(queue: GuildQueue, track: Track): void { const tracks = queue.tracks.toArray() let trackIndex = -1 for (let i = tracks.length - 1; i >= 0; i--) { - const t = tracks[i]! + const t = assertDefined(tracks[i], 'Array index guaranteed by loop bounds') if ( t === track || (Boolean(track.id) && t.id === track.id) || @@ -202,7 +203,7 @@ export function moveUserTrackToPriority(queue: GuildQueue, track: Track): void { return } - const queuedTrack = tracks[trackIndex]! + const queuedTrack = assertDefined(tracks[trackIndex], 'Track index guaranteed by prior check') try { queue.node.remove(queuedTrack)