// Package volcsubtitle implements the native RTS subtitle callback documented at // https://docs.volcengine.com/docs/6348/1337284 (verified 2026-09-06). package volcsubtitle import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/binary" "encoding/json" "errors" "unicode/utf8" ) var ErrInvalid = errors.New("invalid native subtitle packet") type Envelope struct { Message string `json:"message"` Signature string `json:"signature"` } type Segment struct { UserID string `json:"userId"` RoundID *int64 `json:"roundId"` Sequence *int64 `json:"sequence"` Text string `json:"text"` Definite bool `json:"definite"` Paragraph bool `json:"paragraph"` } type Packet struct { Type string `json:"type"` Data []Segment `json:"data"` DeviceName string `json:"deviceName,omitempty"` ProductKey string `json:"productKey,omitempty"` } // Bind authentication to the platform conversation. A delayed callback can // never be reassigned by looking up the device's newest active conversation. func Signature(secret []byte, conversationID string) string { mac := hmac.New(sha256.New, secret) mac.Write([]byte("volc-subtitle-v1\n" + conversationID)) return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) } func Decode(message string) (Packet, error) { var p Packet if len(message) > 350000 { return p, ErrInvalid } data, err := base64.StdEncoding.DecodeString(message) if err != nil || len(data) == 0 { return p, ErrInvalid } if data[0] != '{' { if len(data) < 8 || string(data[:4]) != "subv" || uint64(binary.BigEndian.Uint32(data[4:8])) != uint64(len(data)-8) { return p, ErrInvalid } data = data[8:] } if !utf8.Valid(data) { return p, ErrInvalid } if json.Unmarshal(data, &p) != nil || p.Type != "subtitle" || len(p.Data) == 0 || len(p.Data) > 100 || len(p.DeviceName) > 128 || len(p.ProductKey) > 200 { return Packet{}, ErrInvalid } for _, s := range p.Data { if s.UserID == "" || len(s.UserID) > 200 || s.RoundID == nil || s.Sequence == nil || *s.RoundID < 0 || *s.Sequence < 0 || len([]rune(s.Text)) > 20000 { return Packet{}, ErrInvalid } } return p, nil }