feat: add getBusinessStoryManifest capture + multi-strategy proto probe + no_match hex dump
This commit is contained in:
@@ -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 ----
|
||||
|
||||
Reference in New Issue
Block a user