mirror of
https://github.com/owncast/owncast.git
synced 2025-11-02 11:56:57 +08:00
Add server status as a default field in all webhooks using BaseWebhookData struct (#4410)
* Initial plan * Add server status as default field in all webhooks Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Fix goimports linter error by removing trailing whitespace Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Move serverURL from status object to separate webhook field per feedback Per code review feedback, serverURL is a configuration value, not a status property. This change: - Removes ServerURL from models.Status struct - Adds ServerURL as separate field in WebhookEvent - Populates ServerURL directly when sending webhooks using configrepository.GetServerURL() - Updates all tests to expect new structure This provides the same functionality (server URL in all webhooks) while correctly treating it as configuration rather than status. Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Add omitempty tag to ServerURL field in WebhookEvent struct Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Fix webhook duplication by moving status to eventData for all events Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Restore type safety to webhook EventData using proper typed structs Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Move ServerURL from top-level WebhookEvent to eventData for all webhook types Co-authored-by: gabek <414923+gabek@users.noreply.github.com> * Update core/webhooks/webhooks.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Create BaseWebhookData struct for common webhook fields using struct embedding Co-authored-by: gabek <414923+gabek@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: gabek <414923+gabek@users.noreply.github.com> Co-authored-by: Gabe Kangas <gabek@real-ity.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@ -10,6 +10,10 @@ func SendChatEvent(chatEvent *events.UserMessageEvent) {
|
||||
webhookEvent := WebhookEvent{
|
||||
Type: chatEvent.GetMessageType(),
|
||||
EventData: &WebhookChatMessage{
|
||||
BaseWebhookData: BaseWebhookData{
|
||||
Status: getStatus(),
|
||||
ServerURL: getServerURL(),
|
||||
},
|
||||
User: chatEvent.User,
|
||||
Body: chatEvent.Body,
|
||||
ClientID: chatEvent.ClientID,
|
||||
@ -26,8 +30,17 @@ func SendChatEvent(chatEvent *events.UserMessageEvent) {
|
||||
// SendChatEventUsernameChanged will send a username changed event to webhook destinations.
|
||||
func SendChatEventUsernameChanged(event events.NameChangeEvent) {
|
||||
webhookEvent := WebhookEvent{
|
||||
Type: models.UserNameChanged,
|
||||
EventData: event,
|
||||
Type: models.UserNameChanged,
|
||||
EventData: &WebhookNameChangeEventData{
|
||||
BaseWebhookData: BaseWebhookData{
|
||||
Status: getStatus(),
|
||||
ServerURL: getServerURL(),
|
||||
},
|
||||
ID: event.ID,
|
||||
Timestamp: event.Timestamp,
|
||||
User: event.User,
|
||||
NewName: event.NewName,
|
||||
},
|
||||
}
|
||||
|
||||
SendEventToWebhooks(webhookEvent)
|
||||
@ -36,8 +49,16 @@ func SendChatEventUsernameChanged(event events.NameChangeEvent) {
|
||||
// SendChatEventUserJoined sends a webhook notifying that a user has joined.
|
||||
func SendChatEventUserJoined(event events.UserJoinedEvent) {
|
||||
webhookEvent := WebhookEvent{
|
||||
Type: models.UserJoined,
|
||||
EventData: event,
|
||||
Type: models.UserJoined,
|
||||
EventData: &WebhookUserJoinedEventData{
|
||||
BaseWebhookData: BaseWebhookData{
|
||||
Status: getStatus(),
|
||||
ServerURL: getServerURL(),
|
||||
},
|
||||
ID: event.ID,
|
||||
Timestamp: event.Timestamp,
|
||||
User: event.User,
|
||||
},
|
||||
}
|
||||
|
||||
SendEventToWebhooks(webhookEvent)
|
||||
@ -46,8 +67,16 @@ func SendChatEventUserJoined(event events.UserJoinedEvent) {
|
||||
// SendChatEventUserParted sends a webhook notifying that a user has parted.
|
||||
func SendChatEventUserParted(event events.UserPartEvent) {
|
||||
webhookEvent := WebhookEvent{
|
||||
Type: events.UserParted,
|
||||
EventData: event,
|
||||
Type: events.UserParted,
|
||||
EventData: &WebhookUserPartEventData{
|
||||
BaseWebhookData: BaseWebhookData{
|
||||
Status: getStatus(),
|
||||
ServerURL: getServerURL(),
|
||||
},
|
||||
ID: event.ID,
|
||||
Timestamp: event.Timestamp,
|
||||
User: event.User,
|
||||
},
|
||||
}
|
||||
|
||||
SendEventToWebhooks(webhookEvent)
|
||||
@ -57,8 +86,18 @@ func SendChatEventUserParted(event events.UserPartEvent) {
|
||||
// messages has changed.
|
||||
func SendChatEventSetMessageVisibility(event events.SetMessageVisibilityEvent) {
|
||||
webhookEvent := WebhookEvent{
|
||||
Type: models.VisibiltyToggled,
|
||||
EventData: event,
|
||||
Type: models.VisibiltyToggled,
|
||||
EventData: &WebhookVisibilityToggleEventData{
|
||||
BaseWebhookData: BaseWebhookData{
|
||||
Status: getStatus(),
|
||||
ServerURL: getServerURL(),
|
||||
},
|
||||
ID: event.ID,
|
||||
Timestamp: event.Timestamp,
|
||||
User: event.User,
|
||||
Visible: event.Visible,
|
||||
MessageIDs: event.MessageIDs,
|
||||
},
|
||||
}
|
||||
|
||||
SendEventToWebhooks(webhookEvent)
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/core/chat/events"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/persistence/configrepository"
|
||||
"github.com/owncast/owncast/persistence/webhookrepository"
|
||||
)
|
||||
|
||||
func TestSendChatEvent(t *testing.T) {
|
||||
@ -47,6 +52,17 @@ func TestSendChatEvent(t *testing.T) {
|
||||
"clientId": 51,
|
||||
"id": "id",
|
||||
"rawBody": "raw body",
|
||||
"serverURL": "http://localhost:8080",
|
||||
"status": {
|
||||
"lastConnectTime": null,
|
||||
"lastDisconnectTime": null,
|
||||
"online": true,
|
||||
"overallMaxViewerCount": 420,
|
||||
"sessionMaxViewerCount": 69,
|
||||
"streamTitle": "my stream",
|
||||
"versionNumber": "1.2.3",
|
||||
"viewerCount": 5
|
||||
},
|
||||
"timestamp": "1970-01-01T00:01:12.000000006Z",
|
||||
"user": {
|
||||
"authenticated": false,
|
||||
@ -92,11 +108,20 @@ func TestSendChatEventUsernameChanged(t *testing.T) {
|
||||
NewName: "new name",
|
||||
})
|
||||
}, `{
|
||||
"clientId": 51,
|
||||
"id": "id",
|
||||
"newName": "new name",
|
||||
"serverURL": "http://localhost:8080",
|
||||
"status": {
|
||||
"lastConnectTime": null,
|
||||
"lastDisconnectTime": null,
|
||||
"online": true,
|
||||
"overallMaxViewerCount": 420,
|
||||
"sessionMaxViewerCount": 69,
|
||||
"streamTitle": "my stream",
|
||||
"versionNumber": "1.2.3",
|
||||
"viewerCount": 5
|
||||
},
|
||||
"timestamp": "1970-01-01T00:01:12.000000006Z",
|
||||
"type": "NAME_CHANGE",
|
||||
"user": {
|
||||
"authenticated": false,
|
||||
"createdAt": "1970-01-01T00:00:03.000000026Z",
|
||||
@ -139,9 +164,18 @@ func TestSendChatEventUserJoined(t *testing.T) {
|
||||
},
|
||||
})
|
||||
}, `{
|
||||
"clientId": 51,
|
||||
"id": "id",
|
||||
"type": "USER_JOINED",
|
||||
"serverURL": "http://localhost:8080",
|
||||
"status": {
|
||||
"lastConnectTime": null,
|
||||
"lastDisconnectTime": null,
|
||||
"online": true,
|
||||
"overallMaxViewerCount": 420,
|
||||
"sessionMaxViewerCount": 69,
|
||||
"streamTitle": "my stream",
|
||||
"versionNumber": "1.2.3",
|
||||
"viewerCount": 5
|
||||
},
|
||||
"timestamp": "1970-01-01T00:01:12.000000006Z",
|
||||
"user": {
|
||||
"authenticated": false,
|
||||
@ -170,15 +204,111 @@ func TestSendChatEventSetMessageVisibility(t *testing.T) {
|
||||
Visible: false,
|
||||
})
|
||||
}, `{
|
||||
"MessageIDs": [
|
||||
"id": "id",
|
||||
"ids": [
|
||||
"message1",
|
||||
"message2"
|
||||
],
|
||||
"Visible": false,
|
||||
"body": "",
|
||||
"id": "id",
|
||||
"serverURL": "http://localhost:8080",
|
||||
"status": {
|
||||
"lastConnectTime": null,
|
||||
"lastDisconnectTime": null,
|
||||
"online": true,
|
||||
"overallMaxViewerCount": 420,
|
||||
"sessionMaxViewerCount": 69,
|
||||
"streamTitle": "my stream",
|
||||
"versionNumber": "1.2.3",
|
||||
"viewerCount": 5
|
||||
},
|
||||
"timestamp": "1970-01-01T00:01:12.000000006Z",
|
||||
"type": "VISIBILITY-UPDATE",
|
||||
"user": null
|
||||
"user": null,
|
||||
"visible": false
|
||||
}`)
|
||||
}
|
||||
|
||||
// TestWebhookHasServerStatus verifies that all webhook events include server status
|
||||
func TestWebhookHasServerStatus(t *testing.T) {
|
||||
// Set up server configuration
|
||||
configRepo := configrepository.Get()
|
||||
configRepo.SetServerURL("http://localhost:8080")
|
||||
|
||||
eventChannel := make(chan WebhookEvent)
|
||||
|
||||
// Set up a server.
|
||||
svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
data := WebhookEvent{}
|
||||
json.NewDecoder(r.Body).Decode(&data)
|
||||
eventChannel <- data
|
||||
}))
|
||||
defer svr.Close()
|
||||
|
||||
webhooksRepo := webhookrepository.Get()
|
||||
|
||||
// Subscribe to the webhook.
|
||||
hook, err := webhooksRepo.InsertWebhook(svr.URL, []models.EventType{models.UserJoined})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := webhooksRepo.DeleteWebhook(hook); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Send a chat event
|
||||
timestamp := time.Unix(72, 6).UTC()
|
||||
user := models.User{
|
||||
ID: "user id",
|
||||
DisplayName: "display name",
|
||||
DisplayColor: 4,
|
||||
CreatedAt: time.Unix(3, 26).UTC(),
|
||||
DisabledAt: nil,
|
||||
PreviousNames: []string{"somebody"},
|
||||
NameChangedAt: nil,
|
||||
Scopes: []string{},
|
||||
IsBot: false,
|
||||
AuthenticatedAt: nil,
|
||||
Authenticated: false,
|
||||
}
|
||||
|
||||
SendChatEventUserJoined(events.UserJoinedEvent{
|
||||
Event: events.Event{
|
||||
Type: events.UserJoined,
|
||||
ID: "id",
|
||||
Timestamp: timestamp,
|
||||
},
|
||||
UserEvent: events.UserEvent{
|
||||
User: &user,
|
||||
ClientID: 51,
|
||||
HiddenAt: nil,
|
||||
},
|
||||
})
|
||||
|
||||
// Capture the event
|
||||
event := <-eventChannel
|
||||
|
||||
// Verify the webhook event has a status field in eventData
|
||||
eventData, ok := event.EventData.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Error("Expected EventData to be a map")
|
||||
}
|
||||
|
||||
status, ok := eventData["status"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Error("Expected eventData to contain status field")
|
||||
}
|
||||
|
||||
versionNumber, ok := status["versionNumber"].(string)
|
||||
if !ok || versionNumber == "" {
|
||||
t.Error("Expected eventData.status to have versionNumber, but it was empty")
|
||||
}
|
||||
|
||||
serverURL, ok := eventData["serverURL"].(string)
|
||||
if !ok || serverURL == "" {
|
||||
t.Error("Expected eventData to have serverURL, but it was empty")
|
||||
}
|
||||
|
||||
if event.Type != models.UserJoined {
|
||||
t.Errorf("Expected event type %v but got %v", models.UserJoined, event.Type)
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ func sendStreamStatusEvent(eventType models.EventType, id string, timestamp time
|
||||
"summary": configRepository.GetServerSummary(),
|
||||
"streamTitle": configRepository.GetStreamTitle(),
|
||||
"status": getStatus(),
|
||||
"serverURL": getServerURL(),
|
||||
"timestamp": timestamp,
|
||||
},
|
||||
})
|
||||
|
||||
@ -21,9 +21,7 @@ func TestSendStreamStatusEvent(t *testing.T) {
|
||||
}, `{
|
||||
"id": "id",
|
||||
"name": "my server",
|
||||
"streamTitle": "my stream",
|
||||
"summary": "my server where I stream",
|
||||
"timestamp": "1970-01-01T00:01:12.000000006Z",
|
||||
"serverURL": "http://localhost:8080",
|
||||
"status": {
|
||||
"lastConnectTime": null,
|
||||
"lastDisconnectTime": null,
|
||||
@ -33,6 +31,9 @@ func TestSendStreamStatusEvent(t *testing.T) {
|
||||
"streamTitle": "my stream",
|
||||
"versionNumber": "1.2.3",
|
||||
"viewerCount": 5
|
||||
}
|
||||
},
|
||||
"streamTitle": "my stream",
|
||||
"summary": "my server where I stream",
|
||||
"timestamp": "1970-01-01T00:01:12.000000006Z"
|
||||
}`)
|
||||
}
|
||||
|
||||
@ -5,9 +5,16 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/persistence/configrepository"
|
||||
"github.com/owncast/owncast/persistence/webhookrepository"
|
||||
)
|
||||
|
||||
// BaseWebhookData contains common fields shared across all webhook event data.
|
||||
type BaseWebhookData struct {
|
||||
Status models.Status `json:"status"`
|
||||
ServerURL string `json:"serverURL,omitempty"`
|
||||
}
|
||||
|
||||
// WebhookEvent represents an event sent as a webhook.
|
||||
type WebhookEvent struct {
|
||||
EventData interface{} `json:"eventData,omitempty"`
|
||||
@ -16,6 +23,7 @@ type WebhookEvent struct {
|
||||
|
||||
// WebhookChatMessage represents a single chat message sent as a webhook payload.
|
||||
type WebhookChatMessage struct {
|
||||
BaseWebhookData
|
||||
User *models.User `json:"user,omitempty"`
|
||||
Timestamp *time.Time `json:"timestamp,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
@ -25,6 +33,41 @@ type WebhookChatMessage struct {
|
||||
Visible bool `json:"visible"`
|
||||
}
|
||||
|
||||
// WebhookUserJoinedEventData represents user joined event data sent as a webhook payload.
|
||||
type WebhookUserJoinedEventData struct {
|
||||
BaseWebhookData
|
||||
ID string `json:"id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
User *models.User `json:"user"`
|
||||
}
|
||||
|
||||
// WebhookUserPartEventData represents user parted event data sent as a webhook payload.
|
||||
type WebhookUserPartEventData struct {
|
||||
BaseWebhookData
|
||||
ID string `json:"id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
User *models.User `json:"user"`
|
||||
}
|
||||
|
||||
// WebhookNameChangeEventData represents name change event data sent as a webhook payload.
|
||||
type WebhookNameChangeEventData struct {
|
||||
BaseWebhookData
|
||||
ID string `json:"id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
User *models.User `json:"user"`
|
||||
NewName string `json:"newName"`
|
||||
}
|
||||
|
||||
// WebhookVisibilityToggleEventData represents message visibility toggle event data sent as a webhook payload.
|
||||
type WebhookVisibilityToggleEventData struct {
|
||||
BaseWebhookData
|
||||
ID string `json:"id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
User *models.User `json:"user"`
|
||||
Visible bool `json:"visible"`
|
||||
MessageIDs []string `json:"ids"`
|
||||
}
|
||||
|
||||
// SendEventToWebhooks will send a single webhook event to all webhook destinations.
|
||||
func SendEventToWebhooks(payload WebhookEvent) {
|
||||
sendEventToWebhooks(payload, nil)
|
||||
@ -42,3 +85,8 @@ func sendEventToWebhooks(payload WebhookEvent, wg *sync.WaitGroup) {
|
||||
addToQueue(webhook, payload, wg)
|
||||
}
|
||||
}
|
||||
|
||||
func getServerURL() string {
|
||||
configRepo := configrepository.Get()
|
||||
return configRepo.GetServerURL()
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import (
|
||||
"github.com/owncast/owncast/core/chat/events"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/persistence/configrepository"
|
||||
"github.com/owncast/owncast/persistence/webhookrepository"
|
||||
jsonpatch "gopkg.in/evanphx/json-patch.v5"
|
||||
)
|
||||
@ -42,6 +43,10 @@ func TestMain(m *testing.M) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Set up server URL for tests
|
||||
configRepo := configrepository.Get()
|
||||
configRepo.SetServerURL("http://localhost:8080")
|
||||
|
||||
SetupWebhooks(fakeGetStatus)
|
||||
|
||||
defer close(queue)
|
||||
|
||||
Reference in New Issue
Block a user