Files

68 lines
1.7 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package speechdispatcher
import (
"bufio"
"context"
"fmt"
"net"
"strings"
)
type Rejection struct{ Code string }
func (r *Rejection) Error() string { return "Speech Dispatcher rejected a command: " + r.Code }
// Query owns a separate discovery connection. Its caller serializes commands.
// Discovery never starts a daemon unless preparation explicitly requests it.
type Query struct {
conn net.Conn
reader *bufio.Reader
}
func OpenQuery(ctx context.Context, socket string, autospawn, systemFallback bool) (*Query, error) {
endpoint, err := ResolveAddress(socket)
if err != nil {
return nil, err
}
dial := Dialer((&net.Dialer{Timeout: connectTimeout}).DialContext)
if systemFallback && AutomaticAddress(socket) {
dial = WithSystemFallback(dial)
}
var conn net.Conn
if autospawn {
conn, err = ConnectWithAutospawn(ctx, endpoint, dial, SpawnServer)
} else {
conn, err = dial(ctx, endpoint.Network, endpoint.Address)
}
if err != nil {
return nil, err
}
return &Query{conn: conn, reader: bufio.NewReader(conn)}, nil
}
func (q *Query) Close() { q.conn.Close() }
func (q *Query) Command(ctx context.Context, command string) ([]string, error) {
if strings.ContainsAny(command, "\r\n\x00") {
return nil, fmt.Errorf("invalid SSIP command")
}
finish, err := prepareIO(ctx, q.conn, ioTimeout)
if err != nil {
return nil, err
}
defer finish()
if _, err = fmt.Fprint(q.conn, command+"\r\n"); err != nil {
return nil, err
}
response, err := readReply(q.reader)
if err != nil {
q.Close()
return nil, err
}
if response.code < 200 || response.code >= 300 {
return nil, &Rejection{Code: fmt.Sprint(response.code)}
}
return response.data, nil
}