Files
Codex Local 0c2151ace9
ci / verify (push) Canceled after 0s
chore: checkpoint 第一版
2026-09-06 17:04:11 +08:00

73 lines
2.1 KiB
Go

// 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
}