Files

155 lines
4.2 KiB
Go

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