Public/Edit-BATCRelayBotConfig.ps1

#Requires -Version 5.1

function Edit-BATCRelayBotConfig {
    <#
    .SYNOPSIS
    Changes settings in an existing installation without reinstalling.
 
    .DESCRIPTION
    Edits the fields bot.py actually reads: the bot token, the server ID, the
    voice channel ID and the audio device. Each change is backed up, written
    atomically, read back and verified; a failed verification rolls that file
    back to its backup.
 
    The editor stays open. It used to exit after a single change, so correcting
    two fields meant running it twice and reading the same warnings again.
 
    Disabled since v1.3.10 for good reason - the editor wrote `token` while the
    installer wrote `bot_token`, so an edited token went into a field nothing
    read and the bot kept using the old one. That mapping now has a single
    definition in Get-ConfigFieldMap, and it is covered by tests.
 
    .PARAMETER InstallPath
    Installation directory. Defaults to $env:LOCALAPPDATA\BATCRelayBot.
 
    .PARAMETER PassThru
    Returns the result object. Without it nothing is written to the pipeline,
    so quitting the menu no longer prints "Cancelled by the user" as a table.
 
    .EXAMPLE
    Edit-BATCRelayBotConfig
 
    .EXAMPLE
    $result = Edit-BATCRelayBotConfig -PassThru
    if ($result.Success) { "Changed: $($result.UpdatedFields.Keys)" }
 
    .OUTPUTS
    With -PassThru, a hashtable with Success, BackupPath, UpdatedFields,
    Errors. UpdatedFields holds every field changed in the session, and
    BackupPath the backup taken before the last of them.
    #>

    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [string]$InstallPath = (Join-Path $env:LOCALAPPDATA "BATCRelayBot"),
        [switch]$PassThru
    )

    # Initialised up front. Both of these used to be referenced without ever
    # being created - $errors.Add on the rollback path and $updatedFields[...]
    # on the success path - so the function threw on whichever branch it took.
    $errors = [System.Collections.ArrayList]@()
    $updatedFields = @{}
    $backup = $null

    # ---- Phase 1: can we edit at all? -----------------------------------
    $prereq = Confirm-ConfigEditorPrerequisites -InstallPath $InstallPath

    if (-not $prereq.Valid) {
        Write-Host ""
        foreach ($problem in $prereq.Errors) {
            Write-Host " $problem" -ForegroundColor Red
        }
        Write-Host ""
        Write-Host " Run Install-BATCRelayBot first." -ForegroundColor Yellow
        Write-Host ""
        return (Out-CommandResult -PassThru:$PassThru -Result @{
            Success = $false; BackupPath = $null; UpdatedFields = @{}
            Errors = @($prereq.Errors)
        })
    }

    # ---- Phase 2: change fields until the user quits ---------------------
    while ($true) {
        $menuResult = Show-ConfigEditorMenu -ConfigPath $prereq.ConfigPath
        if ($null -eq $menuResult) { break }

        $field = $menuResult.Field
        $newValue = $menuResult.Value

        # Back up, write, verify, roll back on failure.
        try {
            Write-Host ""
            Write-Host " Applying change..." -ForegroundColor Cyan

            $backup = Backup-ConfigFile -ConfigPath $prereq.ConfigPath
            Write-Host " Backup: $(Split-Path $backup -Leaf)" -ForegroundColor Gray

            $newJson = Update-ConfigJson -ConfigPath $prereq.ConfigPath -Field $field -Value $newValue
            Write-ConfigFile -ConfigPath $prereq.ConfigPath -JsonContent $newJson | Out-Null

            # The write drops the file's restriction along with the old file, so
            # reapply it before anything else can read the token.
            Protect-BotConfigFile -ConfigPath $prereq.ConfigPath | Out-Null

            $verify = Verify-ConfigChange -ConfigPath $prereq.ConfigPath -Field $field -ExpectedValue $newValue

            if (-not $verify.Verified) {
                Copy-Item $backup $prereq.ConfigPath -Force
                Protect-BotConfigFile -ConfigPath $prereq.ConfigPath | Out-Null

                $errors.Add("$($verify.Message). Rolled back to the backup.") | Out-Null
                Write-Host " FAILED: $($verify.Message)" -ForegroundColor Red
                Write-Host " Rolled back - config.json is unchanged." -ForegroundColor Yellow

                # Back to the menu: the change did not happen, and the file is
                # as it was, so there is nothing to stop the user retrying.
                continue
            }

            $definition = Get-ConfigFieldDefinition -Field $field
            $updatedFields[$field] = $newValue

            Write-Host " Verified." -ForegroundColor Green
            Write-Host " $($definition.Label) updated." -ForegroundColor Green
        }
        catch {
            # Sanitised: an exception raised while handling the token can carry it.
            $message = Remove-SensitiveData -Text $_.Exception.Message
            $errors.Add("Error while saving: $message") | Out-Null
            Write-Host " ERROR: $message" -ForegroundColor Red

            if ($backup -and (Test-Path $backup)) {
                Copy-Item $backup $prereq.ConfigPath -Force -ErrorAction SilentlyContinue
                Protect-BotConfigFile -ConfigPath $prereq.ConfigPath | Out-Null
                Write-Host " Rolled back - config.json is unchanged." -ForegroundColor Yellow
            }

            # An unexpected exception is a reason to stop, not to offer the
            # menu again over a state nobody has established.
            break
        }
    }

    Write-Host ""

    # Said once, on the way out. It used to be printed on entry when the bot
    # was running and again after every change, and the menu claimed "No
    # changes made" on quitting even when several had been.
    if ($updatedFields.Count -gt 0 -and $prereq.BotRunning) {
        Write-Host " Restart the bot to apply this: Stop-BATCRelayBot; Start-BATCRelayBot" -ForegroundColor Yellow
        Write-Host ""
    }

    if ($updatedFields.Count -eq 0 -and $errors.Count -eq 0) {
        Write-Host " No changes made." -ForegroundColor Yellow
        Write-Host ""
        return (Out-CommandResult -PassThru:$PassThru -Result @{
            Success = $false; BackupPath = $null; UpdatedFields = @{}
            Errors = @("Cancelled by the user")
        })
    }

    return (Out-CommandResult -PassThru:$PassThru -Result @{
        Success       = ($errors.Count -eq 0)
        BackupPath    = $backup
        UpdatedFields = $updatedFields
        Errors        = @($errors)
    })
}

Export-ModuleMember -Function Edit-BATCRelayBotConfig