diff --git a/example/plugins/api-call-example/mod/zoraxy_plugin/events/events.go b/example/plugins/api-call-example/mod/zoraxy_plugin/events/events.go index df61861..33b08ce 100644 --- a/example/plugins/api-call-example/mod/zoraxy_plugin/events/events.go +++ b/example/plugins/api-call-example/mod/zoraxy_plugin/events/events.go @@ -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) } diff --git a/example/plugins/debugger/mod/zoraxy_plugin/events/events.go b/example/plugins/debugger/mod/zoraxy_plugin/events/events.go index df61861..33b08ce 100644 --- a/example/plugins/debugger/mod/zoraxy_plugin/events/events.go +++ b/example/plugins/debugger/mod/zoraxy_plugin/events/events.go @@ -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) } diff --git a/example/plugins/dynamic-capture-example/mod/zoraxy_plugin/events/events.go b/example/plugins/dynamic-capture-example/mod/zoraxy_plugin/events/events.go index df61861..33b08ce 100644 --- a/example/plugins/dynamic-capture-example/mod/zoraxy_plugin/events/events.go +++ b/example/plugins/dynamic-capture-example/mod/zoraxy_plugin/events/events.go @@ -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) } diff --git a/example/plugins/event-subscriber-example/mod/zoraxy_plugin/events/events.go b/example/plugins/event-subscriber-example/mod/zoraxy_plugin/events/events.go index df61861..33b08ce 100644 --- a/example/plugins/event-subscriber-example/mod/zoraxy_plugin/events/events.go +++ b/example/plugins/event-subscriber-example/mod/zoraxy_plugin/events/events.go @@ -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) } diff --git a/example/plugins/helloworld/mod/zoraxy_plugin/events/events.go b/example/plugins/helloworld/mod/zoraxy_plugin/events/events.go index df61861..33b08ce 100644 --- a/example/plugins/helloworld/mod/zoraxy_plugin/events/events.go +++ b/example/plugins/helloworld/mod/zoraxy_plugin/events/events.go @@ -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) } diff --git a/example/plugins/restful-example/mod/zoraxy_plugin/events/events.go b/example/plugins/restful-example/mod/zoraxy_plugin/events/events.go index df61861..33b08ce 100644 --- a/example/plugins/restful-example/mod/zoraxy_plugin/events/events.go +++ b/example/plugins/restful-example/mod/zoraxy_plugin/events/events.go @@ -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) } diff --git a/example/plugins/static-capture-example/mod/zoraxy_plugin/events/events.go b/example/plugins/static-capture-example/mod/zoraxy_plugin/events/events.go index df61861..33b08ce 100644 --- a/example/plugins/static-capture-example/mod/zoraxy_plugin/events/events.go +++ b/example/plugins/static-capture-example/mod/zoraxy_plugin/events/events.go @@ -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) } diff --git a/example/plugins/upnp/mod/zoraxy_plugin/events/events.go b/example/plugins/upnp/mod/zoraxy_plugin/events/events.go index df61861..33b08ce 100644 --- a/example/plugins/upnp/mod/zoraxy_plugin/events/events.go +++ b/example/plugins/upnp/mod/zoraxy_plugin/events/events.go @@ -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) } diff --git a/src/mod/eventsystem/event_system.go b/src/mod/eventsystem/event_system.go index d529217..5ec9e27 100644 --- a/src/mod/eventsystem/event_system.go +++ b/src/mod/eventsystem/event_system.go @@ -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 } diff --git a/src/mod/plugins/zoraxy_plugin/events/events.go b/src/mod/plugins/zoraxy_plugin/events/events.go index df61861..33b08ce 100644 --- a/src/mod/plugins/zoraxy_plugin/events/events.go +++ b/src/mod/plugins/zoraxy_plugin/events/events.go @@ -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) }