// Package nvda2cthulhu accepts the local WebSocket protocol used by NVDA // compatibility bridges and routes it to Cthulhu's speech and braille queues. package nvda2cthulhu import ( "bytes" "errors" "strconv" "strings" "github.com/gorilla/websocket" ) func (server *Server) deliver(request bridgeRequest) { switch request.kind { case "SpeakText": if server.handler.SpeakText(request.text, true) { server.logger.Debug("NVDA to Cthulhu speech accepted", "bytes", len(request.text)) } else { server.logger.Warn("NVDA to Cthulhu speech rejected", "bytes", len(request.text)) } case "BrailleText": if server.handler.BrailleMessage(request.text) { server.logger.Debug("NVDA to Cthulhu braille accepted", "bytes", len(request.text)) } else { server.logger.Debug("NVDA to Cthulhu braille unavailable") } case "CancelSpeech": if server.handler.CancelSpeech() { server.logger.Debug("NVDA to Cthulhu cancellation accepted") } else { server.logger.Warn("NVDA to Cthulhu cancellation rejected") } } } type bridgeRequest struct { kind, text string } func decodeRequest(messageType int, payload []byte) (bridgeRequest, error) { if len(payload) > maxPayload { return bridgeRequest{}, errors.New("message exceeds 64 KiB") } switch messageType { case websocket.TextMessage: return parseRequest(string(payload)) case websocket.BinaryMessage: reader := bytes.NewReader(payload) value, err := decodeMessagePack(reader) if err != nil { return bridgeRequest{}, err } if reader.Len() != 0 { return bridgeRequest{}, errors.New("trailing MessagePack data") } return parseRequest(value) default: return bridgeRequest{}, errors.New("unsupported WebSocket message type") } } func parseRequest(value any) (bridgeRequest, error) { switch payload := value.(type) { case string: if strings.TrimSpace(payload) == "" { return bridgeRequest{}, errors.New("blank text request") } if payload == "CancelSpeech" { return bridgeRequest{kind: "CancelSpeech"}, nil } return bridgeRequest{kind: "SpeakText", text: payload}, nil case map[any]any: if len(payload) != 1 { return bridgeRequest{}, errors.New("map request must contain exactly one entry") } for key, value := range payload { return keyedRequest(coerceText(key), value) } case map[string]any: if len(payload) != 1 { return bridgeRequest{}, errors.New("map request must contain exactly one entry") } for key, value := range payload { return keyedRequest(key, value) } case []any: if len(payload) == 0 { return bridgeRequest{}, errors.New("array request is empty") } kind := coerceText(payload[0]) if kind == "CancelSpeech" { return bridgeRequest{kind: "CancelSpeech"}, nil } if len(payload) < 2 { return bridgeRequest{}, errors.New("array request has no value") } return keyedRequest(kind, payload[1]) } return bridgeRequest{}, errors.New("unsupported MessagePack payload") } func keyedRequest(kind string, value any) (bridgeRequest, error) { switch kind { case "CancelSpeech": return bridgeRequest{kind: "CancelSpeech"}, nil case "SpeakText", "BrailleText": if strings.TrimSpace(coerceText(value)) == "" { return bridgeRequest{}, errors.New("blank text request") } return bridgeRequest{kind: kind, text: coerceText(value)}, nil default: return bridgeRequest{}, errors.New("unsupported request kind") } } func coerceText(value any) string { switch text := value.(type) { case string: return text case messagePackBinary: return string(text) case nil: return "" case bool: return strconv.FormatBool(text) case int: return strconv.Itoa(text) case int8: return strconv.FormatInt(int64(text), 10) case int16: return strconv.FormatInt(int64(text), 10) case int32: return strconv.FormatInt(int64(text), 10) case int64: return strconv.FormatInt(text, 10) case uint: return strconv.FormatUint(uint64(text), 10) case uint8: return strconv.FormatUint(uint64(text), 10) case uint16: return strconv.FormatUint(uint64(text), 10) case uint32: return strconv.FormatUint(uint64(text), 10) case uint64: return strconv.FormatUint(text, 10) case float32: return strconv.FormatFloat(float64(text), 'g', -1, 32) case float64: return strconv.FormatFloat(text, 'g', -1, 64) default: return "" } }