@@ -0,0 +1,318 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user