docs: add send upload deep dive validation items
This commit is contained in:
375
scripts/extract-send-upload-deep-dive.ps1
Normal file
375
scripts/extract-send-upload-deep-dive.ps1
Normal file
@@ -0,0 +1,375 @@
|
||||
param(
|
||||
[string]$IlDir = "runs/offline-evidence-intake/zyl-qqfile-20260709/metadata/c28-il",
|
||||
[string]$OutputDir = "runs/send-upload-deep-dive",
|
||||
[string]$ReportPath = "docs/source-discovery/2026-07-12-send-upload-deep-dive.md",
|
||||
[string]$ValidationPath = "docs/source-discovery/2026-07-12-login-reachability-validation-items.md",
|
||||
[int]$WindowRadius = 5,
|
||||
[int]$MaxEvidencePerItem = 20
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repo = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path
|
||||
$ilDirFull = Join-Path $repo $IlDir
|
||||
$outDir = Join-Path $repo $OutputDir
|
||||
$reportFullPath = Join-Path $repo $ReportPath
|
||||
$validationFullPath = Join-Path $repo $ValidationPath
|
||||
New-Item -ItemType Directory -Force -Path $outDir | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $reportFullPath) | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $validationFullPath) | Out-Null
|
||||
|
||||
$sources = @(
|
||||
@{ path = "IMPlatformClient.exe.il"; role = "main client; high-level send, file-send orchestration, runtime auth/service state" },
|
||||
@{ path = "smack.dll.il"; role = "Chat/XMPP send implementation and file-message primitive" },
|
||||
@{ path = "IMPP.Service.dll.il"; role = "file upload HTTP/service implementation" },
|
||||
@{ path = "IMPP.ServiceBase.dll.il"; role = "FileUploadPara/FileUploadResult/FileTransferParaBase DTO contracts" },
|
||||
@{ path = "IMPP.Interface.dll.il"; role = "IAppContextManager/IFileTransfer interface contracts" },
|
||||
@{ path = "IMPP.Common.dll.il"; role = "ServiceManager/Auth DTOs and common runtime state" }
|
||||
)
|
||||
|
||||
$items = @(
|
||||
@{
|
||||
key = "text_entry_app_context"
|
||||
title = "Text send high-level entry: AppContextManager.SendTxtMessageByJid"
|
||||
stage = "text_send"
|
||||
meaning = "Best business-level text-send candidate if reachable in logged-in runtime."
|
||||
must_verify = @("Reach AppContextManager instance", "Confirm target JID/chat type mapping", "Invoke dry/probe path without send first", "Correlate sent record/ack after real send test")
|
||||
patterns = @("AppContextManager::SendTxtMessageByJid", "\bSendTxtMessageByJid\b", "IAppContextManager::SendTxtMessageByJid")
|
||||
},
|
||||
@{
|
||||
key = "text_entry_scheduling"
|
||||
title = "Text send scheduling fallback: MessageScheduling.SendChatMessage"
|
||||
stage = "text_send"
|
||||
meaning = "Fallback text-send path if AppContextManager is hard to reach but Chat/Roster args exist."
|
||||
must_verify = @("Locate or construct RosterContactsArgs", "Locate Chat instance", "Confirm direct/group branch", "Correlate sent record/ack")
|
||||
patterns = @("MessageScheduling::SendChatMessage", "SendChatMessage\(")
|
||||
},
|
||||
@{
|
||||
key = "chat_low_level_send"
|
||||
title = "Low-level text primitive: Chat.SendMessage -> XMPPConnection.send"
|
||||
stage = "text_send"
|
||||
meaning = "Confirms final XMPP send primitive; not enough alone without authenticated Chat/XMPP state."
|
||||
must_verify = @("Locate Chat instance", "Locate authenticated XMPPConnection", "Confirm direct/group message payload", "Capture ack/sent-record")
|
||||
patterns = @("com\.vision\.smack\.Chat::SendMessage", "com\.vision\.smack\.XMPPConnection::send", "callvirt instance void com\.vision\.smack\.XMPPConnection::send")
|
||||
},
|
||||
@{
|
||||
key = "file_upload_para_base"
|
||||
title = "Upload parameter base: FileTransferParaBase"
|
||||
stage = "file_upload"
|
||||
meaning = "Defines host, port, domain, local path, timeout/cache and token fields required by upload/download DTOs."
|
||||
must_verify = @("Host", "Port", "Domain", "LocalFilePath", "Token", "Cache/timeout flags if present")
|
||||
patterns = @("FileTransferParaBase", "get_Host\(\)", "get_Port\(\)", "get_Domain\(\)", "get_LocalFilePath\(\)", "get_Token\(\)")
|
||||
},
|
||||
@{
|
||||
key = "file_upload_para"
|
||||
title = "Upload DTO: FileUploadPara"
|
||||
stage = "file_upload"
|
||||
meaning = "Concrete upload request DTO; adds IsImg/IsPublic and invokes FileTransferParaBase constructor."
|
||||
must_verify = @("Constructor arguments", "IsImg", "IsPublic", "Token value", "Local file path normalization")
|
||||
patterns = @("FileUploadPara", "FileUploadPara::\.ctor", "get_IsImg\(\)", "get_IsPublic\(\)", "set_IsImg", "set_IsPublic")
|
||||
},
|
||||
@{
|
||||
key = "file_upload_service"
|
||||
title = "Upload service: IFileTransfer/FileTransfer.FileUpload"
|
||||
stage = "file_upload"
|
||||
meaning = "Managed upload entry; returns FileUploadResult and internally writes multipart token/form data."
|
||||
must_verify = @("Reach IFileTransfer instance", "Call FileUpload with safe test file", "Capture FileUploadResult", "Capture HTTP server/token fields without leaking token")
|
||||
patterns = @("FileTransfer::FileUpload", "IFileTransfer::FileUpload", 'Content-Disposition: form-data; name="token"', "FileUploadResult")
|
||||
},
|
||||
@{
|
||||
key = "file_upload_auth"
|
||||
title = "Upload auth/server state: HTTPFileServer + Auth token"
|
||||
stage = "file_upload"
|
||||
meaning = "File upload depends on runtime server/auth state, not only a local file path."
|
||||
must_verify = @("HTTPFileServer host/port", "UnifiedAuthentication/Auth token", "Domain/resource", "Access token/client id if present")
|
||||
patterns = @("get_HTTPFileServer\(\)", "UnifiedAuthentication", "Auth::get_token", "accessToken", "clientId", "HTTPFileServer")
|
||||
},
|
||||
@{
|
||||
key = "file_upload_result"
|
||||
title = "Upload result: FileUploadResult.FileID and remote metadata"
|
||||
stage = "file_upload_result"
|
||||
meaning = "Server-issued FileID is required before a chat file message is meaningful."
|
||||
must_verify = @("FileID", "URL or remote folder if present", "size/name metadata", "failure code/message")
|
||||
patterns = @("FileUploadResult::get_FileID", "FileUploadResult::set_FileID", "FileUploadResult", "RemoteFolder", "get_Url", "get_URL")
|
||||
},
|
||||
@{
|
||||
key = "file_message_finalizer"
|
||||
title = "File-message finalization: trans_UploadCompleted / SendFileMessage / Chat.sendFileMessage"
|
||||
stage = "send_file_message"
|
||||
meaning = "After upload succeeds, the client constructs offline file-transfer content and sends chat file message."
|
||||
must_verify = @("trans_UploadCompleted receives FileID", "OFFLINE_FILE_TRANSFER payload fields", "Target Chat/JID", "Sent file record/ack")
|
||||
patterns = @("OffLineFileSend::trans_UploadCompleted", "MessageScheduling::SendFileMessage", "Chat::sendFileMessage", "OFFLINE_FILE_TRANSFER")
|
||||
},
|
||||
@{
|
||||
key = "target_chat_resolution"
|
||||
title = "Target resolution: JID -> Chat / ChatRoom"
|
||||
stage = "runtime_context"
|
||||
meaning = "Both text and file send need a target JID mapped to Chat or ChatRoom; group handling may differ."
|
||||
must_verify = @("Contact JID", "Group/conference JID", "ChatManager.GetChat", "ChatRoom branch and permissions")
|
||||
patterns = @("ChatManager::GetChat", "GetChat\(", "ChatRoom", "get_RosterJid", "get_ChatRoomJid", "get_HasSpeechAuthJid")
|
||||
},
|
||||
@{
|
||||
key = "runtime_owner"
|
||||
title = "Runtime owner: frmMain / IMPPManager / MessageCenter"
|
||||
stage = "runtime_context"
|
||||
meaning = "Likely owners of initialized logged-in state and message center callbacks."
|
||||
must_verify = @("Running frmMain", "IMPPManager.UserInfo", "MessageCenter", "Reconnect success state", "loaded assemblies")
|
||||
patterns = @("class IMPP\.Client\.frmMain", "IMPPManager::get_UserInfo", "GetMessageCenter\(\)", "MessageCenter", "OnReConnectionSucceed")
|
||||
}
|
||||
)
|
||||
|
||||
function Get-RelativePath([string]$Path) {
|
||||
if ($Path.StartsWith($repo, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
return $Path.Substring($repo.Length).TrimStart([char[]]@('\', '/'))
|
||||
}
|
||||
return $Path
|
||||
}
|
||||
|
||||
function Write-Utf8NoBomLf([string]$Path, [string]$Text) {
|
||||
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
|
||||
$normalized = $Text -replace "`r`n", "`n"
|
||||
$normalized = $normalized -replace "`r", "`n"
|
||||
$normalized = $normalized.TrimEnd() + "`n"
|
||||
[System.IO.File]::WriteAllText($Path, $normalized, $utf8NoBom)
|
||||
}
|
||||
|
||||
function Get-EvidenceKind([string]$Line) {
|
||||
if ($Line -match "^\s*\.class\b") { return "type_definition" }
|
||||
if ($Line -match "^\s*\.method\b") { return "method_header" }
|
||||
if ($Line -match "^\s*\.field\b") { return "field" }
|
||||
if ($Line -match "^\s*\.property\b") { return "property" }
|
||||
if ($Line -match "\bend of method\b") { return "method_end" }
|
||||
if ($Line -match "\bnewobj\b") { return "constructor_call" }
|
||||
if ($Line -match "\bcall(?:virt)?\b") { return "callsite" }
|
||||
if ($Line -match "\bldstr\b") { return "string_literal" }
|
||||
if ($Line -match "^\s*(instance|static)\b.*\(") { return "signature_continuation" }
|
||||
return "reference"
|
||||
}
|
||||
|
||||
function Get-MethodContext($Lines, [int]$Index) {
|
||||
$start = [Math]::Max(0, $Index - 120)
|
||||
for ($i = $Index; $i -ge $start; $i--) {
|
||||
if ($Lines[$i] -match "^\s*\.method\b") {
|
||||
$header = $Lines[$i].Trim()
|
||||
if (($i + 1) -lt $Lines.Count -and $Lines[$i + 1] -match "^\s+(instance|static|default)\b") {
|
||||
$header = ($header + " " + $Lines[$i + 1].Trim())
|
||||
}
|
||||
return "L$($i + 1): $header"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
function Find-Evidence($Item) {
|
||||
$evidence = New-Object System.Collections.Generic.List[object]
|
||||
foreach ($source in $sources) {
|
||||
$path = Join-Path $ilDirFull $source.path
|
||||
if (-not (Test-Path -LiteralPath $path)) { continue }
|
||||
$lines = [System.IO.File]::ReadAllLines($path)
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
$line = $lines[$i]
|
||||
foreach ($pattern in $Item.patterns) {
|
||||
if ($line -match $pattern) {
|
||||
$evidence.Add([pscustomobject]@{
|
||||
source = Get-RelativePath $path
|
||||
source_role = $source.role
|
||||
line = $i + 1
|
||||
kind = Get-EvidenceKind $line
|
||||
pattern = $pattern
|
||||
snippet = $line.Trim()
|
||||
context = Get-MethodContext $lines $i
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($evidence.Count -ge $MaxEvidencePerItem) { break }
|
||||
}
|
||||
if ($evidence.Count -ge $MaxEvidencePerItem) { break }
|
||||
}
|
||||
return $evidence.ToArray()
|
||||
}
|
||||
|
||||
$sourceSummary = foreach ($source in $sources) {
|
||||
$path = Join-Path $ilDirFull $source.path
|
||||
$exists = Test-Path -LiteralPath $path
|
||||
$lineCount = 0
|
||||
$size = 0
|
||||
if ($exists) {
|
||||
$file = Get-Item -LiteralPath $path
|
||||
$size = $file.Length
|
||||
$lineCount = ([System.IO.File]::ReadLines($path) | Measure-Object).Count
|
||||
}
|
||||
[pscustomobject]@{
|
||||
path = Get-RelativePath $path
|
||||
role = $source.role
|
||||
exists = $exists
|
||||
lines = $lineCount
|
||||
size_bytes = $size
|
||||
}
|
||||
}
|
||||
|
||||
$itemResults = foreach ($item in $items) {
|
||||
$evidence = Find-Evidence $item
|
||||
$status = if ($evidence.Count -gt 0) { "static_evidence_found" } else { "missing_static_evidence" }
|
||||
[pscustomobject]@{
|
||||
key = $item.key
|
||||
title = $item.title
|
||||
stage = $item.stage
|
||||
meaning = $item.meaning
|
||||
status = $status
|
||||
evidence_count = $evidence.Count
|
||||
must_verify = $item.must_verify
|
||||
evidence = $evidence
|
||||
}
|
||||
}
|
||||
|
||||
$newValidationItems = @(
|
||||
[pscustomobject]@{ priority = "P0"; category = "runtime"; item = "Confirm logged-in IMPlatformClient.exe process and loaded assemblies"; evidence = "process id, module list, loaded managed assemblies"; unlocks = "all B-route adapter tests" },
|
||||
[pscustomobject]@{ priority = "P0"; category = "runtime"; item = "Confirm AppContextManager/IAppContextManager reachability"; evidence = "type visible, instance path, SendTxtMessageByJid callable shape"; unlocks = "text send high-level path" },
|
||||
[pscustomobject]@{ priority = "P0"; category = "runtime"; item = "Confirm Chat/XMPPConnection authenticated state"; evidence = "Chat instance, XMPPConnection state, target JID mapping"; unlocks = "text and file message finalization" },
|
||||
[pscustomobject]@{ priority = "P0"; category = "file_upload"; item = "Capture FileUploadPara required values without leaking token"; evidence = "host, port, domain, local path presence, IsImg/IsPublic, token-present boolean/hash only"; unlocks = "upload_file probe" },
|
||||
[pscustomobject]@{ priority = "P0"; category = "file_upload"; item = "Confirm IFileTransfer/FileTransfer.FileUpload reachability"; evidence = "service instance or constructor path, method signature, safe call readiness"; unlocks = "file upload" },
|
||||
[pscustomobject]@{ priority = "P0"; category = "file_upload_result"; item = "Confirm FileUploadResult.FileID after safe test upload"; evidence = "FileID present, size/name or URL/remote folder metadata, failure code if upload fails"; unlocks = "send_file_message" },
|
||||
[pscustomobject]@{ priority = "P0"; category = "send_file_message"; item = "Confirm trans_UploadCompleted/SendFileMessage consumes FileID"; evidence = "FileID enters payload, Chat.sendFileMessage invoked, sent file record/ack"; unlocks = "production file send" },
|
||||
[pscustomobject]@{ priority = "P0"; category = "ack"; item = "Correlate sent record/ack with content hash or file hash"; evidence = "sent record timestamp, target ref, body/file metadata hash, ack/ref id"; unlocks = "production gate" },
|
||||
[pscustomobject]@{ priority = "P1"; category = "group"; item = "Confirm group ChatRoom branch and permissions"; evidence = "ChatRoom type, speech/upload permission state, group JID"; unlocks = "group send/file send" },
|
||||
[pscustomobject]@{ priority = "P1"; category = "fallback"; item = "If B-route reachability fails, capture A-route UI evidence"; evidence = "window hwnd, rtbSendMessage, btnSend, screenshots, failure reason"; unlocks = "demo fallback only" }
|
||||
)
|
||||
|
||||
$result = [pscustomobject]@{
|
||||
ok = $true
|
||||
generated_at = (Get-Date).ToUniversalTime().ToString("o")
|
||||
decision = "continue_offline_deep_dive_then_login_reachability_probe"
|
||||
sources = $sourceSummary
|
||||
items = $itemResults
|
||||
new_validation_items = $newValidationItems
|
||||
originals_copied = $false
|
||||
originals_modified = $false
|
||||
}
|
||||
|
||||
$jsonPath = Join-Path $outDir "send-upload-deep-dive.json"
|
||||
Write-Utf8NoBomLf $jsonPath ($result | ConvertTo-Json -Depth 8)
|
||||
|
||||
$md = New-Object System.Collections.Generic.List[string]
|
||||
$md.Add("# Send / Upload Deep Dive")
|
||||
$md.Add("")
|
||||
$md.Add("Date: 2026-07-12")
|
||||
$md.Add("")
|
||||
$md.Add("## Business summary")
|
||||
$md.Add("")
|
||||
$md.Add("Offline deep dive confirms the remaining work is not generic sending code. The business blocker is logged-in runtime reachability plus online proof. Text send needs reachable AppContextManager/MessageScheduling/Chat. File send needs upload parameters and server-issued FileUploadResult.FileID before Chat.sendFileMessage can be meaningful.")
|
||||
$md.Add("")
|
||||
$md.Add("## Decision")
|
||||
$md.Add("")
|
||||
$md.Add("| Question | Answer |")
|
||||
$md.Add("| --- | --- |")
|
||||
$md.Add("| Should work stop before login? | No |")
|
||||
$md.Add("| Highest-value offline work | Deepen send/upload decomposition and prepare login validation items |")
|
||||
$md.Add("| Can production send be enabled offline? | No |")
|
||||
$md.Add("| Next package | login reachability probe package |")
|
||||
$md.Add("")
|
||||
$md.Add("## Sources")
|
||||
$md.Add("")
|
||||
$md.Add("| Source | Role | Exists | Lines | Size bytes |")
|
||||
$md.Add("| --- | --- | --- | ---: | ---: |")
|
||||
foreach ($source in $sourceSummary) {
|
||||
$md.Add("| ``$($source.path)`` | $($source.role) | $($source.exists) | $($source.lines) | $($source.size_bytes) |")
|
||||
}
|
||||
$md.Add("")
|
||||
$md.Add("## Deep-dive findings")
|
||||
$md.Add("")
|
||||
$md.Add("| Item | Stage | Status | Evidence | Business meaning |")
|
||||
$md.Add("| --- | --- | --- | ---: | --- |")
|
||||
foreach ($item in $itemResults) {
|
||||
$md.Add("| ``$($item.title)`` | ``$($item.stage)`` | ``$($item.status)`` | $($item.evidence_count) | $($item.meaning) |")
|
||||
}
|
||||
$md.Add("")
|
||||
$md.Add("## What this adds to tomorrow's login validation")
|
||||
$md.Add("")
|
||||
$md.Add("| Priority | Category | Validation item | Evidence to capture | Unlocks |")
|
||||
$md.Add("| --- | --- | --- | --- | --- |")
|
||||
foreach ($v in $newValidationItems) {
|
||||
$md.Add("| $($v.priority) | $($v.category) | $($v.item) | $($v.evidence) | $($v.unlocks) |")
|
||||
}
|
||||
$md.Add("")
|
||||
$md.Add("## Evidence by item")
|
||||
foreach ($item in $itemResults) {
|
||||
$md.Add("")
|
||||
$md.Add("### $($item.title)")
|
||||
$md.Add("")
|
||||
$md.Add("Stage: ``$($item.stage)``. Status: ``$($item.status)``. Evidence count: $($item.evidence_count).")
|
||||
$md.Add("")
|
||||
$md.Add("Business meaning: $($item.meaning)")
|
||||
$md.Add("")
|
||||
$md.Add("Must verify tomorrow:")
|
||||
foreach ($verify in $item.must_verify) {
|
||||
$md.Add("- $verify")
|
||||
}
|
||||
$md.Add("")
|
||||
if ($item.evidence_count -eq 0) {
|
||||
$md.Add("No static evidence found in configured IL sources.")
|
||||
continue
|
||||
}
|
||||
$md.Add("| Source | Kind | Snippet | Context |")
|
||||
$md.Add("| --- | --- | --- | --- |")
|
||||
foreach ($e in $item.evidence) {
|
||||
$snippet = ($e.snippet -replace "\|", "\\|")
|
||||
if ($snippet.Length -gt 240) { $snippet = $snippet.Substring(0, 237) + "..." }
|
||||
$context = ($e.context -replace "\|", "\\|")
|
||||
if ($context.Length -gt 180) { $context = $context.Substring(0, 177) + "..." }
|
||||
$md.Add("| ``$($e.source):$($e.line)`` | ``$($e.kind)`` | ``$snippet`` | ``$context`` |")
|
||||
}
|
||||
}
|
||||
$md.Add("")
|
||||
$md.Add("## Updated stop / continue rules")
|
||||
$md.Add("")
|
||||
$md.Add("Continue B-route tomorrow if P0 runtime, text-send, upload, FileID, and ack probes produce positive or actionable evidence. Stop B-route and escalate if logged-in runtime cannot expose AppContextManager/MessageScheduling/Chat/IFileTransfer, or if upload cannot produce FileUploadResult.FileID. Keep A-route/RPA backup-only for demo continuity.")
|
||||
|
||||
Write-Utf8NoBomLf $reportFullPath ($md -join "`n")
|
||||
|
||||
$validation = New-Object System.Collections.Generic.List[string]
|
||||
$validation.Add("# Login Reachability Validation Items")
|
||||
$validation.Add("")
|
||||
$validation.Add("Date: 2026-07-12")
|
||||
$validation.Add("")
|
||||
$validation.Add("Purpose: this is the operator-facing checklist for the 2026-07-13 logged-in environment. It is generated from the offline send/upload deep dive and should be used to build the one-shot reachability probe package.")
|
||||
$validation.Add("")
|
||||
$validation.Add("## P0 items")
|
||||
$validation.Add("")
|
||||
foreach ($v in $newValidationItems | Where-Object { $_.priority -eq "P0" }) {
|
||||
$validation.Add("### $($v.item)")
|
||||
$validation.Add("")
|
||||
$validation.Add("- Category: $($v.category)")
|
||||
$validation.Add("- Evidence to capture: $($v.evidence)")
|
||||
$validation.Add("- Unlocks: $($v.unlocks)")
|
||||
$validation.Add("")
|
||||
}
|
||||
$validation.Add("## P1 items")
|
||||
$validation.Add("")
|
||||
foreach ($v in $newValidationItems | Where-Object { $_.priority -eq "P1" }) {
|
||||
$validation.Add("### $($v.item)")
|
||||
$validation.Add("")
|
||||
$validation.Add("- Category: $($v.category)")
|
||||
$validation.Add("- Evidence to capture: $($v.evidence)")
|
||||
$validation.Add("- Unlocks: $($v.unlocks)")
|
||||
$validation.Add("")
|
||||
}
|
||||
$validation.Add("## Pass criteria")
|
||||
$validation.Add("")
|
||||
$validation.Add("- Text send can move forward when logged-in runtime exposes AppContextManager or MessageScheduling plus Chat/XMPPConnection and sent-record/ack correlation is possible.")
|
||||
$validation.Add("- File send can move forward when logged-in runtime exposes IFileTransfer/FileUpload, FileUploadPara values, FileUploadResult.FileID, and Chat.sendFileMessage finalization evidence.")
|
||||
$validation.Add("- Production remains blocked if any P0 item is missing or only static evidence is available.")
|
||||
|
||||
Write-Utf8NoBomLf $validationFullPath ($validation -join "`n")
|
||||
|
||||
[pscustomobject]@{
|
||||
ok = $true
|
||||
json_path = $jsonPath
|
||||
report_path = $reportFullPath
|
||||
validation_path = $validationFullPath
|
||||
items = $itemResults.Count
|
||||
validation_items = $newValidationItems.Count
|
||||
decision = $result.decision
|
||||
originals_copied = $false
|
||||
originals_modified = $false
|
||||
} | ConvertTo-Json -Depth 5
|
||||
Reference in New Issue
Block a user