Newer
Older
"encoding/json"
Simon Kirsten
committed
"github.com/JamesStewy/sse"
Simon Kirsten
committed
// displayState struct defines the state of the display.
type displayState struct {
LargeChannel string `json:"large_channel"`
SmallChannel string `json:"small_channel"`
Volume float32 `json:"volume"`
SmallScale float32 `json:"small_scale"`
Simon Kirsten
committed
ShowChat bool `json:"show_chat"`
// Display struct defines the Display itself
type Display struct {
State displayState
clientsMu sync.Mutex
// clients holds the clients that are connected to the event handler. It is used to broadcast state changes to all SSE (Server-Sent Events) clients.
// Must acquire clientsMu before accessing the map.
// Note: the only reason we use a sse.Client => bool map is that we can call *delete* with the client as key. The actual bool value that is stored holds no significance whatsoever.
// This is basically a *set* in go.
clients map[*sse.Client]bool
// the prefix used for logging
loggingPrefix string
// NewDisplay creates a new Display with default values
func NewDisplay(namespace string) *Display {
var loggingPrefix string
if namespace == "default" {
loggingPrefix = "display"
} else {
loggingPrefix = fmt.Sprintf("display[%s]", namespace)
}
return &Display{
State: displayState{
LargeChannel: "",
SmallChannel: "",
Volume: 0.5,
SmallScale: 0.3,
ShowChat: false,
},
clients: make(map[*sse.Client]bool),
loggingPrefix: loggingPrefix,
}
}
Simon Kirsten
committed
// stateHandler updated the state based on the query string.
Simon Kirsten
committed
// For example
// /display?large_channel=asdf&small_channel=null&volume=&small_scale=0.25&show_chat=true
Simon Kirsten
committed
// will
// - set large_channel to asdf
Simon Kirsten
committed
// - do nothing with volume (the parameter can also be omitted)
// - set small_scale to 0.25
// - set show_chat to true
//
// If the floats (volume and small_scale) or the boolean (show_chat) could not be parsed, no changes are made at all and an error gets returned even if other fields could be parsed.
func (d *Display) stateHandler(w http.ResponseWriter, r *http.Request) {
Simon Kirsten
committed
// the new state we will replace the current state with IF everything parses correctly.
Simon Kirsten
committed
// we save the error messages of the 5 parsing steps here.
Simon Kirsten
committed
if value, err := util.GetSingleQueryParam(r, "large_channel"); value != nil {
if err != nil {
errs = multierror.Append(err)
if value, err := util.GetSingleQueryParam(r, "small_channel"); value != nil {
if err != nil {
errs = multierror.Append(err)
Simon Kirsten
committed
} else {
Simon Kirsten
committed
}
}
if value, err := util.GetSingleQueryParam(r, "volume"); value != nil {
Simon Kirsten
committed
if err != nil {
newVolume, err := strconv.ParseFloat(*value, 32)
if err != nil {
errs = multierror.Append(errs, err)
} else {
newState.Volume = float32(newVolume)
}
if value, err := util.GetSingleQueryParam(r, "small_scale"); value != nil {
newSmallScale, err := strconv.ParseFloat(*value, 32)
if err != nil {
errs = multierror.Append(errs, err)
} else {
newState.SmallScale = float32(newSmallScale)
}
if value, err := util.GetSingleQueryParam(r, "show_chat"); value != nil {
Simon Kirsten
committed
if err != nil {
Simon Kirsten
committed
} else {
newShowChat, err := strconv.ParseBool(*value)
if err != nil {
errs = multierror.Append(errs, err)
} else {
newState.ShowChat = newShowChat
}
Simon Kirsten
committed
}
}
if errs != nil { // we had errors
http.Error(w, errs.Error(), http.StatusBadRequest)
Simon Kirsten
committed
logger.Printf("%s: Error(s) while parsing /display query: %v\n", d.loggingPrefix, errs)
if d.State != newState {
d.State = newState
body, err := json.Marshal(d.State)
logger.Printf("%s: Updated (%s):\n%s\n", d.loggingPrefix, r.URL.RawQuery, string(body))
Simon Kirsten
committed
// craft the SSE message
msg := sse.Msg{
Data: string(body),
}
// We'd like to broadcast the message to all clients, as
// recorded in the clients map. The message send is a
// potentially blocking operation so we don't want to hold the
// lock across send operations. Thus, we first build up a list
// of clients to send to (while holding clientsMu) and start
// sending only after giving up the lock again.
var sendingTo []*sse.Client
d.clientsMu.Lock()
for client := range d.clients {
sendingTo = append(sendingTo, client)
}
// now broadcast it to all clients
for _, client := range sendingTo {
Simon Kirsten
committed
}
util.ServeIndentedJSON(w, r, d.State)
Simon Kirsten
committed
}
// eventsHandler serves a SSE (Server-sent events) endpoint that the website(s) can connect to. It publishes the state when it gets changed.
func (d *Display) eventsHandler(w http.ResponseWriter, r *http.Request) {
Simon Kirsten
committed
client, err := sse.ClientInit(w)
// return error if unable to initialize sse connection
Simon Kirsten
committed
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Add client to broadcast set. Need to serialize access to clients as
// we're running concurrently with other requests.
d.clientsMu.Lock()
d.clients[client] = true
d.clientsMu.Unlock()
Simon Kirsten
committed
// remove client from broadcast set on exit
d.clientsMu.Lock()
delete(d.clients, client)
d.clientsMu.Unlock()
Simon Kirsten
committed
body, err := json.Marshal(d.State)
if err != nil {
return
}
go func() {
// client.Send will block the channel that client.Run is listening on
// thats why we send the initial state in an goroutine that runs concurrently with client.Run
client.Send(sse.Msg{
Data: string(body),
})
}()
Simon Kirsten
committed
// run the in the context of the request
client.Run(r.Context())