Public/Install-BATCRelayBot.ps1
|
#Requires -Version 5.1 function Install-BATCRelayBot { <# .SYNOPSIS Installs BATCRelayBot: checks prerequisites, collects configuration, writes config.json. .DESCRIPTION Six phases, in an order chosen so that nothing the user types can be wasted: 1. Detect prerequisites (silent) 2. Show what was found and what is missing 3. Resolve missing tools - winget for Python/FFmpeg, guidance for VoiceMeeter and BeyondATC, which must be installed by their vendors 4. Collect Discord credentials and pick the audio device 5. Confirm the summary 6. Install and report Phases 1-3 finish before anything is typed. Until 1.4.0 the credentials were collected in phase 3 and the readiness check ran in phase 4, so a missing tool discarded the token, server ID and channel ID that had just been entered. No administrator rights are required. .PARAMETER BotPath Installation directory. Defaults to $env:LOCALAPPDATA\BATCRelayBot. .PARAMETER SkipAudioDevice Skips audio device selection and leaves audio_device_name empty. The bot will not start until it is filled in - intended for unattended testing. .EXAMPLE Install-BATCRelayBot .EXAMPLE Install-BATCRelayBot -BotPath "D:\MyBot" .OUTPUTS Hashtable with Success, and on success InstallPath, ConfigPath, LogPath. #> [CmdletBinding()] [OutputType([hashtable])] param( [string]$BotPath = (Join-Path $env:LOCALAPPDATA "BATCRelayBot"), [switch]$SkipAudioDevice ) $version = Get-ModuleVersion Write-Host "" Write-Host "=======================================" -ForegroundColor Cyan Write-Host " BATCRelayBot Installation (v$version)" -ForegroundColor Cyan Write-Host "=======================================" -ForegroundColor Cyan Write-Host "" # The log has to exist before anything can fail. Creating it later is why # earlier versions left no trace of the failures users actually hit. $logPath = Initialize-InstallLog -InstallPath $BotPath -Version $version if ($logPath) { Write-Host "Log file: $logPath" -ForegroundColor DarkGray Write-Host "" } try { # ---- Phase 0: migrate an existing installation ------------------- $configPath = Join-Path $BotPath "config.json" if (Test-Path $configPath) { $migration = Convert-LegacyBotConfig -ConfigPath $configPath if ($migration.Migrated) { Write-Host "Existing configuration migrated to the current schema:" -ForegroundColor Cyan foreach ($change in $migration.Changes) { Write-Host " - $change" -ForegroundColor Gray } Write-InstallLog "Migrated existing config: $($migration.Changes -join '; ')" -LogPath $logPath Write-Host "" } elseif ($migration.Error) { Write-Host "WARNING: $($migration.Error)" -ForegroundColor Yellow Write-Host " It will be replaced by this installation." -ForegroundColor Yellow Write-InstallLog "Migration skipped: $($migration.Error)" -LogPath $logPath -Level WARN Write-Host "" } } # ---- Phase 1: detection ------------------------------------------ Write-Host "[1/6] Checking prerequisites..." -ForegroundColor Cyan $prerequisites = @{ Python = Find-Python FFmpeg = Find-FFmpeg VoiceMeeter = Find-VoiceMeeter BeyondATC = Find-BeyondATC } foreach ($name in @('Python', 'FFmpeg', 'VoiceMeeter', 'BeyondATC')) { $item = $prerequisites[$name] $detail = if ($item.Found) { $item.Path } else { $item.Reason } Write-InstallLog "Detected ${name}: Found=$($item.Found) - $detail" -LogPath $logPath } Write-Host "" # ---- Phase 2: status --------------------------------------------- Write-Host "[2/6] Prerequisite status" -ForegroundColor Cyan Show-PrerequisitesInfo -Prerequisites $prerequisites # ---- Phase 3: resolve what is missing ---------------------------- Write-Host "[3/6] Resolving missing components" -ForegroundColor Cyan Write-Host "" $prerequisites = Resolve-MissingTool -Prerequisites $prerequisites -LogPath $logPath if (-not ($prerequisites.Python.Found -and $prerequisites.FFmpeg.Found)) { $stillMissing = @() if (-not $prerequisites.Python.Found) { $stillMissing += "Python" } if (-not $prerequisites.FFmpeg.Found) { $stillMissing += "FFmpeg" } $verb = if ($stillMissing.Count -eq 1) { "is" } else { "are" } Write-Host "$($stillMissing -join ' and ') $verb required and still missing." -ForegroundColor Red Write-Host "" if (-not $prerequisites.Python.Found) { Write-Host " Python : https://www.python.org/downloads/ (tick 'Add python.exe to PATH')" -ForegroundColor Yellow Write-Host " $($prerequisites.Python.Reason)" -ForegroundColor DarkGray } if (-not $prerequisites.FFmpeg.Found) { Write-Host " FFmpeg : winget install Gyan.FFmpeg - or https://ffmpeg.org/download.html" -ForegroundColor Yellow } Write-Host "" Write-Host "Install what is missing, then run Install-BATCRelayBot again." -ForegroundColor Yellow Write-InstallLog "Aborted: required tools still missing after phase 3" -LogPath $logPath -Level ERROR return (Stop-Installation -Reason "Required tools missing" -LogPath $logPath) } # VoiceMeeter has to come from VB-Audio's own installer, so the most # this installer can do is check, explain and let the user decide. if (-not $prerequisites.VoiceMeeter.Found) { if (-not (Confirm-ContinueWithoutVoiceMeeter -Detail $prerequisites.VoiceMeeter.Reason -LogPath $logPath)) { Write-InstallLog "User aborted at the VoiceMeeter warning" -LogPath $logPath return (Stop-Installation -Reason "Cancelled - VoiceMeeter missing" -LogPath $logPath) } } Show-BeyondATCNotice -BeyondATC $prerequisites.BeyondATC # ---- Phase 4: configuration -------------------------------------- Write-Host "[4/6] Configuration" -ForegroundColor Cyan Write-Host "" $discordConfig = Get-DiscordConfiguration -LogPath $logPath if (-not $discordConfig) { Write-Host "Discord configuration was not completed." -ForegroundColor Red Write-InstallLog "Aborted: Discord configuration incomplete" -LogPath $logPath -Level ERROR return (Stop-Installation -Reason "Discord configuration incomplete" -LogPath $logPath) } if ($SkipAudioDevice) { $discordConfig.AudioDeviceName = "" Write-InstallLog "Audio device selection skipped by -SkipAudioDevice" -LogPath $logPath -Level WARN } else { $device = Select-AudioDevice -FFmpegPath $prerequisites.FFmpeg.Path -LogPath $logPath if (-not $device) { Write-Host "No audio device selected - the bot would join the channel but stream silence." -ForegroundColor Red Write-InstallLog "Aborted: no audio device selected" -LogPath $logPath -Level ERROR return (Stop-Installation -Reason "No audio device selected" -LogPath $logPath) } $discordConfig.AudioDeviceName = $device } # ---- Phase 5: summary -------------------------------------------- Write-Host "[5/6] Summary" -ForegroundColor Cyan $summary = Show-InstallationSummary -Prerequisites $prerequisites ` -DiscordConfig $discordConfig -InstallPath $BotPath if (-not $summary.CanProceed) { Write-Host "Installation cannot proceed: $($summary.BlockingReason)" -ForegroundColor Red Write-InstallLog "Aborted at summary: $($summary.BlockingReason)" -LogPath $logPath -Level ERROR return (Stop-Installation -Reason $summary.BlockingReason -LogPath $logPath) } # ---- Phase 6: install -------------------------------------------- Write-Host "[6/6] Installing" -ForegroundColor Cyan $installResult = Start-Installation -Prerequisites $prerequisites ` -DiscordConfig $discordConfig -InstallPath $BotPath -LogPath $logPath if (-not $installResult.Success) { Write-Host "" Write-Host "Installation failed: $($installResult.Error)" -ForegroundColor Red Write-InstallLog "Installation failed: $($installResult.Error)" -LogPath $logPath -Level ERROR return (Stop-Installation -Reason $installResult.Error -LogPath $logPath) } Show-PostInstallationMessage ` -InstallPath $installResult.InstallPath ` -ConfigPath $installResult.ConfigPath ` -LogPath $installResult.LogPath Write-InstallLog "Installation completed successfully" -LogPath $logPath return $installResult } catch { # Nothing below this point may call exit: exit inside a module # function terminates the whole PowerShell session, which is what # made the window close before the error could be read. $message = Remove-SensitiveData -Text $_.Exception.Message Write-Host "" Write-Host "Unexpected error during installation:" -ForegroundColor Red Write-Host " $message" -ForegroundColor Red Write-Host "" Write-InstallLog "Unhandled exception: $message" -LogPath $logPath -Level ERROR Write-InstallLog "At: $($_.ScriptStackTrace)" -LogPath $logPath -Level ERROR return (Stop-Installation -Reason $message -LogPath $logPath) } } function Stop-Installation { <# .SYNOPSIS Ends the installation without killing the host session. .DESCRIPTION Points the user at the log and pauses, so a double-clicked shortcut does not close before the message can be read. The pause is skipped when the session is non-interactive, otherwise automated runs would hang forever. #> [OutputType([hashtable])] param( [string]$Reason, [string]$LogPath ) Write-Host "" if ($LogPath -and (Test-Path $LogPath)) { Write-Host "Details are in the log: $LogPath" -ForegroundColor Yellow } if ([Environment]::UserInteractive) { Write-Host "" Read-Host "Press Enter to close" | Out-Null } return @{ Success = $false; Error = $Reason; LogPath = $LogPath } } function Resolve-MissingTool { <# .SYNOPSIS Offers to install missing Python/FFmpeg, then returns updated detection. #> [OutputType([hashtable])] param( [Parameter(Mandatory = $true)][hashtable]$Prerequisites, [string]$LogPath ) $missing = @() if (-not $Prerequisites.Python.Found) { $missing += "Python 3.10+" } if (-not $Prerequisites.FFmpeg.Found) { $missing += "FFmpeg" } if ($missing.Count -eq 0) { Write-Host " Python and FFmpeg are present." -ForegroundColor Green Write-Host "" return $Prerequisites } Write-Host " Missing: $($missing -join ', ')" -ForegroundColor Yellow if ($Prerequisites.Python.Reason -and -not $Prerequisites.Python.Found) { Write-Host " Python: $($Prerequisites.Python.Reason)" -ForegroundColor DarkGray } Write-Host "" Write-Host " [1] Install them now with winget (per-user, no admin rights)" -ForegroundColor Gray Write-Host " [2] I will install them myself - show the links" -ForegroundColor Gray Write-Host " [3] Continue without installing" -ForegroundColor Gray Write-Host "" $choice = Read-Host " Choice (1/2/3), [Enter] for 1" if ([string]::IsNullOrWhiteSpace($choice)) { $choice = "1" } Write-InstallLog "Missing-tool choice: $choice (missing: $($missing -join ', '))" -LogPath $LogPath Write-Host "" switch ($choice.Trim()) { "2" { Write-Host " Python : https://www.python.org/downloads/" -ForegroundColor Yellow Write-Host " During setup, tick 'Add python.exe to PATH'." -ForegroundColor Gray Write-Host " FFmpeg : winget install Gyan.FFmpeg" -ForegroundColor Yellow Write-Host " or https://ffmpeg.org/download.html" -ForegroundColor Gray Write-Host "" Write-Host " Run Install-BATCRelayBot again once they are installed." -ForegroundColor Yellow Write-Host "" return $Prerequisites } "3" { return $Prerequisites } default { return (Install-MissingPrerequisite -Prerequisites $Prerequisites -LogPath $LogPath) } } } function Confirm-ContinueWithoutVoiceMeeter { <# .SYNOPSIS Explains why VoiceMeeter matters and asks whether to continue anyway. .DESCRIPTION VoiceMeeter must be installed and removed through VB-Audio's own installer - it ships audio drivers, so a third party silently installing or removing it is not safe. This installer therefore only checks and explains. #> [OutputType([bool])] param( [string]$Detail, [string]$LogPath ) Write-Host " VoiceMeeter was not found." -ForegroundColor Yellow if ($Detail) { Write-Host " $Detail" -ForegroundColor DarkGray } Write-Host "" Write-Host " VoiceMeeter provides the virtual audio device this bot streams from." -ForegroundColor Gray Write-Host " Without it there is nothing to relay into Discord." -ForegroundColor Gray Write-Host "" Write-Host " It has to be installed with VB-Audio's own installer, because it" -ForegroundColor Gray Write-Host " ships audio drivers. Short version:" -ForegroundColor Gray Write-Host "" Write-Host " 1. Download from https://vb-audio.com/Voicemeeter/" -ForegroundColor Cyan Write-Host " 2. Run the installer as administrator" -ForegroundColor Cyan Write-Host " 3. Reboot - the virtual audio devices only appear afterwards" -ForegroundColor Cyan Write-Host " 4. Start VoiceMeeter, then run Install-BATCRelayBot again" -ForegroundColor Cyan Write-Host "" Write-Host " Continuing now means the audio device list will be missing the" -ForegroundColor Yellow Write-Host " VoiceMeeter outputs, so audio_device_name has to be fixed by hand later." -ForegroundColor Yellow Write-Host "" $answer = Read-Host " Continue anyway? (y/N)" $continue = $answer -match '^(y|yes|j|ja)$' Write-InstallLog "VoiceMeeter missing - user chose to $(if ($continue) { 'continue' } else { 'abort' })" -LogPath $LogPath Write-Host "" return $continue } function Show-BeyondATCNotice { <# .SYNOPSIS Reports BeyondATC status. Informational only - it is optional and paid. #> param([hashtable]$BeyondATC) if ($BeyondATC.Found -and $BeyondATC.ExePath) { Write-Host " BeyondATC found: $($BeyondATC.ExePath)" -ForegroundColor Green Write-Host " It will be started automatically with the bot." -ForegroundColor Gray } elseif ($BeyondATC.Found) { Write-Host " BeyondATC configuration found, but not the executable." -ForegroundColor Yellow Write-Host " Add batc_path to config.json manually if you want it auto-started." -ForegroundColor Gray } else { Write-Host " BeyondATC not installed - optional, the bot works without it." -ForegroundColor Gray Write-Host " It is commercial software: https://beyondatc.net/" -ForegroundColor DarkGray } Write-Host "" } Export-ModuleMember -Function @( 'Install-BATCRelayBot', 'Resolve-MissingTool', 'Confirm-ContinueWithoutVoiceMeeter', 'Show-BeyondATCNotice', 'Stop-Installation' ) |