Private/Get-DiscordConfiguration.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Collects and validates the Discord credentials the bot needs. .DESCRIPTION Returns a hashtable keyed the way New-BotConfigFile expects: BotToken, GuildId, VoiceChannelId The names deliberately mirror bot.py's config keys (guild_id, voice_channel_id) rather than the Discord UI wording ("Server ID"). The old ServerId/ChannelId naming is what let the installer write server_id and channel_id into config.json for sixteen releases without anyone noticing that bot.py reads neither. #> function Get-DiscordConfiguration { [OutputType([hashtable])] param( [string]$LogPath ) Write-Host "Discord Configuration" -ForegroundColor White -BackgroundColor DarkBlue Write-Host "Three values from the Discord developer portal and your server." -ForegroundColor Gray Write-Host "" $config = @{ BotToken = $null GuildId = $null VoiceChannelId = $null } $token = Read-DiscordToken -LogPath $LogPath if (-not $token) { return $null } $config.BotToken = $token $config.GuildId = Read-DiscordSnowflake ` -Label "Server ID (guild)" ` -Hint "Discord: Settings > Advanced > Developer Mode, then right-click the server > Copy Server ID" ` -LogPath $LogPath if (-not $config.GuildId) { return $null } $config.VoiceChannelId = Read-DiscordSnowflake ` -Label "Voice channel ID" ` -Hint "Right-click the VOICE channel the bot should join > Copy Channel ID" ` -LogPath $LogPath if (-not $config.VoiceChannelId) { return $null } return $config } function Read-DiscordToken { <# .SYNOPSIS Prompts for the bot token and verifies it against the Discord API. .OUTPUTS The token, or $null after three failed attempts. #> [OutputType([string])] param([string]$LogPath) $maxAttempts = 3 for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) { Write-Host "Step 1/3: Bot token" -ForegroundColor Cyan Write-Host " https://discord.com/developers/applications > your app > Bot > Reset Token" -ForegroundColor Gray $secure = Read-Host " Token (hidden)" -AsSecureString $plain = ConvertFrom-SecureStringToPlainText -Secure $secure if ([string]::IsNullOrWhiteSpace($plain)) { Write-Host " No token entered (attempt $attempt/$maxAttempts)." -ForegroundColor Red Write-Host "" continue } if ($plain.Length -lt 50) { # Real bot tokens are ~59-72 characters. A short value is almost # always the application's Client Secret or the Application ID. Write-Host " That looks too short for a bot token (attempt $attempt/$maxAttempts)." -ForegroundColor Red Write-Host " Make sure you copied the Bot token, not the Client Secret or Application ID." -ForegroundColor Yellow Write-InstallLog "Token rejected locally: too short" -LogPath $LogPath -Level WARN Write-Host "" continue } Write-Host " Checking the token against the Discord API..." -ForegroundColor Gray $validation = Test-DiscordBotToken -Token $plain if ($validation.Valid) { Write-Host " Token accepted. Bot: $($validation.BotName)" -ForegroundColor Green Write-InstallLog "Token validated for bot '$($validation.BotName)'" -LogPath $LogPath Write-Host "" return $plain } Write-Host " Rejected (attempt $attempt/$maxAttempts): $($validation.Error)" -ForegroundColor Red Write-InstallLog "Token validation failed: $($validation.Error)" -LogPath $LogPath -Level WARN Write-Host "" } Write-Host " Token could not be validated after $maxAttempts attempts." -ForegroundColor Red Write-Host "" return $null } function Read-DiscordSnowflake { <# .SYNOPSIS Prompts for a Discord ID until it is well-formed or the user gives up. .OUTPUTS The ID as a string, or $null if the user entered nothing to abort. #> [OutputType([string])] param( [Parameter(Mandatory = $true)][string]$Label, [Parameter(Mandatory = $true)][string]$Hint, [string]$LogPath ) $step = if ($Label -like "Server*") { "Step 2/3" } else { "Step 3/3" } while ($true) { Write-Host "${step}: $Label" -ForegroundColor Cyan Write-Host " $Hint" -ForegroundColor Gray $value = Read-Host " $Label" if ([string]::IsNullOrWhiteSpace($value)) { Write-Host " Nothing entered - aborting." -ForegroundColor Yellow Write-Host "" return $null } $value = $value.Trim() # Discord snowflakes are 17-20 digits today and grow over time, so # accept a slightly wider range than the current length. if ($value -notmatch '^\d{17,21}$') { Write-Host " A Discord ID is 17-21 digits with no other characters." -ForegroundColor Red Write-Host " Enable Developer Mode first, otherwise 'Copy ID' will not appear." -ForegroundColor Yellow Write-Host "" continue } Write-Host " Accepted." -ForegroundColor Green Write-InstallLog "$Label accepted: $value" -LogPath $LogPath Write-Host "" return $value } } function ConvertFrom-SecureStringToPlainText { <# .SYNOPSIS Reads a SecureString without leaking the unmanaged buffer. .DESCRIPTION SecureStringToCoTaskMemUnicode allocates memory that the caller must free again; the previous implementation never did, leaving the token in unmanaged memory for the life of the session. #> [OutputType([string])] param([System.Security.SecureString]$Secure) if (-not $Secure) { return $null } $ptr = [System.IntPtr]::Zero try { $ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToCoTaskMemUnicode($Secure) return [System.Runtime.InteropServices.Marshal]::PtrToStringUni($ptr) } finally { if ($ptr -ne [System.IntPtr]::Zero) { [System.Runtime.InteropServices.Marshal]::ZeroFreeCoTaskMemUnicode($ptr) } } } function Test-DiscordBotToken { <# .SYNOPSIS Verifies a bot token against GET /users/@me. .DESCRIPTION The User-Agent must start with "DiscordBot" or Cloudflare blocks the request outright, which older versions surfaced as a generic connection failure. #> [OutputType([hashtable])] param([string]$Token) try { $moduleVersion = Get-ModuleVersion $headers = @{ Authorization = "Bot $Token" "User-Agent" = "DiscordBot (https://github.com/ZulshiBLN/BATCRelayBot, $moduleVersion)" } $response = Invoke-WebRequest -Uri "https://discord.com/api/v10/users/@me" ` -Headers $headers -Method Get -UseBasicParsing ` -WarningAction SilentlyContinue -ErrorAction Stop -TimeoutSec 15 $user = $response.Content | ConvertFrom-Json return @{ Valid = $true BotName = $user.username BotId = $user.id Error = $null } } catch { $statusCode = 0 if ($_.Exception.PSObject.Properties.Name -contains 'Response' -and $_.Exception.Response) { try { $statusCode = [int]$_.Exception.Response.StatusCode } catch { $statusCode = 0 } } $message = switch ($statusCode) { 401 { "Discord rejected the token. Reset it in the developer portal and copy the new one." } 403 { "The token is valid but the bot lacks permission. Check its role and scopes." } 404 { "Not a bot token - make sure you copied it from the Bot tab, not OAuth2." } 429 { "Discord is rate limiting this machine. Wait a moment and try again." } 500 { "Discord had a server error. Try again shortly." } 503 { "The Discord API is temporarily unavailable. Try again shortly." } default { # Sanitised: the raw exception can echo the token back. $raw = Remove-SensitiveData -Text $_.Exception.Message if ($raw -match 'timed out|timeout|No such host|remote name') { "Could not reach the Discord API. Check the internet connection or a proxy." } else { "Could not reach the Discord API ($raw)" } } } return @{ Valid = $false BotName = $null BotId = $null Error = $message } } } Export-ModuleMember -Function @( 'Get-DiscordConfiguration', 'Test-DiscordBotToken', 'Read-DiscordToken', 'Read-DiscordSnowflake', 'ConvertFrom-SecureStringToPlainText' ) |