# Discord bootstrapper: IEX-compatible version. # The server must return this file as plain UTF-8 text. if ($PSVersionTable.PSVersion.Major -lt 5) { throw 'PowerShell 5.1 or newer is required.' } Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 # -------------------- CONFIGURATION -------------------- $BaseUrl = 'https://file.cyberfix.space' $ZapretArchiveUrl = "$BaseUrl/zapret.zip" $DiscordPortableArchiveUrl = "$BaseUrl/discord-portable.zip" # Generate with: # (Get-FileHash .\zapret.zip -Algorithm SHA256).Hash.ToLowerInvariant() $ZapretSha256 = '35f37c791be8a78ac8f89eca584631a64e334ae885e7d40791959f064d51161f' # Generate with: # (Get-FileHash .\discord-portable.zip -Algorithm SHA256).Hash.ToLowerInvariant() $DiscordPortableSha256 = 'a488ad0245014627d7268e18889e45e3f5385126e7d20d3ce93315a4b26b456c' # Used when Enter is pressed in the ALT menu. $DefaultAlt = 11 # New files are created here. Existing AppData Discord folders are not used. $WorkRoot = Join-Path $env:SystemDrive 'DiscordBootstrap' # ------------------------------------------------------- function Write-Stage { param([Parameter(Mandatory)][string]$Text) Write-Host "`n==> $Text" -ForegroundColor Cyan } function Write-Info { param([Parameter(Mandatory)][string]$Text) Write-Host "[i] $Text" } function Assert-Administrator { $identity = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($identity) if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Run the command from an elevated PowerShell or Administrator CMD window.' } } function Assert-Configuration { if ($BaseUrl -match 'example\.win') { throw 'Replace $BaseUrl with your file-server URL.' } if ($ZapretSha256 -notmatch '^[0-9a-fA-F]{64}$') { throw 'Replace $ZapretSha256 with the SHA-256 of zapret.zip.' } if ($DiscordPortableSha256 -notmatch '^[0-9a-fA-F]{64}$') { throw 'Replace $DiscordPortableSha256 with the SHA-256 of discord-portable.zip.' } } function Test-Sha256 { param( [Parameter(Mandatory)][string]$Path, [Parameter(Mandatory)][string]$ExpectedHash ) if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false } try { $actual = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash return $actual.Equals($ExpectedHash, [StringComparison]::OrdinalIgnoreCase) } catch { return $false } } function Download-NewFile { param( [Parameter(Mandatory)][string]$Uri, [Parameter(Mandatory)][string]$DestinationDirectory, [Parameter(Mandatory)][string]$FilePrefix, [Parameter(Mandatory)][string]$Extension ) $name = '{0}-{1}{2}' -f $FilePrefix, ([guid]::NewGuid().ToString('N')), $Extension $path = Join-Path $DestinationDirectory $name Write-Info "Downloading: $Uri" Invoke-WebRequest -Uri $Uri -OutFile $path -UseBasicParsing return $path } function Get-VerifiedZapretArchive { param( [Parameter(Mandatory)][string]$Directory, [Parameter(Mandatory)][string]$Uri, [Parameter(Mandatory)][string]$ExpectedHash ) $hashPrefix = $ExpectedHash.Substring(0, 12).ToLowerInvariant() $cached = Get-ChildItem -LiteralPath $Directory ` -Filter "zapret-$hashPrefix-*.zip" ` -File ` -ErrorAction SilentlyContinue foreach ($file in $cached) { if (Test-Sha256 -Path $file.FullName -ExpectedHash $ExpectedHash) { Write-Info "Using verified cached archive: $($file.FullName)" return $file.FullName } } $downloaded = Download-NewFile ` -Uri $Uri ` -DestinationDirectory $Directory ` -FilePrefix "zapret-$hashPrefix" ` -Extension '.zip' if (-not (Test-Sha256 -Path $downloaded -ExpectedHash $ExpectedHash)) { throw "zapret.zip SHA-256 mismatch. Downloaded file was not executed: $downloaded" } Write-Info 'zapret archive SHA-256 is valid.' return $downloaded } function Test-ZapretDirectory { param([Parameter(Mandatory)][string]$Path) if (-not (Test-Path -LiteralPath $Path -PathType Container)) { return $false } $batchFiles = Get-ChildItem -LiteralPath $Path ` -Filter '*.bat' ` -Recurse ` -File ` -ErrorAction SilentlyContinue foreach ($batchFile in $batchFiles) { if ($batchFile.Name -notmatch '^general \(ALT\d+\)\.bat$') { continue } $winws = Join-Path $batchFile.Directory.FullName 'bin\winws.exe' if (Test-Path -LiteralPath $winws -PathType Leaf) { return $true } } return $false } function Get-OrExpandZapretRoot { param( [Parameter(Mandatory)][string]$ArchivePath, [Parameter(Mandatory)][string]$Directory, [Parameter(Mandatory)][string]$ExpectedHash ) $hashPrefix = $ExpectedHash.Substring(0, 12).ToLowerInvariant() $existingDirectories = Get-ChildItem -LiteralPath $Directory ` -Directory ` -Filter "zapret-$hashPrefix-*" ` -ErrorAction SilentlyContinue foreach ($candidate in $existingDirectories) { if (Test-ZapretDirectory -Path $candidate.FullName) { Write-Info "Using extracted zapret directory: $($candidate.FullName)" return $candidate } } $extractDirectory = Join-Path $Directory ( 'zapret-{0}-{1}' -f $hashPrefix, ([guid]::NewGuid().ToString('N')) ) New-Item -ItemType Directory -Path $extractDirectory -Force | Out-Null Expand-Archive -LiteralPath $ArchivePath -DestinationPath $extractDirectory if (-not (Test-ZapretDirectory -Path $extractDirectory)) { throw 'No usable general (ALT...).bat strategy with bin\winws.exe was found in zapret.zip.' } return Get-Item -LiteralPath $extractDirectory } function Get-ZapretStrategies { param([Parameter(Mandatory)][System.IO.DirectoryInfo]$Root) $strategies = @() $batchFiles = Get-ChildItem -LiteralPath $Root.FullName ` -Filter '*.bat' ` -Recurse ` -File ` -ErrorAction SilentlyContinue foreach ($batchFile in $batchFiles) { if ($batchFile.Name -notmatch '^general \(ALT(?\d+)\)\.bat$') { continue } $winws = Join-Path $batchFile.Directory.FullName 'bin\winws.exe' if (-not (Test-Path -LiteralPath $winws -PathType Leaf)) { continue } $strategies += [pscustomobject]@{ Alt = [int]$Matches['AltNumber'] File = $batchFile } } $strategies = @( $strategies | Sort-Object ` @{ Expression = { $_.Alt }; Ascending = $true }, @{ Expression = { $_.File.FullName }; Ascending = $true } ) if ($strategies.Count -eq 0) { throw 'No available ALT strategies were found.' } return $strategies } function Select-ZapretStrategy { param( [Parameter(Mandatory)][object[]]$Strategies, [Parameter(Mandatory)][int]$DefaultAlt ) $requestedAlt = $null if ($env:ZAPRET_ALT -and $env:ZAPRET_ALT -match '^(?:ALT)?(?\d+)$') { $requestedAlt = [int]$Matches['AltNumber'] } if ($null -ne $requestedAlt) { $requested = @($Strategies | Where-Object { $_.Alt -eq $requestedAlt }) if ($requested.Count -gt 0) { Write-Info "Strategy ALT$requestedAlt selected through ZAPRET_ALT." return $requested[0].File } Write-Warning "ALT$requestedAlt is not present in the downloaded archive." } $defaultIndex = 0 for ($index = 0; $index -lt $Strategies.Count; $index++) { if ($Strategies[$index].Alt -eq $DefaultAlt) { $defaultIndex = $index break } } Write-Host '' Write-Host 'Available zapret strategies:' -ForegroundColor Yellow for ($index = 0; $index -lt $Strategies.Count; $index++) { $strategy = $Strategies[$index] $defaultLabel = if ($index -eq $defaultIndex) { ' [default]' } else { '' } Write-Host ( ' [{0}] ALT{1}{2}' -f ($index + 1), $strategy.Alt, $defaultLabel ) } Write-Host ' [0] Cancel' while ($true) { $choice = Read-Host ( 'Select a strategy number or press Enter for ALT{0}' -f $Strategies[$defaultIndex].Alt ) if ([string]::IsNullOrWhiteSpace($choice)) { return $Strategies[$defaultIndex].File } if ($choice -eq '0' -or $choice -match '^(q|quit|exit)$') { return $null } $number = 0 if ( [int]::TryParse($choice, [ref]$number) -and $number -ge 1 -and $number -le $Strategies.Count ) { return $Strategies[$number - 1].File } Write-Warning 'Enter one of the displayed menu numbers.' } } function Get-WinwsProcesses { try { return @(Get-CimInstance Win32_Process -Filter "Name='winws.exe'" -ErrorAction Stop) } catch { return @() } } function Start-ZapretStrategy { param( [Parameter(Mandatory)][System.IO.FileInfo]$Strategy, [Parameter(Mandatory)][string]$MarkerPath ) $strategyRoot = $Strategy.Directory.FullName $expectedWinws = [IO.Path]::GetFullPath( (Join-Path $strategyRoot 'bin\winws.exe') ) $strategyId = '{0}|{1}' -f $Strategy.Name, $expectedWinws $storedStrategyId = $null if (Test-Path -LiteralPath $MarkerPath -PathType Leaf) { $storedStrategyId = Get-Content -LiteralPath $MarkerPath -Raw -ErrorAction SilentlyContinue if ($storedStrategyId) { $storedStrategyId = $storedStrategyId.Trim() } } $processes = Get-WinwsProcesses $sameExecutable = @( $processes | Where-Object { $_.ExecutablePath -and [IO.Path]::GetFullPath($_.ExecutablePath).Equals( $expectedWinws, [StringComparison]::OrdinalIgnoreCase ) } ) if ( $sameExecutable.Count -gt 0 -and $storedStrategyId -eq $strategyId ) { Write-Info "$($Strategy.BaseName) is already running. A duplicate was not started." return } # Stop an old or differently configured winws process before switching ALT. foreach ($process in $processes) { Write-Info "Stopping winws.exe process (PID $($process.ProcessId))." Stop-Process -Id $process.ProcessId -Force -ErrorAction SilentlyContinue } Remove-Item -LiteralPath $MarkerPath -Force -ErrorAction SilentlyContinue Write-Info "Starting strategy: $($Strategy.FullName)" $batchCommand = '""{0}""' -f $Strategy.FullName Start-Process ` -FilePath $env:ComSpec ` -ArgumentList @('/d', '/c', $batchCommand) ` -WorkingDirectory $strategyRoot ` -WindowStyle Hidden $deadline = (Get-Date).AddSeconds(12) do { Start-Sleep -Milliseconds 500 $running = @( Get-WinwsProcesses | Where-Object { $_.ExecutablePath -and [IO.Path]::GetFullPath($_.ExecutablePath).Equals( $expectedWinws, [StringComparison]::OrdinalIgnoreCase ) } ) if ($running.Count -gt 0) { Set-Content ` -LiteralPath $MarkerPath ` -Value $strategyId ` -Encoding ASCII Write-Info "$($Strategy.BaseName) is running." return } } while ((Get-Date) -lt $deadline) throw "$($Strategy.Name) was started, but the expected winws.exe process did not appear." } function Get-VerifiedPortableArchive { param( [Parameter(Mandatory)][string]$Directory, [Parameter(Mandatory)][string]$Uri, [Parameter(Mandatory)][string]$ExpectedHash ) $hashPrefix = $ExpectedHash.Substring(0, 12).ToLowerInvariant() $cached = Get-ChildItem -LiteralPath $Directory ` -Filter "discord-portable-$hashPrefix-*.zip" ` -File ` -ErrorAction SilentlyContinue foreach ($file in $cached) { if (Test-Sha256 -Path $file.FullName -ExpectedHash $ExpectedHash) { Write-Info "Using verified cached portable archive: $($file.FullName)" return $file.FullName } } $downloaded = Download-NewFile ` -Uri $Uri ` -DestinationDirectory $Directory ` -FilePrefix "discord-portable-$hashPrefix" ` -Extension '.zip' if (-not (Test-Sha256 -Path $downloaded -ExpectedHash $ExpectedHash)) { throw "discord-portable.zip SHA-256 mismatch: $downloaded" } Write-Info 'Discord Portable archive SHA-256 is valid.' return $downloaded } function Find-DiscordPortableExecutable { param([Parameter(Mandatory)][string]$Root) if (-not (Test-Path -LiteralPath $Root -PathType Container)) { return $null } $executables = @( Get-ChildItem -LiteralPath $Root ` -Filter 'discord-portable.exe' ` -Recurse ` -File ` -ErrorAction SilentlyContinue ) foreach ($executable in $executables) { $appDirectory = Join-Path $executable.Directory.FullName 'app' if (Test-Path -LiteralPath $appDirectory -PathType Container) { return $executable } } return $null } function Get-OrExpandDiscordPortable { param( [Parameter(Mandatory)][string]$ArchivePath, [Parameter(Mandatory)][string]$Directory, [Parameter(Mandatory)][string]$ExpectedHash ) $hashPrefix = $ExpectedHash.Substring(0, 12).ToLowerInvariant() $existingDirectories = @( Get-ChildItem -LiteralPath $Directory ` -Directory ` -Filter "discord-portable-$hashPrefix-*" ` -ErrorAction SilentlyContinue ) foreach ($candidate in $existingDirectories) { $existingExe = Find-DiscordPortableExecutable -Root $candidate.FullName if ($existingExe) { Write-Info "Using extracted Discord Portable directory: $($candidate.FullName)" return $existingExe } } $extractDirectory = Join-Path $Directory ( 'discord-portable-{0}-{1}' -f $hashPrefix, ([guid]::NewGuid().ToString('N')) ) New-Item -ItemType Directory -Path $extractDirectory -Force | Out-Null Expand-Archive -LiteralPath $ArchivePath -DestinationPath $extractDirectory $portableExe = Find-DiscordPortableExecutable -Root $extractDirectory if (-not $portableExe) { throw @" discord-portable.exe with a sibling app directory was not found in discord-portable.zip. The archive must contain an already extracted Portapps installation, not the GitHub source code. "@ } return $portableExe } function Initialize-DiscordPortableData { param([Parameter(Mandatory)][System.IO.FileInfo]$PortableExecutable) $portableRoot = $PortableExecutable.Directory.FullName $dataDirectory = Join-Path $portableRoot 'data' $configPath = Join-Path $portableRoot 'discord-portable.yml' New-Item -ItemType Directory -Path $dataDirectory -Force | Out-Null $probe = Join-Path $dataDirectory ( '.write-test-{0}.tmp' -f [guid]::NewGuid().ToString('N') ) try { [IO.File]::WriteAllText($probe, 'test') Remove-Item -LiteralPath $probe -Force } catch { throw "Discord Portable data directory is not writable: $dataDirectory" } if (-not (Test-Path -LiteralPath $configPath -PathType Leaf)) { $config = @" app: cleanup: false "@ $utf8 = New-Object System.Text.UTF8Encoding($false) [IO.File]::WriteAllText($configPath, $config, $utf8) } Write-Info "Discord Portable data directory: $dataDirectory" } function Get-ProcessesInsideDirectory { param([Parameter(Mandatory)][string]$Directory) $root = [IO.Path]::GetFullPath($Directory).TrimEnd('\') + '\' try { return @( Get-CimInstance Win32_Process -ErrorAction Stop | Where-Object { $_.ExecutablePath -and [IO.Path]::GetFullPath($_.ExecutablePath).StartsWith( $root, [StringComparison]::OrdinalIgnoreCase ) } ) } catch { return @() } } function Test-PortableErrorWindow { param( [AllowEmptyCollection()] [object[]]$Processes ) foreach ($process in @($Processes)) { try { $managed = Get-Process -Id $process.ProcessId -ErrorAction Stop $title = $managed.MainWindowTitle if ($title -and $title -match '(?i)javascript|error|ошибка') { return $true } } catch { } } return $false } function Stop-NonPortableDiscordProcesses { param([Parameter(Mandatory)][string]$PortableRoot) $portablePath = [IO.Path]::GetFullPath($PortableRoot).TrimEnd('\') + '\' try { $processes = @( Get-CimInstance Win32_Process -ErrorAction Stop | Where-Object { $_.Name -match '^(Discord|Update|Squirrel|discord-portable)\.exe$' } ) } catch { $processes = @() } foreach ($process in $processes) { $insidePortable = ( $process.ExecutablePath -and [IO.Path]::GetFullPath($process.ExecutablePath).StartsWith( $portablePath, [StringComparison]::OrdinalIgnoreCase ) ) if (-not $insidePortable) { Write-Info "Stopping non-portable Discord process PID $($process.ProcessId)." Stop-Process -Id $process.ProcessId -Force -ErrorAction SilentlyContinue } } Start-Sleep -Seconds 1 } function Start-DiscordPortable { param([Parameter(Mandatory)][System.IO.FileInfo]$PortableExecutable) $portableRoot = $PortableExecutable.Directory.FullName $running = @(Get-ProcessesInsideDirectory -Directory $portableRoot) if ($running.Count -gt 0 -and -not (Test-PortableErrorWindow -Processes $running)) { Write-Info 'Discord Portable is already running.' return } foreach ($process in $running) { Stop-Process -Id $process.ProcessId -Force -ErrorAction SilentlyContinue } Stop-NonPortableDiscordProcesses -PortableRoot $portableRoot Initialize-DiscordPortableData -PortableExecutable $PortableExecutable Write-Info "Starting Discord Portable: $($PortableExecutable.FullName)" Start-Process ` -FilePath $PortableExecutable.FullName ` -WorkingDirectory $portableRoot $deadline = (Get-Date).AddSeconds(45) $stableSince = $null do { Start-Sleep -Milliseconds 500 $processes = @(Get-ProcessesInsideDirectory -Directory $portableRoot) if (Test-PortableErrorWindow -Processes $processes) { throw 'Discord Portable opened an error window.' } if ($processes.Count -gt 0) { if ($null -eq $stableSince) { $stableSince = Get-Date } if (((Get-Date) - $stableSince).TotalSeconds -ge 7) { Write-Info 'Discord Portable is running.' return } } else { $stableSince = $null } } while ((Get-Date) -lt $deadline) throw 'Discord Portable did not remain running.' } try { Assert-Administrator Assert-Configuration Write-Stage 'Preparing working directory' New-Item -ItemType Directory -Path $WorkRoot -Force | Out-Null Write-Info "Work directory: $WorkRoot" Write-Stage 'Preparing zapret-discord-youtube' $zapretArchive = Get-VerifiedZapretArchive ` -Directory $WorkRoot ` -Uri $ZapretArchiveUrl ` -ExpectedHash $ZapretSha256 $zapretRoot = Get-OrExpandZapretRoot ` -ArchivePath $zapretArchive ` -Directory $WorkRoot ` -ExpectedHash $ZapretSha256 $strategies = @(Get-ZapretStrategies -Root $zapretRoot) $strategy = Select-ZapretStrategy ` -Strategies $strategies ` -DefaultAlt $DefaultAlt if ($null -eq $strategy) { Write-Host "`nCancelled. Nothing was changed." -ForegroundColor Yellow return } Write-Stage "Starting zapret-discord-youtube $($strategy.BaseName)" $markerPath = Join-Path $WorkRoot 'active-zapret-strategy.txt' Start-ZapretStrategy ` -Strategy $strategy ` -MarkerPath $markerPath Write-Stage 'Preparing Discord Portable' $portableArchive = Get-VerifiedPortableArchive ` -Directory $WorkRoot ` -Uri $DiscordPortableArchiveUrl ` -ExpectedHash $DiscordPortableSha256 $portableExecutable = Get-OrExpandDiscordPortable ` -ArchivePath $portableArchive ` -Directory $WorkRoot ` -ExpectedHash $DiscordPortableSha256 Start-DiscordPortable -PortableExecutable $portableExecutable Write-Host ( "`nCompleted: {0} and Discord Portable are running." -f $strategy.BaseName ) -ForegroundColor Green } catch { Write-Host "`nFAILED: $($_.Exception.Message)" -ForegroundColor Red exit 1 }