mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
133 lines
4.9 KiB
Go
133 lines
4.9 KiB
Go
package store
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// ConcatMessageIDPrefix marks the stable message id that ingest points assign to
|
|
// every segment of one concatenated (long) SMS. Unlike the per-segment modem/IMS
|
|
// ids (which embed a storage slot, PDU hash, or RP reference), this id is shared
|
|
// by all segments of the message, so SaveSMSMessage folds them into a single row.
|
|
const ConcatMessageIDPrefix = "concat:"
|
|
|
|
// isConcatSMSMessageID reports whether a message id addresses a whole
|
|
// concatenated SMS rather than one physical segment.
|
|
func isConcatSMSMessageID(messageID string) bool {
|
|
return strings.HasPrefix(messageID, ConcatMessageIDPrefix)
|
|
}
|
|
|
|
// ConcatSMSReadyToNotify reports whether an inbound SMS row is ready to surface
|
|
// to a notification consumer. A plain message is always ready; a concatenated
|
|
// (long) SMS row is ready only once every segment has merged (concat_complete).
|
|
// Until then consumers should hold the notification but still advance their
|
|
// cursor — the completed message re-enters as a fresh durable id.
|
|
func ConcatSMSReadyToNotify(messageID string, extra json.RawMessage) bool {
|
|
if !isConcatSMSMessageID(messageID) {
|
|
return true
|
|
}
|
|
document, err := decodeJSONObject(extra)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
complete, _ := document["concat_complete"].(bool)
|
|
return complete
|
|
}
|
|
|
|
// StableConcatMessageID builds the message id shared by every segment of one
|
|
// concatenated SMS. The UDH concat reference is only unique per sender, so the
|
|
// hardware identity and peer scope it; total is folded in to further separate the
|
|
// rare reference reuse between two different long messages from the same peer.
|
|
// The hardware identity matches the row lookup in saveSMSMessage, so a segment
|
|
// always finds the row its siblings started.
|
|
func StableConcatMessageID(source, modemIMEI, deviceID, peer string, reference, total int) string {
|
|
return ConcatMessageIDPrefix + source + ":" + smsHardwareKey(modemIMEI, deviceID) + ":" + peer + ":" +
|
|
strconv.Itoa(reference) + ":" + strconv.Itoa(total)
|
|
}
|
|
|
|
// mergeConcatSegment folds one incoming segment into the progressively merged
|
|
// body of a concatenated SMS. existingExtra is the stored row's Extra (empty for
|
|
// the first segment); segmentBody/segmentExtra are the incoming segment's text
|
|
// and Extra, the latter carrying "concat" ({reference,total,sequence}).
|
|
//
|
|
// Each segment's text is kept under "concat_parts" keyed by its UDH sequence and
|
|
// the body is rebuilt by joining the parts in ascending sequence order with no
|
|
// separator — exactly how a phone reassembles a long message, and correct for any
|
|
// arrival order. The merge is idempotent: redelivering an already-folded sequence
|
|
// reports changed=false so callers can skip the write and avoid id churn.
|
|
// "concat_complete" flips true once Total segments are present.
|
|
func mergeConcatSegment(
|
|
existingExtra json.RawMessage,
|
|
segmentBody string,
|
|
segmentExtra json.RawMessage,
|
|
) (body string, extra json.RawMessage, changed bool, err error) {
|
|
segment, err := decodeJSONObject(segmentExtra)
|
|
if err != nil {
|
|
return "", nil, false, fmt.Errorf("decode segment extra: %w", err)
|
|
}
|
|
concat, _ := segment["concat"].(map[string]any)
|
|
sequence := numberAsInt(concat["sequence"])
|
|
total := numberAsInt(concat["total"])
|
|
if sequence < 1 {
|
|
// No usable UDH sequence: keep the incoming segment as the whole body.
|
|
return segmentBody, json.RawMessage(segmentExtra), true, nil
|
|
}
|
|
|
|
// Seed the per-segment texts from the previously stored parts so an
|
|
// out-of-order arrival always rebuilds in sequence order.
|
|
parts := map[int]string{}
|
|
if len(existingExtra) > 0 {
|
|
if existing, derr := decodeJSONObject(existingExtra); derr == nil {
|
|
if stored, ok := existing["concat_parts"].(map[string]any); ok {
|
|
for key, value := range stored {
|
|
n, aerr := strconv.Atoi(key)
|
|
if aerr != nil || n < 1 {
|
|
continue
|
|
}
|
|
if text, ok := value.(string); ok {
|
|
parts[n] = text
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
prior, alreadyHad := parts[sequence]
|
|
changed = !alreadyHad || prior != segmentBody
|
|
parts[sequence] = segmentBody
|
|
|
|
sequences := make([]int, 0, len(parts))
|
|
for n := range parts {
|
|
sequences = append(sequences, n)
|
|
}
|
|
sort.Ints(sequences)
|
|
|
|
var joined strings.Builder
|
|
stored := make(map[string]string, len(parts))
|
|
for _, n := range sequences {
|
|
joined.WriteString(parts[n])
|
|
stored[strconv.Itoa(n)] = parts[n]
|
|
}
|
|
complete := total > 0 && len(parts) >= total
|
|
|
|
merged := map[string]any{
|
|
"concat": concat,
|
|
"concat_parts": stored,
|
|
"concat_received": len(parts),
|
|
"concat_complete": complete,
|
|
}
|
|
// Preserve non-concat metadata from the latest segment for context.
|
|
for _, key := range []string{"encoding", "storage", "transport", "source"} {
|
|
if value, ok := segment[key]; ok {
|
|
merged[key] = value
|
|
}
|
|
}
|
|
encoded, err := json.Marshal(merged)
|
|
if err != nil {
|
|
return "", nil, false, fmt.Errorf("encode merged concat extra: %w", err)
|
|
}
|
|
return joined.String(), json.RawMessage(encoded), changed, nil
|
|
}
|