package imclient import ( "encoding/base64" "encoding/json" "strconv" "strings" "unicode" "unicode/utf8" "google.golang.org/protobuf/encoding/protowire" ) type historyMessageWire struct { MessageID json.RawMessage `json:"messageId"` ID json.RawMessage `json:"id"` MsgID json.RawMessage `json:"msgId"` MID json.RawMessage `json:"mid"` SenderID int64 `json:"senderId"` ReceiverID int64 `json:"receiverId"` Content string `json:"content"` Text string `json:"text"` Body string `json:"body"` MsgContent string `json:"msgContent"` MessageContent string `json:"messageContent"` Payload json.RawMessage `json:"payload"` ExtInfo json.RawMessage `json:"extInfo"` Attachment json.RawMessage `json:"attachment"` Message json.RawMessage `json:"message"` MessageType json.RawMessage `json:"messageType"` Seq int64 `json:"seq"` CreatedAt int64 `json:"createdAt"` } func (m *MessageDetail) UnmarshalJSON(data []byte) error { var wire historyMessageWire if err := json.Unmarshal(data, &wire); err != nil { return err } m.SenderID = wire.SenderID m.ReceiverID = wire.ReceiverID m.Seq = wire.Seq m.CreatedAt = wire.CreatedAt m.MessageID = firstNonEmpty( jsonScalarString(wire.MessageID), jsonScalarString(wire.MsgID), jsonScalarString(wire.MID), jsonScalarString(wire.ID), ) m.MessageType = normalizeHistoryMessageType(wire.MessageType) if m.MessageType == "" { m.MessageType = extractPayloadMessageType(wire.Payload) } m.Content = firstNonEmpty( strings.TrimSpace(wire.Content), strings.TrimSpace(wire.Text), strings.TrimSpace(wire.Body), strings.TrimSpace(wire.MsgContent), strings.TrimSpace(wire.MessageContent), extractPayloadContent(wire.Payload, m.MessageID, strconv.FormatInt(m.SenderID, 10), strconv.FormatInt(m.ReceiverID, 10)), extractPayloadContent(wire.ExtInfo, m.MessageID, strconv.FormatInt(m.SenderID, 10), strconv.FormatInt(m.ReceiverID, 10)), extractAttachmentContent(wire.Attachment), ) if nested := decodeHistoryMessageWire(wire.Message); nested != nil { if m.MessageID == "" { m.MessageID = nested.messageID } if m.Content == "" { m.Content = nested.content } if m.MessageType == "" { m.MessageType = nested.messageType } } if m.MessageID == "" && m.Seq > 0 { m.MessageID = strconv.FormatInt(m.Seq, 10) } return nil } type decodedHistoryMessage struct { messageID string content string messageType string } func decodeHistoryMessageWire(raw json.RawMessage) *decodedHistoryMessage { if len(raw) == 0 || string(raw) == "null" { return nil } var nested historyMessageWire if err := json.Unmarshal(raw, &nested); err != nil { return nil } msg := &decodedHistoryMessage{ messageID: firstNonEmpty( jsonScalarString(nested.MessageID), jsonScalarString(nested.MsgID), jsonScalarString(nested.MID), jsonScalarString(nested.ID), ), messageType: normalizeHistoryMessageType(nested.MessageType), } if msg.messageType == "" { msg.messageType = extractPayloadMessageType(nested.Payload) } msg.content = firstNonEmpty( strings.TrimSpace(nested.Content), strings.TrimSpace(nested.Text), strings.TrimSpace(nested.Body), strings.TrimSpace(nested.MsgContent), strings.TrimSpace(nested.MessageContent), extractPayloadContent(nested.Payload, msg.messageID, strconv.FormatInt(nested.SenderID, 10), strconv.FormatInt(nested.ReceiverID, 10)), extractPayloadContent(nested.ExtInfo, msg.messageID, strconv.FormatInt(nested.SenderID, 10), strconv.FormatInt(nested.ReceiverID, 10)), extractAttachmentContent(nested.Attachment), ) if msg.messageID == "" && msg.content == "" && msg.messageType == "" { return nil } return msg } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" } func jsonScalarString(raw json.RawMessage) string { if len(raw) == 0 || string(raw) == "null" { return "" } var s string if err := json.Unmarshal(raw, &s); err == nil { return strings.TrimSpace(s) } var n json.Number if err := json.Unmarshal(raw, &n); err == nil { return n.String() } return "" } func extractPayloadContent(raw json.RawMessage, excludes ...string) string { if len(raw) == 0 || string(raw) == "null" { return "" } var s string if err := json.Unmarshal(raw, &s); err == nil { return strings.TrimSpace(s) } var payload map[string]json.RawMessage if err := json.Unmarshal(raw, &payload); err != nil { return "" } for _, key := range []string{"text", "content", "body"} { if value := jsonScalarString(payload[key]); value != "" { return value } } if value := jsonScalarString(payload["data"]); value != "" { if content := extractProtobufPayloadText(value, excludes...); content != "" { return content } } return "" } func extractPayloadMessageType(raw json.RawMessage) string { if len(raw) == 0 || string(raw) == "null" { return "" } var payload map[string]json.RawMessage if err := json.Unmarshal(raw, &payload); err != nil { return "" } return normalizeHistoryMessageType(payload["type"]) } func extractAttachmentContent(raw json.RawMessage) string { if len(raw) == 0 || string(raw) == "null" { return "" } var attachment map[string]json.RawMessage if err := json.Unmarshal(raw, &attachment); err != nil { return "" } if fileName := jsonScalarString(attachment["fileName"]); fileName != "" { return fileName } if url := jsonScalarString(attachment["url"]); url != "" { return url } return "" } func normalizeHistoryMessageType(raw json.RawMessage) string { if len(raw) == 0 || string(raw) == "null" { return "" } var s string if err := json.Unmarshal(raw, &s); err == nil { return strings.TrimSpace(s) } var n int if err := json.Unmarshal(raw, &n); err == nil { return historyMessageTypeName(n) } return strings.TrimSpace(string(raw)) } func historyMessageTypeName(messageType int) string { switch messageType { case MessageTypeText: return "TEXT" case 1: return "AUDIO" case MessageTypeImage: return "IMAGE" case 3: return "VIDEO" case 4: return "FILE" case 5: return "EMOJI" case 100: return "CUSTOMIZED" default: return strconv.Itoa(messageType) } } func extractProtobufPayloadText(encoded string, excludes ...string) string { raw, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return "" } excludeSet := make(map[string]struct{}, len(excludes)) for _, item := range excludes { if item = strings.TrimSpace(item); item != "" { excludeSet[item] = struct{}{} } } candidates := make([]string, 0, 4) collectProtoTextCandidates(raw, 0, excludeSet, &candidates) if len(candidates) == 0 { return "" } return candidates[len(candidates)-1] } func collectProtoTextCandidates(raw []byte, depth int, excludes map[string]struct{}, candidates *[]string) { if len(raw) == 0 || depth > 8 { return } for len(raw) > 0 { _, typ, n := protowire.ConsumeTag(raw) if n < 0 { return } raw = raw[n:] switch typ { case protowire.BytesType: value, m := protowire.ConsumeBytes(raw) if m < 0 { return } if candidate := protoStringCandidate(value, excludes); candidate != "" { *candidates = append(*candidates, candidate) } collectProtoTextCandidates(value, depth+1, excludes, candidates) raw = raw[m:] default: m := protowire.ConsumeFieldValue(0, typ, raw) if m < 0 { return } raw = raw[m:] } } } func protoStringCandidate(raw []byte, excludes map[string]struct{}) string { if len(raw) == 0 || !utf8.Valid(raw) { return "" } value := strings.TrimSpace(string(raw)) if value == "" { return "" } if _, ok := excludes[value]; ok { return "" } if isLongNumber(value) { return "" } for _, r := range value { if unicode.IsControl(r) && r != '\n' && r != '\r' && r != '\t' { return "" } } return value } func isLongNumber(value string) bool { if len(value) < 9 { return false } for _, r := range value { if r < '0' || r > '9' { return false } } return true }