From db67603550c4116f0f26a8830d226780fadaf71e Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Fri, 9 Jun 2023 17:29:02 -0700 Subject: [PATCH] feat: first pass migrating all the handlers to the new architecture --- activitypub/controllers/outbox.go | 2 +- controllers/admin.go | 13 - controllers/admin/appearance.go | 35 - controllers/admin/config.go | 880 ------------------ controllers/admin/disconnect.go | 21 - controllers/admin/federation.go | 179 ---- controllers/admin/notifications.go | 60 -- controllers/constants.go | 7 - controllers/hls.go | 55 -- webserver/handlers/adminApi.go | 1 - .../handlers/adminApiAppearanceConfig.go | 36 + .../handlers/adminApiChatHandlers.go | 149 +-- webserver/handlers/adminApiConfigHandlers.go | 814 ++++++++++++++++ .../handlers/adminApiConnectedClients.go | 12 +- .../handlers/adminApiDirectory.go | 10 +- .../handlers/adminApiEmojiConfig.go | 33 +- .../handlers/adminApiExternalAPIUsers.go | 36 +- .../handlers/adminApiFederationConfig.go | 180 ++++ .../handlers/adminApiFollowers.go | 37 +- .../handlers/adminApiHardwareStats.go | 4 +- .../handlers/adminApiLogs.go | 6 +- .../handlers/adminApiNotificationsConfig.go | 61 ++ .../handlers/adminApiServerConfig.go | 66 +- .../handlers/adminApiSoftwareUpdate.go | 18 +- .../handlers/adminApiSystemStatus.go | 4 +- .../handlers/adminApiVideoConfig.go | 4 +- .../handlers/adminApiViewers.go | 16 +- .../handlers/adminApiWebhooks.go | 32 +- .../handlers}/auth/fediverse/fediverse.go | 22 +- .../handlers}/auth/indieauth/client.go | 16 +- .../handlers}/auth/indieauth/server.go | 12 +- {controllers => webserver/handlers}/chat.go | 21 +- {controllers => webserver/handlers}/config.go | 11 +- .../handlers}/customJavascript.go | 4 +- webserver/handlers/directory.go | 1 - webserver/handlers/disconnect.go | 21 + {controllers => webserver/handlers}/emoji.go | 9 +- webserver/handlers/externalApi.go | 1 - .../handlers}/followers.go | 11 +- webserver/handlers/hls.go | 54 ++ {controllers => webserver/handlers}/images.go | 19 +- {controllers => webserver/handlers}/index.go | 4 +- webserver/handlers/internalApi.go | 1 - {controllers => webserver/handlers}/logo.go | 26 +- .../handlers}/moderation.go | 8 +- .../handlers}/notifications.go | 15 +- {controllers => webserver/handlers}/ping.go | 4 +- .../handlers}/playbackMetrics.go | 11 +- .../handlers}/remoteFollow.go | 15 +- {controllers => webserver/handlers}/robots.go | 4 +- webserver/handlers/static.go | 1 - {controllers => webserver/handlers}/status.go | 7 +- webserver/handlers/testing.go | 7 - {controllers => webserver/handlers}/video.go | 7 +- {controllers => webserver/handlers}/web.go | 2 +- webserver/handlers/webAssets.go | 1 - yp/api.go => webserver/handlers/ypApi.go | 7 +- webserver/requests/requestConfigValue.go | 6 + webserver/requests/requests.go | 50 + webserver/responses/image.go | 17 + .../responses}/pagination.go | 2 +- .../responses/responses.go | 2 +- webserver/router.go | 250 +++-- webserver/webserver_test.go | 9 - 64 files changed, 1696 insertions(+), 1733 deletions(-) delete mode 100644 controllers/admin.go delete mode 100644 controllers/admin/appearance.go delete mode 100644 controllers/admin/config.go delete mode 100644 controllers/admin/disconnect.go delete mode 100644 controllers/admin/federation.go delete mode 100644 controllers/admin/notifications.go delete mode 100644 controllers/constants.go delete mode 100644 controllers/hls.go delete mode 100644 webserver/handlers/adminApi.go create mode 100644 webserver/handlers/adminApiAppearanceConfig.go rename controllers/admin/chat.go => webserver/handlers/adminApiChatHandlers.go (59%) create mode 100644 webserver/handlers/adminApiConfigHandlers.go rename controllers/admin/connectedClients.go => webserver/handlers/adminApiConnectedClients.go (53%) rename controllers/admin/yp.go => webserver/handlers/adminApiDirectory.go (55%) rename controllers/admin/emoji.go => webserver/handlers/adminApiEmojiConfig.go (54%) rename controllers/admin/externalAPIUsers.go => webserver/handlers/adminApiExternalAPIUsers.go (64%) create mode 100644 webserver/handlers/adminApiFederationConfig.go rename controllers/admin/followers.go => webserver/handlers/adminApiFollowers.go (53%) rename controllers/admin/hardware.go => webserver/handlers/adminApiHardwareStats.go (78%) rename controllers/admin/logs.go => webserver/handlers/adminApiLogs.go (88%) create mode 100644 webserver/handlers/adminApiNotificationsConfig.go rename controllers/admin/serverConfig.go => webserver/handlers/adminApiServerConfig.go (69%) rename controllers/admin/update.go => webserver/handlers/adminApiSoftwareUpdate.go (90%) rename controllers/admin/status.go => webserver/handlers/adminApiSystemStatus.go (95%) rename controllers/admin/video.go => webserver/handlers/adminApiVideoConfig.go (97%) rename controllers/admin/viewers.go => webserver/handlers/adminApiViewers.go (70%) rename controllers/admin/webhooks.go => webserver/handlers/adminApiWebhooks.go (59%) rename {controllers => webserver/handlers}/auth/fediverse/fediverse.go (79%) rename {controllers => webserver/handlers}/auth/indieauth/client.go (85%) rename {controllers => webserver/handlers}/auth/indieauth/server.go (87%) rename {controllers => webserver/handlers}/chat.go (77%) rename {controllers => webserver/handlers}/config.go (94%) rename {controllers => webserver/handlers}/customJavascript.go (65%) delete mode 100644 webserver/handlers/directory.go create mode 100644 webserver/handlers/disconnect.go rename {controllers => webserver/handlers}/emoji.go (70%) delete mode 100644 webserver/handlers/externalApi.go rename {controllers => webserver/handlers}/followers.go (51%) rename {controllers => webserver/handlers}/images.go (69%) rename {controllers => webserver/handlers}/index.go (98%) delete mode 100644 webserver/handlers/internalApi.go rename {controllers => webserver/handlers}/logo.go (74%) rename {controllers/moderation => webserver/handlers}/moderation.go (89%) rename {controllers => webserver/handlers}/notifications.go (66%) rename {controllers => webserver/handlers}/ping.go (77%) rename {controllers => webserver/handlers}/playbackMetrics.go (81%) rename {controllers => webserver/handlers}/remoteFollow.go (71%) rename {controllers => webserver/handlers}/robots.go (84%) delete mode 100644 webserver/handlers/static.go rename {controllers => webserver/handlers}/status.go (87%) delete mode 100644 webserver/handlers/testing.go rename {controllers => webserver/handlers}/video.go (87%) rename {controllers => webserver/handlers}/web.go (92%) delete mode 100644 webserver/handlers/webAssets.go rename yp/api.go => webserver/handlers/ypApi.go (92%) create mode 100644 webserver/requests/requestConfigValue.go create mode 100644 webserver/requests/requests.go create mode 100644 webserver/responses/image.go rename {controllers => webserver/responses}/pagination.go (90%) rename controllers/controllers.go => webserver/responses/responses.go (98%) diff --git a/activitypub/controllers/outbox.go b/activitypub/controllers/outbox.go index 5aa75df0f..0dbcb5a03 100644 --- a/activitypub/controllers/outbox.go +++ b/activitypub/controllers/outbox.go @@ -62,7 +62,7 @@ func ActorObjectHandler(w http.ResponseWriter, r *http.Request) { if err != nil { w.WriteHeader(http.StatusNotFound) return - // controllers.WriteSimpleResponse(w, false, err.Error()) + // responses.WriteSimpleResponse(w, false, err.Error()) } if _, err := w.Write([]byte(object)); err != nil { diff --git a/controllers/admin.go b/controllers/admin.go deleted file mode 100644 index 47a400bc0..000000000 --- a/controllers/admin.go +++ /dev/null @@ -1,13 +0,0 @@ -package controllers - -import ( - "net/http" - - "github.com/owncast/owncast/core/rtmp" -) - -// DisconnectInboundConnection will force-disconnect an inbound stream. -func DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) { - rtmp.Disconnect() - w.WriteHeader(http.StatusOK) -} diff --git a/controllers/admin/appearance.go b/controllers/admin/appearance.go deleted file mode 100644 index 4904ec325..000000000 --- a/controllers/admin/appearance.go +++ /dev/null @@ -1,35 +0,0 @@ -package admin - -import ( - "encoding/json" - "net/http" - - "github.com/owncast/owncast/controllers" - "github.com/owncast/owncast/core/data" -) - -// SetCustomColorVariableValues sets the custom color variables. -func SetCustomColorVariableValues(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - type request struct { - Value map[string]string `json:"value"` - } - - decoder := json.NewDecoder(r.Body) - var values request - - if err := decoder.Decode(&values); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update appearance variable values") - return - } - - if err := data.SetCustomColorVariableValues(values.Value); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "custom appearance variables updated") -} diff --git a/controllers/admin/config.go b/controllers/admin/config.go deleted file mode 100644 index 5fd78e7df..000000000 --- a/controllers/admin/config.go +++ /dev/null @@ -1,880 +0,0 @@ -package admin - -import ( - "encoding/json" - "fmt" - "net" - "net/http" - "net/netip" - "os" - "path/filepath" - "reflect" - "strings" - - "github.com/owncast/owncast/activitypub/outbox" - "github.com/owncast/owncast/controllers" - "github.com/owncast/owncast/core/chat" - "github.com/owncast/owncast/core/data" - "github.com/owncast/owncast/core/user" - "github.com/owncast/owncast/core/webhooks" - "github.com/owncast/owncast/models" - "github.com/owncast/owncast/utils" - log "github.com/sirupsen/logrus" - "github.com/teris-io/shortid" -) - -// ConfigValue is a container object that holds a value, is encoded, and saved to the database. -type ConfigValue struct { - Value interface{} `json:"value"` -} - -// SetTags will handle the web config request to set tags. -func SetTags(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValues, success := getValuesFromRequest(w, r) - if !success { - return - } - - tagStrings := make([]string, 0) - for _, tag := range configValues { - tagStrings = append(tagStrings, strings.TrimLeft(tag.Value.(string), "#")) - } - - if err := data.SetServerMetadataTags(tagStrings); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - // Update Fediverse followers about this change. - if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "changed") -} - -// SetStreamTitle will handle the web config request to set the current stream title. -func SetStreamTitle(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - value := configValue.Value.(string) - - if err := data.SetStreamTitle(value); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - if value != "" { - sendSystemChatAction(fmt.Sprintf("Stream title changed to **%s**", value), true) - go webhooks.SendStreamStatusEvent(models.StreamTitleUpdated) - } - controllers.WriteSimpleResponse(w, true, "changed") -} - -// ExternalSetStreamTitle will change the stream title on behalf of an external integration API request. -func ExternalSetStreamTitle(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { - SetStreamTitle(w, r) -} - -func sendSystemChatAction(messageText string, ephemeral bool) { - if err := chat.SendSystemAction(messageText, ephemeral); err != nil { - log.Errorln(err) - } -} - -// SetServerName will handle the web config request to set the server's name. -func SetServerName(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetServerName(configValue.Value.(string)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - // Update Fediverse followers about this change. - if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "changed") -} - -// SetServerSummary will handle the web config request to set the about/summary text. -func SetServerSummary(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetServerSummary(configValue.Value.(string)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - // Update Fediverse followers about this change. - if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "changed") -} - -// SetCustomOfflineMessage will set a message to display when the server is offline. -func SetCustomOfflineMessage(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetCustomOfflineMessage(strings.TrimSpace(configValue.Value.(string))); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "changed") -} - -// SetServerWelcomeMessage will handle the web config request to set the welcome message text. -func SetServerWelcomeMessage(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetServerWelcomeMessage(strings.TrimSpace(configValue.Value.(string))); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "changed") -} - -// SetExtraPageContent will handle the web config request to set the page markdown content. -func SetExtraPageContent(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetExtraPageBodyContent(configValue.Value.(string)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "changed") -} - -// SetAdminPassword will handle the web config request to set the server admin password. -func SetAdminPassword(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetAdminPassword(configValue.Value.(string)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "changed") -} - -// SetLogo will handle a new logo image file being uploaded. -func SetLogo(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - value, ok := configValue.Value.(string) - if !ok { - controllers.WriteSimpleResponse(w, false, "unable to find image data") - return - } - bytes, extension, err := utils.DecodeBase64Image(value) - if err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - imgPath := filepath.Join("data", "logo"+extension) - if err := os.WriteFile(imgPath, bytes, 0o600); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - if err := data.SetLogoPath("logo" + extension); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - if err := data.SetLogoUniquenessString(shortid.MustGenerate()); err != nil { - log.Error("Error saving logo uniqueness string: ", err) - } - - // Update Fediverse followers about this change. - if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "changed") -} - -// SetNSFW will handle the web config request to set the NSFW flag. -func SetNSFW(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetNSFW(configValue.Value.(bool)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "changed") -} - -// SetFfmpegPath will handle the web config request to validate and set an updated copy of ffmpg. -func SetFfmpegPath(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - path := configValue.Value.(string) - if err := utils.VerifyFFMpegPath(path); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - if err := data.SetFfmpegPath(configValue.Value.(string)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "changed") -} - -// SetWebServerPort will handle the web config request to set the server's HTTP port. -func SetWebServerPort(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if port, ok := configValue.Value.(float64); ok { - if (port < 1) || (port > 65535) { - controllers.WriteSimpleResponse(w, false, "Port number must be between 1 and 65535") - return - } - if err := data.SetHTTPPortNumber(port); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "HTTP port set") - return - } - - controllers.WriteSimpleResponse(w, false, "Invalid type or value, port must be a number") -} - -// SetWebServerIP will handle the web config request to set the server's HTTP listen address. -func SetWebServerIP(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if input, ok := configValue.Value.(string); ok { - if ip := net.ParseIP(input); ip != nil { - if err := data.SetHTTPListenAddress(ip.String()); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "HTTP listen address set") - return - } - - controllers.WriteSimpleResponse(w, false, "Invalid IP address") - return - } - controllers.WriteSimpleResponse(w, false, "Invalid type or value, IP address must be a string") -} - -// SetRTMPServerPort will handle the web config request to set the inbound RTMP port. -func SetRTMPServerPort(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetRTMPPortNumber(configValue.Value.(float64)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "rtmp port set") -} - -// SetServerURL will handle the web config request to set the full server URL. -func SetServerURL(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - rawValue, ok := configValue.Value.(string) - if !ok { - controllers.WriteSimpleResponse(w, false, "could not read server url") - return - } - - serverHostString := utils.GetHostnameFromURLString(rawValue) - if serverHostString == "" { - controllers.WriteSimpleResponse(w, false, "server url value invalid") - return - } - - // Block Private IP URLs - ipAddr, ipErr := netip.ParseAddr(utils.GetHostnameWithoutPortFromURLString(rawValue)) - - if ipErr == nil && ipAddr.IsPrivate() { - controllers.WriteSimpleResponse(w, false, "Server URL cannot be private") - return - } - - // Trim any trailing slash - serverURL := strings.TrimRight(rawValue, "/") - - if err := data.SetServerURL(serverURL); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "server url set") -} - -// SetSocketHostOverride will set the host override for the websocket. -func SetSocketHostOverride(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetWebsocketOverrideHost(configValue.Value.(string)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "websocket host override set") -} - -// SetDirectoryEnabled will handle the web config request to enable or disable directory registration. -func SetDirectoryEnabled(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetDirectoryEnabled(configValue.Value.(bool)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - controllers.WriteSimpleResponse(w, true, "directory state changed") -} - -// SetStreamLatencyLevel will handle the web config request to set the stream latency level. -func SetStreamLatencyLevel(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetStreamLatencyLevel(configValue.Value.(float64)); err != nil { - controllers.WriteSimpleResponse(w, false, "error setting stream latency "+err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "set stream latency") -} - -// SetS3Configuration will handle the web config request to set the storage configuration. -func SetS3Configuration(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - type s3ConfigurationRequest struct { - Value models.S3 `json:"value"` - } - - decoder := json.NewDecoder(r.Body) - var newS3Config s3ConfigurationRequest - if err := decoder.Decode(&newS3Config); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update s3 config with provided values") - return - } - - if newS3Config.Value.Enabled { - if newS3Config.Value.Endpoint == "" || !utils.IsValidURL((newS3Config.Value.Endpoint)) { - controllers.WriteSimpleResponse(w, false, "s3 support requires an endpoint") - return - } - - if newS3Config.Value.AccessKey == "" || newS3Config.Value.Secret == "" { - controllers.WriteSimpleResponse(w, false, "s3 support requires an access key and secret") - return - } - - if newS3Config.Value.Region == "" { - controllers.WriteSimpleResponse(w, false, "s3 support requires a region and endpoint") - return - } - - if newS3Config.Value.Bucket == "" { - controllers.WriteSimpleResponse(w, false, "s3 support requires a bucket created for storing public video segments") - return - } - } - - if err := data.SetS3Config(newS3Config.Value); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - controllers.WriteSimpleResponse(w, true, "storage configuration changed") -} - -// SetStreamOutputVariants will handle the web config request to set the video output stream variants. -func SetStreamOutputVariants(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - type streamOutputVariantRequest struct { - Value []models.StreamOutputVariant `json:"value"` - } - - decoder := json.NewDecoder(r.Body) - var videoVariants streamOutputVariantRequest - if err := decoder.Decode(&videoVariants); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update video config with provided values "+err.Error()) - return - } - - if err := data.SetStreamOutputVariants(videoVariants.Value); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update video config with provided values "+err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "stream output variants updated") -} - -// SetSocialHandles will handle the web config request to set the external social profile links. -func SetSocialHandles(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - type socialHandlesRequest struct { - Value []models.SocialHandle `json:"value"` - } - - decoder := json.NewDecoder(r.Body) - var socialHandles socialHandlesRequest - if err := decoder.Decode(&socialHandles); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update social handles with provided values") - return - } - - if err := data.SetSocialHandles(socialHandles.Value); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update social handles with provided values") - return - } - - // Update Fediverse followers about this change. - if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "social handles updated") -} - -// SetChatDisabled will disable chat functionality. -func SetChatDisabled(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - controllers.WriteSimpleResponse(w, false, "unable to update chat disabled") - return - } - - if err := data.SetChatDisabled(configValue.Value.(bool)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "chat disabled status updated") -} - -// SetVideoCodec will change the codec used for video encoding. -func SetVideoCodec(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - controllers.WriteSimpleResponse(w, false, "unable to change video codec") - return - } - - if err := data.SetVideoCodec(configValue.Value.(string)); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update codec") - return - } - - controllers.WriteSimpleResponse(w, true, "video codec updated") -} - -// SetExternalActions will set the 3rd party actions for the web interface. -func SetExternalActions(w http.ResponseWriter, r *http.Request) { - type externalActionsRequest struct { - Value []models.ExternalAction `json:"value"` - } - - decoder := json.NewDecoder(r.Body) - var actions externalActionsRequest - if err := decoder.Decode(&actions); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update external actions with provided values") - return - } - - if err := data.SetExternalActions(actions.Value); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update external actions with provided values") - return - } - - controllers.WriteSimpleResponse(w, true, "external actions update") -} - -// SetCustomStyles will set the CSS string we insert into the page. -func SetCustomStyles(w http.ResponseWriter, r *http.Request) { - customStyles, success := getValueFromRequest(w, r) - if !success { - controllers.WriteSimpleResponse(w, false, "unable to update custom styles") - return - } - - if err := data.SetCustomStyles(customStyles.Value.(string)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "custom styles updated") -} - -// SetCustomJavascript will set the Javascript string we insert into the page. -func SetCustomJavascript(w http.ResponseWriter, r *http.Request) { - customJavascript, success := getValueFromRequest(w, r) - if !success { - controllers.WriteSimpleResponse(w, false, "unable to update custom javascript") - return - } - - if err := data.SetCustomJavascript(customJavascript.Value.(string)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "custom styles updated") -} - -// SetForbiddenUsernameList will set the list of usernames we do not allow to use. -func SetForbiddenUsernameList(w http.ResponseWriter, r *http.Request) { - type forbiddenUsernameListRequest struct { - Value []string `json:"value"` - } - - decoder := json.NewDecoder(r.Body) - var request forbiddenUsernameListRequest - if err := decoder.Decode(&request); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update forbidden usernames with provided values") - return - } - - if err := data.SetForbiddenUsernameList(request.Value); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "forbidden username list updated") -} - -// SetSuggestedUsernameList will set the list of suggested usernames that newly registered users are assigned if it isn't inferred otherwise (i.e. through a proxy). -func SetSuggestedUsernameList(w http.ResponseWriter, r *http.Request) { - type suggestedUsernameListRequest struct { - Value []string `json:"value"` - } - - decoder := json.NewDecoder(r.Body) - var request suggestedUsernameListRequest - - if err := decoder.Decode(&request); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update suggested usernames with provided values") - return - } - - if err := data.SetSuggestedUsernamesList(request.Value); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "suggested username list updated") -} - -// SetChatJoinMessagesEnabled will enable or disable the chat join messages. -func SetChatJoinMessagesEnabled(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - controllers.WriteSimpleResponse(w, false, "unable to update chat join messages enabled") - return - } - - if err := data.SetChatJoinMessagesEnabled(configValue.Value.(bool)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "chat join message status updated") -} - -// SetHideViewerCount will enable or disable hiding the viewer count. -func SetHideViewerCount(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - controllers.WriteSimpleResponse(w, false, "unable to update hiding viewer count") - return - } - - if err := data.SetHideViewerCount(configValue.Value.(bool)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "hide viewer count setting updated") -} - -// SetDisableSearchIndexing will set search indexing support. -func SetDisableSearchIndexing(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - controllers.WriteSimpleResponse(w, false, "unable to update search indexing") - return - } - - if err := data.SetDisableSearchIndexing(configValue.Value.(bool)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "search indexing support updated") -} - -// SetVideoServingEndpoint will save the video serving endpoint. -func SetVideoServingEndpoint(w http.ResponseWriter, r *http.Request) { - endpoint, success := getValueFromRequest(w, r) - if !success { - controllers.WriteSimpleResponse(w, false, "unable to update custom video serving endpoint") - return - } - - value, ok := endpoint.Value.(string) - if !ok { - controllers.WriteSimpleResponse(w, false, "unable to update custom video serving endpoint") - return - } - - if err := data.SetVideoServingEndpoint(value); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "custom video serving endpoint updated") -} - -func requirePOST(w http.ResponseWriter, r *http.Request) bool { - if r.Method != controllers.POST { - controllers.WriteSimpleResponse(w, false, r.Method+" not supported") - return false - } - - return true -} - -func getValueFromRequest(w http.ResponseWriter, r *http.Request) (ConfigValue, bool) { - decoder := json.NewDecoder(r.Body) - var configValue ConfigValue - if err := decoder.Decode(&configValue); err != nil { - log.Warnln(err) - controllers.WriteSimpleResponse(w, false, "unable to parse new value") - return configValue, false - } - - return configValue, true -} - -func getValuesFromRequest(w http.ResponseWriter, r *http.Request) ([]ConfigValue, bool) { - var values []ConfigValue - - decoder := json.NewDecoder(r.Body) - var configValue ConfigValue - if err := decoder.Decode(&configValue); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to parse array of values") - return values, false - } - - object := reflect.ValueOf(configValue.Value) - - for i := 0; i < object.Len(); i++ { - values = append(values, ConfigValue{Value: object.Index(i).Interface()}) - } - - return values, true -} - -// SetStreamKeys will set the valid stream keys. -func SetStreamKeys(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - type streamKeysRequest struct { - Value []models.StreamKey `json:"value"` - } - - decoder := json.NewDecoder(r.Body) - var streamKeys streamKeysRequest - if err := decoder.Decode(&streamKeys); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update stream keys with provided values") - return - } - - if len(streamKeys.Value) == 0 { - controllers.WriteSimpleResponse(w, false, "must provide at least one valid stream key") - return - } - - for _, streamKey := range streamKeys.Value { - if streamKey.Key == "" { - controllers.WriteSimpleResponse(w, false, "stream key cannot be empty") - return - } - } - - if err := data.SetStreamKeys(streamKeys.Value); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "changed") -} diff --git a/controllers/admin/disconnect.go b/controllers/admin/disconnect.go deleted file mode 100644 index d8bf93346..000000000 --- a/controllers/admin/disconnect.go +++ /dev/null @@ -1,21 +0,0 @@ -package admin - -import ( - "net/http" - - "github.com/owncast/owncast/controllers" - "github.com/owncast/owncast/core" - - "github.com/owncast/owncast/core/rtmp" -) - -// DisconnectInboundConnection will force-disconnect an inbound stream. -func DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) { - if !core.GetStatus().Online { - controllers.WriteSimpleResponse(w, false, "no inbound stream connected") - return - } - - rtmp.Disconnect() - controllers.WriteSimpleResponse(w, true, "inbound stream disconnected") -} diff --git a/controllers/admin/federation.go b/controllers/admin/federation.go deleted file mode 100644 index 46844c6e6..000000000 --- a/controllers/admin/federation.go +++ /dev/null @@ -1,179 +0,0 @@ -package admin - -import ( - "net/http" - - "github.com/owncast/owncast/activitypub" - "github.com/owncast/owncast/activitypub/outbox" - "github.com/owncast/owncast/activitypub/persistence" - "github.com/owncast/owncast/controllers" - "github.com/owncast/owncast/core/data" -) - -// SendFederatedMessage will send a manual message to the fediverse. -func SendFederatedMessage(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - message, ok := configValue.Value.(string) - if !ok { - controllers.WriteSimpleResponse(w, false, "unable to send message") - return - } - - if err := activitypub.SendPublicFederatedMessage(message); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "sent") -} - -// SetFederationEnabled will set if Federation features are enabled. -func SetFederationEnabled(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetFederationEnabled(configValue.Value.(bool)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - controllers.WriteSimpleResponse(w, true, "federation features saved") -} - -// SetFederationActivityPrivate will set if Federation features are private to followers. -func SetFederationActivityPrivate(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetFederationIsPrivate(configValue.Value.(bool)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - // Update Fediverse followers about this change. - if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "federation private saved") -} - -// SetFederationShowEngagement will set if Fedivese engagement shows in chat. -func SetFederationShowEngagement(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetFederationShowEngagement(configValue.Value.(bool)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - controllers.WriteSimpleResponse(w, true, "federation show engagement saved") -} - -// SetFederationUsername will set the local actor username used for federation activities. -func SetFederationUsername(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetFederationUsername(configValue.Value.(string)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "username saved") -} - -// SetFederationGoLiveMessage will set the federated message sent when the streamer goes live. -func SetFederationGoLiveMessage(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValue, success := getValueFromRequest(w, r) - if !success { - return - } - - if err := data.SetFederationGoLiveMessage(configValue.Value.(string)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "message saved") -} - -// SetFederationBlockDomains saves a list of domains to block on the Fediverse. -func SetFederationBlockDomains(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - configValues, success := getValuesFromRequest(w, r) - if !success { - controllers.WriteSimpleResponse(w, false, "unable to handle provided domains") - return - } - - domainStrings := make([]string, 0) - for _, domain := range configValues { - domainStrings = append(domainStrings, domain.Value.(string)) - } - - if err := data.SetBlockedFederatedDomains(domainStrings); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - controllers.WriteSimpleResponse(w, true, "saved") -} - -// GetFederatedActions will return the saved list of accepted inbound -// federated activities. -func GetFederatedActions(page int, pageSize int, w http.ResponseWriter, r *http.Request) { - offset := pageSize * page - - activities, total, err := persistence.GetInboundActivities(pageSize, offset) - if err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) - return - } - - response := controllers.PaginatedResponse{ - Total: total, - Results: activities, - } - - controllers.WriteResponse(w, response) -} diff --git a/controllers/admin/notifications.go b/controllers/admin/notifications.go deleted file mode 100644 index 82bdb5f8c..000000000 --- a/controllers/admin/notifications.go +++ /dev/null @@ -1,60 +0,0 @@ -package admin - -import ( - "encoding/json" - "net/http" - - "github.com/owncast/owncast/controllers" - "github.com/owncast/owncast/core/data" - "github.com/owncast/owncast/models" -) - -// SetDiscordNotificationConfiguration will set the discord notification configuration. -func SetDiscordNotificationConfiguration(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - type request struct { - Value models.DiscordConfiguration `json:"value"` - } - - decoder := json.NewDecoder(r.Body) - var config request - if err := decoder.Decode(&config); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update discord config with provided values") - return - } - - if err := data.SetDiscordConfig(config.Value); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update discord config with provided values") - return - } - - controllers.WriteSimpleResponse(w, true, "updated discord config with provided values") -} - -// SetBrowserNotificationConfiguration will set the browser notification configuration. -func SetBrowserNotificationConfiguration(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { - return - } - - type request struct { - Value models.BrowserNotificationConfiguration `json:"value"` - } - - decoder := json.NewDecoder(r.Body) - var config request - if err := decoder.Decode(&config); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update browser push config with provided values") - return - } - - if err := data.SetBrowserPushConfig(config.Value); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to update browser push config with provided values") - return - } - - controllers.WriteSimpleResponse(w, true, "updated browser push config with provided values") -} diff --git a/controllers/constants.go b/controllers/constants.go deleted file mode 100644 index 29b08442a..000000000 --- a/controllers/constants.go +++ /dev/null @@ -1,7 +0,0 @@ -package controllers - -// POST is the HTTP POST method. -const POST = "POST" - -// GET is the HTTP GET method. -const GET = "GET" diff --git a/controllers/hls.go b/controllers/hls.go deleted file mode 100644 index ba329db2c..000000000 --- a/controllers/hls.go +++ /dev/null @@ -1,55 +0,0 @@ -package controllers - -import ( - "net/http" - "path" - "path/filepath" - "strconv" - "strings" - - "github.com/owncast/owncast/config" - "github.com/owncast/owncast/core" - "github.com/owncast/owncast/core/data" - "github.com/owncast/owncast/models" - "github.com/owncast/owncast/utils" - "github.com/owncast/owncast/webserver/middleware" -) - -// HandleHLSRequest will manage all requests to HLS content. -func HandleHLSRequest(w http.ResponseWriter, r *http.Request) { - // Sanity check to limit requests to HLS file types. - if filepath.Ext(r.URL.Path) != ".m3u8" && filepath.Ext(r.URL.Path) != ".ts" { - w.WriteHeader(http.StatusNotFound) - return - } - - requestedPath := r.URL.Path - relativePath := strings.Replace(requestedPath, "/hls/", "", 1) - fullPath := filepath.Join(config.HLSStoragePath, relativePath) - - // If using external storage then only allow requests for the - // master playlist at stream.m3u8, no variants or segments. - if data.GetS3Config().Enabled && relativePath != "stream.m3u8" { - w.WriteHeader(http.StatusNotFound) - return - } - - // Handle playlists - if path.Ext(r.URL.Path) == ".m3u8" { - // Playlists should never be cached. - middleware.DisableCache(w) - - // Force the correct content type - w.Header().Set("Content-Type", "application/x-mpegURL") - - // Use this as an opportunity to mark this viewer as active. - viewer := models.GenerateViewerFromRequest(r) - core.SetViewerActive(&viewer) - } else { - cacheTime := utils.GetCacheDurationSecondsForPath(relativePath) - w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(cacheTime)) - } - - middleware.EnableCors(w) - http.ServeFile(w, r, fullPath) -} diff --git a/webserver/handlers/adminApi.go b/webserver/handlers/adminApi.go deleted file mode 100644 index 5ac8282f4..000000000 --- a/webserver/handlers/adminApi.go +++ /dev/null @@ -1 +0,0 @@ -package handlers diff --git a/webserver/handlers/adminApiAppearanceConfig.go b/webserver/handlers/adminApiAppearanceConfig.go new file mode 100644 index 000000000..25f48ef09 --- /dev/null +++ b/webserver/handlers/adminApiAppearanceConfig.go @@ -0,0 +1,36 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/owncast/owncast/core/data" + "github.com/owncast/owncast/webserver/requests" + "github.com/owncast/owncast/webserver/responses" +) + +// SetCustomColorVariableValues sets the custom color variables. +func (h *Handlers) SetCustomColorVariableValues(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + type request struct { + Value map[string]string `json:"value"` + } + + decoder := json.NewDecoder(r.Body) + var values request + + if err := decoder.Decode(&values); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update appearance variable values") + return + } + + if err := data.SetCustomColorVariableValues(values.Value); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "custom appearance variables updated") +} diff --git a/controllers/admin/chat.go b/webserver/handlers/adminApiChatHandlers.go similarity index 59% rename from controllers/admin/chat.go rename to webserver/handlers/adminApiChatHandlers.go index 6c68b6dd4..08f452bda 100644 --- a/controllers/admin/chat.go +++ b/webserver/handlers/adminApiChatHandlers.go @@ -1,4 +1,4 @@ -package admin +package handlers // this is endpoint logic @@ -9,30 +9,31 @@ import ( "net/http" "strconv" - "github.com/owncast/owncast/controllers" "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/user" "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/webserver/requests" + "github.com/owncast/owncast/webserver/responses" + log "github.com/sirupsen/logrus" ) // ExternalUpdateMessageVisibility updates an array of message IDs to have the same visiblity. -func ExternalUpdateMessageVisibility(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { - UpdateMessageVisibility(w, r) +func (h *Handlers) ExternalUpdateMessageVisibility(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { + h.UpdateMessageVisibility(w, r) } // UpdateMessageVisibility updates an array of message IDs to have the same visiblity. -func UpdateMessageVisibility(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) UpdateMessageVisibility(w http.ResponseWriter, r *http.Request) { type messageVisibilityUpdateRequest struct { IDArray []string `json:"idArray"` Visible bool `json:"visible"` } - if r.Method != controllers.POST { - // nolint:goconst - controllers.WriteSimpleResponse(w, false, r.Method+" not supported") + if r.Method != http.MethodPost { + responses.WriteSimpleResponse(w, false, r.Method+" not supported") return } @@ -41,78 +42,78 @@ func UpdateMessageVisibility(w http.ResponseWriter, r *http.Request) { if err := decoder.Decode(&request); err != nil { log.Errorln(err) - controllers.WriteSimpleResponse(w, false, "") + responses.WriteSimpleResponse(w, false, "") return } if err := chat.SetMessagesVisibility(request.IDArray, request.Visible); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } - controllers.WriteSimpleResponse(w, true, "changed") + responses.WriteSimpleResponse(w, true, "changed") } // BanIPAddress will manually ban an IP address. -func BanIPAddress(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { +func (h *Handlers) BanIPAddress(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { return } - configValue, success := getValueFromRequest(w, r) + configValue, success := requests.GetValueFromRequest(w, r) if !success { - controllers.WriteSimpleResponse(w, false, "unable to ban IP address") + responses.WriteSimpleResponse(w, false, "unable to ban IP address") return } if err := data.BanIPAddress(configValue.Value.(string), "manually added"); err != nil { - controllers.WriteSimpleResponse(w, false, "error saving IP address ban") + responses.WriteSimpleResponse(w, false, "error saving IP address ban") return } - controllers.WriteSimpleResponse(w, true, "IP address banned") + responses.WriteSimpleResponse(w, true, "IP address banned") } // UnBanIPAddress will remove an IP address ban. -func UnBanIPAddress(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { +func (h *Handlers) UnBanIPAddress(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { return } - configValue, success := getValueFromRequest(w, r) + configValue, success := requests.GetValueFromRequest(w, r) if !success { - controllers.WriteSimpleResponse(w, false, "unable to unban IP address") + responses.WriteSimpleResponse(w, false, "unable to unban IP address") return } if err := data.RemoveIPAddressBan(configValue.Value.(string)); err != nil { - controllers.WriteSimpleResponse(w, false, "error removing IP address ban") + responses.WriteSimpleResponse(w, false, "error removing IP address ban") return } - controllers.WriteSimpleResponse(w, true, "IP address unbanned") + responses.WriteSimpleResponse(w, true, "IP address unbanned") } // GetIPAddressBans will return all the banned IP addresses. -func GetIPAddressBans(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetIPAddressBans(w http.ResponseWriter, r *http.Request) { bans, err := data.GetIPAddressBans() if err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } - controllers.WriteResponse(w, bans) + responses.WriteResponse(w, bans) } // UpdateUserEnabled enable or disable a single user by ID. -func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) UpdateUserEnabled(w http.ResponseWriter, r *http.Request) { type blockUserRequest struct { UserID string `json:"userId"` Enabled bool `json:"enabled"` } - if r.Method != controllers.POST { - controllers.WriteSimpleResponse(w, false, r.Method+" not supported") + if r.Method != http.MethodPost { + responses.WriteSimpleResponse(w, false, r.Method+" not supported") return } @@ -121,19 +122,19 @@ func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) { if err := decoder.Decode(&request); err != nil { log.Errorln(err) - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } if request.UserID == "" { - controllers.WriteSimpleResponse(w, false, "must provide userId") + responses.WriteSimpleResponse(w, false, "must provide userId") return } // Disable/enable the user if err := user.SetEnabled(request.UserID, request.Enabled); err != nil { log.Errorln("error changing user enabled status", err) - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -142,7 +143,7 @@ func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) { if !request.Enabled { if err := chat.SetMessageVisibilityForUserID(request.UserID, request.Enabled); err != nil { log.Errorln("error changing user messages visibility", err) - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } } @@ -157,7 +158,7 @@ func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) { if err != nil { log.Errorln("error fetching clients for user: ", err) - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -180,26 +181,26 @@ func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) { } } - controllers.WriteSimpleResponse(w, true, fmt.Sprintf("%s enabled: %t", request.UserID, request.Enabled)) + responses.WriteSimpleResponse(w, true, fmt.Sprintf("%s enabled: %t", request.UserID, request.Enabled)) } // GetDisabledUsers will return all the disabled users. -func GetDisabledUsers(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetDisabledUsers(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") users := user.GetDisabledUsers() - controllers.WriteResponse(w, users) + responses.WriteResponse(w, users) } // UpdateUserModerator will set the moderator status for a user ID. -func UpdateUserModerator(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) UpdateUserModerator(w http.ResponseWriter, r *http.Request) { type request struct { UserID string `json:"userId"` IsModerator bool `json:"isModerator"` } - if r.Method != controllers.POST { - controllers.WriteSimpleResponse(w, false, r.Method+" not supported") + if r.Method != http.MethodPost { + responses.WriteSimpleResponse(w, false, r.Method+" not supported") return } @@ -207,13 +208,13 @@ func UpdateUserModerator(w http.ResponseWriter, r *http.Request) { var req request if err := decoder.Decode(&req); err != nil { - controllers.WriteSimpleResponse(w, false, "") + responses.WriteSimpleResponse(w, false, "") return } // Update the user object with new moderation access. if err := user.SetModerator(req.UserID, req.IsModerator); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -222,87 +223,87 @@ func UpdateUserModerator(w http.ResponseWriter, r *http.Request) { log.Debugln(err) } - controllers.WriteSimpleResponse(w, true, fmt.Sprintf("%s is moderator: %t", req.UserID, req.IsModerator)) + responses.WriteSimpleResponse(w, true, fmt.Sprintf("%s is moderator: %t", req.UserID, req.IsModerator)) } // GetModerators will return a list of moderator users. -func GetModerators(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetModerators(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") users := user.GetModeratorUsers() - controllers.WriteResponse(w, users) + responses.WriteResponse(w, users) } // GetChatMessages returns all of the chat messages, unfiltered. -func GetChatMessages(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetAdminChatMessages(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") messages := chat.GetChatModerationHistory() - controllers.WriteResponse(w, messages) + responses.WriteResponse(w, messages) } // SendSystemMessage will send an official "SYSTEM" message to chat on behalf of your server. -func SendSystemMessage(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { +func (h *Handlers) SendSystemMessage(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") var message events.SystemMessageEvent if err := json.NewDecoder(r.Body).Decode(&message); err != nil { - controllers.InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) return } if err := chat.SendSystemMessage(message.Body, false); err != nil { - controllers.BadRequestHandler(w, err) + responses.BadRequestHandler(w, err) } - controllers.WriteSimpleResponse(w, true, "sent") + responses.WriteSimpleResponse(w, true, "sent") } // SendSystemMessageToConnectedClient will handle incoming requests to send a single message to a single connected client by ID. -func SendSystemMessageToConnectedClient(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { +func (h *Handlers) SendSystemMessageToConnectedClient(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") clientIDText, err := utils.ReadRestURLParameter(r, "clientId") if err != nil { - controllers.BadRequestHandler(w, err) + responses.BadRequestHandler(w, err) return } clientIDNumeric, err := strconv.ParseUint(clientIDText, 10, 32) if err != nil { - controllers.BadRequestHandler(w, err) + responses.BadRequestHandler(w, err) return } var message events.SystemMessageEvent if err := json.NewDecoder(r.Body).Decode(&message); err != nil { - controllers.InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) return } chat.SendSystemMessageToClient(uint(clientIDNumeric), message.Body) - controllers.WriteSimpleResponse(w, true, "sent") + responses.WriteSimpleResponse(w, true, "sent") } // SendUserMessage will send a message to chat on behalf of a user. *Depreciated*. -func SendUserMessage(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { +func (h *Handlers) SendUserMessage(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - controllers.BadRequestHandler(w, errors.New("no longer supported. see /api/integrations/chat/send")) + responses.BadRequestHandler(w, errors.New("no longer supported. see /api/integrations/chat/send")) } // SendIntegrationChatMessage will send a chat message on behalf of an external chat integration. -func SendIntegrationChatMessage(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { +func (h *Handlers) SendIntegrationChatMessage(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") name := integration.DisplayName if name == "" { - controllers.BadRequestHandler(w, errors.New("unknown integration for provided access token")) + responses.BadRequestHandler(w, errors.New("unknown integration for provided access token")) return } var event events.UserMessageEvent if err := json.NewDecoder(r.Body).Decode(&event); err != nil { - controllers.InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) return } event.SetDefaults() @@ -310,7 +311,7 @@ func SendIntegrationChatMessage(integration user.ExternalAPIUser, w http.Respons event.Type = "CHAT" if event.Empty() { - controllers.BadRequestHandler(w, errors.New("invalid message")) + responses.BadRequestHandler(w, errors.New("invalid message")) return } @@ -323,22 +324,22 @@ func SendIntegrationChatMessage(integration user.ExternalAPIUser, w http.Respons } if err := chat.Broadcast(&event); err != nil { - controllers.BadRequestHandler(w, err) + responses.BadRequestHandler(w, err) return } chat.SaveUserMessage(event) - controllers.WriteSimpleResponse(w, true, "sent") + responses.WriteSimpleResponse(w, true, "sent") } // SendChatAction will send a generic chat action. -func SendChatAction(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { +func (h *Handlers) SendChatAction(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") var message events.SystemActionEvent if err := json.NewDecoder(r.Body).Decode(&message); err != nil { - controllers.InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) return } @@ -346,30 +347,30 @@ func SendChatAction(integration user.ExternalAPIUser, w http.ResponseWriter, r * message.RenderBody() if err := chat.SendSystemAction(message.Body, false); err != nil { - controllers.BadRequestHandler(w, err) + responses.BadRequestHandler(w, err) return } - controllers.WriteSimpleResponse(w, true, "sent") + responses.WriteSimpleResponse(w, true, "sent") } // SetEnableEstablishedChatUserMode sets the requirement for a chat user // to be "established" for some time before taking part in chat. -func SetEnableEstablishedChatUserMode(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { +func (h *Handlers) SetEnableEstablishedChatUserMode(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { return } - configValue, success := getValueFromRequest(w, r) + configValue, success := requests.GetValueFromRequest(w, r) if !success { - controllers.WriteSimpleResponse(w, false, "unable to update chat established user only mode") + responses.WriteSimpleResponse(w, false, "unable to update chat established user only mode") return } if err := data.SetChatEstablishedUsersOnlyMode(configValue.Value.(bool)); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } - controllers.WriteSimpleResponse(w, true, "chat established users only mode updated") + responses.WriteSimpleResponse(w, true, "chat established users only mode updated") } diff --git a/webserver/handlers/adminApiConfigHandlers.go b/webserver/handlers/adminApiConfigHandlers.go new file mode 100644 index 000000000..d58f0492d --- /dev/null +++ b/webserver/handlers/adminApiConfigHandlers.go @@ -0,0 +1,814 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/owncast/owncast/activitypub/outbox" + "github.com/owncast/owncast/core/chat" + "github.com/owncast/owncast/core/data" + "github.com/owncast/owncast/core/user" + "github.com/owncast/owncast/core/webhooks" + "github.com/owncast/owncast/models" + "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/webserver/requests" + "github.com/owncast/owncast/webserver/responses" + log "github.com/sirupsen/logrus" + "github.com/teris-io/shortid" +) + +// SetTags will handle the web config request to set tags. +func (h *Handlers) SetTags(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValues, success := requests.GetValuesFromRequest(w, r) + if !success { + return + } + + tagStrings := make([]string, 0) + for _, tag := range configValues { + tagStrings = append(tagStrings, strings.TrimLeft(tag.Value.(string), "#")) + } + + if err := data.SetServerMetadataTags(tagStrings); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + // Update Fediverse followers about this change. + if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "changed") +} + +// SetStreamTitle will handle the web config request to set the current stream title. +func (h *Handlers) SetStreamTitle(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + value := configValue.Value.(string) + + if err := data.SetStreamTitle(value); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + if value != "" { + sendSystemChatAction(fmt.Sprintf("Stream title changed to **%s**", value), true) + go webhooks.SendStreamStatusEvent(models.StreamTitleUpdated) + } + responses.WriteSimpleResponse(w, true, "changed") +} + +// ExternalSetStreamTitle will change the stream title on behalf of an external integration API request. +func (h *Handlers) ExternalSetStreamTitle(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { + h.SetStreamTitle(w, r) +} + +func sendSystemChatAction(messageText string, ephemeral bool) { + if err := chat.SendSystemAction(messageText, ephemeral); err != nil { + log.Errorln(err) + } +} + +// SetServerName will handle the web config request to set the server's name. +func (h *Handlers) SetServerName(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetServerName(configValue.Value.(string)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + // Update Fediverse followers about this change. + if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "changed") +} + +// SetServerSummary will handle the web config request to set the about/summary text. +func (h *Handlers) SetServerSummary(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetServerSummary(configValue.Value.(string)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + // Update Fediverse followers about this change. + if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "changed") +} + +// SetCustomOfflineMessage will set a message to display when the server is offline. +func (h *Handlers) SetCustomOfflineMessage(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetCustomOfflineMessage(strings.TrimSpace(configValue.Value.(string))); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "changed") +} + +// SetServerWelcomeMessage will handle the web config request to set the welcome message text. +func (h *Handlers) SetServerWelcomeMessage(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetServerWelcomeMessage(strings.TrimSpace(configValue.Value.(string))); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "changed") +} + +// SetExtraPageContent will handle the web config request to set the page markdown content. +func (h *Handlers) SetExtraPageContent(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetExtraPageBodyContent(configValue.Value.(string)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "changed") +} + +// SetAdminPassword will handle the web config request to set the server admin password. +func (h *Handlers) SetAdminPassword(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetAdminPassword(configValue.Value.(string)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "changed") +} + +// SetLogo will handle a new logo image file being uploaded. +func (h *Handlers) SetLogo(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + value, ok := configValue.Value.(string) + if !ok { + responses.WriteSimpleResponse(w, false, "unable to find image data") + return + } + bytes, extension, err := utils.DecodeBase64Image(value) + if err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + imgPath := filepath.Join("data", "logo"+extension) + if err := os.WriteFile(imgPath, bytes, 0o600); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + if err := data.SetLogoPath("logo" + extension); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + if err := data.SetLogoUniquenessString(shortid.MustGenerate()); err != nil { + log.Error("Error saving logo uniqueness string: ", err) + } + + // Update Fediverse followers about this change. + if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "changed") +} + +// SetNSFW will handle the web config request to set the NSFW flag. +func (h *Handlers) SetNSFW(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetNSFW(configValue.Value.(bool)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "changed") +} + +// SetFfmpegPath will handle the web config request to validate and set an updated copy of ffmpg. +func (h *Handlers) SetFfmpegPath(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + path := configValue.Value.(string) + if err := utils.VerifyFFMpegPath(path); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + if err := data.SetFfmpegPath(configValue.Value.(string)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "changed") +} + +// SetWebServerPort will handle the web config request to set the server's HTTP port. +func (h *Handlers) SetWebServerPort(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if port, ok := configValue.Value.(float64); ok { + if (port < 1) || (port > 65535) { + responses.WriteSimpleResponse(w, false, "Port number must be between 1 and 65535") + return + } + if err := data.SetHTTPPortNumber(port); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "HTTP port set") + return + } + + responses.WriteSimpleResponse(w, false, "Invalid type or value, port must be a number") +} + +// SetWebServerIP will handle the web config request to set the server's HTTP listen address. +func (h *Handlers) SetWebServerIP(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if input, ok := configValue.Value.(string); ok { + if ip := net.ParseIP(input); ip != nil { + if err := data.SetHTTPListenAddress(ip.String()); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "HTTP listen address set") + return + } + + responses.WriteSimpleResponse(w, false, "Invalid IP address") + return + } + responses.WriteSimpleResponse(w, false, "Invalid type or value, IP address must be a string") +} + +// SetRTMPServerPort will handle the web config request to set the inbound RTMP port. +func (h *Handlers) SetRTMPServerPort(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetRTMPPortNumber(configValue.Value.(float64)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "rtmp port set") +} + +// SetServerURL will handle the web config request to set the full server URL. +func (h *Handlers) SetServerURL(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + rawValue, ok := configValue.Value.(string) + if !ok { + responses.WriteSimpleResponse(w, false, "could not read server url") + return + } + + serverHostString := utils.GetHostnameFromURLString(rawValue) + if serverHostString == "" { + responses.WriteSimpleResponse(w, false, "server url value invalid") + return + } + + // Trim any trailing slash + serverURL := strings.TrimRight(rawValue, "/") + + if err := data.SetServerURL(serverURL); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "server url set") +} + +// SetSocketHostOverride will set the host override for the websocket. +func (h *Handlers) SetSocketHostOverride(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetWebsocketOverrideHost(configValue.Value.(string)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "websocket host override set") +} + +// SetDirectoryEnabled will handle the web config request to enable or disable directory registration. +func (h *Handlers) SetDirectoryEnabled(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetDirectoryEnabled(configValue.Value.(bool)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + responses.WriteSimpleResponse(w, true, "directory state changed") +} + +// SetStreamLatencyLevel will handle the web config request to set the stream latency level. +func (h *Handlers) SetStreamLatencyLevel(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetStreamLatencyLevel(configValue.Value.(float64)); err != nil { + responses.WriteSimpleResponse(w, false, "error setting stream latency "+err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "set stream latency") +} + +// SetS3Configuration will handle the web config request to set the storage configuration. +func (h *Handlers) SetS3Configuration(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + type s3ConfigurationRequest struct { + Value models.S3 `json:"value"` + } + + decoder := json.NewDecoder(r.Body) + var newS3Config s3ConfigurationRequest + if err := decoder.Decode(&newS3Config); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update s3 config with provided values") + return + } + + if newS3Config.Value.Enabled { + if newS3Config.Value.Endpoint == "" || !utils.IsValidURL((newS3Config.Value.Endpoint)) { + responses.WriteSimpleResponse(w, false, "s3 support requires an endpoint") + return + } + + if newS3Config.Value.AccessKey == "" || newS3Config.Value.Secret == "" { + responses.WriteSimpleResponse(w, false, "s3 support requires an access key and secret") + return + } + + if newS3Config.Value.Region == "" { + responses.WriteSimpleResponse(w, false, "s3 support requires a region and endpoint") + return + } + + if newS3Config.Value.Bucket == "" { + responses.WriteSimpleResponse(w, false, "s3 support requires a bucket created for storing public video segments") + return + } + } + + if err := data.SetS3Config(newS3Config.Value); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + responses.WriteSimpleResponse(w, true, "storage configuration changed") +} + +// SetStreamOutputVariants will handle the web config request to set the video output stream variants. +func (h *Handlers) SetStreamOutputVariants(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + type streamOutputVariantRequest struct { + Value []models.StreamOutputVariant `json:"value"` + } + + decoder := json.NewDecoder(r.Body) + var videoVariants streamOutputVariantRequest + if err := decoder.Decode(&videoVariants); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update video config with provided values "+err.Error()) + return + } + + if err := data.SetStreamOutputVariants(videoVariants.Value); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update video config with provided values "+err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "stream output variants updated") +} + +// SetSocialHandles will handle the web config request to set the external social profile links. +func (h *Handlers) SetSocialHandles(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + type socialHandlesRequest struct { + Value []models.SocialHandle `json:"value"` + } + + decoder := json.NewDecoder(r.Body) + var socialHandles socialHandlesRequest + if err := decoder.Decode(&socialHandles); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update social handles with provided values") + return + } + + if err := data.SetSocialHandles(socialHandles.Value); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update social handles with provided values") + return + } + + // Update Fediverse followers about this change. + if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "social handles updated") +} + +// SetChatDisabled will disable chat functionality. +func (h *Handlers) SetChatDisabled(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + responses.WriteSimpleResponse(w, false, "unable to update chat disabled") + return + } + + if err := data.SetChatDisabled(configValue.Value.(bool)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "chat disabled status updated") +} + +// SetVideoCodec will change the codec used for video encoding. +func (h *Handlers) SetVideoCodec(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + responses.WriteSimpleResponse(w, false, "unable to change video codec") + return + } + + if err := data.SetVideoCodec(configValue.Value.(string)); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update codec") + return + } + + responses.WriteSimpleResponse(w, true, "video codec updated") +} + +// SetExternalActions will set the 3rd party actions for the web interface. +func (h *Handlers) SetExternalActions(w http.ResponseWriter, r *http.Request) { + type externalActionsRequest struct { + Value []models.ExternalAction `json:"value"` + } + + decoder := json.NewDecoder(r.Body) + var actions externalActionsRequest + if err := decoder.Decode(&actions); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update external actions with provided values") + return + } + + if err := data.SetExternalActions(actions.Value); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update external actions with provided values") + return + } + + responses.WriteSimpleResponse(w, true, "external actions update") +} + +// SetCustomStyles will set the CSS string we insert into the page. +func (h *Handlers) SetCustomStyles(w http.ResponseWriter, r *http.Request) { + customStyles, success := requests.GetValueFromRequest(w, r) + if !success { + responses.WriteSimpleResponse(w, false, "unable to update custom styles") + return + } + + if err := data.SetCustomStyles(customStyles.Value.(string)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "custom styles updated") +} + +// SetCustomJavascript will set the Javascript string we insert into the page. +func (h *Handlers) SetCustomJavascript(w http.ResponseWriter, r *http.Request) { + customJavascript, success := requests.GetValueFromRequest(w, r) + if !success { + responses.WriteSimpleResponse(w, false, "unable to update custom javascript") + return + } + + if err := data.SetCustomJavascript(customJavascript.Value.(string)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "custom styles updated") +} + +// SetForbiddenUsernameList will set the list of usernames we do not allow to use. +func (h *Handlers) SetForbiddenUsernameList(w http.ResponseWriter, r *http.Request) { + type forbiddenUsernameListRequest struct { + Value []string `json:"value"` + } + + decoder := json.NewDecoder(r.Body) + var request forbiddenUsernameListRequest + if err := decoder.Decode(&request); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update forbidden usernames with provided values") + return + } + + if err := data.SetForbiddenUsernameList(request.Value); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "forbidden username list updated") +} + +// SetSuggestedUsernameList will set the list of suggested usernames that newly registered users are assigned if it isn't inferred otherwise (i.e. through a proxy). +func (h *Handlers) SetSuggestedUsernameList(w http.ResponseWriter, r *http.Request) { + type suggestedUsernameListRequest struct { + Value []string `json:"value"` + } + + decoder := json.NewDecoder(r.Body) + var request suggestedUsernameListRequest + + if err := decoder.Decode(&request); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update suggested usernames with provided values") + return + } + + if err := data.SetSuggestedUsernamesList(request.Value); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "suggested username list updated") +} + +// SetChatJoinMessagesEnabled will enable or disable the chat join messages. +func (h *Handlers) SetChatJoinMessagesEnabled(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + responses.WriteSimpleResponse(w, false, "unable to update chat join messages enabled") + return + } + + if err := data.SetChatJoinMessagesEnabled(configValue.Value.(bool)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "chat join message status updated") +} + +// SetHideViewerCount will enable or disable hiding the viewer count. +func (h *Handlers) SetHideViewerCount(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + responses.WriteSimpleResponse(w, false, "unable to update hiding viewer count") + return + } + + if err := data.SetHideViewerCount(configValue.Value.(bool)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "hide viewer count setting updated") +} + +// SetDisableSearchIndexing will set search indexing support. +func (h *Handlers) SetDisableSearchIndexing(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + responses.WriteSimpleResponse(w, false, "unable to update search indexing") + return + } + + if err := data.SetDisableSearchIndexing(configValue.Value.(bool)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "search indexing support updated") +} + +// SetVideoServingEndpoint will save the video serving endpoint. +func (h *Handlers) SetVideoServingEndpoint(w http.ResponseWriter, r *http.Request) { + endpoint, success := requests.GetValueFromRequest(w, r) + if !success { + responses.WriteSimpleResponse(w, false, "unable to update custom video serving endpoint") + return + } + + value, ok := endpoint.Value.(string) + if !ok { + responses.WriteSimpleResponse(w, false, "unable to update custom video serving endpoint") + return + } + + if err := data.SetVideoServingEndpoint(value); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "custom video serving endpoint updated") +} + +// SetStreamKeys will set the valid stream keys. +func (h *Handlers) SetStreamKeys(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + type streamKeysRequest struct { + Value []models.StreamKey `json:"value"` + } + + decoder := json.NewDecoder(r.Body) + var streamKeys streamKeysRequest + if err := decoder.Decode(&streamKeys); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update stream keys with provided values") + return + } + + if err := data.SetStreamKeys(streamKeys.Value); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "changed") +} diff --git a/controllers/admin/connectedClients.go b/webserver/handlers/adminApiConnectedClients.go similarity index 53% rename from controllers/admin/connectedClients.go rename to webserver/handlers/adminApiConnectedClients.go index 01c2f99fe..f2f47ac83 100644 --- a/controllers/admin/connectedClients.go +++ b/webserver/handlers/adminApiConnectedClients.go @@ -1,25 +1,25 @@ -package admin +package handlers import ( "encoding/json" "net/http" - "github.com/owncast/owncast/controllers" "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/core/user" + "github.com/owncast/owncast/webserver/responses" ) // GetConnectedChatClients returns currently connected clients. -func GetConnectedChatClients(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetConnectedChatClients(w http.ResponseWriter, r *http.Request) { clients := chat.GetClients() w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(clients); err != nil { - controllers.InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) } } // ExternalGetConnectedChatClients returns currently connected clients. -func ExternalGetConnectedChatClients(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { - GetConnectedChatClients(w, r) +func (h *Handlers) ExternalGetConnectedChatClients(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { + h.GetConnectedChatClients(w, r) } diff --git a/controllers/admin/yp.go b/webserver/handlers/adminApiDirectory.go similarity index 55% rename from controllers/admin/yp.go rename to webserver/handlers/adminApiDirectory.go index c6595b86e..e38d2dd12 100644 --- a/controllers/admin/yp.go +++ b/webserver/handlers/adminApiDirectory.go @@ -1,20 +1,20 @@ -package admin +package handlers import ( "net/http" - "github.com/owncast/owncast/controllers" "github.com/owncast/owncast/core/data" + "github.com/owncast/owncast/webserver/responses" log "github.com/sirupsen/logrus" ) // ResetYPRegistration will clear the YP protocol registration key. -func ResetYPRegistration(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) ResetYPRegistration(w http.ResponseWriter, r *http.Request) { log.Traceln("Resetting YP registration key") if err := data.SetDirectoryRegistrationKey(""); err != nil { log.Errorln(err) - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } - controllers.WriteSimpleResponse(w, true, "reset") + responses.WriteSimpleResponse(w, true, "reset") } diff --git a/controllers/admin/emoji.go b/webserver/handlers/adminApiEmojiConfig.go similarity index 54% rename from controllers/admin/emoji.go rename to webserver/handlers/adminApiEmojiConfig.go index 43028e6b7..086a489b0 100644 --- a/controllers/admin/emoji.go +++ b/webserver/handlers/adminApiEmojiConfig.go @@ -1,4 +1,4 @@ -package admin +package handlers import ( "encoding/json" @@ -8,13 +8,14 @@ import ( "path/filepath" "github.com/owncast/owncast/config" - "github.com/owncast/owncast/controllers" "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/webserver/requests" + "github.com/owncast/owncast/webserver/responses" ) // UploadCustomEmoji allows POSTing a new custom emoji to the server. -func UploadCustomEmoji(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { +func (h *Handlers) UploadCustomEmoji(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { return } @@ -26,13 +27,13 @@ func UploadCustomEmoji(w http.ResponseWriter, r *http.Request) { emoji := new(postEmoji) if err := json.NewDecoder(r.Body).Decode(emoji); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } bytes, _, err := utils.DecodeBase64Image(emoji.Data) if err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -42,26 +43,26 @@ func UploadCustomEmoji(w http.ResponseWriter, r *http.Request) { err = os.MkdirAll(config.CustomEmojiPath, 0o700) if err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } if utils.DoesFileExists(targetPath) { - controllers.WriteSimpleResponse(w, false, fmt.Sprintf("An emoji with the name %q already exists", emojiFileName)) + responses.WriteSimpleResponse(w, false, fmt.Sprintf("An emoji with the name %q already exists", emojiFileName)) return } if err = os.WriteFile(targetPath, bytes, 0o600); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } - controllers.WriteSimpleResponse(w, true, fmt.Sprintf("Emoji %q has been uploaded", emojiFileName)) + responses.WriteSimpleResponse(w, true, fmt.Sprintf("Emoji %q has been uploaded", emojiFileName)) } // DeleteCustomEmoji deletes a custom emoji. -func DeleteCustomEmoji(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { +func (h *Handlers) DeleteCustomEmoji(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { return } @@ -72,7 +73,7 @@ func DeleteCustomEmoji(w http.ResponseWriter, r *http.Request) { emoji := new(deleteEmoji) if err := json.NewDecoder(r.Body).Decode(emoji); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -81,12 +82,12 @@ func DeleteCustomEmoji(w http.ResponseWriter, r *http.Request) { if err := os.Remove(targetPath); err != nil { if os.IsNotExist(err) { - controllers.WriteSimpleResponse(w, false, fmt.Sprintf("Emoji %q doesn't exist", emoji.Name)) + responses.WriteSimpleResponse(w, false, fmt.Sprintf("Emoji %q doesn't exist", emoji.Name)) } else { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) } return } - controllers.WriteSimpleResponse(w, true, fmt.Sprintf("Emoji %q has been deleted", emoji.Name)) + responses.WriteSimpleResponse(w, true, fmt.Sprintf("Emoji %q has been deleted", emoji.Name)) } diff --git a/controllers/admin/externalAPIUsers.go b/webserver/handlers/adminApiExternalAPIUsers.go similarity index 64% rename from controllers/admin/externalAPIUsers.go rename to webserver/handlers/adminApiExternalAPIUsers.go index a6bc1a5d7..09e0a26af 100644 --- a/controllers/admin/externalAPIUsers.go +++ b/webserver/handlers/adminApiExternalAPIUsers.go @@ -1,4 +1,4 @@ -package admin +package handlers import ( "encoding/json" @@ -7,9 +7,9 @@ import ( "time" "github.com/owncast/owncast/config" - "github.com/owncast/owncast/controllers" "github.com/owncast/owncast/core/user" "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/webserver/responses" ) type deleteExternalAPIUserRequest struct { @@ -22,35 +22,35 @@ type createExternalAPIUserRequest struct { } // CreateExternalAPIUser will generate a 3rd party access token. -func CreateExternalAPIUser(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) CreateExternalAPIUser(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) var request createExternalAPIUserRequest if err := decoder.Decode(&request); err != nil { - controllers.BadRequestHandler(w, err) + responses.BadRequestHandler(w, err) return } // Verify all the scopes provided are valid if !user.HasValidScopes(request.Scopes) { - controllers.BadRequestHandler(w, errors.New("one or more invalid scopes provided")) + responses.BadRequestHandler(w, errors.New("one or more invalid scopes provided")) return } token, err := utils.GenerateAccessToken() if err != nil { - controllers.InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) return } color := utils.GenerateRandomDisplayColor(config.MaxUserColor) if err := user.InsertExternalAPIUser(token, request.Name, color, request.Scopes); err != nil { - controllers.InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) return } w.Header().Set("Content-Type", "application/json") - controllers.WriteResponse(w, user.ExternalAPIUser{ + responses.WriteResponse(w, user.ExternalAPIUser{ AccessToken: token, DisplayName: request.Name, DisplayColor: color, @@ -61,42 +61,42 @@ func CreateExternalAPIUser(w http.ResponseWriter, r *http.Request) { } // GetExternalAPIUsers will return all 3rd party access tokens. -func GetExternalAPIUsers(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetExternalAPIUsers(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") tokens, err := user.GetExternalAPIUser() if err != nil { - controllers.InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) return } - controllers.WriteResponse(w, tokens) + responses.WriteResponse(w, tokens) } // DeleteExternalAPIUser will return a single 3rd party access token. -func DeleteExternalAPIUser(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) DeleteExternalAPIUser(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - if r.Method != controllers.POST { - controllers.WriteSimpleResponse(w, false, r.Method+" not supported") + if r.Method != http.MethodPost { + responses.WriteSimpleResponse(w, false, r.Method+" not supported") return } decoder := json.NewDecoder(r.Body) var request deleteExternalAPIUserRequest if err := decoder.Decode(&request); err != nil { - controllers.BadRequestHandler(w, err) + responses.BadRequestHandler(w, err) return } if request.Token == "" { - controllers.BadRequestHandler(w, errors.New("must provide a token")) + responses.BadRequestHandler(w, errors.New("must provide a token")) return } if err := user.DeleteExternalAPIUser(request.Token); err != nil { - controllers.InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) return } - controllers.WriteSimpleResponse(w, true, "deleted token") + responses.WriteSimpleResponse(w, true, "deleted token") } diff --git a/webserver/handlers/adminApiFederationConfig.go b/webserver/handlers/adminApiFederationConfig.go new file mode 100644 index 000000000..e8368f848 --- /dev/null +++ b/webserver/handlers/adminApiFederationConfig.go @@ -0,0 +1,180 @@ +package handlers + +import ( + "net/http" + + "github.com/owncast/owncast/activitypub" + "github.com/owncast/owncast/activitypub/outbox" + "github.com/owncast/owncast/activitypub/persistence" + "github.com/owncast/owncast/core/data" + "github.com/owncast/owncast/webserver/requests" + "github.com/owncast/owncast/webserver/responses" +) + +// SendFederatedMessage will send a manual message to the fediverse. +func (h *Handlers) SendFederatedMessage(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + message, ok := configValue.Value.(string) + if !ok { + responses.WriteSimpleResponse(w, false, "unable to send message") + return + } + + if err := activitypub.SendPublicFederatedMessage(message); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "sent") +} + +// SetFederationEnabled will set if Federation features are enabled. +func (h *Handlers) SetFederationEnabled(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetFederationEnabled(configValue.Value.(bool)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + responses.WriteSimpleResponse(w, true, "federation features saved") +} + +// SetFederationActivityPrivate will set if Federation features are private to followers. +func (h *Handlers) SetFederationActivityPrivate(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetFederationIsPrivate(configValue.Value.(bool)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + // Update Fediverse followers about this change. + if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "federation private saved") +} + +// SetFederationShowEngagement will set if Fedivese engagement shows in chat. +func (h *Handlers) SetFederationShowEngagement(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetFederationShowEngagement(configValue.Value.(bool)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + responses.WriteSimpleResponse(w, true, "federation show engagement saved") +} + +// SetFederationUsername will set the local actor username used for federation activities. +func (h *Handlers) SetFederationUsername(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetFederationUsername(configValue.Value.(string)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "username saved") +} + +// SetFederationGoLiveMessage will set the federated message sent when the streamer goes live. +func (h *Handlers) SetFederationGoLiveMessage(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValue, success := requests.GetValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetFederationGoLiveMessage(configValue.Value.(string)); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "message saved") +} + +// SetFederationBlockDomains saves a list of domains to block on the Fediverse. +func (h *Handlers) SetFederationBlockDomains(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + configValues, success := requests.GetValuesFromRequest(w, r) + if !success { + responses.WriteSimpleResponse(w, false, "unable to handle provided domains") + return + } + + domainStrings := make([]string, 0) + for _, domain := range configValues { + domainStrings = append(domainStrings, domain.Value.(string)) + } + + if err := data.SetBlockedFederatedDomains(domainStrings); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + responses.WriteSimpleResponse(w, true, "saved") +} + +// GetFederatedActions will return the saved list of accepted inbound +// federated activities. +func (h *Handlers) GetFederatedActions(page int, pageSize int, w http.ResponseWriter, r *http.Request) { + offset := pageSize * page + + activities, total, err := persistence.GetInboundActivities(pageSize, offset) + if err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) + return + } + + response := responses.PaginatedResponse{ + Total: total, + Results: activities, + } + + responses.WriteResponse(w, response) +} diff --git a/controllers/admin/followers.go b/webserver/handlers/adminApiFollowers.go similarity index 53% rename from controllers/admin/followers.go rename to webserver/handlers/adminApiFollowers.go index dd144791c..2c67fc8bb 100644 --- a/controllers/admin/followers.go +++ b/webserver/handlers/adminApiFollowers.go @@ -1,18 +1,19 @@ -package admin +package handlers import ( "encoding/json" "net/http" "github.com/owncast/owncast/activitypub/persistence" - "github.com/owncast/owncast/activitypub/requests" - "github.com/owncast/owncast/controllers" + aprequests "github.com/owncast/owncast/activitypub/requests" "github.com/owncast/owncast/core/data" + "github.com/owncast/owncast/webserver/requests" + "github.com/owncast/owncast/webserver/responses" ) // ApproveFollower will approve a federated follow request. -func ApproveFollower(w http.ResponseWriter, r *http.Request) { - if !requirePOST(w, r) { +func (h *Handlers) ApproveFollower(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { return } @@ -24,14 +25,14 @@ func ApproveFollower(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) var approval approveFollowerRequest if err := decoder.Decode(&approval); err != nil { - controllers.WriteSimpleResponse(w, false, "unable to handle follower state with provided values") + responses.WriteSimpleResponse(w, false, "unable to handle follower state with provided values") return } if approval.Approved { // Approve a follower if err := persistence.ApprovePreviousFollowRequest(approval.ActorIRI); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -39,44 +40,44 @@ func ApproveFollower(w http.ResponseWriter, r *http.Request) { followRequest, err := persistence.GetFollower(approval.ActorIRI) if err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } // Send the approval to the follow requestor. - if err := requests.SendFollowAccept(followRequest.Inbox, followRequest.RequestObject, localAccountName); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + if err := aprequests.SendFollowAccept(followRequest.Inbox, followRequest.RequestObject, localAccountName); err != nil { + responses.WriteSimpleResponse(w, false, err.Error()) return } } else { // Remove/block a follower if err := persistence.BlockOrRejectFollower(approval.ActorIRI); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } } - controllers.WriteSimpleResponse(w, true, "follower updated") + responses.WriteSimpleResponse(w, true, "follower updated") } // GetPendingFollowRequests will return a list of pending follow requests. -func GetPendingFollowRequests(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetPendingFollowRequests(w http.ResponseWriter, r *http.Request) { requests, err := persistence.GetPendingFollowRequests() if err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } - controllers.WriteResponse(w, requests) + responses.WriteResponse(w, requests) } // GetBlockedAndRejectedFollowers will return blocked and rejected followers. -func GetBlockedAndRejectedFollowers(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetBlockedAndRejectedFollowers(w http.ResponseWriter, r *http.Request) { rejections, err := persistence.GetBlockedAndRejectedFollowers() if err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } - controllers.WriteResponse(w, rejections) + responses.WriteResponse(w, rejections) } diff --git a/controllers/admin/hardware.go b/webserver/handlers/adminApiHardwareStats.go similarity index 78% rename from controllers/admin/hardware.go rename to webserver/handlers/adminApiHardwareStats.go index fc036bc3f..441220d81 100644 --- a/controllers/admin/hardware.go +++ b/webserver/handlers/adminApiHardwareStats.go @@ -1,4 +1,4 @@ -package admin +package handlers import ( "encoding/json" @@ -9,7 +9,7 @@ import ( ) // GetHardwareStats will return hardware utilization over time. -func GetHardwareStats(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetHardwareStats(w http.ResponseWriter, r *http.Request) { m := metrics.GetMetrics() w.Header().Set("Content-Type", "application/json") diff --git a/controllers/admin/logs.go b/webserver/handlers/adminApiLogs.go similarity index 88% rename from controllers/admin/logs.go rename to webserver/handlers/adminApiLogs.go index 266d75c7e..abc6889d3 100644 --- a/controllers/admin/logs.go +++ b/webserver/handlers/adminApiLogs.go @@ -1,4 +1,4 @@ -package admin +package handlers import ( "encoding/json" @@ -11,7 +11,7 @@ import ( ) // GetLogs will return all logs. -func GetLogs(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetLogs(w http.ResponseWriter, r *http.Request) { logs := logging.Logger.AllEntries() response := make([]logsResponse, 0) @@ -26,7 +26,7 @@ func GetLogs(w http.ResponseWriter, r *http.Request) { } // GetWarnings will return only warning and error logs. -func GetWarnings(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetWarnings(w http.ResponseWriter, r *http.Request) { logs := logging.Logger.WarningEntries() response := make([]logsResponse, 0) diff --git a/webserver/handlers/adminApiNotificationsConfig.go b/webserver/handlers/adminApiNotificationsConfig.go new file mode 100644 index 000000000..5dbf6a149 --- /dev/null +++ b/webserver/handlers/adminApiNotificationsConfig.go @@ -0,0 +1,61 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/owncast/owncast/core/data" + "github.com/owncast/owncast/models" + "github.com/owncast/owncast/webserver/requests" + "github.com/owncast/owncast/webserver/responses" +) + +// SetDiscordNotificationConfiguration will set the discord notification configuration. +func (h *Handlers) SetDiscordNotificationConfiguration(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + type request struct { + Value models.DiscordConfiguration `json:"value"` + } + + decoder := json.NewDecoder(r.Body) + var config request + if err := decoder.Decode(&config); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update discord config with provided values") + return + } + + if err := data.SetDiscordConfig(config.Value); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update discord config with provided values") + return + } + + responses.WriteSimpleResponse(w, true, "updated discord config with provided values") +} + +// SetBrowserNotificationConfiguration will set the browser notification configuration. +func (h *Handlers) SetBrowserNotificationConfiguration(w http.ResponseWriter, r *http.Request) { + if !requests.RequirePOST(w, r) { + return + } + + type request struct { + Value models.BrowserNotificationConfiguration `json:"value"` + } + + decoder := json.NewDecoder(r.Body) + var config request + if err := decoder.Decode(&config); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update browser push config with provided values") + return + } + + if err := data.SetBrowserPushConfig(config.Value); err != nil { + responses.WriteSimpleResponse(w, false, "unable to update browser push config with provided values") + return + } + + responses.WriteSimpleResponse(w, true, "updated browser push config with provided values") +} diff --git a/controllers/admin/serverConfig.go b/webserver/handlers/adminApiServerConfig.go similarity index 69% rename from controllers/admin/serverConfig.go rename to webserver/handlers/adminApiServerConfig.go index 5c95f7e7d..2dbbbe3dd 100644 --- a/controllers/admin/serverConfig.go +++ b/webserver/handlers/adminApiServerConfig.go @@ -1,4 +1,4 @@ -package admin +package handlers import ( "encoding/json" @@ -14,7 +14,7 @@ import ( ) // GetServerConfig gets the config details of the server. -func GetServerConfig(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetServerConfig(w http.ResponseWriter, r *http.Request) { ffmpeg := utils.ValidatedFfmpegPath(data.GetFfMpegPath()) usernameBlocklist := data.GetForbiddenUsernameList() usernameSuggestions := data.GetSuggestedUsernamesList() @@ -34,7 +34,7 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { }) } response := serverConfigAdminResponse{ - InstanceDetails: webConfigResponse{ + InstanceDetails: adminWebConfigResponse{ Name: data.GetServerName(), Summary: data.GetServerSummary(), Tags: data.GetServerMetadataTags(), @@ -77,7 +77,7 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { VideoCodec: data.GetVideoCodec(), ForbiddenUsernames: usernameBlocklist, SuggestedUsernames: usernameSuggestions, - Federation: federationConfigResponse{ + Federation: adminFederationConfigResponse{ Enabled: data.GetFederationEnabled(), IsPrivate: data.GetFederationIsPrivate(), Username: data.GetFederationUsername(), @@ -85,7 +85,7 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { ShowEngagement: data.GetFederationShowEngagement(), BlockedDomains: data.GetBlockedFederatedDomains(), }, - Notifications: notificationsConfigResponse{ + Notifications: adminNotificationsConfigResponse{ Discord: data.GetDiscordConfig(), Browser: data.GetBrowserPushConfig(), }, @@ -100,31 +100,31 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { } type serverConfigAdminResponse struct { - InstanceDetails webConfigResponse `json:"instanceDetails"` - Notifications notificationsConfigResponse `json:"notifications"` - YP yp `json:"yp"` - FFmpegPath string `json:"ffmpegPath"` - AdminPassword string `json:"adminPassword"` - SocketHostOverride string `json:"socketHostOverride,omitempty"` - WebServerIP string `json:"webServerIP"` - VideoCodec string `json:"videoCodec"` - VideoServingEndpoint string `json:"videoServingEndpoint"` - S3 models.S3 `json:"s3"` - Federation federationConfigResponse `json:"federation"` - SupportedCodecs []string `json:"supportedCodecs"` - ExternalActions []models.ExternalAction `json:"externalActions"` - ForbiddenUsernames []string `json:"forbiddenUsernames"` - SuggestedUsernames []string `json:"suggestedUsernames"` - StreamKeys []models.StreamKey `json:"streamKeys"` - VideoSettings videoSettings `json:"videoSettings"` - RTMPServerPort int `json:"rtmpServerPort"` - WebServerPort int `json:"webServerPort"` - ChatDisabled bool `json:"chatDisabled"` - ChatJoinMessagesEnabled bool `json:"chatJoinMessagesEnabled"` - ChatEstablishedUserMode bool `json:"chatEstablishedUserMode"` - DisableSearchIndexing bool `json:"disableSearchIndexing"` - StreamKeyOverridden bool `json:"streamKeyOverridden"` - HideViewerCount bool `json:"hideViewerCount"` + InstanceDetails adminWebConfigResponse `json:"instanceDetails"` + Notifications adminNotificationsConfigResponse `json:"notifications"` + YP yp `json:"yp"` + FFmpegPath string `json:"ffmpegPath"` + AdminPassword string `json:"adminPassword"` + SocketHostOverride string `json:"socketHostOverride,omitempty"` + WebServerIP string `json:"webServerIP"` + VideoCodec string `json:"videoCodec"` + VideoServingEndpoint string `json:"videoServingEndpoint"` + S3 models.S3 `json:"s3"` + Federation adminFederationConfigResponse `json:"federation"` + SupportedCodecs []string `json:"supportedCodecs"` + ExternalActions []models.ExternalAction `json:"externalActions"` + ForbiddenUsernames []string `json:"forbiddenUsernames"` + SuggestedUsernames []string `json:"suggestedUsernames"` + StreamKeys []models.StreamKey `json:"streamKeys"` + VideoSettings videoSettings `json:"videoSettings"` + RTMPServerPort int `json:"rtmpServerPort"` + WebServerPort int `json:"webServerPort"` + ChatDisabled bool `json:"chatDisabled"` + ChatJoinMessagesEnabled bool `json:"chatJoinMessagesEnabled"` + ChatEstablishedUserMode bool `json:"chatEstablishedUserMode"` + DisableSearchIndexing bool `json:"disableSearchIndexing"` + StreamKeyOverridden bool `json:"streamKeyOverridden"` + HideViewerCount bool `json:"hideViewerCount"` } type videoSettings struct { @@ -132,7 +132,7 @@ type videoSettings struct { LatencyLevel int `json:"latencyLevel"` } -type webConfigResponse struct { +type adminWebConfigResponse struct { AppearanceVariables map[string]string `json:"appearanceVariables"` Version string `json:"version"` WelcomeMessage string `json:"welcomeMessage"` @@ -155,7 +155,7 @@ type yp struct { Enabled bool `json:"enabled"` } -type federationConfigResponse struct { +type adminFederationConfigResponse struct { Username string `json:"username"` GoLiveMessage string `json:"goLiveMessage"` BlockedDomains []string `json:"blockedDomains"` @@ -164,7 +164,7 @@ type federationConfigResponse struct { ShowEngagement bool `json:"showEngagement"` } -type notificationsConfigResponse struct { +type adminNotificationsConfigResponse struct { Browser models.BrowserNotificationConfiguration `json:"browser"` Discord models.DiscordConfiguration `json:"discord"` } diff --git a/controllers/admin/update.go b/webserver/handlers/adminApiSoftwareUpdate.go similarity index 90% rename from controllers/admin/update.go rename to webserver/handlers/adminApiSoftwareUpdate.go index 8f93fa37b..217887f53 100644 --- a/controllers/admin/update.go +++ b/webserver/handlers/adminApiSoftwareUpdate.go @@ -1,4 +1,4 @@ -package admin +package handlers import ( "fmt" @@ -10,7 +10,7 @@ import ( "strings" "github.com/owncast/owncast/config" - "github.com/owncast/owncast/controllers" + "github.com/owncast/owncast/webserver/responses" log "github.com/sirupsen/logrus" ) @@ -33,7 +33,7 @@ specific conditions are met. */ // AutoUpdateOptions will return what auto update options are available. -func AutoUpdateOptions(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) AutoUpdateOptions(w http.ResponseWriter, r *http.Request) { type autoUpdateOptionsResponse struct { SupportsUpdate bool `json:"supportsUpdate"` CanRestart bool `json:"canRestart"` @@ -47,7 +47,7 @@ func AutoUpdateOptions(w http.ResponseWriter, r *http.Request) { // Nothing is supported when running under "dev" or the feature is // explicitly disabled. if config.BuildPlatform == "dev" || !config.EnableAutoUpdate { - controllers.WriteResponse(w, updateOptions) + responses.WriteResponse(w, updateOptions) return } @@ -58,11 +58,11 @@ func AutoUpdateOptions(w http.ResponseWriter, r *http.Request) { updateOptions.CanRestart = isRunningUnderSystemD() - controllers.WriteResponse(w, updateOptions) + responses.WriteResponse(w, updateOptions) } // AutoUpdateStart will begin the auto update process. -func AutoUpdateStart(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) AutoUpdateStart(w http.ResponseWriter, r *http.Request) { // We return the console output directly to the client. w.Header().Set("Content-Type", "text/plain") @@ -70,7 +70,7 @@ func AutoUpdateStart(w http.ResponseWriter, r *http.Request) { updater, err := downloadInstaller() if err != nil { log.Errorln(err) - controllers.WriteSimpleResponse(w, false, "failed to download and run installer") + responses.WriteSimpleResponse(w, false, "failed to download and run installer") return } @@ -95,12 +95,12 @@ func AutoUpdateStart(w http.ResponseWriter, r *http.Request) { } // AutoUpdateForceQuit will force quit the service. -func AutoUpdateForceQuit(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) AutoUpdateForceQuit(w http.ResponseWriter, r *http.Request) { log.Warnln("Owncast is exiting due to request.") go func() { os.Exit(0) }() - controllers.WriteSimpleResponse(w, true, "forcing quit") + responses.WriteSimpleResponse(w, true, "forcing quit") } func downloadInstaller() (string, error) { diff --git a/controllers/admin/status.go b/webserver/handlers/adminApiSystemStatus.go similarity index 95% rename from controllers/admin/status.go rename to webserver/handlers/adminApiSystemStatus.go index 6f30edf96..f363525bc 100644 --- a/controllers/admin/status.go +++ b/webserver/handlers/adminApiSystemStatus.go @@ -1,4 +1,4 @@ -package admin +package handlers import ( "encoding/json" @@ -13,7 +13,7 @@ import ( ) // Status gets the details of the inbound broadcaster. -func Status(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetAdminStatus(w http.ResponseWriter, r *http.Request) { broadcaster := core.GetBroadcaster() status := core.GetStatus() currentBroadcast := core.GetCurrentBroadcast() diff --git a/controllers/admin/video.go b/webserver/handlers/adminApiVideoConfig.go similarity index 97% rename from controllers/admin/video.go rename to webserver/handlers/adminApiVideoConfig.go index 65f7a69e1..45374cc5e 100644 --- a/controllers/admin/video.go +++ b/webserver/handlers/adminApiVideoConfig.go @@ -1,4 +1,4 @@ -package admin +package handlers import ( "encoding/json" @@ -11,7 +11,7 @@ import ( ) // GetVideoPlaybackMetrics returns video playback metrics. -func GetVideoPlaybackMetrics(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetVideoPlaybackMetrics(w http.ResponseWriter, r *http.Request) { type response struct { Errors []metrics.TimestampedValue `json:"errors"` QualityVariantChanges []metrics.TimestampedValue `json:"qualityVariantChanges"` diff --git a/controllers/admin/viewers.go b/webserver/handlers/adminApiViewers.go similarity index 70% rename from controllers/admin/viewers.go rename to webserver/handlers/adminApiViewers.go index ed542a31a..3f0ea0459 100644 --- a/controllers/admin/viewers.go +++ b/webserver/handlers/adminApiViewers.go @@ -1,4 +1,4 @@ -package admin +package handlers import ( "encoding/json" @@ -6,20 +6,20 @@ import ( "strconv" "time" - "github.com/owncast/owncast/controllers" "github.com/owncast/owncast/core" "github.com/owncast/owncast/core/user" "github.com/owncast/owncast/metrics" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/webserver/responses" log "github.com/sirupsen/logrus" ) // GetViewersOverTime will return the number of viewers at points in time. -func GetViewersOverTime(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetViewersOverTime(w http.ResponseWriter, r *http.Request) { windowStartAtStr := r.URL.Query().Get("windowStart") windowStartAtUnix, err := strconv.Atoi(windowStartAtStr) if err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -35,7 +35,7 @@ func GetViewersOverTime(w http.ResponseWriter, r *http.Request) { } // GetActiveViewers returns currently connected clients. -func GetActiveViewers(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetActiveViewers(w http.ResponseWriter, r *http.Request) { c := core.GetActiveViewers() viewers := make([]models.Viewer, 0, len(c)) for _, v := range c { @@ -45,11 +45,11 @@ func GetActiveViewers(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(viewers); err != nil { - controllers.InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) } } // ExternalGetActiveViewers returns currently connected clients. -func ExternalGetActiveViewers(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { - GetConnectedChatClients(w, r) +func (h *Handlers) ExternalGetActiveViewers(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { + h.GetConnectedChatClients(w, r) } diff --git a/controllers/admin/webhooks.go b/webserver/handlers/adminApiWebhooks.go similarity index 59% rename from controllers/admin/webhooks.go rename to webserver/handlers/adminApiWebhooks.go index 9407b1996..e50644fb1 100644 --- a/controllers/admin/webhooks.go +++ b/webserver/handlers/adminApiWebhooks.go @@ -1,4 +1,4 @@ -package admin +package handlers import ( "encoding/json" @@ -6,9 +6,9 @@ import ( "net/http" "time" - "github.com/owncast/owncast/controllers" "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/webserver/responses" ) type deleteWebhookRequest struct { @@ -21,27 +21,27 @@ type createWebhookRequest struct { } // CreateWebhook will add a single webhook. -func CreateWebhook(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) CreateWebhook(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) var request createWebhookRequest if err := decoder.Decode(&request); err != nil { - controllers.BadRequestHandler(w, err) + responses.BadRequestHandler(w, err) return } // Verify all the scopes provided are valid if !models.HasValidEvents(request.Events) { - controllers.BadRequestHandler(w, errors.New("one or more invalid event provided")) + responses.BadRequestHandler(w, errors.New("one or more invalid event provided")) return } newWebhookID, err := data.InsertWebhook(request.URL, request.Events) if err != nil { - controllers.InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) return } - controllers.WriteResponse(w, models.Webhook{ + responses.WriteResponse(w, models.Webhook{ ID: newWebhookID, URL: request.URL, Events: request.Events, @@ -51,34 +51,34 @@ func CreateWebhook(w http.ResponseWriter, r *http.Request) { } // GetWebhooks will return all webhooks. -func GetWebhooks(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetWebhooks(w http.ResponseWriter, r *http.Request) { webhooks, err := data.GetWebhooks() if err != nil { - controllers.InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) return } - controllers.WriteResponse(w, webhooks) + responses.WriteResponse(w, webhooks) } // DeleteWebhook will delete a single webhook. -func DeleteWebhook(w http.ResponseWriter, r *http.Request) { - if r.Method != controllers.POST { - controllers.WriteSimpleResponse(w, false, r.Method+" not supported") +func (h *Handlers) DeleteWebhook(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + responses.WriteSimpleResponse(w, false, r.Method+" not supported") return } decoder := json.NewDecoder(r.Body) var request deleteWebhookRequest if err := decoder.Decode(&request); err != nil { - controllers.BadRequestHandler(w, err) + responses.BadRequestHandler(w, err) return } if err := data.DeleteWebhook(request.ID); err != nil { - controllers.InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) return } - controllers.WriteSimpleResponse(w, true, "deleted webhook") + responses.WriteSimpleResponse(w, true, "deleted webhook") } diff --git a/controllers/auth/fediverse/fediverse.go b/webserver/handlers/auth/fediverse/fediverse.go similarity index 79% rename from controllers/auth/fediverse/fediverse.go rename to webserver/handlers/auth/fediverse/fediverse.go index 6f7002bff..2e2df52dd 100644 --- a/controllers/auth/fediverse/fediverse.go +++ b/webserver/handlers/auth/fediverse/fediverse.go @@ -8,10 +8,10 @@ import ( "github.com/owncast/owncast/activitypub" "github.com/owncast/owncast/auth" fediverseauth "github.com/owncast/owncast/auth/fediverse" - "github.com/owncast/owncast/controllers" "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/user" + "github.com/owncast/owncast/webserver/responses" log "github.com/sirupsen/logrus" ) @@ -23,29 +23,29 @@ func RegisterFediverseOTPRequest(u user.User, w http.ResponseWriter, r *http.Req var req request decoder := json.NewDecoder(r.Body) if err := decoder.Decode(&req); err != nil { - controllers.WriteSimpleResponse(w, false, "Could not decode request: "+err.Error()) + responses.WriteSimpleResponse(w, false, "Could not decode request: "+err.Error()) return } accessToken := r.URL.Query().Get("accessToken") reg, success, err := fediverseauth.RegisterFediverseOTP(accessToken, u.ID, u.DisplayName, req.FediverseAccount) if err != nil { - controllers.WriteSimpleResponse(w, false, "Could not register auth request: "+err.Error()) + responses.WriteSimpleResponse(w, false, "Could not register auth request: "+err.Error()) return } if !success { - controllers.WriteSimpleResponse(w, false, "Could not register auth request. One may already be pending. Try again later.") + responses.WriteSimpleResponse(w, false, "Could not register auth request. One may already be pending. Try again later.") return } msg := fmt.Sprintf("

This is an automated message from %s. If you did not request this message please ignore or block. Your requested one-time code is:

%s

", data.GetServerName(), reg.Code) if err := activitypub.SendDirectFederatedMessage(msg, reg.Account); err != nil { - controllers.WriteSimpleResponse(w, false, "Could not send code to fediverse: "+err.Error()) + responses.WriteSimpleResponse(w, false, "Could not send code to fediverse: "+err.Error()) return } - controllers.WriteSimpleResponse(w, true, "") + responses.WriteSimpleResponse(w, true, "") } // VerifyFediverseOTPRequest verifies the given OTP code for the given access token. @@ -57,7 +57,7 @@ func VerifyFediverseOTPRequest(w http.ResponseWriter, r *http.Request) { var req request decoder := json.NewDecoder(r.Body) if err := decoder.Decode(&req); err != nil { - controllers.WriteSimpleResponse(w, false, "Could not decode request: "+err.Error()) + responses.WriteSimpleResponse(w, false, "Could not decode request: "+err.Error()) return } accessToken := r.URL.Query().Get("accessToken") @@ -75,7 +75,7 @@ func VerifyFediverseOTPRequest(w http.ResponseWriter, r *http.Request) { // Update the current user's access token to point to the existing user id. userID := u.ID if err := user.SetAccessTokenToOwner(accessToken, userID); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -86,7 +86,7 @@ func VerifyFediverseOTPRequest(w http.ResponseWriter, r *http.Request) { } } - controllers.WriteSimpleResponse(w, true, "") + responses.WriteSimpleResponse(w, true, "") return } @@ -94,7 +94,7 @@ func VerifyFediverseOTPRequest(w http.ResponseWriter, r *http.Request) { // Otherwise, save this as new auth. log.Debug("fediverse account does not already exist, saving it as a new one for the current user") if err := auth.AddAuth(authRegistration.UserID, authRegistration.Account, auth.Fediverse); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -104,5 +104,5 @@ func VerifyFediverseOTPRequest(w http.ResponseWriter, r *http.Request) { log.Errorln(err) } - controllers.WriteSimpleResponse(w, true, "") + responses.WriteSimpleResponse(w, true, "") } diff --git a/controllers/auth/indieauth/client.go b/webserver/handlers/auth/indieauth/client.go similarity index 85% rename from controllers/auth/indieauth/client.go rename to webserver/handlers/auth/indieauth/client.go index 8e7960c76..5f3ac8b4b 100644 --- a/controllers/auth/indieauth/client.go +++ b/webserver/handlers/auth/indieauth/client.go @@ -8,9 +8,9 @@ import ( "github.com/owncast/owncast/auth" ia "github.com/owncast/owncast/auth/indieauth" - "github.com/owncast/owncast/controllers" "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/core/user" + "github.com/owncast/owncast/webserver/responses" log "github.com/sirupsen/logrus" ) @@ -27,12 +27,12 @@ func StartAuthFlow(u user.User, w http.ResponseWriter, r *http.Request) { var authRequest request p, err := io.ReadAll(r.Body) if err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } if err := json.Unmarshal(p, &authRequest); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -40,14 +40,14 @@ func StartAuthFlow(u user.User, w http.ResponseWriter, r *http.Request) { redirectURL, err := ia.StartAuthFlow(authRequest.AuthHost, u.ID, accessToken, u.DisplayName) if err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } redirectResponse := response{ Redirect: redirectURL.String(), } - controllers.WriteResponse(w, redirectResponse) + responses.WriteResponse(w, redirectResponse) } // HandleRedirect will handle the redirect from an IndieAuth server to @@ -59,7 +59,7 @@ func HandleRedirect(w http.ResponseWriter, r *http.Request) { if err != nil { log.Debugln(err) msg := `Unable to complete authentication. Go back.
` - _ = controllers.WriteString(w, msg, http.StatusBadRequest) + _ = responses.WriteString(w, msg, http.StatusBadRequest) return } @@ -72,7 +72,7 @@ func HandleRedirect(w http.ResponseWriter, r *http.Request) { accessToken := request.CurrentAccessToken userID := u.ID if err := user.SetAccessTokenToOwner(accessToken, userID); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -91,7 +91,7 @@ func HandleRedirect(w http.ResponseWriter, r *http.Request) { // Otherwise, save this as new auth. log.Debug("indieauth token does not already exist, saving it as a new one for the current user") if err := auth.AddAuth(request.UserID, response.Me, auth.IndieAuth); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } diff --git a/controllers/auth/indieauth/server.go b/webserver/handlers/auth/indieauth/server.go similarity index 87% rename from controllers/auth/indieauth/server.go rename to webserver/handlers/auth/indieauth/server.go index c3179f9ff..58ee82793 100644 --- a/controllers/auth/indieauth/server.go +++ b/webserver/handlers/auth/indieauth/server.go @@ -5,8 +5,8 @@ import ( "net/url" ia "github.com/owncast/owncast/auth/indieauth" - "github.com/owncast/owncast/controllers" "github.com/owncast/owncast/webserver/middleware" + "github.com/owncast/owncast/webserver/responses" ) // HandleAuthEndpoint will handle the IndieAuth auth endpoint. @@ -33,7 +33,7 @@ func handleAuthEndpointGet(w http.ResponseWriter, r *http.Request) { request, err := ia.StartServerAuth(clientID, redirectURI, codeChallenge, state, me) if err != nil { - _ = controllers.WriteString(w, err.Error(), http.StatusInternalServerError) + _ = responses.WriteString(w, err.Error(), http.StatusInternalServerError) return } @@ -42,7 +42,7 @@ func handleAuthEndpointGet(w http.ResponseWriter, r *http.Request) { // If the URL is invalid then return with specific "invalid_request" error. u, err := url.Parse(redirectURI) if err != nil { - controllers.WriteResponse(w, ia.Response{ + responses.WriteResponse(w, ia.Response{ Error: "invalid_request", ErrorDescription: err.Error(), }) @@ -59,7 +59,7 @@ func handleAuthEndpointGet(w http.ResponseWriter, r *http.Request) { func handleAuthEndpointPost(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { - controllers.WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -72,12 +72,12 @@ func handleAuthEndpointPost(w http.ResponseWriter, r *http.Request) { // "invalid_client" error. response, err := ia.CompleteServerAuth(code, redirectURI, clientID, codeVerifier) if err != nil { - controllers.WriteResponse(w, ia.Response{ + responses.WriteResponse(w, ia.Response{ Error: "invalid_client", ErrorDescription: err.Error(), }) return } - controllers.WriteResponse(w, response) + responses.WriteResponse(w, response) } diff --git a/controllers/chat.go b/webserver/handlers/chat.go similarity index 77% rename from controllers/chat.go rename to webserver/handlers/chat.go index 3d9682d85..69c16aec8 100644 --- a/controllers/chat.go +++ b/webserver/handlers/chat.go @@ -1,7 +1,8 @@ -package controllers +package handlers import ( "encoding/json" + "errors" "net/http" "github.com/owncast/owncast/config" @@ -9,17 +10,18 @@ import ( "github.com/owncast/owncast/core/user" "github.com/owncast/owncast/utils" "github.com/owncast/owncast/webserver/middleware" + "github.com/owncast/owncast/webserver/responses" log "github.com/sirupsen/logrus" ) // ExternalGetChatMessages gets all of the chat messages. -func ExternalGetChatMessages(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { +func (h *Handlers) ExternalGetChatMessages(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { middleware.EnableCors(w) getChatMessages(w, r) } // GetChatMessages gets all of the chat messages. -func GetChatMessages(u user.User, w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetChatMessages(u user.User, w http.ResponseWriter, r *http.Request) { middleware.EnableCors(w) getChatMessages(w, r) } @@ -36,14 +38,12 @@ func getChatMessages(w http.ResponseWriter, r *http.Request) { } default: w.WriteHeader(http.StatusNotImplemented) - if err := json.NewEncoder(w).Encode(j{"error": "method not implemented (PRs are accepted)"}); err != nil { - InternalErrorHandler(w, err) - } + responses.BadRequestHandler(w, errors.New("method not implemented")) } } // RegisterAnonymousChatUser will register a new user. -func RegisterAnonymousChatUser(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) RegisterAnonymousChatUser(w http.ResponseWriter, r *http.Request) { middleware.EnableCors(w) if r.Method == "OPTIONS" { @@ -54,8 +54,7 @@ func RegisterAnonymousChatUser(w http.ResponseWriter, r *http.Request) { } if r.Method != http.MethodPost { - // nolint:goconst - WriteSimpleResponse(w, false, r.Method+" not supported") + responses.WriteSimpleResponse(w, false, r.Method+" not supported") return } @@ -82,7 +81,7 @@ func RegisterAnonymousChatUser(w http.ResponseWriter, r *http.Request) { proposedNewDisplayName := utils.MakeSafeStringOfLength(request.DisplayName, config.MaxChatDisplayNameLength) newUser, accessToken, err := user.CreateAnonymousUser(proposedNewDisplayName) if err != nil { - WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -95,5 +94,5 @@ func RegisterAnonymousChatUser(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") middleware.DisableCache(w) - WriteResponse(w, response) + responses.WriteResponse(w, response) } diff --git a/controllers/config.go b/webserver/handlers/config.go similarity index 94% rename from controllers/config.go rename to webserver/handlers/config.go index e9ead530b..cfabe0bbe 100644 --- a/controllers/config.go +++ b/webserver/handlers/config.go @@ -1,4 +1,4 @@ -package controllers +package handlers import ( "encoding/json" @@ -12,6 +12,7 @@ import ( "github.com/owncast/owncast/models" "github.com/owncast/owncast/utils" "github.com/owncast/owncast/webserver/middleware" + "github.com/owncast/owncast/webserver/responses" log "github.com/sirupsen/logrus" ) @@ -58,7 +59,7 @@ type authenticationConfigResponse struct { } // GetWebConfig gets the status of the server. -func GetWebConfig(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetWebConfig(w http.ResponseWriter, r *http.Request) { middleware.EnableCors(w) middleware.DisableCache(w) w.Header().Set("Content-Type", "application/json") @@ -66,7 +67,7 @@ func GetWebConfig(w http.ResponseWriter, r *http.Request) { configuration := getConfigResponse() if err := json.NewEncoder(w).Encode(configuration); err != nil { - BadRequestHandler(w, err) + responses.BadRequestHandler(w, err) } } @@ -142,12 +143,12 @@ func getConfigResponse() webConfigResponse { } // GetAllSocialPlatforms will return a list of all social platform types. -func GetAllSocialPlatforms(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetAllSocialPlatforms(w http.ResponseWriter, r *http.Request) { middleware.EnableCors(w) w.Header().Set("Content-Type", "application/json") platforms := models.GetAllSocialHandles() if err := json.NewEncoder(w).Encode(platforms); err != nil { - InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) } } diff --git a/controllers/customJavascript.go b/webserver/handlers/customJavascript.go similarity index 65% rename from controllers/customJavascript.go rename to webserver/handlers/customJavascript.go index f20dfdf25..29cd9d3d2 100644 --- a/controllers/customJavascript.go +++ b/webserver/handlers/customJavascript.go @@ -1,4 +1,4 @@ -package controllers +package handlers import ( "net/http" @@ -7,7 +7,7 @@ import ( ) // ServeCustomJavascript will serve optional custom Javascript. -func ServeCustomJavascript(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) ServeCustomJavascript(w http.ResponseWriter, r *http.Request) { js := data.GetCustomJavascript() _, _ = w.Write([]byte(js)) } diff --git a/webserver/handlers/directory.go b/webserver/handlers/directory.go deleted file mode 100644 index 5ac8282f4..000000000 --- a/webserver/handlers/directory.go +++ /dev/null @@ -1 +0,0 @@ -package handlers diff --git a/webserver/handlers/disconnect.go b/webserver/handlers/disconnect.go new file mode 100644 index 000000000..c9cf6547c --- /dev/null +++ b/webserver/handlers/disconnect.go @@ -0,0 +1,21 @@ +package handlers + +import ( + "net/http" + + "github.com/owncast/owncast/core" + "github.com/owncast/owncast/webserver/responses" + + "github.com/owncast/owncast/core/rtmp" +) + +// DisconnectInboundConnection will force-disconnect an inbound stream. +func (h *Handlers) DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) { + if !core.GetStatus().Online { + responses.WriteSimpleResponse(w, false, "no inbound stream connected") + return + } + + rtmp.Disconnect() + responses.WriteSimpleResponse(w, true, "inbound stream disconnected") +} diff --git a/controllers/emoji.go b/webserver/handlers/emoji.go similarity index 70% rename from controllers/emoji.go rename to webserver/handlers/emoji.go index 745ae66c5..302f988a9 100644 --- a/controllers/emoji.go +++ b/webserver/handlers/emoji.go @@ -1,4 +1,4 @@ -package controllers +package handlers import ( "encoding/json" @@ -9,20 +9,21 @@ import ( "github.com/owncast/owncast/config" "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/router/middleware" + "github.com/owncast/owncast/webserver/responses" ) // GetCustomEmojiList returns a list of emoji via the API. -func GetCustomEmojiList(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetCustomEmojiList(w http.ResponseWriter, r *http.Request) { emojiList := data.GetEmojiList() middleware.SetCachingHeaders(w, r) if err := json.NewEncoder(w).Encode(emojiList); err != nil { - InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) } } // GetCustomEmojiImage returns a single emoji image. -func GetCustomEmojiImage(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetCustomEmojiImage(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/img/emoji/") r.URL.Path = path diff --git a/webserver/handlers/externalApi.go b/webserver/handlers/externalApi.go deleted file mode 100644 index 5ac8282f4..000000000 --- a/webserver/handlers/externalApi.go +++ /dev/null @@ -1 +0,0 @@ -package handlers diff --git a/controllers/followers.go b/webserver/handlers/followers.go similarity index 51% rename from controllers/followers.go rename to webserver/handlers/followers.go index 0f9c98eaa..61b502676 100644 --- a/controllers/followers.go +++ b/webserver/handlers/followers.go @@ -1,22 +1,23 @@ -package controllers +package handlers import ( "net/http" "github.com/owncast/owncast/activitypub/persistence" + "github.com/owncast/owncast/webserver/responses" ) // GetFollowers will handle an API request to fetch the list of followers (non-activitypub response). -func GetFollowers(offset int, limit int, w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetFollowers(offset int, limit int, w http.ResponseWriter, r *http.Request) { followers, total, err := persistence.GetFederationFollowers(limit, offset) if err != nil { - WriteSimpleResponse(w, false, "unable to fetch followers") + responses.WriteSimpleResponse(w, false, "unable to fetch followers") return } - response := PaginatedResponse{ + response := responses.PaginatedResponse{ Total: total, Results: followers, } - WriteResponse(w, response) + responses.WriteResponse(w, response) } diff --git a/webserver/handlers/hls.go b/webserver/handlers/hls.go index 5ac8282f4..c1c0a9645 100644 --- a/webserver/handlers/hls.go +++ b/webserver/handlers/hls.go @@ -1 +1,55 @@ package handlers + +import ( + "net/http" + "path" + "path/filepath" + "strconv" + "strings" + + "github.com/owncast/owncast/config" + "github.com/owncast/owncast/core" + "github.com/owncast/owncast/core/data" + "github.com/owncast/owncast/models" + "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/webserver/middleware" +) + +// HandleHLSRequest will manage all requests to HLS content. +func (h *Handlers) HandleHLSRequest(w http.ResponseWriter, r *http.Request) { + // Sanity check to limit requests to HLS file types. + if filepath.Ext(r.URL.Path) != ".m3u8" && filepath.Ext(r.URL.Path) != ".ts" { + w.WriteHeader(http.StatusNotFound) + return + } + + requestedPath := r.URL.Path + relativePath := strings.Replace(requestedPath, "/hls/", "", 1) + fullPath := filepath.Join(config.HLSStoragePath, relativePath) + + // If using external storage then only allow requests for the + // master playlist at stream.m3u8, no variants or segments. + if data.GetS3Config().Enabled && relativePath != "stream.m3u8" { + w.WriteHeader(http.StatusNotFound) + return + } + + // Handle playlists + if path.Ext(r.URL.Path) == ".m3u8" { + // Playlists should never be cached. + middleware.DisableCache(w) + + // Force the correct content type + w.Header().Set("Content-Type", "application/x-mpegURL") + + // Use this as an opportunity to mark this viewer as active. + viewer := models.GenerateViewerFromRequest(r) + core.SetViewerActive(&viewer) + } else { + cacheTime := utils.GetCacheDurationSecondsForPath(relativePath) + w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(cacheTime)) + } + + middleware.EnableCors(w) + http.ServeFile(w, r, fullPath) +} diff --git a/controllers/images.go b/webserver/handlers/images.go similarity index 69% rename from controllers/images.go rename to webserver/handlers/images.go index 6ea3eadf3..ff70810a4 100644 --- a/controllers/images.go +++ b/webserver/handlers/images.go @@ -1,4 +1,4 @@ -package controllers +package handlers import ( "net/http" @@ -6,6 +6,7 @@ import ( "github.com/owncast/owncast/config" "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/webserver/responses" ) const ( @@ -14,7 +15,7 @@ const ( ) // GetThumbnail will return the thumbnail image as a response. -func GetThumbnail(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetThumbnail(w http.ResponseWriter, r *http.Request) { imageFilename := "thumbnail.jpg" imagePath := filepath.Join(config.TempDir, imageFilename) @@ -24,21 +25,21 @@ func GetThumbnail(w http.ResponseWriter, r *http.Request) { if utils.DoesFileExists(imagePath) { imageBytes, err = getImage(imagePath) } else { - GetLogo(w, r) + h.GetLogo(w, r) return } if err != nil { - GetLogo(w, r) + h.GetLogo(w, r) return } cacheTime := utils.GetCacheDurationSecondsForPath(imagePath) - writeBytesAsImage(imageBytes, contentTypeJPEG, w, cacheTime) + responses.WriteBytesAsImage(imageBytes, contentTypeJPEG, w, cacheTime) } // GetPreview will return the preview gif as a response. -func GetPreview(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetPreview(w http.ResponseWriter, r *http.Request) { imageFilename := "preview.gif" imagePath := filepath.Join(config.TempDir, imageFilename) @@ -48,15 +49,15 @@ func GetPreview(w http.ResponseWriter, r *http.Request) { if utils.DoesFileExists(imagePath) { imageBytes, err = getImage(imagePath) } else { - GetLogo(w, r) + h.GetLogo(w, r) return } if err != nil { - GetLogo(w, r) + h.GetLogo(w, r) return } cacheTime := utils.GetCacheDurationSecondsForPath(imagePath) - writeBytesAsImage(imageBytes, contentTypeGIF, w, cacheTime) + responses.WriteBytesAsImage(imageBytes, contentTypeGIF, w, cacheTime) } diff --git a/controllers/index.go b/webserver/handlers/index.go similarity index 98% rename from controllers/index.go rename to webserver/handlers/index.go index b67e8c796..92689c78d 100644 --- a/controllers/index.go +++ b/webserver/handlers/index.go @@ -1,4 +1,4 @@ -package controllers +package handlers import ( "encoding/json" @@ -19,7 +19,7 @@ import ( ) // IndexHandler handles the default index route. -func IndexHandler(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) IndexHandler(w http.ResponseWriter, r *http.Request) { middleware.EnableCors(w) isIndexRequest := r.URL.Path == "/" || filepath.Base(r.URL.Path) == "index.html" || filepath.Base(r.URL.Path) == "" diff --git a/webserver/handlers/internalApi.go b/webserver/handlers/internalApi.go deleted file mode 100644 index 5ac8282f4..000000000 --- a/webserver/handlers/internalApi.go +++ /dev/null @@ -1 +0,0 @@ -package handlers diff --git a/controllers/logo.go b/webserver/handlers/logo.go similarity index 74% rename from controllers/logo.go rename to webserver/handlers/logo.go index 2bcda25b1..67a8debca 100644 --- a/controllers/logo.go +++ b/webserver/handlers/logo.go @@ -1,22 +1,22 @@ -package controllers +package handlers import ( "net/http" "os" "path/filepath" - "strconv" "github.com/owncast/owncast/config" "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/static" "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/webserver/responses" log "github.com/sirupsen/logrus" ) var _hasWarnedSVGLogo = false // GetLogo will return the logo image as a response. -func GetLogo(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetLogo(w http.ResponseWriter, r *http.Request) { imageFilename := data.GetLogoPath() if imageFilename == "" { returnDefault(w) @@ -39,20 +39,20 @@ func GetLogo(w http.ResponseWriter, r *http.Request) { } cacheTime := utils.GetCacheDurationSecondsForPath(imagePath) - writeBytesAsImage(imageBytes, contentType, w, cacheTime) + responses.WriteBytesAsImage(imageBytes, contentType, w, cacheTime) } // GetCompatibleLogo will return the logo unless it's a SVG // and in that case will return a default placeholder. // Used for sharing to external social networks that generally // don't support SVG. -func GetCompatibleLogo(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetCompatibleLogo(w http.ResponseWriter, r *http.Request) { imageFilename := data.GetLogoPath() // If the logo image is not a SVG then we can return it // without any problems. if imageFilename != "" && filepath.Ext(imageFilename) != ".svg" { - GetLogo(w, r) + h.GetLogo(w, r) return } @@ -66,7 +66,7 @@ func GetCompatibleLogo(w http.ResponseWriter, r *http.Request) { } cacheTime := utils.GetCacheDurationSecondsForPath(imagePath) - writeBytesAsImage(imageBytes, contentType, w, cacheTime) + responses.WriteBytesAsImage(imageBytes, contentType, w, cacheTime) if !_hasWarnedSVGLogo { log.Warnf("an external site requested your logo. because many social networks do not support SVGs we returned a placeholder instead. change your current logo to a png or jpeg to be most compatible with external social networking sites.") @@ -77,17 +77,7 @@ func GetCompatibleLogo(w http.ResponseWriter, r *http.Request) { func returnDefault(w http.ResponseWriter) { imageBytes := static.GetLogo() cacheTime := utils.GetCacheDurationSecondsForPath("logo.png") - writeBytesAsImage(imageBytes, "image/png", w, cacheTime) -} - -func writeBytesAsImage(data []byte, contentType string, w http.ResponseWriter, cacheSeconds int) { - w.Header().Set("Content-Type", contentType) - w.Header().Set("Content-Length", strconv.Itoa(len(data))) - w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(cacheSeconds)) - - if _, err := w.Write(data); err != nil { - log.Println("unable to write image.") - } + responses.WriteBytesAsImage(imageBytes, "image/png", w, cacheTime) } func getImage(path string) ([]byte, error) { diff --git a/controllers/moderation/moderation.go b/webserver/handlers/moderation.go similarity index 89% rename from controllers/moderation/moderation.go rename to webserver/handlers/moderation.go index b3304fec6..0ca5383f4 100644 --- a/controllers/moderation/moderation.go +++ b/webserver/handlers/moderation.go @@ -1,4 +1,4 @@ -package moderation +package handlers import ( "encoding/json" @@ -6,15 +6,15 @@ import ( "strings" "time" - "github.com/owncast/owncast/controllers" "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/core/user" + "github.com/owncast/owncast/webserver/responses" log "github.com/sirupsen/logrus" ) // GetUserDetails returns the details of a chat user for moderators. -func GetUserDetails(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetUserDetails(w http.ResponseWriter, r *http.Request) { type connectedClient struct { ConnectedAt time.Time `json:"connectedAt"` UserAgent string `json:"userAgent"` @@ -68,6 +68,6 @@ func GetUserDetails(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(res); err != nil { - controllers.InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) } } diff --git a/controllers/notifications.go b/webserver/handlers/notifications.go similarity index 66% rename from controllers/notifications.go rename to webserver/handlers/notifications.go index 08d6757fc..35ed47e75 100644 --- a/controllers/notifications.go +++ b/webserver/handlers/notifications.go @@ -1,4 +1,4 @@ -package controllers +package handlers import ( "encoding/json" @@ -6,6 +6,7 @@ import ( "github.com/owncast/owncast/core/user" "github.com/owncast/owncast/notifications" + "github.com/owncast/owncast/webserver/responses" "github.com/owncast/owncast/utils" @@ -14,9 +15,9 @@ import ( // RegisterForLiveNotifications will register a channel + destination to be // notified when a stream goes live. -func RegisterForLiveNotifications(u user.User, w http.ResponseWriter, r *http.Request) { - if r.Method != POST { - WriteSimpleResponse(w, false, r.Method+" not supported") +func (h *Handlers) RegisterForLiveNotifications(u user.User, w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + responses.WriteSimpleResponse(w, false, r.Method+" not supported") return } @@ -31,7 +32,7 @@ func RegisterForLiveNotifications(u user.User, w http.ResponseWriter, r *http.Re var req request if err := decoder.Decode(&req); err != nil { log.Errorln(err) - WriteSimpleResponse(w, false, "unable to register for notifications") + responses.WriteSimpleResponse(w, false, "unable to register for notifications") return } @@ -39,13 +40,13 @@ func RegisterForLiveNotifications(u user.User, w http.ResponseWriter, r *http.Re validTypes := []string{notifications.BrowserPushNotification} _, validChannel := utils.FindInSlice(validTypes, req.Channel) if !validChannel { - WriteSimpleResponse(w, false, "invalid notification channel: "+req.Channel) + responses.WriteSimpleResponse(w, false, "invalid notification channel: "+req.Channel) return } if err := notifications.AddNotification(req.Channel, req.Destination); err != nil { log.Errorln(err) - WriteSimpleResponse(w, false, "unable to save notification") + responses.WriteSimpleResponse(w, false, "unable to save notification") return } } diff --git a/controllers/ping.go b/webserver/handlers/ping.go similarity index 77% rename from controllers/ping.go rename to webserver/handlers/ping.go index 07d46552a..e6b0b4814 100644 --- a/controllers/ping.go +++ b/webserver/handlers/ping.go @@ -1,4 +1,4 @@ -package controllers +package handlers import ( "net/http" @@ -8,7 +8,7 @@ import ( ) // Ping is fired by a client to show they are still an active viewer. -func Ping(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) Ping(w http.ResponseWriter, r *http.Request) { viewer := models.GenerateViewerFromRequest(r) core.SetViewerActive(&viewer) w.WriteHeader(http.StatusOK) diff --git a/controllers/playbackMetrics.go b/webserver/handlers/playbackMetrics.go similarity index 81% rename from controllers/playbackMetrics.go rename to webserver/handlers/playbackMetrics.go index b18bc8fbf..014ce370c 100644 --- a/controllers/playbackMetrics.go +++ b/webserver/handlers/playbackMetrics.go @@ -1,4 +1,4 @@ -package controllers +package handlers import ( "encoding/json" @@ -6,14 +6,15 @@ import ( "github.com/owncast/owncast/metrics" "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/webserver/responses" log "github.com/sirupsen/logrus" ) // ReportPlaybackMetrics will accept playback metrics from a client and save // them for future video health reporting. -func ReportPlaybackMetrics(w http.ResponseWriter, r *http.Request) { - if r.Method != POST { - WriteSimpleResponse(w, false, r.Method+" not supported") +func (h *Handlers) ReportPlaybackMetrics(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + responses.WriteSimpleResponse(w, false, r.Method+" not supported") return } @@ -29,7 +30,7 @@ func ReportPlaybackMetrics(w http.ResponseWriter, r *http.Request) { var request reportPlaybackMetricsRequest if err := decoder.Decode(&request); err != nil { log.Errorln("error decoding playback metrics payload:", err) - WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } diff --git a/controllers/remoteFollow.go b/webserver/handlers/remoteFollow.go similarity index 71% rename from controllers/remoteFollow.go rename to webserver/handlers/remoteFollow.go index fdd012cce..d8b5240f7 100644 --- a/controllers/remoteFollow.go +++ b/webserver/handlers/remoteFollow.go @@ -1,4 +1,4 @@ -package controllers +package handlers import ( "encoding/json" @@ -9,10 +9,11 @@ import ( "github.com/owncast/owncast/activitypub/webfinger" "github.com/owncast/owncast/core/data" + "github.com/owncast/owncast/webserver/responses" ) // RemoteFollow handles a request to begin the remote follow redirect flow. -func RemoteFollow(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) RemoteFollow(w http.ResponseWriter, r *http.Request) { type followRequest struct { Account string `json:"account"` } @@ -24,12 +25,12 @@ func RemoteFollow(w http.ResponseWriter, r *http.Request) { var request followRequest decoder := json.NewDecoder(r.Body) if err := decoder.Decode(&request); err != nil { - WriteSimpleResponse(w, false, "unable to parse request") + responses.WriteSimpleResponse(w, false, "unable to parse request") return } if request.Account == "" { - WriteSimpleResponse(w, false, "Remote Fediverse account is required to follow.") + responses.WriteSimpleResponse(w, false, "Remote Fediverse account is required to follow.") return } @@ -38,7 +39,7 @@ func RemoteFollow(w http.ResponseWriter, r *http.Request) { var template string links, err := webfinger.GetWebfingerLinks(request.Account) if err != nil { - WriteSimpleResponse(w, false, err.Error()) + responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -52,7 +53,7 @@ func RemoteFollow(w http.ResponseWriter, r *http.Request) { } if localActorPath.String() == "" || template == "" { - WriteSimpleResponse(w, false, "unable to determine remote follow information for "+request.Account) + responses.WriteSimpleResponse(w, false, "unable to determine remote follow information for "+request.Account) return } @@ -61,5 +62,5 @@ func RemoteFollow(w http.ResponseWriter, r *http.Request) { RedirectURL: redirectURL, } - WriteResponse(w, response) + responses.WriteResponse(w, response) } diff --git a/controllers/robots.go b/webserver/handlers/robots.go similarity index 84% rename from controllers/robots.go rename to webserver/handlers/robots.go index 4605d3da4..3ffbc1951 100644 --- a/controllers/robots.go +++ b/webserver/handlers/robots.go @@ -1,4 +1,4 @@ -package controllers +package handlers import ( "net/http" @@ -8,7 +8,7 @@ import ( ) // GetRobotsDotTxt returns the contents of our robots.txt. -func GetRobotsDotTxt(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetRobotsDotTxt(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") contents := []string{ "User-agent: *", diff --git a/webserver/handlers/static.go b/webserver/handlers/static.go deleted file mode 100644 index 5ac8282f4..000000000 --- a/webserver/handlers/static.go +++ /dev/null @@ -1 +0,0 @@ -package handlers diff --git a/controllers/status.go b/webserver/handlers/status.go similarity index 87% rename from controllers/status.go rename to webserver/handlers/status.go index 0db538e4b..c5caf325f 100644 --- a/controllers/status.go +++ b/webserver/handlers/status.go @@ -1,4 +1,4 @@ -package controllers +package handlers import ( "encoding/json" @@ -9,17 +9,18 @@ import ( "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/utils" "github.com/owncast/owncast/webserver/middleware" + "github.com/owncast/owncast/webserver/responses" ) // GetStatus gets the status of the server. -func GetStatus(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetStatus(w http.ResponseWriter, r *http.Request) { response := getStatusResponse() w.Header().Set("Content-Type", "application/json") middleware.DisableCache(w) if err := json.NewEncoder(w).Encode(response); err != nil { - InternalErrorHandler(w, err) + responses.InternalErrorHandler(w, err) } } diff --git a/webserver/handlers/testing.go b/webserver/handlers/testing.go deleted file mode 100644 index 7c543841d..000000000 --- a/webserver/handlers/testing.go +++ /dev/null @@ -1,7 +0,0 @@ -package handlers - -import "net/http" - -func (s *Handlers) HandleTesting(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte("testing")) -} diff --git a/controllers/video.go b/webserver/handlers/video.go similarity index 87% rename from controllers/video.go rename to webserver/handlers/video.go index a3e69c357..5c4a7f5d6 100644 --- a/controllers/video.go +++ b/webserver/handlers/video.go @@ -1,10 +1,11 @@ -package controllers +package handlers import ( "net/http" "sort" "github.com/owncast/owncast/core/data" + "github.com/owncast/owncast/webserver/responses" ) type variantsSort struct { @@ -20,7 +21,7 @@ type variantsResponse struct { } // GetVideoStreamOutputVariants will return the video variants available. -func GetVideoStreamOutputVariants(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetVideoStreamOutputVariants(w http.ResponseWriter, r *http.Request) { outputVariants := data.GetStreamOutputVariants() streamSortVariants := make([]variantsSort, len(outputVariants)) @@ -55,5 +56,5 @@ func GetVideoStreamOutputVariants(w http.ResponseWriter, r *http.Request) { response[i] = variantResponse } - WriteResponse(w, response) + responses.WriteResponse(w, response) } diff --git a/controllers/web.go b/webserver/handlers/web.go similarity index 92% rename from controllers/web.go rename to webserver/handlers/web.go index 7b16fe74a..8042684ea 100644 --- a/controllers/web.go +++ b/webserver/handlers/web.go @@ -1,4 +1,4 @@ -package controllers +package handlers import ( "net/http" diff --git a/webserver/handlers/webAssets.go b/webserver/handlers/webAssets.go deleted file mode 100644 index 5ac8282f4..000000000 --- a/webserver/handlers/webAssets.go +++ /dev/null @@ -1 +0,0 @@ -package handlers diff --git a/yp/api.go b/webserver/handlers/ypApi.go similarity index 92% rename from yp/api.go rename to webserver/handlers/ypApi.go index ccfcb97e8..8f1a8e4be 100644 --- a/yp/api.go +++ b/webserver/handlers/ypApi.go @@ -1,9 +1,10 @@ -package yp +package handlers import ( "encoding/json" "net/http" + "github.com/owncast/owncast/core" "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/models" "github.com/owncast/owncast/utils" @@ -27,13 +28,13 @@ type ypDetailsResponse struct { } // GetYPResponse gets the status of the server for YP purposes. -func GetYPResponse(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) GetYPResponse(w http.ResponseWriter, r *http.Request) { if !data.GetDirectoryEnabled() { w.WriteHeader(http.StatusNotFound) return } - status := getStatus() + status := core.GetStatus() streamTitle := data.GetStreamTitle() diff --git a/webserver/requests/requestConfigValue.go b/webserver/requests/requestConfigValue.go new file mode 100644 index 000000000..86a3497fc --- /dev/null +++ b/webserver/requests/requestConfigValue.go @@ -0,0 +1,6 @@ +package requests + +// ConfigValue is a container object that holds a value, is encoded, and saved to the database. +type ConfigValue struct { + Value interface{} `json:"value"` +} diff --git a/webserver/requests/requests.go b/webserver/requests/requests.go new file mode 100644 index 000000000..b2d68a771 --- /dev/null +++ b/webserver/requests/requests.go @@ -0,0 +1,50 @@ +package requests + +import ( + "encoding/json" + "net/http" + "reflect" + + "github.com/owncast/owncast/webserver/responses" + log "github.com/sirupsen/logrus" +) + +func RequirePOST(w http.ResponseWriter, r *http.Request) bool { + if r.Method != http.MethodPost { + responses.WriteSimpleResponse(w, false, r.Method+" not supported") + return false + } + + return true +} + +func GetValueFromRequest(w http.ResponseWriter, r *http.Request) (ConfigValue, bool) { + decoder := json.NewDecoder(r.Body) + var configValue ConfigValue + if err := decoder.Decode(&configValue); err != nil { + log.Warnln(err) + responses.WriteSimpleResponse(w, false, "unable to parse new value") + return configValue, false + } + + return configValue, true +} + +func GetValuesFromRequest(w http.ResponseWriter, r *http.Request) ([]ConfigValue, bool) { + var values []ConfigValue + + decoder := json.NewDecoder(r.Body) + var configValue ConfigValue + if err := decoder.Decode(&configValue); err != nil { + responses.WriteSimpleResponse(w, false, "unable to parse array of values") + return values, false + } + + object := reflect.ValueOf(configValue.Value) + + for i := 0; i < object.Len(); i++ { + values = append(values, ConfigValue{Value: object.Index(i).Interface()}) + } + + return values, true +} diff --git a/webserver/responses/image.go b/webserver/responses/image.go new file mode 100644 index 000000000..42eb8387f --- /dev/null +++ b/webserver/responses/image.go @@ -0,0 +1,17 @@ +package responses + +import ( + "log" + "net/http" + "strconv" +) + +func WriteBytesAsImage(data []byte, contentType string, w http.ResponseWriter, cacheSeconds int) { + w.Header().Set("Content-Type", contentType) + w.Header().Set("Content-Length", strconv.Itoa(len(data))) + w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(cacheSeconds)) + + if _, err := w.Write(data); err != nil { + log.Println("unable to write image.") + } +} diff --git a/controllers/pagination.go b/webserver/responses/pagination.go similarity index 90% rename from controllers/pagination.go rename to webserver/responses/pagination.go index 103bdc23b..27b3d6dcc 100644 --- a/controllers/pagination.go +++ b/webserver/responses/pagination.go @@ -1,4 +1,4 @@ -package controllers +package responses // PaginatedResponse is a structure for returning a total count with results. type PaginatedResponse struct { diff --git a/controllers/controllers.go b/webserver/responses/responses.go similarity index 98% rename from controllers/controllers.go rename to webserver/responses/responses.go index 2c8faa3b4..01dc35525 100644 --- a/controllers/controllers.go +++ b/webserver/responses/responses.go @@ -1,4 +1,4 @@ -package controllers +package responses import ( "encoding/json" diff --git a/webserver/router.go b/webserver/router.go index 8e95f135a..0f17d102e 100644 --- a/webserver/router.go +++ b/webserver/router.go @@ -5,30 +5,24 @@ import ( "github.com/owncast/owncast/activitypub" "github.com/owncast/owncast/config" - "github.com/owncast/owncast/controllers" - "github.com/owncast/owncast/controllers/admin" - fediverseauth "github.com/owncast/owncast/controllers/auth/fediverse" - "github.com/owncast/owncast/controllers/auth/indieauth" - "github.com/owncast/owncast/controllers/moderation" "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/user" "github.com/owncast/owncast/utils" + fediverseauth "github.com/owncast/owncast/webserver/handlers/auth/fediverse" + "github.com/owncast/owncast/webserver/handlers/auth/indieauth" "github.com/owncast/owncast/webserver/middleware" - "github.com/owncast/owncast/yp" "github.com/prometheus/client_golang/prometheus/promhttp" ) func (s *webServer) setupRoutes() { - s.router.HandleFunc("/test", s.handlers.HandleTesting) - s.setupWebAssetRoutes() s.setupInternalAPIRoutes() s.setupAdminAPIRoutes() s.setupExternalThirdPartyAPIRoutes() s.setupModerationAPIRoutes() - s.router.HandleFunc("/hls/", controllers.HandleHLSRequest) + s.router.HandleFunc("/hls/", s.handlers.HandleHLSRequest) // websocket s.router.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { @@ -45,7 +39,7 @@ func (s *webServer) setupRoutes() { // Redirect /embed/chat http.Redirect(w, r, "/embed/chat/readonly", http.StatusTemporaryRedirect) } else { - controllers.IndexHandler(w, r) + s.handlers.IndexHandler(w, r) // s.ServeHTTP(w, r) } }) @@ -56,26 +50,26 @@ func (s *webServer) setupRoutes() { func (s *webServer) setupWebAssetRoutes() { // The admin web app. - s.router.HandleFunc("/admin/", middleware.RequireAdminAuth(controllers.IndexHandler)) + s.router.HandleFunc("/admin/", middleware.RequireAdminAuth(s.handlers.IndexHandler)) // Images - s.router.HandleFunc("/thumbnail.jpg", controllers.GetThumbnail) - s.router.HandleFunc("/preview.gif", controllers.GetPreview) - s.router.HandleFunc("/logo", controllers.GetLogo) + s.router.HandleFunc("/thumbnail.jpg", s.handlers.GetThumbnail) + s.router.HandleFunc("/preview.gif", s.handlers.GetPreview) + s.router.HandleFunc("/logo", s.handlers.GetLogo) // Custom Javascript - s.router.HandleFunc("/customjavascript", controllers.ServeCustomJavascript) + s.router.HandleFunc("/customjavascript", s.handlers.ServeCustomJavascript) // Return a single emoji image. - s.router.HandleFunc(config.EmojiDir, controllers.GetCustomEmojiImage) + s.router.HandleFunc(config.EmojiDir, s.handlers.GetCustomEmojiImage) // return the logo // return a logo that's compatible with external social networks - s.router.HandleFunc("/logo/external", controllers.GetCompatibleLogo) + s.router.HandleFunc("/logo/external", s.handlers.GetCompatibleLogo) // robots.txt - s.router.HandleFunc("/robots.txt", controllers.GetRobotsDotTxt) + s.router.HandleFunc("/robots.txt", s.handlers.GetRobotsDotTxt) // Optional public static files s.router.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir(config.PublicFilesPath)))) @@ -85,289 +79,289 @@ func (s *webServer) setupInternalAPIRoutes() { // Internal APIs // status of the system - s.router.HandleFunc("/api/status", controllers.GetStatus) + s.router.HandleFunc("/api/status", s.handlers.GetStatus) // custom emoji supported in the chat - s.router.HandleFunc("/api/emoji", controllers.GetCustomEmojiList) + s.router.HandleFunc("/api/emoji", s.handlers.GetCustomEmojiList) // chat history api - s.router.HandleFunc("/api/chat", middleware.RequireUserAccessToken(controllers.GetChatMessages)) + s.router.HandleFunc("/api/chat", middleware.RequireUserAccessToken(s.handlers.GetChatMessages)) // web config api - s.router.HandleFunc("/api/config", controllers.GetWebConfig) + s.router.HandleFunc("/api/config", s.handlers.GetWebConfig) // return the YP protocol data - s.router.HandleFunc("/api/yp", yp.GetYPResponse) + s.router.HandleFunc("/api/yp", s.handlers.GetYPResponse) // list of all social platforms - s.router.HandleFunc("/api/socialplatforms", controllers.GetAllSocialPlatforms) + s.router.HandleFunc("/api/socialplatforms", s.handlers.GetAllSocialPlatforms) // return the list of video variants available - s.router.HandleFunc("/api/video/variants", controllers.GetVideoStreamOutputVariants) + s.router.HandleFunc("/api/video/variants", s.handlers.GetVideoStreamOutputVariants) // tell the backend you're an active viewer - s.router.HandleFunc("/api/ping", controllers.Ping) + s.router.HandleFunc("/api/ping", s.handlers.Ping) // register a new chat user - s.router.HandleFunc("/api/chat/register", controllers.RegisterAnonymousChatUser) + s.router.HandleFunc("/api/chat/register", s.handlers.RegisterAnonymousChatUser) // return remote follow details - s.router.HandleFunc("/api/remotefollow", controllers.RemoteFollow) + s.router.HandleFunc("/api/remotefollow", s.handlers.RemoteFollow) // return followers - s.router.HandleFunc("/api/followers", middleware.HandlePagination(controllers.GetFollowers)) + s.router.HandleFunc("/api/followers", middleware.HandlePagination(s.handlers.GetFollowers)) // save client video playback metrics - s.router.HandleFunc("/api/metrics/playback", controllers.ReportPlaybackMetrics) + s.router.HandleFunc("/api/metrics/playback", s.handlers.ReportPlaybackMetrics) // Register for notifications - s.router.HandleFunc("/api/notifications/register", middleware.RequireUserAccessToken(controllers.RegisterForLiveNotifications)) + s.router.HandleFunc("/api/notifications/register", middleware.RequireUserAccessToken(s.handlers.RegisterForLiveNotifications)) // Start auth flow - http.HandleFunc("/api/auth/indieauth", middleware.RequireUserAccessToken(indieauth.StartAuthFlow)) - http.HandleFunc("/api/auth/indieauth/callback", indieauth.HandleRedirect) - http.HandleFunc("/api/auth/provider/indieauth", indieauth.HandleAuthEndpoint) + s.router.HandleFunc("/api/auth/indieauth", middleware.RequireUserAccessToken(indieauth.StartAuthFlow)) + s.router.HandleFunc("/api/auth/indieauth/callback", indieauth.HandleRedirect) + s.router.HandleFunc("/api/auth/provider/indieauth", indieauth.HandleAuthEndpoint) - http.HandleFunc("/api/auth/fediverse", middleware.RequireUserAccessToken(fediverseauth.RegisterFediverseOTPRequest)) - http.HandleFunc("/api/auth/fediverse/verify", fediverseauth.VerifyFediverseOTPRequest) + s.router.HandleFunc("/api/auth/fediverse", middleware.RequireUserAccessToken(fediverseauth.RegisterFediverseOTPRequest)) + s.router.HandleFunc("/api/auth/fediverse/verify", fediverseauth.VerifyFediverseOTPRequest) } func (s *webServer) setupAdminAPIRoutes() { // Current inbound broadcaster - s.router.HandleFunc("/api/admin/status", middleware.RequireAdminAuth(admin.Status)) + s.router.HandleFunc("/api/admin/status", middleware.RequireAdminAuth(s.handlers.GetAdminStatus)) // Disconnect inbound stream - s.router.HandleFunc("/api/admin/disconnect", middleware.RequireAdminAuth(admin.DisconnectInboundConnection)) + s.router.HandleFunc("/api/admin/disconnect", middleware.RequireAdminAuth(s.handlers.DisconnectInboundConnection)) // Server config - s.router.HandleFunc("/api/admin/serverconfig", middleware.RequireAdminAuth(admin.GetServerConfig)) + s.router.HandleFunc("/api/admin/serverconfig", middleware.RequireAdminAuth(s.handlers.GetServerConfig)) // Get viewer count over time - s.router.HandleFunc("/api/admin/viewersOverTime", middleware.RequireAdminAuth(admin.GetViewersOverTime)) + s.router.HandleFunc("/api/admin/viewersOverTime", middleware.RequireAdminAuth(s.handlers.GetViewersOverTime)) // Get active viewers - s.router.HandleFunc("/api/admin/viewers", middleware.RequireAdminAuth(admin.GetActiveViewers)) + s.router.HandleFunc("/api/admin/viewers", middleware.RequireAdminAuth(s.handlers.GetActiveViewers)) // Get hardware stats - s.router.HandleFunc("/api/admin/hardwarestats", middleware.RequireAdminAuth(admin.GetHardwareStats)) + s.router.HandleFunc("/api/admin/hardwarestats", middleware.RequireAdminAuth(s.handlers.GetHardwareStats)) // Get a a detailed list of currently connected chat clients - s.router.HandleFunc("/api/admin/chat/clients", middleware.RequireAdminAuth(admin.GetConnectedChatClients)) + s.router.HandleFunc("/api/admin/chat/clients", middleware.RequireAdminAuth(s.handlers.GetConnectedChatClients)) // Get all logs - s.router.HandleFunc("/api/admin/logs", middleware.RequireAdminAuth(admin.GetLogs)) + s.router.HandleFunc("/api/admin/logs", middleware.RequireAdminAuth(s.handlers.GetLogs)) // Get warning/error logs - s.router.HandleFunc("/api/admin/logs/warnings", middleware.RequireAdminAuth(admin.GetWarnings)) + s.router.HandleFunc("/api/admin/logs/warnings", middleware.RequireAdminAuth(s.handlers.GetWarnings)) // Get all chat messages for the admin, unfiltered. - s.router.HandleFunc("/api/admin/chat/messages", middleware.RequireAdminAuth(admin.GetChatMessages)) + s.router.HandleFunc("/api/admin/chat/messages", middleware.RequireAdminAuth(s.handlers.GetAdminChatMessages)) // Update chat message visibility - s.router.HandleFunc("/api/admin/chat/messagevisibility", middleware.RequireAdminAuth(admin.UpdateMessageVisibility)) + s.router.HandleFunc("/api/admin/chat/messagevisibility", middleware.RequireAdminAuth(s.handlers.UpdateMessageVisibility)) // Enable/disable a user - s.router.HandleFunc("/api/admin/chat/users/setenabled", middleware.RequireAdminAuth(admin.UpdateUserEnabled)) + s.router.HandleFunc("/api/admin/chat/users/setenabled", middleware.RequireAdminAuth(s.handlers.UpdateUserEnabled)) // Ban/unban an IP address - s.router.HandleFunc("/api/admin/chat/users/ipbans/create", middleware.RequireAdminAuth(admin.BanIPAddress)) + s.router.HandleFunc("/api/admin/chat/users/ipbans/create", middleware.RequireAdminAuth(s.handlers.BanIPAddress)) // Remove an IP address ban - s.router.HandleFunc("/api/admin/chat/users/ipbans/remove", middleware.RequireAdminAuth(admin.UnBanIPAddress)) + s.router.HandleFunc("/api/admin/chat/users/ipbans/remove", middleware.RequireAdminAuth(s.handlers.UnBanIPAddress)) // Return all the banned IP addresses - s.router.HandleFunc("/api/admin/chat/users/ipbans", middleware.RequireAdminAuth(admin.GetIPAddressBans)) + s.router.HandleFunc("/api/admin/chat/users/ipbans", middleware.RequireAdminAuth(s.handlers.GetIPAddressBans)) // Get a list of disabled users - s.router.HandleFunc("/api/admin/chat/users/disabled", middleware.RequireAdminAuth(admin.GetDisabledUsers)) + s.router.HandleFunc("/api/admin/chat/users/disabled", middleware.RequireAdminAuth(s.handlers.GetDisabledUsers)) // Set moderator status for a user - s.router.HandleFunc("/api/admin/chat/users/setmoderator", middleware.RequireAdminAuth(admin.UpdateUserModerator)) + s.router.HandleFunc("/api/admin/chat/users/setmoderator", middleware.RequireAdminAuth(s.handlers.UpdateUserModerator)) // Get a list of moderator users - s.router.HandleFunc("/api/admin/chat/users/moderators", middleware.RequireAdminAuth(admin.GetModerators)) + s.router.HandleFunc("/api/admin/chat/users/moderators", middleware.RequireAdminAuth(s.handlers.GetModerators)) // return followers - s.router.HandleFunc("/api/admin/followers", middleware.RequireAdminAuth(middleware.HandlePagination(controllers.GetFollowers))) + s.router.HandleFunc("/api/admin/followers", middleware.RequireAdminAuth(middleware.HandlePagination(s.handlers.GetFollowers))) // Get a list of pending follow requests - s.router.HandleFunc("/api/admin/followers/pending", middleware.RequireAdminAuth(admin.GetPendingFollowRequests)) + s.router.HandleFunc("/api/admin/followers/pending", middleware.RequireAdminAuth(s.handlers.GetPendingFollowRequests)) // Get a list of rejected or blocked follows - s.router.HandleFunc("/api/admin/followers/blocked", middleware.RequireAdminAuth(admin.GetBlockedAndRejectedFollowers)) + s.router.HandleFunc("/api/admin/followers/blocked", middleware.RequireAdminAuth(s.handlers.GetBlockedAndRejectedFollowers)) // Set the following state of a follower or follow request. - s.router.HandleFunc("/api/admin/followers/approve", middleware.RequireAdminAuth(admin.ApproveFollower)) + s.router.HandleFunc("/api/admin/followers/approve", middleware.RequireAdminAuth(s.handlers.ApproveFollower)) // Upload custom emoji - s.router.HandleFunc("/api/admin/emoji/upload", middleware.RequireAdminAuth(admin.UploadCustomEmoji)) + s.router.HandleFunc("/api/admin/emoji/upload", middleware.RequireAdminAuth(s.handlers.UploadCustomEmoji)) // Delete custom emoji - s.router.HandleFunc("/api/admin/emoji/delete", middleware.RequireAdminAuth(admin.DeleteCustomEmoji)) + s.router.HandleFunc("/api/admin/emoji/delete", middleware.RequireAdminAuth(s.handlers.DeleteCustomEmoji)) // Update config values // Change the current streaming key in memory - s.router.HandleFunc("/api/admin/config/adminpass", middleware.RequireAdminAuth(admin.SetAdminPassword)) + s.router.HandleFunc("/api/admin/config/adminpass", middleware.RequireAdminAuth(s.handlers.SetAdminPassword)) // Set an array of valid stream keys - s.router.HandleFunc("/api/admin/config/streamkeys", middleware.RequireAdminAuth(admin.SetStreamKeys)) + s.router.HandleFunc("/api/admin/config/streamkeys", middleware.RequireAdminAuth(s.handlers.SetStreamKeys)) // Change the extra page content in memory - s.router.HandleFunc("/api/admin/config/pagecontent", middleware.RequireAdminAuth(admin.SetExtraPageContent)) + s.router.HandleFunc("/api/admin/config/pagecontent", middleware.RequireAdminAuth(s.handlers.SetExtraPageContent)) // Stream title - s.router.HandleFunc("/api/admin/config/streamtitle", middleware.RequireAdminAuth(admin.SetStreamTitle)) + s.router.HandleFunc("/api/admin/config/streamtitle", middleware.RequireAdminAuth(s.handlers.SetStreamTitle)) // Server name - s.router.HandleFunc("/api/admin/config/name", middleware.RequireAdminAuth(admin.SetServerName)) + s.router.HandleFunc("/api/admin/config/name", middleware.RequireAdminAuth(s.handlers.SetServerName)) // Server summary - s.router.HandleFunc("/api/admin/config/serversummary", middleware.RequireAdminAuth(admin.SetServerSummary)) + s.router.HandleFunc("/api/admin/config/serversummary", middleware.RequireAdminAuth(s.handlers.SetServerSummary)) // Offline message - s.router.HandleFunc("/api/admin/config/offlinemessage", middleware.RequireAdminAuth(admin.SetCustomOfflineMessage)) + s.router.HandleFunc("/api/admin/config/offlinemessage", middleware.RequireAdminAuth(s.handlers.SetCustomOfflineMessage)) // Server welcome message - s.router.HandleFunc("/api/admin/config/welcomemessage", middleware.RequireAdminAuth(admin.SetServerWelcomeMessage)) + s.router.HandleFunc("/api/admin/config/welcomemessage", middleware.RequireAdminAuth(s.handlers.SetServerWelcomeMessage)) // Disable chat - s.router.HandleFunc("/api/admin/config/chat/disable", middleware.RequireAdminAuth(admin.SetChatDisabled)) + s.router.HandleFunc("/api/admin/config/chat/disable", middleware.RequireAdminAuth(s.handlers.SetChatDisabled)) // Disable chat user join messages - s.router.HandleFunc("/api/admin/config/chat/joinmessagesenabled", middleware.RequireAdminAuth(admin.SetChatJoinMessagesEnabled)) + s.router.HandleFunc("/api/admin/config/chat/joinmessagesenabled", middleware.RequireAdminAuth(s.handlers.SetChatJoinMessagesEnabled)) // Enable/disable chat established user mode - s.router.HandleFunc("/api/admin/config/chat/establishedusermode", middleware.RequireAdminAuth(admin.SetEnableEstablishedChatUserMode)) + s.router.HandleFunc("/api/admin/config/chat/establishedusermode", middleware.RequireAdminAuth(s.handlers.SetEnableEstablishedChatUserMode)) // Set chat usernames that are not allowed - s.router.HandleFunc("/api/admin/config/chat/forbiddenusernames", middleware.RequireAdminAuth(admin.SetForbiddenUsernameList)) + s.router.HandleFunc("/api/admin/config/chat/forbiddenusernames", middleware.RequireAdminAuth(s.handlers.SetForbiddenUsernameList)) // Set the suggested chat usernames that will be assigned automatically - s.router.HandleFunc("/api/admin/config/chat/suggestedusernames", middleware.RequireAdminAuth(admin.SetSuggestedUsernameList)) + s.router.HandleFunc("/api/admin/config/chat/suggestedusernames", middleware.RequireAdminAuth(s.handlers.SetSuggestedUsernameList)) // Set video codec - s.router.HandleFunc("/api/admin/config/video/codec", middleware.RequireAdminAuth(admin.SetVideoCodec)) + s.router.HandleFunc("/api/admin/config/video/codec", middleware.RequireAdminAuth(s.handlers.SetVideoCodec)) // Set style/color/css values - s.router.HandleFunc("/api/admin/config/appearance", middleware.RequireAdminAuth(admin.SetCustomColorVariableValues)) + s.router.HandleFunc("/api/admin/config/appearance", middleware.RequireAdminAuth(s.handlers.SetCustomColorVariableValues)) // Return all webhooks - s.router.HandleFunc("/api/admin/webhooks", middleware.RequireAdminAuth(admin.GetWebhooks)) + s.router.HandleFunc("/api/admin/webhooks", middleware.RequireAdminAuth(s.handlers.GetWebhooks)) // Delete a single webhook - s.router.HandleFunc("/api/admin/webhooks/delete", middleware.RequireAdminAuth(admin.DeleteWebhook)) + s.router.HandleFunc("/api/admin/webhooks/delete", middleware.RequireAdminAuth(s.handlers.DeleteWebhook)) // Create a single webhook - s.router.HandleFunc("/api/admin/webhooks/create", middleware.RequireAdminAuth(admin.CreateWebhook)) + s.router.HandleFunc("/api/admin/webhooks/create", middleware.RequireAdminAuth(s.handlers.CreateWebhook)) // Get all access tokens - s.router.HandleFunc("/api/admin/accesstokens", middleware.RequireAdminAuth(admin.GetExternalAPIUsers)) + s.router.HandleFunc("/api/admin/accesstokens", middleware.RequireAdminAuth(s.handlers.GetExternalAPIUsers)) // Delete a single access token - s.router.HandleFunc("/api/admin/accesstokens/delete", middleware.RequireAdminAuth(admin.DeleteExternalAPIUser)) + s.router.HandleFunc("/api/admin/accesstokens/delete", middleware.RequireAdminAuth(s.handlers.DeleteExternalAPIUser)) // Create a single access token - s.router.HandleFunc("/api/admin/accesstokens/create", middleware.RequireAdminAuth(admin.CreateExternalAPIUser)) + s.router.HandleFunc("/api/admin/accesstokens/create", middleware.RequireAdminAuth(s.handlers.CreateExternalAPIUser)) // Return the auto-update features that are supported for this instance. - s.router.HandleFunc("/api/admin/update/options", middleware.RequireAdminAuth(admin.AutoUpdateOptions)) + s.router.HandleFunc("/api/admin/update/options", middleware.RequireAdminAuth(s.handlers.AutoUpdateOptions)) // Begin the auto update - s.router.HandleFunc("/api/admin/update/start", middleware.RequireAdminAuth(admin.AutoUpdateStart)) + s.router.HandleFunc("/api/admin/update/start", middleware.RequireAdminAuth(s.handlers.AutoUpdateStart)) // Force quit the service to restart it - s.router.HandleFunc("/api/admin/update/forcequit", middleware.RequireAdminAuth(admin.AutoUpdateForceQuit)) + s.router.HandleFunc("/api/admin/update/forcequit", middleware.RequireAdminAuth(s.handlers.AutoUpdateForceQuit)) // Logo path - s.router.HandleFunc("/api/admin/config/logo", middleware.RequireAdminAuth(admin.SetLogo)) + s.router.HandleFunc("/api/admin/config/logo", middleware.RequireAdminAuth(s.handlers.SetLogo)) // Server tags - s.router.HandleFunc("/api/admin/config/tags", middleware.RequireAdminAuth(admin.SetTags)) + s.router.HandleFunc("/api/admin/config/tags", middleware.RequireAdminAuth(s.handlers.SetTags)) // ffmpeg - s.router.HandleFunc("/api/admin/config/ffmpegpath", middleware.RequireAdminAuth(admin.SetFfmpegPath)) + s.router.HandleFunc("/api/admin/config/ffmpegpath", middleware.RequireAdminAuth(s.handlers.SetFfmpegPath)) // Server http port - s.router.HandleFunc("/api/admin/config/webserverport", middleware.RequireAdminAuth(admin.SetWebServerPort)) + s.router.HandleFunc("/api/admin/config/webserverport", middleware.RequireAdminAuth(s.handlers.SetWebServerPort)) // Server http listen address - s.router.HandleFunc("/api/admin/config/webserverip", middleware.RequireAdminAuth(admin.SetWebServerIP)) + s.router.HandleFunc("/api/admin/config/webserverip", middleware.RequireAdminAuth(s.handlers.SetWebServerIP)) // Server rtmp port - s.router.HandleFunc("/api/admin/config/rtmpserverport", middleware.RequireAdminAuth(admin.SetRTMPServerPort)) + s.router.HandleFunc("/api/admin/config/rtmpserverport", middleware.RequireAdminAuth(s.handlers.SetRTMPServerPort)) // Websocket host override - s.router.HandleFunc("/api/admin/config/sockethostoverride", middleware.RequireAdminAuth(admin.SetSocketHostOverride)) + s.router.HandleFunc("/api/admin/config/sockethostoverride", middleware.RequireAdminAuth(s.handlers.SetSocketHostOverride)) // Custom video serving endpoint - s.router.HandleFunc("/api/admin/config/videoservingendpoint", middleware.RequireAdminAuth(admin.SetVideoServingEndpoint)) + s.router.HandleFunc("/api/admin/config/videoservingendpoint", middleware.RequireAdminAuth(s.handlers.SetVideoServingEndpoint)) // Is server marked as NSFW - s.router.HandleFunc("/api/admin/config/nsfw", middleware.RequireAdminAuth(admin.SetNSFW)) + s.router.HandleFunc("/api/admin/config/nsfw", middleware.RequireAdminAuth(s.handlers.SetNSFW)) // directory enabled - s.router.HandleFunc("/api/admin/config/directoryenabled", middleware.RequireAdminAuth(admin.SetDirectoryEnabled)) + s.router.HandleFunc("/api/admin/config/directoryenabled", middleware.RequireAdminAuth(s.handlers.SetDirectoryEnabled)) // social handles - s.router.HandleFunc("/api/admin/config/socialhandles", middleware.RequireAdminAuth(admin.SetSocialHandles)) + s.router.HandleFunc("/api/admin/config/socialhandles", middleware.RequireAdminAuth(s.handlers.SetSocialHandles)) // set the number of video segments and duration per segment in a playlist - s.router.HandleFunc("/api/admin/config/video/streamlatencylevel", middleware.RequireAdminAuth(admin.SetStreamLatencyLevel)) + s.router.HandleFunc("/api/admin/config/video/streamlatencylevel", middleware.RequireAdminAuth(s.handlers.SetStreamLatencyLevel)) // set an array of video output configurations - s.router.HandleFunc("/api/admin/config/video/streamoutputvariants", middleware.RequireAdminAuth(admin.SetStreamOutputVariants)) + s.router.HandleFunc("/api/admin/config/video/streamoutputvariants", middleware.RequireAdminAuth(s.handlers.SetStreamOutputVariants)) // set s3 configuration - s.router.HandleFunc("/api/admin/config/s3", middleware.RequireAdminAuth(admin.SetS3Configuration)) + s.router.HandleFunc("/api/admin/config/s3", middleware.RequireAdminAuth(s.handlers.SetS3Configuration)) // set server url - s.router.HandleFunc("/api/admin/config/serverurl", middleware.RequireAdminAuth(admin.SetServerURL)) + s.router.HandleFunc("/api/admin/config/serverurl", middleware.RequireAdminAuth(s.handlers.SetServerURL)) // reset the YP registration - s.router.HandleFunc("/api/admin/yp/reset", middleware.RequireAdminAuth(admin.ResetYPRegistration)) + s.router.HandleFunc("/api/admin/yp/reset", middleware.RequireAdminAuth(s.handlers.ResetYPRegistration)) // set external action links - s.router.HandleFunc("/api/admin/config/externalactions", middleware.RequireAdminAuth(admin.SetExternalActions)) + s.router.HandleFunc("/api/admin/config/externalactions", middleware.RequireAdminAuth(s.handlers.SetExternalActions)) // set custom style css - s.router.HandleFunc("/api/admin/config/customstyles", middleware.RequireAdminAuth(admin.SetCustomStyles)) + s.router.HandleFunc("/api/admin/config/customstyles", middleware.RequireAdminAuth(s.handlers.SetCustomStyles)) // set custom style javascript - s.router.HandleFunc("/api/admin/config/customjavascript", middleware.RequireAdminAuth(admin.SetCustomJavascript)) + s.router.HandleFunc("/api/admin/config/customjavascript", middleware.RequireAdminAuth(s.handlers.SetCustomJavascript)) // Video playback metrics - s.router.HandleFunc("/api/admin/metrics/video", middleware.RequireAdminAuth(admin.GetVideoPlaybackMetrics)) + s.router.HandleFunc("/api/admin/metrics/video", middleware.RequireAdminAuth(s.handlers.GetVideoPlaybackMetrics)) // Is the viewer count hidden from viewers - s.router.HandleFunc("/api/admin/config/hideviewercount", middleware.RequireAdminAuth(admin.SetHideViewerCount)) + s.router.HandleFunc("/api/admin/config/hideviewercount", middleware.RequireAdminAuth(s.handlers.SetHideViewerCount)) // set disabling of search indexing - s.router.HandleFunc("/api/admin/config/disablesearchindexing", middleware.RequireAdminAuth(admin.SetDisableSearchIndexing)) + s.router.HandleFunc("/api/admin/config/disablesearchindexing", middleware.RequireAdminAuth(s.handlers.SetDisableSearchIndexing)) // enable/disable federation features - s.router.HandleFunc("/api/admin/config/federation/enable", middleware.RequireAdminAuth(admin.SetFederationEnabled)) + s.router.HandleFunc("/api/admin/config/federation/enable", middleware.RequireAdminAuth(s.handlers.SetFederationEnabled)) // set if federation activities are private - s.router.HandleFunc("/api/admin/config/federation/private", middleware.RequireAdminAuth(admin.SetFederationActivityPrivate)) + s.router.HandleFunc("/api/admin/config/federation/private", middleware.RequireAdminAuth(s.handlers.SetFederationActivityPrivate)) // set if fediverse engagement appears in chat - s.router.HandleFunc("/api/admin/config/federation/showengagement", middleware.RequireAdminAuth(admin.SetFederationShowEngagement)) + s.router.HandleFunc("/api/admin/config/federation/showengagement", middleware.RequireAdminAuth(s.handlers.SetFederationShowEngagement)) // set local federated username - s.router.HandleFunc("/api/admin/config/federation/username", middleware.RequireAdminAuth(admin.SetFederationUsername)) + s.router.HandleFunc("/api/admin/config/federation/username", middleware.RequireAdminAuth(s.handlers.SetFederationUsername)) // set federated go live message - s.router.HandleFunc("/api/admin/config/federation/livemessage", middleware.RequireAdminAuth(admin.SetFederationGoLiveMessage)) + s.router.HandleFunc("/api/admin/config/federation/livemessage", middleware.RequireAdminAuth(s.handlers.SetFederationGoLiveMessage)) // Federation blocked domains - s.router.HandleFunc("/api/admin/config/federation/blockdomains", middleware.RequireAdminAuth(admin.SetFederationBlockDomains)) + s.router.HandleFunc("/api/admin/config/federation/blockdomains", middleware.RequireAdminAuth(s.handlers.SetFederationBlockDomains)) // send a public message to the Fediverse from the server's user - s.router.HandleFunc("/api/admin/federation/send", middleware.RequireAdminAuth(admin.SendFederatedMessage)) + s.router.HandleFunc("/api/admin/federation/send", middleware.RequireAdminAuth(s.handlers.SendFederatedMessage)) // Return federated activities - s.router.HandleFunc("/api/admin/federation/actions", middleware.RequireAdminAuth(middleware.HandlePagination(admin.GetFederatedActions))) + s.router.HandleFunc("/api/admin/federation/actions", middleware.RequireAdminAuth(middleware.HandlePagination(s.handlers.GetFederatedActions))) // Prometheus metrics s.router.Handle("/api/admin/prometheus", middleware.RequireAdminAuth(func(rw http.ResponseWriter, r *http.Request) { @@ -375,46 +369,46 @@ func (s *webServer) setupAdminAPIRoutes() { })) // Configure outbound notification channels. - http.HandleFunc("/api/admin/config/notifications/discord", middleware.RequireAdminAuth(admin.SetDiscordNotificationConfiguration)) - http.HandleFunc("/api/admin/config/notifications/browser", middleware.RequireAdminAuth(admin.SetBrowserNotificationConfiguration)) + s.router.HandleFunc("/api/admin/config/notifications/discord", middleware.RequireAdminAuth(s.handlers.SetDiscordNotificationConfiguration)) + s.router.HandleFunc("/api/admin/config/notifications/browser", middleware.RequireAdminAuth(s.handlers.SetBrowserNotificationConfiguration)) } func (s *webServer) setupExternalThirdPartyAPIRoutes() { // Send a system message to chat - s.router.HandleFunc("/api/integrations/chat/system", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendSystemMessage)) + s.router.HandleFunc("/api/integrations/chat/system", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, s.handlers.SendSystemMessage)) // Send a system message to a single client - s.router.HandleFunc(utils.RestEndpoint("/api/integrations/chat/system/client/{clientId}", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendSystemMessageToConnectedClient))) + s.router.HandleFunc(utils.RestEndpoint("/api/integrations/chat/system/client/{clientId}", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, s.handlers.SendSystemMessageToConnectedClient))) // Send a user message to chat *NO LONGER SUPPORTED - s.router.HandleFunc("/api/integrations/chat/user", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendChatMessages, admin.SendUserMessage)) + s.router.HandleFunc("/api/integrations/chat/user", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendChatMessages, s.handlers.SendUserMessage)) // Send a message to chat as a specific 3rd party bot/integration based on its access token - s.router.HandleFunc("/api/integrations/chat/send", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendChatMessages, admin.SendIntegrationChatMessage)) + s.router.HandleFunc("/api/integrations/chat/send", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendChatMessages, s.handlers.SendIntegrationChatMessage)) // Send a user action to chat - s.router.HandleFunc("/api/integrations/chat/action", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendChatAction)) + s.router.HandleFunc("/api/integrations/chat/action", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, s.handlers.SendChatAction)) // Hide chat message - s.router.HandleFunc("/api/integrations/chat/messagevisibility", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, admin.ExternalUpdateMessageVisibility)) + s.router.HandleFunc("/api/integrations/chat/messagevisibility", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, s.handlers.ExternalUpdateMessageVisibility)) // Stream title - s.router.HandleFunc("/api/integrations/streamtitle", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, admin.ExternalSetStreamTitle)) + s.router.HandleFunc("/api/integrations/streamtitle", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, s.handlers.ExternalSetStreamTitle)) // Get chat history - s.router.HandleFunc("/api/integrations/chat", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, controllers.ExternalGetChatMessages)) + s.router.HandleFunc("/api/integrations/chat", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, s.handlers.ExternalGetChatMessages)) // Connected clients - s.router.HandleFunc("/api/integrations/clients", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, admin.ExternalGetConnectedChatClients)) + s.router.HandleFunc("/api/integrations/clients", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, s.handlers.ExternalGetConnectedChatClients)) } func (s *webServer) setupModerationAPIRoutes() { // Update chat message visibility - s.router.HandleFunc("/api/chat/messagevisibility", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateMessageVisibility)) + s.router.HandleFunc("/api/chat/messagevisibility", middleware.RequireUserModerationScopeAccesstoken(s.handlers.UpdateMessageVisibility)) // Enable/disable a user - s.router.HandleFunc("/api/chat/users/setenabled", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateUserEnabled)) + s.router.HandleFunc("/api/chat/users/setenabled", middleware.RequireUserModerationScopeAccesstoken(s.handlers.UpdateUserEnabled)) // Get a user's details - s.router.HandleFunc("/api/moderation/chat/user/", middleware.RequireUserModerationScopeAccesstoken(moderation.GetUserDetails)) + s.router.HandleFunc("/api/moderation/chat/user/", middleware.RequireUserModerationScopeAccesstoken(s.handlers.GetUserDetails)) } diff --git a/webserver/webserver_test.go b/webserver/webserver_test.go index fb273a7b1..aebc71dd6 100644 --- a/webserver/webserver_test.go +++ b/webserver/webserver_test.go @@ -33,12 +33,3 @@ func TestPrometheusDebugPath(t *testing.T) { t.Errorf("Expected 404, got %d", w.Result().StatusCode) } } - -func TestTestingEndpoint(t *testing.T) { - r := httptest.NewRequest(http.MethodGet, "/test", nil) - w := httptest.NewRecorder() - srv.ServeHTTP(w, r) - if w.Result().StatusCode != http.StatusOK { - t.Errorf("Expected 200, got %d", w.Result().StatusCode) - } -}