fix(shangwutong): map kind 24 contact cid (#12)
Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
@@ -515,7 +515,7 @@ func (i *Inbound) recordMappingResult(event *dbgen.InboundEvent, mapped mappedEv
|
||||
|
||||
func knownInboundKind(kind int64) bool {
|
||||
switch kind {
|
||||
case -8, -5, -4, 0, 2, 3, 5, 7, 8, 11, 12, 15, 26, 29, 30, 31, 34, 35, 39, 41, 52, 56, 58, 61, 65, 66, 67, 71:
|
||||
case -8, -5, -4, 0, 2, 3, 5, 7, 8, 11, 12, 15, 24, 26, 29, 30, 31, 34, 35, 39, 41, 52, 56, 58, 61, 65, 66, 67, 71:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -70,6 +70,29 @@ func TestInboundKind52PersistsCIDWithoutCreatingResources(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInboundKind24PersistsCIDWithoutCreatingResources(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
database, account := deliveryDatabase(t, ctx)
|
||||
persistInboundEvent(t, database, account, swt.HeartbeatEvent{
|
||||
SessionID: "visitor", Kind: 24, Text: "cookie/123", SeqID: 43,
|
||||
Timestamp: time.Now().Format(time.RFC3339Nano), RawLine: "visitor 24 [REDACTED] 43 timestamp",
|
||||
})
|
||||
client := &inboundRecorder{}
|
||||
worker, err := NewInbound(database, client, nil, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if worked, err := worker.processInbound(ctx); err != nil || !worked {
|
||||
t.Fatalf("process kind=24 = %v, %v", worked, err)
|
||||
}
|
||||
if client.metadataUpdates != 1 || client.lastCID != "cookie/123" || client.lastSourceID != "visitor" || client.lastInboxID != account.GochatInboxID {
|
||||
t.Fatalf("metadata updates = %#v", client)
|
||||
}
|
||||
if len(client.imports) != 0 {
|
||||
t.Fatalf("kind=24 must not import a message: %#v", client.imports)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInboundKind3ConfirmsUniqueOutboundEcho(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
database, account := deliveryDatabase(t, ctx)
|
||||
|
||||
@@ -43,6 +43,10 @@ type mediaReference struct {
|
||||
|
||||
func mapInboundEvent(kind int64, seqID int64, text, operator, rawTimestamp, sourceID string, inboxID int64, fallbackTime time.Time) mappedEvent {
|
||||
mapped := mappedEvent{Strategy: "raw_only", RawOnly: true}
|
||||
mapped.ContactCID = parseContactCID(kind, text)
|
||||
if mapped.ContactCID != "" {
|
||||
mapped.Strategy = "contact_attributes"
|
||||
}
|
||||
baseAttributes := map[string]any{"swt": map[string]any{
|
||||
"kind": kind, "seq_id": seqID, "raw_timestamp": rawTimestamp, "historical": false, "unparsed": false,
|
||||
}}
|
||||
@@ -167,8 +171,7 @@ func mapInboundEvent(kind int64, seqID int64, text, operator, rawTimestamp, sour
|
||||
case 61:
|
||||
mapped.Strategy, mapped.RawOnly, mapped.RequiresContact = "contact_attributes", false, true
|
||||
mapped.ContactName = cleanText(text)
|
||||
case 52:
|
||||
mapped.ContactCID = parseHistoryCID(text)
|
||||
case 24, 52:
|
||||
case 56:
|
||||
mapped.Strategy, mapped.RawOnly, mapped.RequiresContact, mapped.RequiresConversation = "conversation_attributes", false, true, true
|
||||
mapped.ConversationAttrs = map[string]any{"swt_conversation_type": cleanText(text)}
|
||||
@@ -192,7 +195,15 @@ func mapInboundEvent(kind int64, seqID int64, text, operator, rawTimestamp, sour
|
||||
return mapped
|
||||
}
|
||||
|
||||
func parseHistoryCID(text string) string {
|
||||
func parseContactCID(kind int64, text string) string {
|
||||
// Android HeartBeat maps kind=24 text and kind=52 history field 1 to
|
||||
// ChatBean.cookies, which RenamedThread sends as changecname.aspx cid.
|
||||
if kind == 24 {
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
if kind != 52 {
|
||||
return ""
|
||||
}
|
||||
for _, record := range strings.Split(text, "#") {
|
||||
fields := strings.Split(record, "|")
|
||||
if len(fields) > 5 && strings.TrimSpace(fields[1]) != "" {
|
||||
|
||||
@@ -70,6 +70,33 @@ func TestKind52ExtractsCIDWithoutTreatingHistoryAsMessages(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCIDExtractionUsesOnlyVerifiedKinds(t *testing.T) {
|
||||
tests := []struct {
|
||||
kind int64
|
||||
text string
|
||||
want string
|
||||
}{
|
||||
{24, "cookie/123", "cookie/123"},
|
||||
{52, "a|history%2F123|x|y|z|name", "history/123"},
|
||||
}
|
||||
for _, kind := range []int64{0, 1, 3, 6, 7, 8, 11, 23, 31, 65, 67, 71, 73, 82} {
|
||||
tests = append(tests, struct {
|
||||
kind int64
|
||||
text string
|
||||
want string
|
||||
}{kind, `{"cid":"must-not-parse","SWTCID":"must-not-parse"}`, ""})
|
||||
}
|
||||
for _, test := range tests {
|
||||
mapped := mapInboundEvent(test.kind, 42, test.text, "", "", "source", 10, time.Now())
|
||||
if mapped.ContactCID != test.want {
|
||||
t.Fatalf("kind=%d ContactCID=%q, want %q", test.kind, mapped.ContactCID, test.want)
|
||||
}
|
||||
if test.want != "" && mapped.Strategy != "contact_attributes" {
|
||||
t.Fatalf("kind=%d strategy=%q, want contact_attributes", test.kind, mapped.Strategy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestKind26UpdatesCurrentContactPhone(t *testing.T) {
|
||||
mapped := mapInboundEvent(26, 42, "张三|13800138000", "", "", "source", 10, time.Now())
|
||||
if mapped.ContactName != "张三" || mapped.ContactPhone != "+8613800138000" || !mapped.RequiresContact {
|
||||
|
||||
@@ -15,6 +15,7 @@ func TestMetricsRenderProductionSeriesWithoutHighCardinalityLabels(t *testing.T)
|
||||
metrics.Delivery("inbound", "delivered")
|
||||
metrics.StatusSync("success", "sent")
|
||||
metrics.Mapping(2, "native_message", "delivered")
|
||||
metrics.Mapping(24, "contact_attributes", "delivered")
|
||||
metrics.Unknown(999)
|
||||
metrics.UnmappedRetraction("incoming")
|
||||
metrics.ContractError("gochat_to_connector", "invalid_signature")
|
||||
@@ -45,6 +46,9 @@ func TestMetricsRenderProductionSeriesWithoutHighCardinalityLabels(t *testing.T)
|
||||
t.Fatalf("high-cardinality label %q found in:\n%s", forbidden, payload)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(payload, `swt_connector_event_mapping_total{kind="24",strategy="contact_attributes",result="delivered"} 1`) {
|
||||
t.Fatalf("redacted kind=24 mapping diagnostic missing from:\n%s", payload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetricKindKeepsEveryDocumentedKindBounded(t *testing.T) {
|
||||
|
||||
@@ -63,7 +63,7 @@ func (s *Store) PersistHeartbeat(ctx context.Context, account *dbgen.Account, ev
|
||||
|
||||
func ignoredKind(kind int) bool {
|
||||
switch kind {
|
||||
case -7, 1, 14, 24, 38, 44, 62:
|
||||
case -7, 1, 14, 38, 44, 62:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -48,12 +48,12 @@ func TestPersistHeartbeatStoresEventsBeforeCursorAndIsIdempotent(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIgnoredKindMatrix(t *testing.T) {
|
||||
for _, kind := range []int{-7, 1, 14, 24, 38, 44, 62} {
|
||||
for _, kind := range []int{-7, 1, 14, 38, 44, 62} {
|
||||
if !ignoredKind(kind) {
|
||||
t.Fatalf("kind %d should be ignored", kind)
|
||||
}
|
||||
}
|
||||
for _, kind := range []int{-8, -5, -4, 0, 2, 3, 11, 31, 52, 67, 71} {
|
||||
for _, kind := range []int{-8, -5, -4, 0, 2, 3, 11, 24, 31, 52, 67, 71} {
|
||||
if ignoredKind(kind) {
|
||||
t.Fatalf("kind %d must remain deliverable", kind)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,16 @@ func TestParseHeartbeatLinePreservesTextSpaces(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseHeartbeatLineDecodesKind24CID(t *testing.T) {
|
||||
event, err := ParseHeartbeatLine("visitor 24 cookie%2F123 43 639183484622475272")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if event.Kind != 24 || event.Text != "cookie/123" || event.OpName != "" {
|
||||
t.Fatalf("unexpected event: %#v", event)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetractionTargetIsTextNotEventSequence(t *testing.T) {
|
||||
event, err := ParseHeartbeatLine("sid -4 4360377 4360400 639183484622475272")
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
日期:2026-08-13
|
||||
|
||||
依赖:`reference/shang-wu-tong/reverse/swt-decompiled/`、H-47 脱敏真实 kind 统计。
|
||||
|
||||
## 结论
|
||||
|
||||
商务通实时入站的 `cid` 来自 `kind=24` 的 text,不需要等待 `kind=52` 历史批次,也不能用 `sid` 替代。
|
||||
|
||||
## 协议证据
|
||||
|
||||
- `HeartBeat.java:520` 将 `kind=24` 排除出普通事件分支,`:3458-3463` 直接把已解码 `text2` 写入 `ChatBean.cookies`。
|
||||
- `HeartBeat.java:566-578` 将 `kind=52` 历史记录第二字段 URL 解码后写入同一 `ChatBean.cookies`。
|
||||
- `ChatDetailFragmentMessage.java:780-860` 调用 `RenamedThread` 修改昵称。
|
||||
- `RenamedThread.java:35-48` 把 `ChatBean.cookies` 作为 `oc/changecname.aspx` 的 `cid` 表单字段。
|
||||
|
||||
因此 Connector 只接受 APK 已证实的 `kind=24` 和 `kind=52` CID 来源。`kind=65`/渠道 JSON 可能有同名字段,但属于渠道扩展协议,不作为通用来源;其他 kind 即使正文出现 `cid`、`SWTCID` 或 `sid` 也不得猜测。
|
||||
|
||||
## 脱敏真实样本
|
||||
|
||||
授权轮询仅保留 kind 分布:`0/1/3/6/7/8/11/23/24/31/65/67/71/73/82`,`kind=52` 为零。未保留访客正文、完整 SID、CID 或凭据。
|
||||
|
||||
该分布覆盖到 `kind=24`,说明旧实现把它标记为 ignored 是昵称回写无 CID 的直接根因。
|
||||
Reference in New Issue
Block a user