From c5910e7f5db16f507df47fbf6de4e5295c894e4d Mon Sep 17 00:00:00 2001 From: Tyler Date: Fri, 19 Jun 2020 22:50:55 -0400 Subject: [PATCH] Better event handling via DiscordGo's handlers --- decoder.go | 5 + event.go | 189 +++++++++++++++++++++++++++++++++---- eventhandlers.go | 165 ++++++++++++++++++++++++++++++++ events.go | 41 ++++++++ example/main.go | 5 +- lavalink.go | 42 +++++---- model.go | 9 +- node.go | 66 ++++++++----- player.go | 2 +- tools/cmd/eventhandlers.go | 101 ++++++++++++++++++++ 10 files changed, 560 insertions(+), 65 deletions(-) create mode 100644 eventhandlers.go create mode 100644 events.go create mode 100644 tools/cmd/eventhandlers.go diff --git a/decoder.go b/decoder.go index bb9c919..c9c3c2e 100644 --- a/decoder.go +++ b/decoder.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/base64" "encoding/binary" + "encoding/json" "github.com/valyala/fastjson" "io" "time" @@ -144,3 +145,7 @@ func jsonStringValue(v *fastjson.Value, keys ...string) string { return string(strB) } + +func jsonUnmarshal(v *fastjson.Value, dst interface{}) error { + return json.Unmarshal(v.MarshalTo(nil), dst) +} diff --git a/event.go b/event.go index ead8ee2..794f916 100644 --- a/event.go +++ b/event.go @@ -1,32 +1,183 @@ package gavalink -// EventHandler defines events that Lavalink may send to a player +// EventHandler is an interface for Lavalink events. type EventHandler interface { - OnTrackEnd(player *Player, track string, reason string) error - OnTrackException(player *Player, track string, reason string) error - OnTrackStuck(player *Player, track string, threshold int) error - OnVoiceProcessed(player *Player, data *VoiceProcessingData, hotword, override bool) error + // Type returns the type of event this handler belongs to. + Type() string + + // Handle is called whenever an event of Type() happens. + // It is the receivers responsibility to type assert that the interface + // is the expected struct. + Handle(*Player, interface{}) } -// DummyEventHandler provides an empty event handler for users who -// wish to drop events outright. This is not recommended. -type DummyEventHandler struct{} +// EventInterfaceProvider is an interface for providing empty interfaces for +// Lavalink events. +type EventInterfaceProvider interface { + // Type is the type of event this handler belongs to. + Type() string -// OnTrackEnd is raised when a track ends -func (d DummyEventHandler) OnTrackEnd(player *Player, track string, reason string) error { - return nil + // New returns a new instance of the struct this event handler handles. + // This is called once per event. + // The struct is provided to all handlers of the same Type(). + New() interface{} } -// OnTrackException is raised when a track throws an exception -func (d DummyEventHandler) OnTrackException(player *Player, track string, reason string) error { - return nil +// interfaceEventType is the event handler type for interface{} events. +const interfaceEventType = "__INTERFACE__" + +// interfaceEventHandler is an event handler for interface{} events. +type interfaceEventHandler func(*Player, interface{}) + +// Type returns the event type for interface{} events. +func (eh interfaceEventHandler) Type() string { + return interfaceEventType } -// OnTrackStuck is raised when a track gets stuck -func (d DummyEventHandler) OnTrackStuck(player *Player, track string, threshold int) error { - return nil +// Handle is the handler for an interface{} event. +func (eh interfaceEventHandler) Handle(p *Player, i interface{}) { + eh(p, i) } -func (d DummyEventHandler) OnVoiceProcessed(player *Player, data *VoiceProcessingData, hotword, override bool) error { - return nil +var registeredInterfaceProviders = map[string]EventInterfaceProvider{} + +// registerInterfaceProvider registers a provider so that Gavalink can +// access it's New() method. +func registerInterfaceProvider(eh EventInterfaceProvider) { + if _, ok := registeredInterfaceProviders[eh.Type()]; ok { + return + // XXX: + // if we should error here, we need to do something with it. + // fmt.Errorf("event %s already registered", eh.Type()) + } + registeredInterfaceProviders[eh.Type()] = eh + return +} + +// eventHandlerInstance is a wrapper around an event handler, as functions +// cannot be compared directly. +type eventHandlerInstance struct { + eventHandler EventHandler +} + +// addEventHandler adds an event handler that will be fired anytime +// the Lavalink event matching eventHandler.Type() fires. +func (l *Lavalink) addEventHandler(eventHandler EventHandler) func() { + l.handlersMu.Lock() + defer l.handlersMu.Unlock() + + if l.handlers == nil { + l.handlers = map[string][]*eventHandlerInstance{} + } + + ehi := &eventHandlerInstance{eventHandler} + l.handlers[eventHandler.Type()] = append(l.handlers[eventHandler.Type()], ehi) + + return func() { + l.removeEventHandlerInstance(eventHandler.Type(), ehi) + } +} + +// addEventHandler adds an event handler that will be fired the next time +// the Lavalink event matching eventHandler.Type() fires. +func (l *Lavalink) addEventHandlerOnce(eventHandler EventHandler) func() { + l.handlersMu.Lock() + defer l.handlersMu.Unlock() + + if l.onceHandlers == nil { + l.onceHandlers = map[string][]*eventHandlerInstance{} + } + + ehi := &eventHandlerInstance{eventHandler} + l.onceHandlers[eventHandler.Type()] = append(l.onceHandlers[eventHandler.Type()], ehi) + + return func() { + l.removeEventHandlerInstance(eventHandler.Type(), ehi) + } +} + +// AddHandler allows you to add an event handler that will be fired anytime +// the Lavalink event that matches the function fires. +// The first parameter is a *Session, and the second parameter is a pointer +// to a struct corresponding to the event for which you want to listen. +// +// eg: +// Player.AddHandler(func(s *gavalink.Player, m *gavalink.TrackStart) { +// }) +// +// or: +// Player.AddHandler(func(s *gavalink.Player, m *gavalink.TrackEnd) { +// }) +// +// +// The return value of this method is a function, that when called will remove the +// event handler. +func (l *Lavalink) AddHandler(handler interface{}) func() { + eh := handlerForInterface(handler) + + if eh == nil { + return func() {} + } + + return l.addEventHandler(eh) +} + +// AddHandlerOnce allows you to add an event handler that will be fired the next time +// the Lavalink event that matches the function fires. +// See AddHandler for more details. +func (l *Lavalink) AddHandlerOnce(handler interface{}) func() { + eh := handlerForInterface(handler) + + if eh == nil { + return func() {} + } + + return l.addEventHandlerOnce(eh) +} + +// removeEventHandler instance removes an event handler instance. +func (l *Lavalink) removeEventHandlerInstance(t string, ehi *eventHandlerInstance) { + l.handlersMu.Lock() + defer l.handlersMu.Unlock() + + handlers := l.handlers[t] + for i := range handlers { + if handlers[i] == ehi { + l.handlers[t] = append(handlers[:i], handlers[i+1:]...) + } + } + + onceHandlers := l.onceHandlers[t] + for i := range onceHandlers { + if onceHandlers[i] == ehi { + l.onceHandlers[t] = append(onceHandlers[:i], handlers[i+1:]...) + } + } +} + +// Handles calling permanent and once handlers for an event type. +func (l *Lavalink) handle(p *Player, t string, i interface{}) { + for _, eh := range l.handlers[t] { + go eh.eventHandler.Handle(p, i) + } + + if len(l.onceHandlers[t]) > 0 { + for _, eh := range l.onceHandlers[t] { + go eh.eventHandler.Handle(p, i) + } + l.onceHandlers[t] = nil + } +} + +// Handles an event type by calling internal methods, firing handlers and firing the +// interface{} event. +func (l *Lavalink) handleEvent(p *Player, t string, i interface{}) { + l.handlersMu.RLock() + defer l.handlersMu.RUnlock() + + // Then they are dispatched to anyone handling interface{} events. + l.handle(p, interfaceEventType, i) + + // Finally they are dispatched to any typed handlers. + l.handle(p, t, i) } diff --git a/eventhandlers.go b/eventhandlers.go new file mode 100644 index 0000000..cf58f0c --- /dev/null +++ b/eventhandlers.go @@ -0,0 +1,165 @@ +// Code generated by "eventhandlers"; DO NOT EDIT +// See events.go + +package gavalink + +// Following are all the event types. +// Event type values are used to match the events returned by Lavalink. +const ( + trackEndEventType = "TrackEndEvent" + trackExceptionEventType = "TrackExceptionEvent" + trackStartEventType = "TrackStartEvent" + trackStuckEventType = "TrackStuckEvent" + voiceProcessedEventType = "VoiceProcessedEvent" + webSocketClosedEventType = "WebSocketClosedEvent" +) + +// trackEndEventHandler is an event handler for TrackEnd events. +type trackEndEventHandler func(*Player, *TrackEnd) + +// Type returns the event type for TrackEnd events. +func (eh trackEndEventHandler) Type() string { + return trackEndEventType +} + +// New returns a new instance of TrackEnd. +func (eh trackEndEventHandler) New() interface{} { + return &TrackEnd{} +} + +// Handle is the handler for TrackEnd events. +func (eh trackEndEventHandler) Handle(p *Player, i interface{}) { + if t, ok := i.(*TrackEnd); ok { + eh(p, t) + } +} + +// trackExceptionEventHandler is an event handler for TrackException events. +type trackExceptionEventHandler func(*Player, *TrackException) + +// Type returns the event type for TrackException events. +func (eh trackExceptionEventHandler) Type() string { + return trackExceptionEventType +} + +// New returns a new instance of TrackException. +func (eh trackExceptionEventHandler) New() interface{} { + return &TrackException{} +} + +// Handle is the handler for TrackException events. +func (eh trackExceptionEventHandler) Handle(p *Player, i interface{}) { + if t, ok := i.(*TrackException); ok { + eh(p, t) + } +} + +// trackStartEventHandler is an event handler for TrackStart events. +type trackStartEventHandler func(*Player, *TrackStart) + +// Type returns the event type for TrackStart events. +func (eh trackStartEventHandler) Type() string { + return trackStartEventType +} + +// New returns a new instance of TrackStart. +func (eh trackStartEventHandler) New() interface{} { + return &TrackStart{} +} + +// Handle is the handler for TrackStart events. +func (eh trackStartEventHandler) Handle(p *Player, i interface{}) { + if t, ok := i.(*TrackStart); ok { + eh(p, t) + } +} + +// trackStuckEventHandler is an event handler for TrackStuck events. +type trackStuckEventHandler func(*Player, *TrackStuck) + +// Type returns the event type for TrackStuck events. +func (eh trackStuckEventHandler) Type() string { + return trackStuckEventType +} + +// New returns a new instance of TrackStuck. +func (eh trackStuckEventHandler) New() interface{} { + return &TrackStuck{} +} + +// Handle is the handler for TrackStuck events. +func (eh trackStuckEventHandler) Handle(p *Player, i interface{}) { + if t, ok := i.(*TrackStuck); ok { + eh(p, t) + } +} + +// voiceProcessedEventHandler is an event handler for VoiceProcessed events. +type voiceProcessedEventHandler func(*Player, *VoiceProcessed) + +// Type returns the event type for VoiceProcessed events. +func (eh voiceProcessedEventHandler) Type() string { + return voiceProcessedEventType +} + +// New returns a new instance of VoiceProcessed. +func (eh voiceProcessedEventHandler) New() interface{} { + return &VoiceProcessed{} +} + +// Handle is the handler for VoiceProcessed events. +func (eh voiceProcessedEventHandler) Handle(p *Player, i interface{}) { + if t, ok := i.(*VoiceProcessed); ok { + eh(p, t) + } +} + +// webSocketClosedEventHandler is an event handler for WebSocketClosed events. +type webSocketClosedEventHandler func(*Player, *WebSocketClosed) + +// Type returns the event type for WebSocketClosed events. +func (eh webSocketClosedEventHandler) Type() string { + return webSocketClosedEventType +} + +// New returns a new instance of WebSocketClosed. +func (eh webSocketClosedEventHandler) New() interface{} { + return &WebSocketClosed{} +} + +// Handle is the handler for WebSocketClosed events. +func (eh webSocketClosedEventHandler) Handle(p *Player, i interface{}) { + if t, ok := i.(*WebSocketClosed); ok { + eh(p, t) + } +} + +func handlerForInterface(handler interface{}) EventHandler { + switch v := handler.(type) { + case func(*Player, interface{}): + return interfaceEventHandler(v) + case func(*Player, *TrackEnd): + return trackEndEventHandler(v) + case func(*Player, *TrackException): + return trackExceptionEventHandler(v) + case func(*Player, *TrackStart): + return trackStartEventHandler(v) + case func(*Player, *TrackStuck): + return trackStuckEventHandler(v) + case func(*Player, *VoiceProcessed): + return voiceProcessedEventHandler(v) + case func(*Player, *WebSocketClosed): + return webSocketClosedEventHandler(v) + } + + return nil +} + +func init() { + registerInterfaceProvider(trackEndEventHandler(nil)) + registerInterfaceProvider(trackExceptionEventHandler(nil)) + registerInterfaceProvider(trackStartEventHandler(nil)) + registerInterfaceProvider(trackStuckEventHandler(nil)) + registerInterfaceProvider(voiceProcessedEventHandler(nil)) + registerInterfaceProvider(webSocketClosedEventHandler(nil)) +} diff --git a/events.go b/events.go new file mode 100644 index 0000000..cf63094 --- /dev/null +++ b/events.go @@ -0,0 +1,41 @@ +package gavalink + +import "time" + +// Event for when a track starts playing +type TrackStart struct { + Track string +} + +// Event for when a track ends. +type TrackEnd struct { + Track string + Reason string +} + +// Event for when a track encounters an error in playback. +type TrackException struct { + Track string + Error string + Exception Exception +} + +// Event when a track gets stuck +type TrackStuck struct { + Track string + Threshold time.Duration +} + +// Event for when voice is processed and sent back to the client. +type VoiceProcessed struct { + Data *VoiceProcessingData + Hotword bool + Override bool +} + +// Event fired when the websocket is closed. +type WebSocketClosed struct { + Code int + Reason string + ByRemote bool +} diff --git a/example/main.go b/example/main.go index 3cb35df..b7ecadf 100644 --- a/example/main.go +++ b/example/main.go @@ -10,7 +10,7 @@ import ( "syscall" "github.com/bwmarrin/discordgo" - "github.com/foxbot/gavalink" + "meow.tf/astra/gavalink" ) var token string @@ -162,8 +162,7 @@ func voiceServerUpdate(s *discordgo.Session, event *discordgo.VoiceServerUpdate) return } - handler := new(gavalink.DummyEventHandler) - player, err = node.CreatePlayer(event.GuildID, s.State.SessionID, vsu, handler) + player, err = node.CreatePlayer(event.GuildID, s.State.SessionID, vsu) if err != nil { log.Println(err) return diff --git a/lavalink.go b/lavalink.go index 28e189d..8261eac 100644 --- a/lavalink.go +++ b/lavalink.go @@ -5,6 +5,7 @@ import ( "log" "net/http" "os" + "sync" "time" ) @@ -23,6 +24,11 @@ type Lavalink struct { nodes []*Node players map[string]*Player + // Event handlers + handlersMu sync.RWMutex + handlers map[string][]*eventHandlerInstance + onceHandlers map[string][]*eventHandlerInstance + capabilities map[string]interface{} BestNodeFunc func([]*Node) (*Node, error) @@ -50,7 +56,7 @@ func NewLavalink(shards string, userID string) *Lavalink { } // AddNodes adds a node to the Lavalink manager -func (lavalink *Lavalink) AddNodes(nodeConfigs ...NodeConfig) error { +func (l *Lavalink) AddNodes(nodeConfigs ...NodeConfig) error { nodes := make([]*Node, len(nodeConfigs)) client := &http.Client{ @@ -60,7 +66,7 @@ func (lavalink *Lavalink) AddNodes(nodeConfigs ...NodeConfig) error { for i, c := range nodeConfigs { n := &Node{ config: c, - manager: lavalink, + manager: l, client: client, } @@ -73,15 +79,15 @@ func (lavalink *Lavalink) AddNodes(nodeConfigs ...NodeConfig) error { nodes[i] = n } - lavalink.nodes = append(lavalink.nodes, nodes...) + l.nodes = append(l.nodes, nodes...) return nil } // RemoveNode removes a node from the manager -func (lavalink *Lavalink) removeNode(node *Node) error { +func (l *Lavalink) removeNode(node *Node) error { idx := -1 - for i, n := range lavalink.nodes { + for i, n := range l.nodes { if n == node { idx = i break @@ -93,9 +99,9 @@ func (lavalink *Lavalink) removeNode(node *Node) error { node.stop() - for _, player := range lavalink.players { + for _, player := range l.players { if player.node == node { - n, err := lavalink.BestNode() + n, err := l.BestNode() if err != nil { continue @@ -106,28 +112,28 @@ func (lavalink *Lavalink) removeNode(node *Node) error { } // temp var for easier reading - n := lavalink.nodes + n := l.nodes z := len(n) - 1 n[idx] = n[z] // swap idx with last n = n[:z] - lavalink.nodes = n + l.nodes = n return nil } // BestNode returns the Node with the lowest latency -func (lavalink *Lavalink) BestNode() (*Node, error) { - if len(lavalink.nodes) < 1 { +func (l *Lavalink) BestNode() (*Node, error) { + if len(l.nodes) < 1 { return nil, errNoNodes } - return lavalink.BestNodeFunc(lavalink.nodes) + return l.BestNodeFunc(l.nodes) } // GetPlayer gets a player for a guild -func (lavalink *Lavalink) GetPlayer(guild string) (*Player, error) { - p, ok := lavalink.players[guild] +func (l *Lavalink) GetPlayer(guild string) (*Player, error) { + p, ok := l.players[guild] if !ok { return nil, errPlayerNotFound @@ -137,10 +143,10 @@ func (lavalink *Lavalink) GetPlayer(guild string) (*Player, error) { } // Add capabilities mappings to the client, letting the server know what we support -func (lavalink *Lavalink) AddCapability(key string, i interface{}) { - if lavalink.capabilities == nil { - lavalink.capabilities = make(map[string]interface{}) +func (l *Lavalink) AddCapability(key string, i interface{}) { + if l.capabilities == nil { + l.capabilities = make(map[string]interface{}) } - lavalink.capabilities[key] = i + l.capabilities[key] = i } diff --git a/model.go b/model.go index a32cadb..ce75831 100644 --- a/model.go +++ b/model.go @@ -88,7 +88,6 @@ func (t *TrackInfo) UnmarshalJSON(data []byte) error { const ( opVoiceUpdate = "voiceUpdate" - opVoiceProcessed = "voiceProcessed" opUserJoin = "userJoin" opUserLeave = "userLeave" opUserListen = "userListen" @@ -105,8 +104,16 @@ const ( eventTrackEnd = "TrackEndEvent" eventTrackException = "TrackExceptionEvent" eventTrackStuck = "TrackStuckEvent" + eventVoiceProcessed = "VoiceProcessedEvent" ) +// Exception is a message from the Lavalink server +type Exception struct { + Message string + Severity string + Cause string +} + // VoiceServerUpdate is a raw Discord VOICE_SERVER_UPDATE event type VoiceServerUpdate struct { GuildID string `json:"guild_id"` diff --git a/node.go b/node.go index 7bcea8f..80cdc0a 100644 --- a/node.go +++ b/node.go @@ -8,6 +8,7 @@ import ( "net/url" "strconv" "strings" + "time" "github.com/gorilla/websocket" ) @@ -189,33 +190,53 @@ func (node *Node) onEvent(v *fastjson.Value, msg []byte) error { track := jsonStringValue(v, "track") switch jsonStringValue(v, "type") { + case eventTrackStart: + player.track = track + + player.handle(eventTrackStart, &TrackStart{ + Track: track, + }) case eventTrackEnd: player.track = "" - err = player.handler.OnTrackEnd(player, track, jsonStringValue(v, "reason")) + + player.handle(eventTrackEnd, &TrackEnd{ + Track: track, + Reason: jsonStringValue(v, "reason"), + }) case eventTrackException: - err = player.handler.OnTrackException(player, track, jsonStringValue(v, "reason")) + ex := &TrackException{ + Track: track, + Error: jsonStringValue(v, "error"), + } + + if obj := v.Get("exception"); obj != nil { + var exception Exception + jsonUnmarshal(obj, &exception) + ex.Exception = exception + } + + player.handle(eventTrackException, ex) case eventTrackStuck: - err = player.handler.OnTrackStuck(player, track, v.GetInt("thresholdMs")) + player.handle(eventTrackStuck, &TrackStuck{ + Track: track, + Threshold: time.Duration(v.GetInt("thresholdMs")) * time.Millisecond, + }) + case eventVoiceProcessed: + data := &VoiceProcessingData{ + node: node, + UserID: jsonStringValue(v, "userId"), + URL: fmt.Sprintf("%s/audio/%s", node.config.REST, track), + File: track, + } + + player.handle(eventVoiceProcessed, &VoiceProcessed{ + Data: data, + Hotword: v.GetBool("hotword"), + Override: v.GetBool("override"), + }) } - return err - case opVoiceProcessed: - player, err := node.manager.GetPlayer(jsonStringValue(v, "guildId")) - - if err != nil { - return err - } - - track := jsonStringValue(v, "track") - - data := &VoiceProcessingData{ - node: node, - UserID: jsonStringValue(v, "userId"), - URL: fmt.Sprintf("%s/audio/%s", node.config.REST, track), - File: track, - } - - return player.handler.OnVoiceProcessed(player, data, v.GetBool("hotword"), v.GetBool("override")) + return nil default: return errUnknownPayload } @@ -224,7 +245,7 @@ func (node *Node) onEvent(v *fastjson.Value, msg []byte) error { } // CreatePlayer creates an audio player on this node -func (node *Node) CreatePlayer(guildID string, sessionID string, event VoiceServerUpdate, handler EventHandler) (*Player, error) { +func (node *Node) CreatePlayer(guildID string, sessionID string, event VoiceServerUpdate) (*Player, error) { msg := voiceUpdateMessage{ Op: opVoiceUpdate, GuildID: guildID, @@ -243,7 +264,6 @@ func (node *Node) CreatePlayer(guildID string, sessionID string, event VoiceServ sessionID: sessionID, manager: node.manager, node: node, - handler: handler, vol: 100, lastVoiceServerUpdate: event, } diff --git a/player.go b/player.go index 73ed965..7bad5f7 100644 --- a/player.go +++ b/player.go @@ -2,6 +2,7 @@ package gavalink import ( "strconv" + "sync" ) // Player is a Lavalink player @@ -15,7 +16,6 @@ type Player struct { track string manager *Lavalink node *Node - handler EventHandler lastVoiceServerUpdate VoiceServerUpdate } diff --git a/tools/cmd/eventhandlers.go b/tools/cmd/eventhandlers.go new file mode 100644 index 0000000..5b68b2b --- /dev/null +++ b/tools/cmd/eventhandlers.go @@ -0,0 +1,101 @@ +package main + +import ( + "bytes" + "go/format" + "go/parser" + "go/token" + "io/ioutil" + "log" + "path/filepath" + "sort" + "strings" + "text/template" +) + +var eventHandlerTmpl = template.Must(template.New("eventHandler").Funcs(template.FuncMap{ + "constName": constName, + "privateName": privateName, +}).Parse(`// Code generated by "eventhandlers"; DO NOT EDIT +// See events.go + +package gavalink + +// Following are all the event types. +// Event type values are used to match the events returned by Lavalink. +const ({{range .}} + {{privateName .}}EventType = "{{constName .}}"{{end}} +) +{{range .}} +// {{privateName .}}EventHandler is an event handler for {{.}} events. +type {{privateName .}}EventHandler func(*Player, *{{.}}) + +// Type returns the event type for {{.}} events. +func (eh {{privateName .}}EventHandler) Type() string { + return {{privateName .}}EventType +} +// New returns a new instance of {{.}}. +func (eh {{privateName .}}EventHandler) New() interface{} { + return &{{.}}{} +} +// Handle is the handler for {{.}} events. +func (eh {{privateName .}}EventHandler) Handle(p *Player, i interface{}) { + if t, ok := i.(*{{.}}); ok { + eh(p, t) + } +} + +{{end}} +func handlerForInterface(handler interface{}) EventHandler { + switch v := handler.(type) { + case func(*Player, interface{}): + return interfaceEventHandler(v){{range .}} + case func(*Player, *{{.}}): + return {{privateName .}}EventHandler(v){{end}} + } + + return nil +} + +func init() { {{range .}} + registerInterfaceProvider({{privateName .}}EventHandler(nil)){{end}} +} +`)) + +func main() { + var buf bytes.Buffer + dir := filepath.Dir(".") + + fs := token.NewFileSet() + parsedFile, err := parser.ParseFile(fs, "events.go", nil, 0) + if err != nil { + log.Fatalf("warning: internal error: could not parse events.go: %s", err) + return + } + + names := []string{} + for object := range parsedFile.Scope.Objects { + names = append(names, object) + } + sort.Strings(names) + eventHandlerTmpl.Execute(&buf, names) + + src, err := format.Source(buf.Bytes()) + if err != nil { + log.Println("warning: internal error: invalid Go generated:", err) + src = buf.Bytes() + } + + err = ioutil.WriteFile(filepath.Join(dir, strings.ToLower("eventhandlers.go")), src, 0644) + if err != nil { + log.Fatal(buf, "writing output: %s", err) + } +} + +func constName(name string) string { + return name + "Event" +} + +func privateName(name string) string { + return strings.ToLower(string(name[0])) + name[1:] +}