319 lines
8.1 KiB
Go
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
|
|
}
|