# Litsl log grab (Windows) # # What this script does, in order: # 1. Looks in the usual Minecraft launcher folders in your user folder for log files. # 2. Picks the newest log that shows your Litsl ban. If no log shows it (normal when you were # banned while offline), picks the log of your most recent Litsl session instead. # 3. Shows you which file it picked and asks "y/N". Nothing is sent unless you answer y. # 4. Makes a copy in memory with access tokens, your Windows username, emails and # IP addresses (except Litsl's) removed. # 5. Sends that one copy to Litsl for your ban appeal ticket. # # It needs no admin rights, installs nothing, writes no files and changes none of yours. # The same source is shown at __LG_BASE__/ with the values below left as placeholders. # Written for Windows PowerShell 5.1 and newer. $LgBase = '__LG_BASE__' # where the log is sent $LgId = '__LG_ID__' # your one-time upload link $LgBan = '__LG_BAN__' # your Ban ID (digits), or empty when the ticket has none $LgTicket = '__LG_TICKET__' # your ticket number, only shown to you below # Litsl's own server addresses (current, and the one used until September 2026). These stay in the log. $LgLitslIps = @('170.205.24.78', '135.148.136.134') $LgMaxText = 26214400 # 25 MB of text $LgMaxGzip = 5242880 # 5 MB compressed $LgMaxFiles = 400 # newest log files looked at # Folders that never hold logs. Skipping them keeps the search fast. $LgSkipDirs = @('assets', 'libraries', 'versions', 'natives', 'cache', 'caches', 'saves', 'mods', 'config', 'resourcepacks', 'shaderpacks', 'screenshots', 'backups', 'crash-reports', 'jre', 'runtime', 'java', 'meta', 'webcache', 'node_modules', '.git') function Get-LgRoots { $roots = New-Object System.Collections.Generic.List[string] $appData = $env:APPDATA $local = $env:LOCALAPPDATA $userHome = $env:USERPROFILE if ($appData) { foreach ($n in @('.minecraft', 'ModrinthApp', 'com.modrinth.theseus', 'PrismLauncher', 'PolyMC', 'PollyMC', 'FjordLauncher', 'ElyPrismLauncher', 'MultiMC', 'ATLauncher', 'gdlauncher_next', 'gdlauncher_carbon', 'gdlauncher', '.feather', 'LabyMod', '.tlauncher', '.technic', '.ftba', 'Badlion Client')) { $roots.Add((Join-Path $appData $n)) } } if ($local) { foreach ($n in @('.ftba', 'FTBA', 'ATLauncher', 'Programs/PrismLauncher', 'Programs/MultiMC')) { $roots.Add((Join-Path $local $n)) } } if ($userHome) { foreach ($n in @('.lunarclient', '.feather', '.tlauncher', 'curseforge/minecraft', 'Documents/curseforge/minecraft', 'ATLauncher', 'MultiMC', 'Desktop/MultiMC', 'Downloads/MultiMC', 'scoop/persist/prismlauncher', 'scoop/persist/multimc', 'scoop/persist/atlauncher')) { $roots.Add((Join-Path $userHome $n)) } } $found = New-Object System.Collections.Generic.List[string] foreach ($r in $roots) { if ((Test-Path -LiteralPath $r -PathType Container) -and -not $found.Contains($r)) { $found.Add($r) } } return , $found } # Every *.log and *.log.gz inside a "logs" folder under the launcher folders, newest first. function Get-LgCandidates { param($Roots) $files = New-Object System.Collections.Generic.List[object] $seen = New-Object System.Collections.Generic.HashSet[string] foreach ($root in $Roots) { $queue = New-Object System.Collections.Generic.Queue[object] $queue.Enqueue(@($root, 0, $false)) while ($queue.Count -gt 0) { $cur = $queue.Dequeue() $path = $cur[0]; $depth = $cur[1]; $inLogs = $cur[2] $entries = @(Get-ChildItem -LiteralPath $path -Force -ErrorAction SilentlyContinue) foreach ($e in $entries) { if ($e.PSIsContainer) { if ($depth -ge 9) { continue } $name = $e.Name.ToLowerInvariant() if ($LgSkipDirs -contains $name) { continue } $queue.Enqueue(@($e.FullName, ($depth + 1), ($inLogs -or $name -eq 'logs'))) continue } if (-not $inLogs) { continue } $fn = $e.Name.ToLowerInvariant() if ($fn.StartsWith('debug')) { continue } if (-not ($fn.EndsWith('.log') -or $fn.EndsWith('.log.gz'))) { continue } if ($e.Length -le 0) { continue } if ($seen.Add($e.FullName)) { $files.Add($e) } } } } return , @($files | Sort-Object -Property LastWriteTimeUtc -Descending | Select-Object -First $LgMaxFiles) } # The log as text. Handles .gz, and logs that an older Java wrote in the Windows code page. function Read-LgText { param([string]$Path) # Opened so that it also works while Minecraft is running and still writing the log. $fs = New-Object System.IO.FileStream($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) try { $mem = New-Object System.IO.MemoryStream $fs.CopyTo($mem) $bytes = $mem.ToArray() $mem.Dispose() } finally { $fs.Dispose() } if ($Path.ToLowerInvariant().EndsWith('.gz')) { $in = New-Object System.IO.MemoryStream(, $bytes) $gz = New-Object System.IO.Compression.GZipStream($in, [System.IO.Compression.CompressionMode]::Decompress) $out = New-Object System.IO.MemoryStream try { $buf = New-Object byte[] 65536 while ($true) { $n = $gz.Read($buf, 0, $buf.Length) if ($n -le 0) { break } $out.Write($buf, 0, $n) if ($out.Length -gt (2 * $LgMaxText)) { throw 'log too large' } } $bytes = $out.ToArray() } finally { $gz.Dispose(); $in.Dispose(); $out.Dispose() } } try { $strict = New-Object System.Text.UTF8Encoding($false, $true) return $strict.GetString($bytes) } catch { return [System.Text.Encoding]::Default.GetString($bytes) } } # Does this log show the ban with this Ban ID? "#1234" not followed by another digit. function Test-LgBanId { param([string]$Text) if (-not $LgBan) { return $false } return [regex]::IsMatch($Text, '#' + [regex]::Escape($LgBan) + '(?!\d)') } # Does this log show a ban screen from Litsl? It has to mention Litsl, a disconnect, and a ban. function Test-LgLitslBan { param([string]$Text) $i = [System.Text.RegularExpressions.RegexOptions]::IgnoreCase if (-not [regex]::IsMatch($Text, 'litsl', $i)) { return $false } if (-not [regex]::IsMatch($Text, 'disconnect|connection lost|lost connection|kicked', $i)) { return $false } return [regex]::IsMatch($Text, 'you are (ip[- ]?)?banned|you have been (ip[- ]?)?banned|banned from|ban id|ban appeal|has been banned', $i) } # Did the player connect to Litsl in this log? The client writes "Connecting to , ". function Test-LgLitslSession { param([string]$Text) $ips = ($LgLitslIps | ForEach-Object { [regex]::Escape($_) }) -join '|' return [regex]::IsMatch($Text, 'Connecting to (?:(?:[a-z0-9-]+\.)*litsl\.net\.?|' + $ips + ')(?=[, \r\n]|$)', 'IgnoreCase') } function Get-LgLauncher { param([string]$Path) $p = $Path.ToLowerInvariant() if ($p -match 'modrinth|theseus') { return 'Modrinth App' } if ($p -match 'elyprism|fjordlauncher|prismlauncher') { return 'Prism Launcher' } if ($p -match 'pollymc|polymc') { return 'PolyMC' } if ($p -match 'multimc') { return 'MultiMC' } if ($p -match 'atlauncher') { return 'ATLauncher' } if ($p -match 'gdlauncher') { return 'GDLauncher' } if ($p -match 'curseforge') { return 'CurseForge' } if ($p -match 'lunarclient') { return 'Lunar Client' } if ($p -match 'blclient|badlion') { return 'Badlion Client' } if ($p -match 'feather') { return 'Feather Client' } if ($p -match 'labymod') { return 'LabyMod' } if ($p -match 'tlauncher') { return 'TLauncher' } if ($p -match 'technic') { return 'Technic Launcher' } if ($p -match 'ftba') { return 'FTB App' } if ($p -match 'minecraft') { return 'Minecraft Launcher' } return 'Unknown launcher' } # Pick the log, newest first. Three kinds of find, best first: # 1. a log with your Ban ID in it # 2. a log that shows a Litsl ban screen # 3. the log of your most recent Litsl session (no ban screen in it) function Find-LgLog { param($Candidates) $banScreen = $null $session = $null foreach ($f in $Candidates) { if ($f.Length -gt (2 * $LgMaxText)) { continue } $text = $null try { $text = Read-LgText -Path $f.FullName } catch { continue } if (Test-LgBanId -Text $text) { return @{ File = $f; Text = $text; Kind = 'ban'; HasId = $true } } if (($null -eq $banScreen) -and (Test-LgLitslBan -Text $text)) { $banScreen = @{ File = $f; Text = $text; Kind = 'ban'; HasId = $false } # Without a Ban ID to look for, the newest log with a Litsl ban screen is the answer. if (-not $LgBan) { return $banScreen } } if (($null -eq $session) -and ($null -eq $banScreen) -and (Test-LgLitslSession -Text $text)) { $session = @{ File = $f; Text = $text; Kind = 'session'; HasId = $false } } } if ($null -ne $banScreen) { return $banScreen } return $session } # The text with private details removed. function Protect-LgText { param([string]$Text) $t = $Text $t = [regex]::Replace($t, '(--accessToken[=, ]+"?)[^\] ,"]+', '$1[removed]', 'IgnoreCase') $t = [regex]::Replace($t, '(--session[=, ]+"?)[^\] ,"]+', '$1[removed]', 'IgnoreCase') $t = [regex]::Replace($t, '(--(xuid|clientId|userProperties|profileProperties)[=, ]+"?)[^\] ,"]+', '$1[removed]', 'IgnoreCase') $t = [regex]::Replace($t, '(((refresh|access|id|client)_?token|session_?id)\\?"?[ ]*[:=][ ]*\\?"?)[^ ,"}\\\r\n]+', '$1[removed]', 'IgnoreCase') $t = [regex]::Replace($t, '(token:)[A-Za-z0-9._-]{6,}(:[0-9A-Fa-f-]{32,36})', '$1[removed]$2') $t = [regex]::Replace($t, '(Session ID is token:)[^:) ]+', '$1[removed]') $t = [regex]::Replace($t, '(Bearer[ ]+)[A-Za-z0-9._~+/=-]{8,}', '$1[removed]', 'IgnoreCase') $t = [regex]::Replace($t, 'eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*', '[removed-token]') # Your user folder, however the log wrote it (C:\Users\You, C:\\Users\\You, C:/Users/You). $userHome = $env:USERPROFILE if ($userHome) { $parent = Split-Path -Parent $userHome if (-not $parent) { $parent = $userHome } $shown = (Join-Path $parent '[user]') foreach ($variant in @($userHome, $userHome.Replace('\', '\\'), $userHome.Replace('\', '/'))) { $t = [regex]::Replace($t, [regex]::Escape($variant), $shown.Replace('$', '$$'), 'IgnoreCase') } } $t = [regex]::Replace($t, '([A-Za-z]:[\\/]+Users[\\/]+)[^\\/:"''\r\n]+', '$1[user]', 'IgnoreCase') $t = [regex]::Replace($t, '(/Users/|/home/|/run/media/|/media/|/Volumes/)[^/\s:"'']+', '$1[user]') $t = [regex]::Replace($t, '[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}', '[email]') $t = [regex]::Replace($t, '([0-9A-Fa-f]{1,4}:){5,7}[0-9A-Fa-f]{1,4}', '[ip]') $t = [regex]::Replace($t, '(?