feat(eventsystem): plumbing for plugin<->plugin comms

The only thing left is to add an API endpoint for broadcasting
EventCustom events (other event types should not be emittible by
plugins, the use-case isn't there since plugins can already talk to
Zoraxy via the API).
Input to the endput should be a json-encoded `CustomEvent`
This commit is contained in:
Anthony Rubick
2025-09-07 18:03:48 -05:00
parent 2f98ecd0c6
commit c57fa39554
10 changed files with 310 additions and 41 deletions

View File

@@ -20,10 +20,8 @@ type EventPayload interface {
// Event represents a system event
type Event struct {
Name EventName `json:"name"`
// Unix timestamp
Timestamp int64 `json:"timestamp"`
// UUID for the event
UUID string `json:"uuid"`
Timestamp int64 `json:"timestamp"` // Unix timestamp
UUID string `json:"uuid"` // UUID for the event
Data EventPayload `json:"data"`
}
@@ -34,6 +32,9 @@ const (
EventBlacklistToggled EventName = "blacklistToggled"
// EventAccessRuleCreated is emitted when a new access ruleset is created
EventAccessRuleCreated EventName = "accessRuleCreated"
// A custom event emitted by a plugin, with the intention of being broadcast
// to the designated recipient(s)
EventCustom EventName = "customEvent"
// Add more event types as needed
)
@@ -42,6 +43,7 @@ var validEventNames = map[EventName]bool{
EventBlacklistedIPBlocked: true,
EventBlacklistToggled: true,
EventAccessRuleCreated: true,
EventCustom: true,
// Add more event types as needed
// NOTE: Keep up-to-date with event names specified above
}
@@ -100,6 +102,20 @@ func (e *AccessRuleCreatedEvent) GetEventSource() string {
return "accesslist-api"
}
type CustomEvent struct {
SourcePlugin string `json:"source_plugin"`
Recipients []string `json:"recipients"`
Payload map[string]any `json:"payload"`
}
func (e *CustomEvent) GetName() EventName {
return EventCustom
}
func (e *CustomEvent) GetEventSource() string {
return e.SourcePlugin
}
// ParseEvent parses a JSON byte slice into an Event struct
func ParseEvent(jsonData []byte, event *Event) error {
// First, determine the event type, and parse shared fields, from the JSON data
@@ -146,6 +162,15 @@ func ParseEvent(jsonData []byte, event *Event) error {
return err
}
event.Data = &payload.Data
case EventCustom:
type tempData struct {
Data CustomEvent `json:"data"`
}
var payload tempData
if err := json.Unmarshal(jsonData, &payload); err != nil {
return err
}
event.Data = &payload.Data
default:
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
}

View File

@@ -20,10 +20,8 @@ type EventPayload interface {
// Event represents a system event
type Event struct {
Name EventName `json:"name"`
// Unix timestamp
Timestamp int64 `json:"timestamp"`
// UUID for the event
UUID string `json:"uuid"`
Timestamp int64 `json:"timestamp"` // Unix timestamp
UUID string `json:"uuid"` // UUID for the event
Data EventPayload `json:"data"`
}
@@ -34,6 +32,9 @@ const (
EventBlacklistToggled EventName = "blacklistToggled"
// EventAccessRuleCreated is emitted when a new access ruleset is created
EventAccessRuleCreated EventName = "accessRuleCreated"
// A custom event emitted by a plugin, with the intention of being broadcast
// to the designated recipient(s)
EventCustom EventName = "customEvent"
// Add more event types as needed
)
@@ -42,6 +43,7 @@ var validEventNames = map[EventName]bool{
EventBlacklistedIPBlocked: true,
EventBlacklistToggled: true,
EventAccessRuleCreated: true,
EventCustom: true,
// Add more event types as needed
// NOTE: Keep up-to-date with event names specified above
}
@@ -100,6 +102,20 @@ func (e *AccessRuleCreatedEvent) GetEventSource() string {
return "accesslist-api"
}
type CustomEvent struct {
SourcePlugin string `json:"source_plugin"`
Recipients []string `json:"recipients"`
Payload map[string]any `json:"payload"`
}
func (e *CustomEvent) GetName() EventName {
return EventCustom
}
func (e *CustomEvent) GetEventSource() string {
return e.SourcePlugin
}
// ParseEvent parses a JSON byte slice into an Event struct
func ParseEvent(jsonData []byte, event *Event) error {
// First, determine the event type, and parse shared fields, from the JSON data
@@ -146,6 +162,15 @@ func ParseEvent(jsonData []byte, event *Event) error {
return err
}
event.Data = &payload.Data
case EventCustom:
type tempData struct {
Data CustomEvent `json:"data"`
}
var payload tempData
if err := json.Unmarshal(jsonData, &payload); err != nil {
return err
}
event.Data = &payload.Data
default:
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
}

View File

@@ -20,10 +20,8 @@ type EventPayload interface {
// Event represents a system event
type Event struct {
Name EventName `json:"name"`
// Unix timestamp
Timestamp int64 `json:"timestamp"`
// UUID for the event
UUID string `json:"uuid"`
Timestamp int64 `json:"timestamp"` // Unix timestamp
UUID string `json:"uuid"` // UUID for the event
Data EventPayload `json:"data"`
}
@@ -34,6 +32,9 @@ const (
EventBlacklistToggled EventName = "blacklistToggled"
// EventAccessRuleCreated is emitted when a new access ruleset is created
EventAccessRuleCreated EventName = "accessRuleCreated"
// A custom event emitted by a plugin, with the intention of being broadcast
// to the designated recipient(s)
EventCustom EventName = "customEvent"
// Add more event types as needed
)
@@ -42,6 +43,7 @@ var validEventNames = map[EventName]bool{
EventBlacklistedIPBlocked: true,
EventBlacklistToggled: true,
EventAccessRuleCreated: true,
EventCustom: true,
// Add more event types as needed
// NOTE: Keep up-to-date with event names specified above
}
@@ -100,6 +102,20 @@ func (e *AccessRuleCreatedEvent) GetEventSource() string {
return "accesslist-api"
}
type CustomEvent struct {
SourcePlugin string `json:"source_plugin"`
Recipients []string `json:"recipients"`
Payload map[string]any `json:"payload"`
}
func (e *CustomEvent) GetName() EventName {
return EventCustom
}
func (e *CustomEvent) GetEventSource() string {
return e.SourcePlugin
}
// ParseEvent parses a JSON byte slice into an Event struct
func ParseEvent(jsonData []byte, event *Event) error {
// First, determine the event type, and parse shared fields, from the JSON data
@@ -146,6 +162,15 @@ func ParseEvent(jsonData []byte, event *Event) error {
return err
}
event.Data = &payload.Data
case EventCustom:
type tempData struct {
Data CustomEvent `json:"data"`
}
var payload tempData
if err := json.Unmarshal(jsonData, &payload); err != nil {
return err
}
event.Data = &payload.Data
default:
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
}

View File

@@ -20,10 +20,8 @@ type EventPayload interface {
// Event represents a system event
type Event struct {
Name EventName `json:"name"`
// Unix timestamp
Timestamp int64 `json:"timestamp"`
// UUID for the event
UUID string `json:"uuid"`
Timestamp int64 `json:"timestamp"` // Unix timestamp
UUID string `json:"uuid"` // UUID for the event
Data EventPayload `json:"data"`
}
@@ -34,6 +32,9 @@ const (
EventBlacklistToggled EventName = "blacklistToggled"
// EventAccessRuleCreated is emitted when a new access ruleset is created
EventAccessRuleCreated EventName = "accessRuleCreated"
// A custom event emitted by a plugin, with the intention of being broadcast
// to the designated recipient(s)
EventCustom EventName = "customEvent"
// Add more event types as needed
)
@@ -42,6 +43,7 @@ var validEventNames = map[EventName]bool{
EventBlacklistedIPBlocked: true,
EventBlacklistToggled: true,
EventAccessRuleCreated: true,
EventCustom: true,
// Add more event types as needed
// NOTE: Keep up-to-date with event names specified above
}
@@ -100,6 +102,20 @@ func (e *AccessRuleCreatedEvent) GetEventSource() string {
return "accesslist-api"
}
type CustomEvent struct {
SourcePlugin string `json:"source_plugin"`
Recipients []string `json:"recipients"`
Payload map[string]any `json:"payload"`
}
func (e *CustomEvent) GetName() EventName {
return EventCustom
}
func (e *CustomEvent) GetEventSource() string {
return e.SourcePlugin
}
// ParseEvent parses a JSON byte slice into an Event struct
func ParseEvent(jsonData []byte, event *Event) error {
// First, determine the event type, and parse shared fields, from the JSON data
@@ -146,6 +162,15 @@ func ParseEvent(jsonData []byte, event *Event) error {
return err
}
event.Data = &payload.Data
case EventCustom:
type tempData struct {
Data CustomEvent `json:"data"`
}
var payload tempData
if err := json.Unmarshal(jsonData, &payload); err != nil {
return err
}
event.Data = &payload.Data
default:
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
}

View File

@@ -20,10 +20,8 @@ type EventPayload interface {
// Event represents a system event
type Event struct {
Name EventName `json:"name"`
// Unix timestamp
Timestamp int64 `json:"timestamp"`
// UUID for the event
UUID string `json:"uuid"`
Timestamp int64 `json:"timestamp"` // Unix timestamp
UUID string `json:"uuid"` // UUID for the event
Data EventPayload `json:"data"`
}
@@ -34,6 +32,9 @@ const (
EventBlacklistToggled EventName = "blacklistToggled"
// EventAccessRuleCreated is emitted when a new access ruleset is created
EventAccessRuleCreated EventName = "accessRuleCreated"
// A custom event emitted by a plugin, with the intention of being broadcast
// to the designated recipient(s)
EventCustom EventName = "customEvent"
// Add more event types as needed
)
@@ -42,6 +43,7 @@ var validEventNames = map[EventName]bool{
EventBlacklistedIPBlocked: true,
EventBlacklistToggled: true,
EventAccessRuleCreated: true,
EventCustom: true,
// Add more event types as needed
// NOTE: Keep up-to-date with event names specified above
}
@@ -100,6 +102,20 @@ func (e *AccessRuleCreatedEvent) GetEventSource() string {
return "accesslist-api"
}
type CustomEvent struct {
SourcePlugin string `json:"source_plugin"`
Recipients []string `json:"recipients"`
Payload map[string]any `json:"payload"`
}
func (e *CustomEvent) GetName() EventName {
return EventCustom
}
func (e *CustomEvent) GetEventSource() string {
return e.SourcePlugin
}
// ParseEvent parses a JSON byte slice into an Event struct
func ParseEvent(jsonData []byte, event *Event) error {
// First, determine the event type, and parse shared fields, from the JSON data
@@ -146,6 +162,15 @@ func ParseEvent(jsonData []byte, event *Event) error {
return err
}
event.Data = &payload.Data
case EventCustom:
type tempData struct {
Data CustomEvent `json:"data"`
}
var payload tempData
if err := json.Unmarshal(jsonData, &payload); err != nil {
return err
}
event.Data = &payload.Data
default:
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
}

View File

@@ -20,10 +20,8 @@ type EventPayload interface {
// Event represents a system event
type Event struct {
Name EventName `json:"name"`
// Unix timestamp
Timestamp int64 `json:"timestamp"`
// UUID for the event
UUID string `json:"uuid"`
Timestamp int64 `json:"timestamp"` // Unix timestamp
UUID string `json:"uuid"` // UUID for the event
Data EventPayload `json:"data"`
}
@@ -34,6 +32,9 @@ const (
EventBlacklistToggled EventName = "blacklistToggled"
// EventAccessRuleCreated is emitted when a new access ruleset is created
EventAccessRuleCreated EventName = "accessRuleCreated"
// A custom event emitted by a plugin, with the intention of being broadcast
// to the designated recipient(s)
EventCustom EventName = "customEvent"
// Add more event types as needed
)
@@ -42,6 +43,7 @@ var validEventNames = map[EventName]bool{
EventBlacklistedIPBlocked: true,
EventBlacklistToggled: true,
EventAccessRuleCreated: true,
EventCustom: true,
// Add more event types as needed
// NOTE: Keep up-to-date with event names specified above
}
@@ -100,6 +102,20 @@ func (e *AccessRuleCreatedEvent) GetEventSource() string {
return "accesslist-api"
}
type CustomEvent struct {
SourcePlugin string `json:"source_plugin"`
Recipients []string `json:"recipients"`
Payload map[string]any `json:"payload"`
}
func (e *CustomEvent) GetName() EventName {
return EventCustom
}
func (e *CustomEvent) GetEventSource() string {
return e.SourcePlugin
}
// ParseEvent parses a JSON byte slice into an Event struct
func ParseEvent(jsonData []byte, event *Event) error {
// First, determine the event type, and parse shared fields, from the JSON data
@@ -146,6 +162,15 @@ func ParseEvent(jsonData []byte, event *Event) error {
return err
}
event.Data = &payload.Data
case EventCustom:
type tempData struct {
Data CustomEvent `json:"data"`
}
var payload tempData
if err := json.Unmarshal(jsonData, &payload); err != nil {
return err
}
event.Data = &payload.Data
default:
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
}

View File

@@ -20,10 +20,8 @@ type EventPayload interface {
// Event represents a system event
type Event struct {
Name EventName `json:"name"`
// Unix timestamp
Timestamp int64 `json:"timestamp"`
// UUID for the event
UUID string `json:"uuid"`
Timestamp int64 `json:"timestamp"` // Unix timestamp
UUID string `json:"uuid"` // UUID for the event
Data EventPayload `json:"data"`
}
@@ -34,6 +32,9 @@ const (
EventBlacklistToggled EventName = "blacklistToggled"
// EventAccessRuleCreated is emitted when a new access ruleset is created
EventAccessRuleCreated EventName = "accessRuleCreated"
// A custom event emitted by a plugin, with the intention of being broadcast
// to the designated recipient(s)
EventCustom EventName = "customEvent"
// Add more event types as needed
)
@@ -42,6 +43,7 @@ var validEventNames = map[EventName]bool{
EventBlacklistedIPBlocked: true,
EventBlacklistToggled: true,
EventAccessRuleCreated: true,
EventCustom: true,
// Add more event types as needed
// NOTE: Keep up-to-date with event names specified above
}
@@ -100,6 +102,20 @@ func (e *AccessRuleCreatedEvent) GetEventSource() string {
return "accesslist-api"
}
type CustomEvent struct {
SourcePlugin string `json:"source_plugin"`
Recipients []string `json:"recipients"`
Payload map[string]any `json:"payload"`
}
func (e *CustomEvent) GetName() EventName {
return EventCustom
}
func (e *CustomEvent) GetEventSource() string {
return e.SourcePlugin
}
// ParseEvent parses a JSON byte slice into an Event struct
func ParseEvent(jsonData []byte, event *Event) error {
// First, determine the event type, and parse shared fields, from the JSON data
@@ -146,6 +162,15 @@ func ParseEvent(jsonData []byte, event *Event) error {
return err
}
event.Data = &payload.Data
case EventCustom:
type tempData struct {
Data CustomEvent `json:"data"`
}
var payload tempData
if err := json.Unmarshal(jsonData, &payload); err != nil {
return err
}
event.Data = &payload.Data
default:
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
}

View File

@@ -20,10 +20,8 @@ type EventPayload interface {
// Event represents a system event
type Event struct {
Name EventName `json:"name"`
// Unix timestamp
Timestamp int64 `json:"timestamp"`
// UUID for the event
UUID string `json:"uuid"`
Timestamp int64 `json:"timestamp"` // Unix timestamp
UUID string `json:"uuid"` // UUID for the event
Data EventPayload `json:"data"`
}
@@ -34,6 +32,9 @@ const (
EventBlacklistToggled EventName = "blacklistToggled"
// EventAccessRuleCreated is emitted when a new access ruleset is created
EventAccessRuleCreated EventName = "accessRuleCreated"
// A custom event emitted by a plugin, with the intention of being broadcast
// to the designated recipient(s)
EventCustom EventName = "customEvent"
// Add more event types as needed
)
@@ -42,6 +43,7 @@ var validEventNames = map[EventName]bool{
EventBlacklistedIPBlocked: true,
EventBlacklistToggled: true,
EventAccessRuleCreated: true,
EventCustom: true,
// Add more event types as needed
// NOTE: Keep up-to-date with event names specified above
}
@@ -100,6 +102,20 @@ func (e *AccessRuleCreatedEvent) GetEventSource() string {
return "accesslist-api"
}
type CustomEvent struct {
SourcePlugin string `json:"source_plugin"`
Recipients []string `json:"recipients"`
Payload map[string]any `json:"payload"`
}
func (e *CustomEvent) GetName() EventName {
return EventCustom
}
func (e *CustomEvent) GetEventSource() string {
return e.SourcePlugin
}
// ParseEvent parses a JSON byte slice into an Event struct
func ParseEvent(jsonData []byte, event *Event) error {
// First, determine the event type, and parse shared fields, from the JSON data
@@ -146,6 +162,15 @@ func ParseEvent(jsonData []byte, event *Event) error {
return err
}
event.Data = &payload.Data
case EventCustom:
type tempData struct {
Data CustomEvent `json:"data"`
}
var payload tempData
if err := json.Unmarshal(jsonData, &payload); err != nil {
return err
}
event.Data = &payload.Data
default:
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
}

View File

@@ -86,8 +86,39 @@ func (em *eventManager) UnregisterSubscriber(listenerID ListenerID) error {
return nil
}
// EmitToSubscribersAnd dispatches an event to the specific listeners in addition to the events subscribers.
//
// The primary use-case of this function is for plugin-to-plugin communication
func (em *eventManager) EmitToSubscribersAnd(listenerIDs []ListenerID, payload events.EventPayload) {
eventName := payload.GetName()
if len(listenerIDs) == 0 {
return // No subscribers
}
// Create the event
event := events.Event{
Name: eventName,
Timestamp: time.Now().Unix(),
UUID: uuid.New().String(),
Data: payload,
}
// Dispatch to all specified listeners asynchronously
em.emitTo(listenerIDs, event)
// Also emit to all subscribers of the event as usual
em.mutex.RLock()
subscribers, exists := em.subscriptions[eventName]
em.mutex.RUnlock()
if !exists || len(subscribers) == 0 {
return // No subscribers
}
em.emitTo(subscribers, event)
}
// Emit dispatches an event to all subscribed listeners
func (em *eventManager) Emit(payload events.EventPayload) error {
func (em *eventManager) Emit(payload events.EventPayload) {
eventName := payload.GetName()
em.mutex.RLock()
@@ -95,7 +126,7 @@ func (em *eventManager) Emit(payload events.EventPayload) error {
subscribers, exists := em.subscriptions[eventName]
if !exists || len(subscribers) == 0 {
return nil // No subscribers
return // No subscribers
}
// Create the event
@@ -107,11 +138,26 @@ func (em *eventManager) Emit(payload events.EventPayload) error {
}
// Dispatch to all subscribers asynchronously
for _, listenerID := range subscribers {
em.emitTo(subscribers, event)
}
// Dispatch event to all specified listeners asynchronously
func (em *eventManager) emitTo(listenerIDs []ListenerID, event events.Event) {
if len(listenerIDs) == 0 {
return
}
// Dispatch to all specified listeners asynchronously
em.mutex.RLock()
defer em.mutex.RUnlock()
for _, listenerID := range listenerIDs {
listener, exists := em.subscribers[listenerID]
if !exists {
em.logger.PrintAndLog("event-system", "Failed to get listener for event dispatch, removing "+string(listenerID)+" from subscriptions", nil)
// Remove the listener from the subscription list
// This is done in a separate goroutine to avoid deadlock
go em.UnregisterSubscriber(listenerID)
continue
}
@@ -121,6 +167,4 @@ func (em *eventManager) Emit(payload events.EventPayload) error {
}
}(listener)
}
return nil
}

View File

@@ -20,10 +20,8 @@ type EventPayload interface {
// Event represents a system event
type Event struct {
Name EventName `json:"name"`
// Unix timestamp
Timestamp int64 `json:"timestamp"`
// UUID for the event
UUID string `json:"uuid"`
Timestamp int64 `json:"timestamp"` // Unix timestamp
UUID string `json:"uuid"` // UUID for the event
Data EventPayload `json:"data"`
}
@@ -34,6 +32,9 @@ const (
EventBlacklistToggled EventName = "blacklistToggled"
// EventAccessRuleCreated is emitted when a new access ruleset is created
EventAccessRuleCreated EventName = "accessRuleCreated"
// A custom event emitted by a plugin, with the intention of being broadcast
// to the designated recipient(s)
EventCustom EventName = "customEvent"
// Add more event types as needed
)
@@ -42,6 +43,7 @@ var validEventNames = map[EventName]bool{
EventBlacklistedIPBlocked: true,
EventBlacklistToggled: true,
EventAccessRuleCreated: true,
EventCustom: true,
// Add more event types as needed
// NOTE: Keep up-to-date with event names specified above
}
@@ -100,6 +102,20 @@ func (e *AccessRuleCreatedEvent) GetEventSource() string {
return "accesslist-api"
}
type CustomEvent struct {
SourcePlugin string `json:"source_plugin"`
Recipients []string `json:"recipients"`
Payload map[string]any `json:"payload"`
}
func (e *CustomEvent) GetName() EventName {
return EventCustom
}
func (e *CustomEvent) GetEventSource() string {
return e.SourcePlugin
}
// ParseEvent parses a JSON byte slice into an Event struct
func ParseEvent(jsonData []byte, event *Event) error {
// First, determine the event type, and parse shared fields, from the JSON data
@@ -146,6 +162,15 @@ func ParseEvent(jsonData []byte, event *Event) error {
return err
}
event.Data = &payload.Data
case EventCustom:
type tempData struct {
Data CustomEvent `json:"data"`
}
var payload tempData
if err := json.Unmarshal(jsonData, &payload); err != nil {
return err
}
event.Data = &payload.Data
default:
return fmt.Errorf("unknown event: %s, %v", temp.Name, jsonData)
}