155 lines
4.2 KiB
Go
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 ""
|
|
}
|
|
}
|