2024-06-28 17:51:57 +09:30
|
|
|
// Package streamdeck interfaces with the Stream Deck plugin API,
|
|
|
|
// allowing you to create go-based plugins for the platform.
|
2024-06-27 20:10:18 +09:30
|
|
|
package streamdeck
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/json"
|
2024-06-27 20:14:55 +09:30
|
|
|
"flag"
|
2024-06-27 20:10:18 +09:30
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"reflect"
|
|
|
|
|
2024-06-28 17:51:57 +09:30
|
|
|
"github.com/tardisx/streamdeck-plugin/events"
|
|
|
|
|
2024-06-27 20:10:18 +09:30
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
)
|
|
|
|
|
2024-06-28 17:51:57 +09:30
|
|
|
// these are automatically populated by parseFlags
|
2024-06-27 20:14:55 +09:30
|
|
|
var flagPort int
|
|
|
|
var flagEvent, flagInfo string
|
|
|
|
var UUID string // the UUID this plugin is assigned
|
|
|
|
|
2024-06-27 20:10:18 +09:30
|
|
|
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 Connection struct {
|
|
|
|
ws *websocket.Conn
|
|
|
|
logger logger
|
|
|
|
handlers map[reflect.Type]reflect.Value
|
|
|
|
done chan (bool)
|
|
|
|
}
|
|
|
|
|
2024-06-27 20:25:10 +09:30
|
|
|
// New creates a new struct for communication with the streamdeck
|
2024-06-28 17:51:57 +09:30
|
|
|
// plugin API. The websocket will not connect until Connect is called.
|
2024-06-27 20:10:18 +09:30
|
|
|
func New() Connection {
|
|
|
|
return Connection{
|
|
|
|
handlers: make(map[reflect.Type]reflect.Value),
|
|
|
|
logger: nullLogger{},
|
|
|
|
done: make(chan bool),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-27 20:25:10 +09:30
|
|
|
// NewWithLogger is the same as New, but allows you to set a logger
|
|
|
|
// for debugging the websocket connection.
|
2024-06-27 20:10:18 +09:30
|
|
|
func NewWithLogger(l logger) Connection {
|
|
|
|
c := New()
|
|
|
|
c.logger = l
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
2024-06-28 17:51:57 +09:30
|
|
|
// parseFlags parses the command line flags to get the values provided
|
|
|
|
// by the Stream Deck plugin API.
|
|
|
|
func parseFlags() {
|
|
|
|
flag.IntVar(&flagPort, "port", 0, "streamdeck sdk port")
|
|
|
|
flag.StringVar(&flagEvent, "registerEvent", "", "streamdeck sdk register event")
|
|
|
|
flag.StringVar(&flagInfo, "info", "", "streamdeck application info")
|
|
|
|
flag.StringVar(&UUID, "pluginUUID", "", "uuid")
|
|
|
|
flag.Parse()
|
|
|
|
}
|
|
|
|
|
2024-06-27 20:25:10 +09:30
|
|
|
// Connect connects the plugin to the Stream Deck API via the websocket.
|
|
|
|
// Once connected, events will be passed to handlers you have registered.
|
|
|
|
// Handlers should thus be registered via RegisterHandler before calling
|
|
|
|
// Connect.
|
|
|
|
// Connect returns immediately if the connection is successful, you should
|
|
|
|
// then call WaitForPluginExit to block until the connection is closed.
|
2024-06-27 20:14:55 +09:30
|
|
|
func (conn *Connection) Connect() error {
|
2024-06-27 20:10:18 +09:30
|
|
|
|
2024-06-28 17:51:57 +09:30
|
|
|
parseFlags()
|
2024-06-27 20:14:55 +09:30
|
|
|
c, _, err := websocket.DefaultDialer.Dial(fmt.Sprintf("ws://localhost:%d", flagPort), nil)
|
2024-06-27 20:10:18 +09:30
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
conn.ws = c
|
2024-06-28 17:51:57 +09:30
|
|
|
msg := events.ESOpenMessage{
|
|
|
|
ESCommonNoContext: events.ESCommonNoContext{
|
2024-06-27 20:14:55 +09:30
|
|
|
Event: flagEvent,
|
2024-06-27 20:10:18 +09:30
|
|
|
},
|
2024-06-27 20:14:55 +09:30
|
|
|
UUID: UUID,
|
2024-06-27 20:10:18 +09:30
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-06-27 20:25:10 +09:30
|
|
|
// WaitForPluginExit waits until the Stream Deck API closes
|
|
|
|
// the websocket connection.
|
2024-06-28 17:51:57 +09:30
|
|
|
func (conn *Connection) WaitForPluginExit() {
|
|
|
|
<-conn.done
|
2024-06-27 20:10:18 +09:30
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2024-06-28 17:51:57 +09:30
|
|
|
func (conn *Connection) RegisterHandler(handler any) {
|
2024-06-27 20:10:18 +09:30
|
|
|
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)
|
2024-06-28 17:51:57 +09:30
|
|
|
if !events.ValidEventType(argType) {
|
2024-06-27 20:10:18 +09:30
|
|
|
panic("you cannot register a handler with this argument type")
|
|
|
|
}
|
|
|
|
|
2024-06-28 17:51:57 +09:30
|
|
|
_, alreadyExists := conn.handlers[argType]
|
2024-06-27 20:10:18 +09:30
|
|
|
if alreadyExists {
|
|
|
|
panic("handler for " + argType.Name() + " already exists")
|
|
|
|
}
|
|
|
|
|
2024-06-28 17:51:57 +09:30
|
|
|
conn.handlers[argType] = reflect.ValueOf(handler)
|
2024-06-27 20:10:18 +09:30
|
|
|
}
|
|
|
|
|
2024-06-27 20:25:10 +09:30
|
|
|
// Send sends a message to the API. It should be one of the
|
2024-06-28 17:51:57 +09:30
|
|
|
// events.ES* structs, such as events.ESOpenURL.
|
2024-06-27 20:10:18 +09:30
|
|
|
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) {
|
2024-06-27 20:35:18 +09:30
|
|
|
// conn.logger.Debug(fmt.Sprintf("handle: incoming a %T", event))
|
2024-06-27 20:10:18 +09:30
|
|
|
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)}
|
2024-06-27 20:35:18 +09:30
|
|
|
// conn.logger.Debug(fmt.Sprintf("handle: handler func: %+v", handler))
|
|
|
|
// conn.logger.Debug(fmt.Sprintf("handle: handler var: %+v", v))
|
2024-06-27 20:10:18 +09:30
|
|
|
|
2024-06-27 20:35:18 +09:30
|
|
|
// conn.logger.Debug("handle: calling handler function")
|
2024-06-27 20:10:18 +09:30
|
|
|
|
|
|
|
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)
|
2024-06-28 17:51:57 +09:30
|
|
|
base := events.ERBase{}
|
2024-06-27 20:10:18 +09:30
|
|
|
err = json.NewDecoder(r).Decode(&base)
|
|
|
|
if err != nil {
|
|
|
|
conn.logger.Error("cannot decode: " + err.Error())
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2024-06-28 17:51:57 +09:30
|
|
|
t, ok := events.TypeForEvent(base.Event)
|
2024-06-27 20:10:18 +09:30
|
|
|
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()
|
2024-06-27 20:33:59 +09:30
|
|
|
// conn.logger.Info(fmt.Sprintf("instance is a %T", d))
|
2024-06-27 20:10:18 +09:30
|
|
|
|
|
|
|
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()
|
2024-06-27 20:33:59 +09:30
|
|
|
// conn.logger.Debug(fmt.Sprintf("NOW instance is a %T", v))
|
|
|
|
// conn.logger.Debug(fmt.Sprintf("reader: unmarshalled to: %+v", v))
|
2024-06-27 20:10:18 +09:30
|
|
|
return v, nil
|
|
|
|
}
|