Files
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

319 lines
8.1 KiB
Go

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
}