feat: add getBusinessStoryManifest capture + multi-strategy proto probe + no_match hex dump

This commit is contained in:
Majjo
2026-04-20 14:22:19 +02:00
parent cab8c69f72
commit d9593fa629

View File

@@ -216,14 +216,40 @@ class CompanionServer : Feature("CompanionServer") {
private fun subscribeToStoryEvents() {
context.event.subscribe(NetworkApiRequestEvent::class) { event ->
val urlPath = event.url.substringBefore("?")
if (!urlPath.contains("df-mixer-prod")) return@subscribe
val isStoryUrl = urlPath.contains("df-mixer-prod") || urlPath.contains("story-service")
if (!isStoryUrl) return@subscribe
val endpoint = urlPath.substringAfterLast("/")
// Log every df-mixer-prod URL hit for debugging
// Log every hit so the Stories debug panel shows what's being seen
pushStoryDebug("seen", mapOf("url" to urlPath, "endpoint" to endpoint))
context.log.info("CompanionServer: [story] seen url=$urlPath", "CompanionServer")
if (endpoint !in setOf("stories", "batch_stories", "story_lookup", "batch_story_lookup")) return@subscribe
val knownEndpoints = setOf(
"stories", "batch_stories", "story_lookup", "batch_story_lookup",
"getBusinessStoryManifest"
)
if (endpoint !in knownEndpoints) return@subscribe
pushStoryDebug("matched", mapOf("endpoint" to endpoint))
context.log.info("CompanionServer: [$endpoint] matched, awaiting response", "CompanionServer")
// For getBusinessStoryManifest: capture the POST request body to extract the creator userId
var requestUserId: String? = null
if (endpoint == "getBusinessStoryManifest" && event.uploadDataProvider != null) {
event.hookRequestBuffer { reqBuffer ->
runCatching {
// Creator userId or username is typically in the request proto
// Try a few common string fields: f1, f2, f3, f4
val reader = ProtoReader(reqBuffer)
requestUserId = reader.getString(1)
?: reader.getString(2)
?: reader.getString(3)
?: reader.getString(4)
if (requestUserId != null)
context.log.info("CompanionServer: [getBusinessStoryManifest] request userId=$requestUserId", "CompanionServer")
}
reqBuffer
}
}
event.onSuccess { buffer ->
if (buffer == null) {
pushStoryDebug("null_buffer", mapOf("endpoint" to endpoint))
@@ -234,43 +260,129 @@ class CompanionServer : Feature("CompanionServer") {
pushStoryDebug("response", mapOf("endpoint" to endpoint, "bytes" to buffer.size))
runCatching {
var parsed = 0
// story_lookup / batch_story_lookup: field 1 → repeated story entries
// Each entry: field 4 = userId, field 3 → media block → field 36 → field 1 → key/iv/url
ProtoReader(buffer).eachBuffer(1) {
val userId = getString(4, 1) ?: return@eachBuffer
eachBuffer(3) {
followPath(36) {
if (endpoint == "getBusinessStoryManifest") {
// Strategy A: field 1 = repeated snap entries, each has media at field 2
// media: url=(2,2), key=(2,5), iv=(2,4), postedAt=3, createdAt=27
var countA = 0
val fallbackUserId = requestUserId ?: "business_unknown"
ProtoReader(buffer).eachBuffer(1) {
val userId = getString(4) ?: getString(8) ?: getString(1) ?: fallbackUserId
val url = getString(2, 2)?.substringBefore("?")
val postedAt = getVarInt(3)
val createdAt = getVarInt(27) ?: -1L
val keyB64 = getString(2, 5)
val ivB64 = getString(2, 4)
if (url != null && postedAt != null && keyB64 != null && ivB64 != null) {
if (storeStory(userId, url, postedAt, createdAt, keyB64, ivB64)) countA++
}
}
pushStoryDebug("probe", mapOf("endpoint" to endpoint, "strategy" to "A_f1_media", "found" to countA))
// Strategy B: field 1 → field 1 → snap media
var countB = 0
if (countA == 0) {
ProtoReader(buffer).eachBuffer(1) {
val outerUserId = getString(4) ?: getString(1) ?: fallbackUserId
eachBuffer(1) {
val url = getString(2, 2)?.substringBefore("?") ?: return@eachBuffer
val postedAt = getVarInt(3) ?: return@eachBuffer
val url = getString(2, 2)?.substringBefore("?")
val postedAt = getVarInt(3)
val createdAt = getVarInt(27) ?: -1L
val keyB64 = getString(2, 5) ?: return@eachBuffer
val ivB64 = getString(2, 4) ?: return@eachBuffer
if (storeStory(userId, url, postedAt, createdAt, keyB64, ivB64)) parsed++
val keyB64 = getString(2, 5)
val ivB64 = getString(2, 4)
if (url != null && postedAt != null && keyB64 != null && ivB64 != null) {
if (storeStory(outerUserId, url, postedAt, createdAt, keyB64, ivB64)) countB++
}
}
}
pushStoryDebug("probe", mapOf("endpoint" to endpoint, "strategy" to "B_f1_f1", "found" to countB))
}
// Strategy C: mixer-style field 36 → field 1 (maybe publisher stories reuse this path)
var countC = 0
if (countA == 0 && countB == 0) {
ProtoReader(buffer).followPath(36) {
eachBuffer(1) {
val userId = getString(8, 1) ?: getString(4) ?: fallbackUserId
val url = getString(2, 2)?.substringBefore("?")
val postedAt = getVarInt(3)
val createdAt = getVarInt(27) ?: -1L
val keyB64 = getString(2, 5)
val ivB64 = getString(2, 4)
if (url != null && postedAt != null && keyB64 != null && ivB64 != null) {
if (storeStory(userId, url, postedAt, createdAt, keyB64, ivB64)) countC++
}
}
}
pushStoryDebug("probe", mapOf("endpoint" to endpoint, "strategy" to "C_f36_f1", "found" to countC))
}
// Strategy D: field 2 → field 1 (alternate outer wrapper)
var countD = 0
if (countA == 0 && countB == 0 && countC == 0) {
ProtoReader(buffer).eachBuffer(2) {
val outerUserId = getString(4) ?: getString(1) ?: fallbackUserId
eachBuffer(1) {
val url = getString(2, 2)?.substringBefore("?")
val postedAt = getVarInt(3)
val createdAt = getVarInt(27) ?: -1L
val keyB64 = getString(2, 5)
val ivB64 = getString(2, 4)
if (url != null && postedAt != null && keyB64 != null && ivB64 != null) {
if (storeStory(outerUserId, url, postedAt, createdAt, keyB64, ivB64)) countD++
}
}
}
pushStoryDebug("probe", mapOf("endpoint" to endpoint, "strategy" to "D_f2_f1", "found" to countD))
}
parsed = countA + countB + countC + countD
// If all strategies returned 0, log the first 64 bytes as hex for manual inspection
if (parsed == 0) {
val hexPreview = buffer.take(64).joinToString(" ") { "%02x".format(it) }
pushStoryDebug("no_match", mapOf("endpoint" to endpoint, "hex_preview" to hexPreview))
context.log.warn("CompanionServer: [$endpoint] all strategies returned 0 — hex: $hexPreview", "CompanionServer")
}
} else {
// df-mixer-prod endpoints: story_lookup / batch_story_lookup
ProtoReader(buffer).eachBuffer(1) {
val userId = getString(4, 1) ?: return@eachBuffer
eachBuffer(3) {
followPath(36) {
eachBuffer(1) {
val url = getString(2, 2)?.substringBefore("?") ?: return@eachBuffer
val postedAt = getVarInt(3) ?: return@eachBuffer
val createdAt = getVarInt(27) ?: -1L
val keyB64 = getString(2, 5) ?: return@eachBuffer
val ivB64 = getString(2, 4) ?: return@eachBuffer
if (storeStory(userId, url, postedAt, createdAt, keyB64, ivB64)) parsed++
}
}
}
}
}
// batch_stories fallback: field 3 → field 3 → field 3 → field 36 → field 1
if (parsed == 0) {
ProtoReader(buffer).eachBuffer(3) {
eachBuffer(3) {
// batch_stories fallback: field 3 → field 3 → field 3 → field 36 → field 1
if (parsed == 0) {
ProtoReader(buffer).eachBuffer(3) {
eachBuffer(3) {
followPath(36) {
eachBuffer(1) {
val userId = getString(8, 1) ?: return@eachBuffer
val url = getString(2, 2)?.substringBefore("?") ?: return@eachBuffer
val postedAt = getVarInt(3) ?: return@eachBuffer
val createdAt = getVarInt(27) ?: -1L
val keyB64 = getString(2, 5) ?: return@eachBuffer
val ivB64 = getString(2, 4) ?: return@eachBuffer
if (storeStory(userId, url, postedAt, createdAt, keyB64, ivB64)) parsed++
eachBuffer(3) {
followPath(36) {
eachBuffer(1) {
val userId = getString(8, 1) ?: return@eachBuffer
val url = getString(2, 2)?.substringBefore("?") ?: return@eachBuffer
val postedAt = getVarInt(3) ?: return@eachBuffer
val createdAt = getVarInt(27) ?: -1L
val keyB64 = getString(2, 5) ?: return@eachBuffer
val ivB64 = getString(2, 4) ?: return@eachBuffer
if (storeStory(userId, url, postedAt, createdAt, keyB64, ivB64)) parsed++
}
}
}
}
}
}
}
context.log.info("CompanionServer: [$endpoint] parsed $parsed new stories (total=${capturedStories.values.sumOf { it.size }})", "CompanionServer")
pushStoryDebug("parsed", mapOf("endpoint" to endpoint, "count" to parsed, "totalStories" to capturedStories.values.sumOf { it.size }))
}.onFailure {
@@ -611,6 +723,7 @@ a#logout:hover{background:#222}
#stories-panel{flex:1;display:none;flex-direction:column;overflow:hidden}
.sd-row{padding:2px 6px;white-space:pre-wrap;word-break:break-all}.sd-row:hover{background:#141414}
.sd-seen{color:#555}.sd-matched{color:#5c8fbd}.sd-response{color:#4caf50}.sd-parsed{color:#c9944a}.sd-null_buffer{color:#c45252}.sd-parse_error{color:#c45252}
.sd-probe{color:#8a6bb5}.sd-no_match{color:#c45252;font-weight:bold}
/* Detail panel */
#detail{flex-direction:column;flex-shrink:0;height:45%;border-top:2px solid #2a2a2a;background:#0a0a0a;overflow:hidden}
#detail-bar{padding:4px 8px;background:#111;border-bottom:1px solid #222;display:flex;align-items:center;gap:6px;flex-wrap:wrap;flex-shrink:0}
@@ -905,6 +1018,8 @@ function addStoryDebugRow(e){
else if(t==='parsed')extra=' endpoint='+e.endpoint+' new='+e.count+' total='+e.totalStories;
else if(t==='null_buffer')extra=' endpoint='+e.endpoint+' (null buffer!)';
else if(t==='parse_error')extra=' endpoint='+e.endpoint+' ERR: '+e.error;
else if(t==='probe')extra=' ['+e.endpoint+'] strategy='+e.strategy+' found='+e.found;
else if(t==='no_match')extra=' ['+e.endpoint+'] ALL STRATEGIES FAILED hex='+e.hex_preview;
el.textContent='['+ts(e.ts||'0')+'] '+t.toUpperCase()+extra;
q('sd-log').appendChild(el);
}// ---- Shared ----