diff --git a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/CompanionServer.kt b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/CompanionServer.kt index 6e8c50dc..4f07329f 100644 --- a/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/CompanionServer.kt +++ b/core/src/main/kotlin/me/eternal/purrfectsnap/core/features/impl/global/CompanionServer.kt @@ -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 ----