package asteriskws import ( "encoding/json" "strconv" "strings" "ai-operator/internal/media" ) type ControlEventType string const ( ControlMediaStart ControlEventType = "MEDIA_START" ControlDTMFEnd ControlEventType = "DTMF_END" ControlMediaXOFF ControlEventType = "MEDIA_XOFF" ControlMediaXON ControlEventType = "MEDIA_XON" ControlStatus ControlEventType = "STATUS" ControlMediaBufferingCompleted ControlEventType = "MEDIA_BUFFERING_COMPLETED" ControlMediaMarkProcessed ControlEventType = "MEDIA_MARK_PROCESSED" ControlQueueDrained ControlEventType = "QUEUE_DRAINED" ControlUnknown ControlEventType = "UNKNOWN" ) type ControlEvent struct { Type ControlEventType Raw string Fields map[string]string } func ParseControlEvent(text string) ControlEvent { raw := strings.TrimSpace(text) if raw == "" { return ControlEvent{Type: ControlUnknown, Raw: text, Fields: map[string]string{}} } if strings.HasPrefix(raw, "{") { return parseJSONControl(raw) } parts := strings.Fields(raw) fields := map[string]string{} for _, part := range parts[1:] { if k, v, ok := strings.Cut(part, ":"); ok { fields[k] = v } else if fields["correlation_id"] == "" { fields["correlation_id"] = part } } return ControlEvent{Type: knownType(parts[0]), Raw: raw, Fields: fields} } func parseJSONControl(raw string) ControlEvent { var data map[string]any if err := json.Unmarshal([]byte(raw), &data); err != nil { return ControlEvent{Type: ControlUnknown, Raw: raw, Fields: map[string]string{"error": "invalid_json"}} } typeName, _ := data["event"].(string) if typeName == "" { typeName, _ = data["type"].(string) } fields := map[string]string{} for k, v := range data { switch t := v.(type) { case string: fields[k] = t case float64: fields[k] = strconv.Itoa(int(t)) case bool: fields[k] = strconv.FormatBool(t) } } return ControlEvent{Type: knownType(typeName), Raw: raw, Fields: fields} } func knownType(v string) ControlEventType { switch ControlEventType(v) { case ControlMediaStart, ControlDTMFEnd, ControlMediaXOFF, ControlMediaXON, ControlStatus, ControlMediaBufferingCompleted, ControlMediaMarkProcessed, ControlQueueDrained: return ControlEventType(v) } return ControlUnknown } func (e ControlEvent) MediaStart() *media.MediaStartInfo { if e.Type != ControlMediaStart { return nil } opt, _ := strconv.Atoi(value(e.Fields, "optimal_frame_size", "optimalFrameSize")) ptime, _ := strconv.Atoi(value(e.Fields, "ptime", "ptime_ms")) return &media.MediaStartInfo{ConnectionID: e.Fields["connection_id"], Channel: e.Fields["channel"], ChannelID: e.Fields["channel_id"], Format: e.Fields["format"], OptimalFrameSize: opt, PTimeMS: ptime} } func value(m map[string]string, keys ...string) string { for _, k := range keys { if m[k] != "" { return m[k] } } return "" }