203 lines
4.9 KiB
Go
203 lines
4.9 KiB
Go
package streamdeck
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"reflect"
|
|
|
|
"github.com/gorilla/websocket"
|
|
)
|
|
|
|
type logger interface {
|
|
Info(string, ...any)
|
|
Error(string, ...any)
|
|
Debug(string, ...any)
|
|
}
|
|
|
|
type nullLogger struct{}
|
|
|
|
func (nl nullLogger) Info(string, ...any) {}
|
|
func (nl nullLogger) Error(string, ...any) {}
|
|
func (nl nullLogger) Debug(string, ...any) {}
|
|
|
|
type EventHandler struct {
|
|
MsgType string
|
|
Handler func()
|
|
}
|
|
|
|
type Connection struct {
|
|
ws *websocket.Conn
|
|
logger logger
|
|
handlers map[reflect.Type]reflect.Value
|
|
done chan (bool)
|
|
}
|
|
|
|
func New() Connection {
|
|
return Connection{
|
|
handlers: make(map[reflect.Type]reflect.Value),
|
|
logger: nullLogger{},
|
|
done: make(chan bool),
|
|
}
|
|
}
|
|
|
|
func NewWithLogger(l logger) Connection {
|
|
c := New()
|
|
c.logger = l
|
|
return c
|
|
}
|
|
|
|
func (conn *Connection) Connect(port int, openEvent string, uuid string) error {
|
|
|
|
c, _, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://localhost:%d", port), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
conn.ws = c
|
|
msg := ESOpenMessage{
|
|
ESCommonNoContext: ESCommonNoContext{
|
|
Event: openEvent,
|
|
},
|
|
UUID: uuid,
|
|
}
|
|
conn.logger.Debug(fmt.Sprintf("writing openMessage: %+v", msg))
|
|
err = c.WriteJSON(msg)
|
|
if err != nil {
|
|
conn.logger.Error(err.Error())
|
|
panic(err)
|
|
}
|
|
|
|
// run the reader forever
|
|
conn.logger.Info("starting reader")
|
|
go conn.reader()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *Connection) WaitForPluginExit() {
|
|
<-c.done
|
|
}
|
|
|
|
// RegisterHandler registers a function to be called for a particular event. The
|
|
// event to be handled is determined by the functions single parameter type.
|
|
// This should be called before Connect to be sure your application is ready to
|
|
// receive events. You can register as many handlers as you like, but only one
|
|
// function per event type. This function will panic if the wrong kind of
|
|
// function is passed in, or if you try to register more than one for a single
|
|
// event type.
|
|
func (r *Connection) RegisterHandler(handler any) {
|
|
hType := reflect.TypeOf(handler)
|
|
if hType.Kind() != reflect.Func {
|
|
panic("handler must be a function")
|
|
}
|
|
// Assuming the function takes exactly one argument
|
|
if hType.NumIn() != 1 {
|
|
panic("handler func must take exactly one argument")
|
|
}
|
|
|
|
argType := hType.In(0)
|
|
|
|
// check its a valid one (one that matches an event type)
|
|
valid := false
|
|
for i := range receivedEventTypeMap {
|
|
if receivedEventTypeMap[i] == argType {
|
|
valid = true
|
|
break
|
|
}
|
|
}
|
|
if !valid {
|
|
panic("you cannot register a handler with this argument type")
|
|
}
|
|
|
|
_, alreadyExists := r.handlers[argType]
|
|
if alreadyExists {
|
|
panic("handler for " + argType.Name() + " already exists")
|
|
}
|
|
|
|
r.handlers[argType] = reflect.ValueOf(handler)
|
|
}
|
|
|
|
func (conn *Connection) Send(e any) error {
|
|
b, _ := json.Marshal(e)
|
|
conn.logger.Debug(fmt.Sprintf("sending: %s", string(b)))
|
|
|
|
return conn.ws.WriteJSON(e)
|
|
}
|
|
|
|
func (conn *Connection) handle(event any) {
|
|
conn.logger.Debug(fmt.Sprintf("handle: incoming a %T", event))
|
|
argType := reflect.TypeOf(event)
|
|
handler, ok := conn.handlers[argType]
|
|
if !ok {
|
|
conn.logger.Debug(fmt.Sprintf("handle: no handler registered for type %s", argType))
|
|
return
|
|
} else {
|
|
conn.logger.Debug(fmt.Sprintf("handle: found handler function for type %s", argType))
|
|
|
|
v := []reflect.Value{reflect.ValueOf(event)}
|
|
conn.logger.Debug(fmt.Sprintf("handle: handler func: %+v", handler))
|
|
conn.logger.Debug(fmt.Sprintf("handle: handler var: %+v", v))
|
|
|
|
conn.logger.Debug("handle: calling handler function")
|
|
|
|
handler.Call(v)
|
|
}
|
|
}
|
|
|
|
func (conn *Connection) reader() {
|
|
|
|
for {
|
|
_, r, err := conn.ws.NextReader()
|
|
if err != nil {
|
|
conn.logger.Error(err.Error())
|
|
break
|
|
}
|
|
|
|
b := bytes.Buffer{}
|
|
r = io.TeeReader(r, &b)
|
|
base := ERBase{}
|
|
err = json.NewDecoder(r).Decode(&base)
|
|
if err != nil {
|
|
conn.logger.Error("cannot decode: " + err.Error())
|
|
continue
|
|
}
|
|
|
|
t, ok := receivedEventTypeMap[base.Event]
|
|
if !ok {
|
|
conn.logger.Error(fmt.Sprintf("no type registered for event '%s'", base.Event))
|
|
continue
|
|
}
|
|
|
|
d, err := conn.unmarshalToConcrete(t, b.Bytes())
|
|
if err != nil {
|
|
conn.logger.Error("cannot unmarshal: " + err.Error())
|
|
continue
|
|
}
|
|
conn.handle(d)
|
|
}
|
|
conn.logger.Info("websocket closed, shutting down reader")
|
|
conn.done <- true
|
|
}
|
|
|
|
// unmarshalToConcrete takes a reflect.Type and a byte slice, creates a concrete
|
|
// instance of the type and unmarshals the JSON byte slice into it.
|
|
func (conn *Connection) unmarshalToConcrete(t reflect.Type, b []byte) (any, error) {
|
|
// t is a reflect.Type of the thing we need to decode into
|
|
d := reflect.New(t).Interface()
|
|
conn.logger.Info(fmt.Sprintf("instance is a %T", d))
|
|
|
|
err := json.Unmarshal(b, &d)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not unmarshal this:\n%s\ninto a %v (%v)\nbecause: %s", string(b), d, t, err.Error())
|
|
}
|
|
|
|
// get concrete instance of d into v
|
|
v := reflect.ValueOf(d).Elem().Interface()
|
|
conn.logger.Info(fmt.Sprintf("NOW instance is a %T", v))
|
|
|
|
conn.logger.Debug(fmt.Sprintf("reader: unmarshalled to: %+v", v))
|
|
return v, nil
|
|
}
|