chore(api): reorganize handlers into webserver package

This commit is contained in:
Gabe Kangas
2024-07-01 21:44:37 -07:00
parent e200692502
commit 93c0a20935
54 changed files with 904 additions and 933 deletions

View File

@@ -3,8 +3,8 @@ package handlers
import (
"net/http"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/controllers/admin"
"github.com/owncast/owncast/core/rtmp"
"github.com/owncast/owncast/webserver/handlers/admin"
"github.com/owncast/owncast/webserver/handlers/generated"
"github.com/owncast/owncast/webserver/router/middleware"
)
@@ -154,11 +154,11 @@ func (*ServerInterfaceImpl) GetWarningsOptions(w http.ResponseWriter, r *http.Re
}
func (*ServerInterfaceImpl) GetFollowersAdmin(w http.ResponseWriter, r *http.Request, params generated.GetFollowersAdminParams) {
middleware.RequireAdminAuth(middleware.HandlePagination(controllers.GetFollowers))(w, r)
middleware.RequireAdminAuth(middleware.HandlePagination(GetFollowers))(w, r)
}
func (*ServerInterfaceImpl) GetFollowersAdminOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(middleware.HandlePagination(controllers.GetFollowers))(w, r)
middleware.RequireAdminAuth(middleware.HandlePagination(GetFollowers))(w, r)
}
func (*ServerInterfaceImpl) GetPendingFollowRequests(w http.ResponseWriter, r *http.Request) {
@@ -304,3 +304,9 @@ func (*ServerInterfaceImpl) GetFederatedActions(w http.ResponseWriter, r *http.R
func (*ServerInterfaceImpl) GetFederatedActionsOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(middleware.HandlePagination(admin.GetFederatedActions))(w, r)
}
// DisconnectInboundConnection will force-disconnect an inbound stream.
func DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) {
rtmp.Disconnect()
w.WriteHeader(http.StatusOK)
}

View File

@@ -0,0 +1,35 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/core/data"
webutils "github.com/owncast/owncast/webserver/utils"
)
// 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 {
webutils.WriteSimpleResponse(w, false, "unable to update appearance variable values")
return
}
if err := data.SetCustomColorVariableValues(values.Value); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.WriteSimpleResponse(w, true, "custom appearance variables updated")
}

View File

@@ -0,0 +1,384 @@
package admin
// this is endpoint logic
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/persistence/userrepository"
"github.com/owncast/owncast/utils"
webutils "github.com/owncast/owncast/webserver/utils"
log "github.com/sirupsen/logrus"
)
// ExternalUpdateMessageVisibility updates an array of message IDs to have the same visiblity.
func ExternalUpdateMessageVisibility(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
UpdateMessageVisibility(w, r)
}
// UpdateMessageVisibility updates an array of message IDs to have the same visiblity.
func UpdateMessageVisibility(w http.ResponseWriter, r *http.Request) {
type messageVisibilityUpdateRequest struct {
IDArray []string `json:"idArray"`
Visible bool `json:"visible"`
}
if r.Method != http.MethodPost {
// nolint:goconst
webutils.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var request messageVisibilityUpdateRequest
if err := decoder.Decode(&request); err != nil {
log.Errorln(err)
webutils.WriteSimpleResponse(w, false, "")
return
}
if err := chat.SetMessagesVisibility(request.IDArray, request.Visible); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.WriteSimpleResponse(w, true, "changed")
}
// BanIPAddress will manually ban an IP address.
func BanIPAddress(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
webutils.WriteSimpleResponse(w, false, "unable to ban IP address")
return
}
if err := data.BanIPAddress(configValue.Value.(string), "manually added"); err != nil {
webutils.WriteSimpleResponse(w, false, "error saving IP address ban")
return
}
webutils.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) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
webutils.WriteSimpleResponse(w, false, "unable to unban IP address")
return
}
if err := data.RemoveIPAddressBan(configValue.Value.(string)); err != nil {
webutils.WriteSimpleResponse(w, false, "error removing IP address ban")
return
}
webutils.WriteSimpleResponse(w, true, "IP address unbanned")
}
// GetIPAddressBans will return all the banned IP addresses.
func GetIPAddressBans(w http.ResponseWriter, r *http.Request) {
bans, err := data.GetIPAddressBans()
if err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.WriteResponse(w, bans)
}
// UpdateUserEnabled enable or disable a single user by ID.
func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) {
type blockUserRequest struct {
UserID string `json:"userId"`
Enabled bool `json:"enabled"`
}
if r.Method != http.MethodPost {
webutils.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var request blockUserRequest
if err := decoder.Decode(&request); err != nil {
log.Errorln(err)
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
if request.UserID == "" {
webutils.WriteSimpleResponse(w, false, "must provide userId")
return
}
userRepository := userrepository.Get()
// Disable/enable the user
if err := userRepository.SetEnabled(request.UserID, request.Enabled); err != nil {
log.Errorln("error changing user enabled status", err)
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
// Hide/show the user's chat messages if disabling.
// Leave hidden messages hidden to be safe.
if !request.Enabled {
if err := chat.SetMessageVisibilityForUserID(request.UserID, request.Enabled); err != nil {
log.Errorln("error changing user messages visibility", err)
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
}
// Forcefully disconnect the user from the chat
if !request.Enabled {
clients, err := chat.GetClientsForUser(request.UserID)
if len(clients) == 0 {
// Nothing to do
return
}
if err != nil {
log.Errorln("error fetching clients for user: ", err)
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
chat.DisconnectClients(clients)
disconnectedUser := userRepository.GetUserByID(request.UserID)
_ = chat.SendSystemAction(fmt.Sprintf("**%s** has been removed from chat.", disconnectedUser.DisplayName), true)
localIP4Address := "127.0.0.1"
localIP6Address := "::1"
// Ban this user's IP address.
for _, client := range clients {
ipAddress := client.IPAddress
if ipAddress != localIP4Address && ipAddress != localIP6Address {
reason := fmt.Sprintf("Banning of %s", disconnectedUser.DisplayName)
if err := data.BanIPAddress(ipAddress, reason); err != nil {
log.Errorln("error banning IP address: ", err)
}
}
}
}
webutils.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) {
w.Header().Set("Content-Type", "application/json")
userRepository := userrepository.Get()
users := userRepository.GetDisabledUsers()
webutils.WriteResponse(w, users)
}
// UpdateUserModerator will set the moderator status for a user ID.
func UpdateUserModerator(w http.ResponseWriter, r *http.Request) {
type request struct {
UserID string `json:"userId"`
IsModerator bool `json:"isModerator"`
}
if r.Method != http.MethodPost {
webutils.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var req request
if err := decoder.Decode(&req); err != nil {
webutils.WriteSimpleResponse(w, false, "")
return
}
userRepository := userrepository.Get()
// Update the user object with new moderation access.
if err := userRepository.SetModerator(req.UserID, req.IsModerator); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
// Update the clients for this user to know about the moderator access change.
if err := chat.SendConnectedClientInfoToUser(req.UserID); err != nil {
log.Debugln(err)
}
webutils.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) {
w.Header().Set("Content-Type", "application/json")
userRepository := userrepository.Get()
users := userRepository.GetModeratorUsers()
webutils.WriteResponse(w, users)
}
// GetChatMessages returns all of the chat messages, unfiltered.
func GetChatMessages(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
messages := chat.GetChatModerationHistory()
webutils.WriteResponse(w, messages)
}
// SendSystemMessage will send an official "SYSTEM" message to chat on behalf of your server.
func SendSystemMessage(integration models.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 {
webutils.InternalErrorHandler(w, err)
return
}
if err := chat.SendSystemMessage(message.Body, false); err != nil {
webutils.BadRequestHandler(w, err)
}
webutils.WriteSimpleResponse(w, true, "sent")
}
// SendSystemMessageToConnectedClient will handle incoming requests to send a single message to a single connected client by ID.
func SendSystemMessageToConnectedClient(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
clientIDText, err := utils.GetURLParam(r, "clientId")
if err != nil {
webutils.BadRequestHandler(w, err)
return
}
clientIDNumeric, err := strconv.ParseUint(clientIDText, 10, 32)
if err != nil {
webutils.BadRequestHandler(w, err)
return
}
var message events.SystemMessageEvent
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
webutils.InternalErrorHandler(w, err)
return
}
chat.SendSystemMessageToClient(uint(clientIDNumeric), message.Body)
webutils.WriteSimpleResponse(w, true, "sent")
}
// SendUserMessage will send a message to chat on behalf of a user. *Depreciated*.
func SendUserMessage(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
webutils.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 models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
name := integration.DisplayName
if name == "" {
webutils.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 {
webutils.InternalErrorHandler(w, err)
return
}
event.SetDefaults()
event.RenderBody()
event.Type = "CHAT"
if event.Empty() {
webutils.BadRequestHandler(w, errors.New("invalid message"))
return
}
event.User = &models.User{
ID: integration.ID,
DisplayName: name,
DisplayColor: integration.DisplayColor,
CreatedAt: integration.CreatedAt,
IsBot: true,
}
if err := chat.Broadcast(&event); err != nil {
webutils.BadRequestHandler(w, err)
return
}
chat.SaveUserMessage(event)
webutils.WriteSimpleResponse(w, true, "sent")
}
// SendChatAction will send a generic chat action.
func SendChatAction(integration models.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 {
webutils.InternalErrorHandler(w, err)
return
}
message.SetDefaults()
message.RenderBody()
if err := chat.SendSystemAction(message.Body, false); err != nil {
webutils.BadRequestHandler(w, err)
return
}
webutils.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) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
webutils.WriteSimpleResponse(w, false, "unable to update chat established user only mode")
return
}
if err := data.SetChatEstablishedUsersOnlyMode(configValue.Value.(bool)); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.WriteSimpleResponse(w, true, "chat established users only mode updated")
}

View File

@@ -0,0 +1,915 @@
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/core/chat"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/webhooks"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
webutils "github.com/owncast/owncast/webserver/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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
// Update Fediverse followers about this change.
if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
if value != "" {
sendSystemChatAction(fmt.Sprintf("Stream title changed to **%s**", value), true)
go webhooks.SendStreamStatusEvent(models.StreamTitleUpdated)
}
webutils.WriteSimpleResponse(w, true, "changed")
}
// ExternalSetStreamTitle will change the stream title on behalf of an external integration API request.
func ExternalSetStreamTitle(integration models.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
// Update Fediverse followers about this change.
if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
// Update Fediverse followers about this change.
if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to find image data")
return
}
bytes, extension, err := utils.DecodeBase64Image(value)
if err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
imgPath := filepath.Join("data", "logo"+extension)
if err := os.WriteFile(imgPath, bytes, 0o600); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
if err := data.SetLogoPath("logo" + extension); err != nil {
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
if err := data.SetFfmpegPath(configValue.Value.(string)); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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) {
webutils.WriteSimpleResponse(w, false, "Port number must be between 1 and 65535")
return
}
if err := data.SetHTTPPortNumber(port); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.WriteSimpleResponse(w, true, "HTTP port set")
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.WriteSimpleResponse(w, true, "HTTP listen address set")
return
}
webutils.WriteSimpleResponse(w, false, "Invalid IP address")
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "could not read server url")
return
}
serverHostString := utils.GetHostnameFromURLString(rawValue)
if serverHostString == "" {
webutils.WriteSimpleResponse(w, false, "server url value invalid")
return
}
// Block Private IP URLs
ipAddr, ipErr := netip.ParseAddr(utils.GetHostnameWithoutPortFromURLString(rawValue))
if ipErr == nil && ipAddr.IsPrivate() {
webutils.WriteSimpleResponse(w, false, "Server URL cannot be private")
return
}
// Trim any trailing slash
serverURL := strings.TrimRight(rawValue, "/")
if err := data.SetServerURL(serverURL); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "error setting stream latency "+err.Error())
return
}
webutils.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 {
webutils.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)) {
webutils.WriteSimpleResponse(w, false, "s3 support requires an endpoint")
return
}
if newS3Config.Value.AccessKey == "" || newS3Config.Value.Secret == "" {
webutils.WriteSimpleResponse(w, false, "s3 support requires an access key and secret")
return
}
if newS3Config.Value.Region == "" {
webutils.WriteSimpleResponse(w, false, "s3 support requires a region and endpoint")
return
}
if newS3Config.Value.Bucket == "" {
webutils.WriteSimpleResponse(w, false, "s3 support requires a bucket created for storing public video segments")
return
}
}
if err := data.SetS3Config(newS3Config.Value); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to update video config with provided values "+err.Error())
return
}
if err := data.SetStreamOutputVariants(videoVariants.Value); err != nil {
webutils.WriteSimpleResponse(w, false, "unable to update video config with provided values "+err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to update social handles with provided values")
return
}
if err := data.SetSocialHandles(socialHandles.Value); err != nil {
webutils.WriteSimpleResponse(w, false, "unable to update social handles with provided values")
return
}
// Update Fediverse followers about this change.
if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to update chat disabled")
return
}
if err := data.SetChatDisabled(configValue.Value.(bool)); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to change video codec")
return
}
if err := data.SetVideoCodec(configValue.Value.(string)); err != nil {
webutils.WriteSimpleResponse(w, false, "unable to update codec")
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to update external actions with provided values")
return
}
if err := data.SetExternalActions(actions.Value); err != nil {
webutils.WriteSimpleResponse(w, false, "unable to update external actions with provided values")
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to update custom styles")
return
}
if err := data.SetCustomStyles(customStyles.Value.(string)); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to update custom javascript")
return
}
if err := data.SetCustomJavascript(customJavascript.Value.(string)); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to update forbidden usernames with provided values")
return
}
if err := data.SetForbiddenUsernameList(request.Value); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to update suggested usernames with provided values")
return
}
if err := data.SetSuggestedUsernamesList(request.Value); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to update chat join messages enabled")
return
}
if err := data.SetChatJoinMessagesEnabled(configValue.Value.(bool)); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to update hiding viewer count")
return
}
if err := data.SetHideViewerCount(configValue.Value.(bool)); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to update search indexing")
return
}
if err := data.SetDisableSearchIndexing(configValue.Value.(bool)); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to update custom video serving endpoint")
return
}
value, ok := endpoint.Value.(string)
if !ok {
webutils.WriteSimpleResponse(w, false, "unable to update custom video serving endpoint")
return
}
if err := data.SetVideoServingEndpoint(value); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.WriteSimpleResponse(w, true, "custom video serving endpoint updated")
}
// SetChatSpamProtectionEnabled will enable or disable the chat spam protection.
func SetChatSpamProtectionEnabled(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetChatSpamProtectionEnabled(configValue.Value.(bool)); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.WriteSimpleResponse(w, true, "chat spam protection changed")
}
// SetChatSlurFilterEnabled will enable or disable the chat slur filter.
func SetChatSlurFilterEnabled(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetChatSlurFilterEnabled(configValue.Value.(bool)); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.WriteSimpleResponse(w, true, "chat message slur filter changed")
}
func requirePOST(w http.ResponseWriter, r *http.Request) bool {
if r.Method != http.MethodPost {
webutils.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)
webutils.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 {
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to update stream keys with provided values")
return
}
if len(streamKeys.Value) == 0 {
webutils.WriteSimpleResponse(w, false, "must provide at least one valid stream key")
return
}
for _, streamKey := range streamKeys.Value {
if streamKey.Key == "" {
webutils.WriteSimpleResponse(w, false, "stream key cannot be empty")
return
}
}
if err := data.SetStreamKeys(streamKeys.Value); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.WriteSimpleResponse(w, true, "changed")
}

View File

@@ -0,0 +1,25 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/models"
webutils "github.com/owncast/owncast/webserver/utils"
)
// GetConnectedChatClients returns currently connected clients.
func 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 {
webutils.InternalErrorHandler(w, err)
}
}
// ExternalGetConnectedChatClients returns currently connected clients.
func ExternalGetConnectedChatClients(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
GetConnectedChatClients(w, r)
}

View File

@@ -0,0 +1,21 @@
package admin
import (
"net/http"
"github.com/owncast/owncast/core"
webutils "github.com/owncast/owncast/webserver/utils"
"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 {
webutils.WriteSimpleResponse(w, false, "no inbound stream connected")
return
}
rtmp.Disconnect()
webutils.WriteSimpleResponse(w, true, "inbound stream disconnected")
}

View File

@@ -0,0 +1,96 @@
package admin
import (
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/utils"
webutils "github.com/owncast/owncast/webserver/utils"
)
// UploadCustomEmoji allows POSTing a new custom emoji to the server.
func UploadCustomEmoji(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type postEmoji struct {
Name string `json:"name"`
Data string `json:"data"`
}
emoji := new(postEmoji)
if err := json.NewDecoder(r.Body).Decode(emoji); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
bytes, _, err := utils.DecodeBase64Image(emoji.Data)
if err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
// Prevent path traversal attacks
emojiFileName := filepath.Base(emoji.Name)
targetPath := filepath.Join(config.CustomEmojiPath, emojiFileName)
err = os.MkdirAll(config.CustomEmojiPath, 0o700)
if err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
if utils.DoesFileExists(targetPath) {
webutils.WriteSimpleResponse(w, false, fmt.Sprintf("An emoji with the name %q already exists", emojiFileName))
return
}
if err = os.WriteFile(targetPath, bytes, 0o600); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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) {
return
}
type deleteEmoji struct {
Name string `json:"name"`
}
emoji := new(deleteEmoji)
if err := json.NewDecoder(r.Body).Decode(emoji); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
targetPath := filepath.Join(config.CustomEmojiPath, emoji.Name)
if !filepath.IsLocal(targetPath) {
webutils.WriteSimpleResponse(w, false, "Emoji path is not valid")
return
}
if err := os.Remove(targetPath); err != nil {
if os.IsNotExist(err) {
webutils.WriteSimpleResponse(w, false, fmt.Sprintf("Emoji %q doesn't exist", emoji.Name))
} else {
webutils.WriteSimpleResponse(w, false, err.Error())
}
return
}
webutils.WriteSimpleResponse(w, true, fmt.Sprintf("Emoji %q has been deleted", emoji.Name))
}

View File

@@ -0,0 +1,109 @@
package admin
import (
"encoding/json"
"errors"
"net/http"
"time"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/persistence/userrepository"
"github.com/owncast/owncast/utils"
webutils "github.com/owncast/owncast/webserver/utils"
)
type deleteExternalAPIUserRequest struct {
Token string `json:"token"`
}
type createExternalAPIUserRequest struct {
Name string `json:"name"`
Scopes []string `json:"scopes"`
}
// CreateExternalAPIUser will generate a 3rd party access token.
func CreateExternalAPIUser(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var request createExternalAPIUserRequest
if err := decoder.Decode(&request); err != nil {
webutils.BadRequestHandler(w, err)
return
}
userRepository := userrepository.Get()
// Verify all the scopes provided are valid
if !userRepository.HasValidScopes(request.Scopes) {
webutils.BadRequestHandler(w, errors.New("one or more invalid scopes provided"))
return
}
token, err := utils.GenerateAccessToken()
if err != nil {
webutils.InternalErrorHandler(w, err)
return
}
color := utils.GenerateRandomDisplayColor(config.MaxUserColor)
if err := userRepository.InsertExternalAPIUser(token, request.Name, color, request.Scopes); err != nil {
webutils.InternalErrorHandler(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
webutils.WriteResponse(w, models.ExternalAPIUser{
AccessToken: token,
DisplayName: request.Name,
DisplayColor: color,
Scopes: request.Scopes,
CreatedAt: time.Now(),
LastUsedAt: nil,
})
}
// GetExternalAPIUsers will return all 3rd party access tokens.
func GetExternalAPIUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
userRepository := userrepository.Get()
tokens, err := userRepository.GetExternalAPIUser()
if err != nil {
webutils.InternalErrorHandler(w, err)
return
}
webutils.WriteResponse(w, tokens)
}
// DeleteExternalAPIUser will return a single 3rd party access token.
func DeleteExternalAPIUser(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodPost {
webutils.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var request deleteExternalAPIUserRequest
if err := decoder.Decode(&request); err != nil {
webutils.BadRequestHandler(w, err)
return
}
if request.Token == "" {
webutils.BadRequestHandler(w, errors.New("must provide a token"))
return
}
userRepository := userrepository.Get()
if err := userRepository.DeleteExternalAPIUser(request.Token); err != nil {
webutils.InternalErrorHandler(w, err)
return
}
webutils.WriteSimpleResponse(w, true, "deleted token")
}

View File

@@ -0,0 +1,179 @@
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/core/data"
webutils "github.com/owncast/owncast/webserver/utils"
)
// 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 {
webutils.WriteSimpleResponse(w, false, "unable to send message")
return
}
if err := activitypub.SendPublicFederatedMessage(message); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
// Update Fediverse followers about this change.
if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
response := webutils.PaginatedResponse{
Total: total,
Results: activities,
}
webutils.WriteResponse(w, response)
}

View File

@@ -0,0 +1,82 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/activitypub/persistence"
"github.com/owncast/owncast/activitypub/requests"
"github.com/owncast/owncast/core/data"
webutils "github.com/owncast/owncast/webserver/utils"
)
// ApproveFollower will approve a federated follow request.
func ApproveFollower(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type approveFollowerRequest struct {
ActorIRI string `json:"actorIRI"`
Approved bool `json:"approved"`
}
decoder := json.NewDecoder(r.Body)
var approval approveFollowerRequest
if err := decoder.Decode(&approval); err != nil {
webutils.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 {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
localAccountName := data.GetDefaultFederationUsername()
followRequest, err := persistence.GetFollower(approval.ActorIRI)
if err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
// Send the approval to the follow requestor.
if err := requests.SendFollowAccept(followRequest.Inbox, followRequest.RequestObject, localAccountName); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
} else {
// Remove/block a follower
if err := persistence.BlockOrRejectFollower(approval.ActorIRI); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
}
webutils.WriteSimpleResponse(w, true, "follower updated")
}
// GetPendingFollowRequests will return a list of pending follow requests.
func GetPendingFollowRequests(w http.ResponseWriter, r *http.Request) {
requests, err := persistence.GetPendingFollowRequests()
if err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.WriteResponse(w, requests)
}
// GetBlockedAndRejectedFollowers will return blocked and rejected followers.
func GetBlockedAndRejectedFollowers(w http.ResponseWriter, r *http.Request) {
rejections, err := persistence.GetBlockedAndRejectedFollowers()
if err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.WriteResponse(w, rejections)
}

View File

@@ -0,0 +1,20 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/metrics"
log "github.com/sirupsen/logrus"
)
// GetHardwareStats will return hardware utilization over time.
func GetHardwareStats(w http.ResponseWriter, r *http.Request) {
m := metrics.GetMetrics()
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(m)
if err != nil {
log.Errorln(err)
}
}

View File

@@ -0,0 +1,58 @@
package admin
import (
"encoding/json"
"net/http"
"time"
"github.com/owncast/owncast/logging"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
)
// GetLogs will return all logs.
func GetLogs(w http.ResponseWriter, r *http.Request) {
logs := logging.Logger.AllEntries()
response := make([]logsResponse, 0)
for i := 0; i < len(logs); i++ {
response = append(response, fromEntry(logs[i]))
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Errorln(err)
}
}
// GetWarnings will return only warning and error logs.
func GetWarnings(w http.ResponseWriter, r *http.Request) {
logs := logging.Logger.WarningEntries()
response := make([]logsResponse, 0)
for i := 0; i < len(logs); i++ {
logEntry := logs[i]
if logEntry != nil {
response = append(response, fromEntry(logEntry))
}
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Errorln(err)
}
}
type logsResponse struct {
Time time.Time `json:"time"`
Message string `json:"message"`
Level string `json:"level"`
}
func fromEntry(e *logrus.Entry) logsResponse {
return logsResponse{
Message: e.Message,
Level: e.Level.String(),
Time: e.Time,
}
}

View File

@@ -0,0 +1,60 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
webutils "github.com/owncast/owncast/webserver/utils"
)
// 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 {
webutils.WriteSimpleResponse(w, false, "unable to update discord config with provided values")
return
}
if err := data.SetDiscordConfig(config.Value); err != nil {
webutils.WriteSimpleResponse(w, false, "unable to update discord config with provided values")
return
}
webutils.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 {
webutils.WriteSimpleResponse(w, false, "unable to update browser push config with provided values")
return
}
if err := data.SetBrowserPushConfig(config.Value); err != nil {
webutils.WriteSimpleResponse(w, false, "unable to update browser push config with provided values")
return
}
webutils.WriteSimpleResponse(w, true, "updated browser push config with provided values")
}

View File

@@ -0,0 +1,174 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/transcoder"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
"github.com/owncast/owncast/webserver/router/middleware"
log "github.com/sirupsen/logrus"
)
// GetServerConfig gets the config details of the server.
func GetServerConfig(w http.ResponseWriter, r *http.Request) {
ffmpeg := utils.ValidatedFfmpegPath(data.GetFfMpegPath())
usernameBlocklist := data.GetForbiddenUsernameList()
usernameSuggestions := data.GetSuggestedUsernamesList()
videoQualityVariants := make([]models.StreamOutputVariant, 0)
for _, variant := range data.GetStreamOutputVariants() {
videoQualityVariants = append(videoQualityVariants, models.StreamOutputVariant{
Name: variant.GetName(),
IsAudioPassthrough: variant.GetIsAudioPassthrough(),
IsVideoPassthrough: variant.IsVideoPassthrough,
Framerate: variant.GetFramerate(),
VideoBitrate: variant.VideoBitrate,
AudioBitrate: variant.AudioBitrate,
CPUUsageLevel: variant.CPUUsageLevel,
ScaledWidth: variant.ScaledWidth,
ScaledHeight: variant.ScaledHeight,
})
}
response := serverConfigAdminResponse{
InstanceDetails: webConfigResponse{
Name: data.GetServerName(),
Summary: data.GetServerSummary(),
Tags: data.GetServerMetadataTags(),
ExtraPageContent: data.GetExtraPageBodyContent(),
StreamTitle: data.GetStreamTitle(),
WelcomeMessage: data.GetServerWelcomeMessage(),
OfflineMessage: data.GetCustomOfflineMessage(),
Logo: data.GetLogoPath(),
SocialHandles: data.GetSocialHandles(),
NSFW: data.GetNSFW(),
CustomStyles: data.GetCustomStyles(),
CustomJavascript: data.GetCustomJavascript(),
AppearanceVariables: data.GetCustomColorVariableValues(),
},
FFmpegPath: ffmpeg,
AdminPassword: data.GetAdminPassword(),
StreamKeys: data.GetStreamKeys(),
StreamKeyOverridden: config.TemporaryStreamKey != "",
WebServerPort: config.WebServerPort,
WebServerIP: config.WebServerIP,
RTMPServerPort: data.GetRTMPPortNumber(),
ChatDisabled: data.GetChatDisabled(),
ChatJoinMessagesEnabled: data.GetChatJoinPartMessagesEnabled(),
SocketHostOverride: data.GetWebsocketOverrideHost(),
VideoServingEndpoint: data.GetVideoServingEndpoint(),
ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(),
ChatSpamProtectionEnabled: data.GetChatSpamProtectionEnabled(),
ChatSlurFilterEnabled: data.GetChatSlurFilterEnabled(),
HideViewerCount: data.GetHideViewerCount(),
DisableSearchIndexing: data.GetDisableSearchIndexing(),
VideoSettings: videoSettings{
VideoQualityVariants: videoQualityVariants,
LatencyLevel: data.GetStreamLatencyLevel().Level,
},
YP: yp{
Enabled: data.GetDirectoryEnabled(),
InstanceURL: data.GetServerURL(),
},
S3: data.GetS3Config(),
ExternalActions: data.GetExternalActions(),
SupportedCodecs: transcoder.GetCodecs(ffmpeg),
VideoCodec: data.GetVideoCodec(),
ForbiddenUsernames: usernameBlocklist,
SuggestedUsernames: usernameSuggestions,
Federation: federationConfigResponse{
Enabled: data.GetFederationEnabled(),
IsPrivate: data.GetFederationIsPrivate(),
Username: data.GetFederationUsername(),
GoLiveMessage: data.GetFederationGoLiveMessage(),
ShowEngagement: data.GetFederationShowEngagement(),
BlockedDomains: data.GetBlockedFederatedDomains(),
},
Notifications: notificationsConfigResponse{
Discord: data.GetDiscordConfig(),
Browser: data.GetBrowserPushConfig(),
},
}
w.Header().Set("Content-Type", "application/json")
middleware.DisableCache(w)
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Errorln(err)
}
}
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"`
ChatSpamProtectionEnabled bool `json:"chatSpamProtectionEnabled"`
ChatSlurFilterEnabled bool `json:"chatSlurFilterEnabled"`
DisableSearchIndexing bool `json:"disableSearchIndexing"`
StreamKeyOverridden bool `json:"streamKeyOverridden"`
HideViewerCount bool `json:"hideViewerCount"`
}
type videoSettings struct {
VideoQualityVariants []models.StreamOutputVariant `json:"videoQualityVariants"`
LatencyLevel int `json:"latencyLevel"`
}
type webConfigResponse struct {
AppearanceVariables map[string]string `json:"appearanceVariables"`
Version string `json:"version"`
WelcomeMessage string `json:"welcomeMessage"`
OfflineMessage string `json:"offlineMessage"`
Logo string `json:"logo"`
Name string `json:"name"`
ExtraPageContent string `json:"extraPageContent"`
StreamTitle string `json:"streamTitle"` // What's going on with the current stream
CustomStyles string `json:"customStyles"`
CustomJavascript string `json:"customJavascript"`
Summary string `json:"summary"`
Tags []string `json:"tags"`
SocialHandles []models.SocialHandle `json:"socialHandles"`
NSFW bool `json:"nsfw"`
}
type yp struct {
InstanceURL string `json:"instanceUrl"` // The public URL the directory should link to
YPServiceURL string `json:"-"` // The base URL to the YP API to register with (optional)
Enabled bool `json:"enabled"`
}
type federationConfigResponse struct {
Username string `json:"username"`
GoLiveMessage string `json:"goLiveMessage"`
BlockedDomains []string `json:"blockedDomains"`
Enabled bool `json:"enabled"`
IsPrivate bool `json:"isPrivate"`
ShowEngagement bool `json:"showEngagement"`
}
type notificationsConfigResponse struct {
Browser models.BrowserNotificationConfiguration `json:"browser"`
Discord models.DiscordConfiguration `json:"discord"`
}

View File

@@ -0,0 +1,52 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/metrics"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/webserver/router/middleware"
log "github.com/sirupsen/logrus"
)
// Status gets the details of the inbound broadcaster.
func Status(w http.ResponseWriter, r *http.Request) {
broadcaster := core.GetBroadcaster()
status := core.GetStatus()
currentBroadcast := core.GetCurrentBroadcast()
health := metrics.GetStreamHealthOverview()
response := adminStatusResponse{
Broadcaster: broadcaster,
CurrentBroadcast: currentBroadcast,
Online: status.Online,
Health: health,
ViewerCount: status.ViewerCount,
OverallPeakViewerCount: status.OverallMaxViewerCount,
SessionPeakViewerCount: status.SessionMaxViewerCount,
VersionNumber: status.VersionNumber,
StreamTitle: data.GetStreamTitle(),
}
w.Header().Set("Content-Type", "application/json")
middleware.DisableCache(w)
err := json.NewEncoder(w).Encode(response)
if err != nil {
log.Errorln(err)
}
}
type adminStatusResponse struct {
Broadcaster *models.Broadcaster `json:"broadcaster"`
CurrentBroadcast *models.CurrentBroadcast `json:"currentBroadcast"`
Health *models.StreamHealthOverview `json:"health"`
StreamTitle string `json:"streamTitle"`
VersionNumber string `json:"versionNumber"`
ViewerCount int `json:"viewerCount"`
OverallPeakViewerCount int `json:"overallPeakViewerCount"`
SessionPeakViewerCount int `json:"sessionPeakViewerCount"`
Online bool `json:"online"`
}

View File

@@ -0,0 +1,202 @@
package admin
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"strconv"
"strings"
"github.com/owncast/owncast/config"
webutils "github.com/owncast/owncast/webserver/utils"
log "github.com/sirupsen/logrus"
)
/*
The auto-update relies on some guesses and hacks to determine how the binary
is being run.
It determines if is running under systemd by asking systemd about the PID
and checking the parent pid or INVOCATION_ID property is set for it.
It also determines if the binary is running under a container by figuring out
the container ID as a fallback to refuse an in-place update within a container.
In general it's disabled for everyone and the features are enabled only if
specific conditions are met.
1. Cannot be run inside a container.
2. Cannot be run from source (aka platform is "dev")
3. Must be run under systemd to support auto-restart.
*/
// AutoUpdateOptions will return what auto update options are available.
func AutoUpdateOptions(w http.ResponseWriter, r *http.Request) {
type autoUpdateOptionsResponse struct {
SupportsUpdate bool `json:"supportsUpdate"`
CanRestart bool `json:"canRestart"`
}
updateOptions := autoUpdateOptionsResponse{
SupportsUpdate: false,
CanRestart: false,
}
// Nothing is supported when running under "dev" or the feature is
// explicitly disabled.
if config.BuildPlatform == "dev" || !config.EnableAutoUpdate {
webutils.WriteResponse(w, updateOptions)
return
}
// If we are not in a container then we can update in place.
if getContainerID() == "" {
updateOptions.SupportsUpdate = true
}
updateOptions.CanRestart = isRunningUnderSystemD()
webutils.WriteResponse(w, updateOptions)
}
// AutoUpdateStart will begin the auto update process.
func AutoUpdateStart(w http.ResponseWriter, r *http.Request) {
// We return the console output directly to the client.
w.Header().Set("Content-Type", "text/plain")
// Download the installer and save it to a temp file.
updater, err := downloadInstaller()
if err != nil {
log.Errorln(err)
webutils.WriteSimpleResponse(w, false, "failed to download and run installer")
return
}
fw := flushWriter{w: w}
if f, ok := w.(http.Flusher); ok {
fw.f = f
}
// Run the installer.
cmd := exec.Command("bash", updater)
cmd.Env = append(os.Environ(), "NO_COLOR=true")
cmd.Stdout = &fw
cmd.Stderr = &fw
if err := cmd.Run(); err != nil {
log.Debugln(err)
if _, err := w.Write([]byte("Unable to complete update: " + err.Error())); err != nil {
log.Errorln(err)
}
return
}
}
// AutoUpdateForceQuit will force quit the service.
func AutoUpdateForceQuit(w http.ResponseWriter, r *http.Request) {
log.Warnln("Owncast is exiting due to request.")
go func() {
os.Exit(0)
}()
webutils.WriteSimpleResponse(w, true, "forcing quit")
}
func downloadInstaller() (string, error) {
installer := "https://owncast.online/install.sh"
out, err := os.CreateTemp(os.TempDir(), "updater.sh")
if err != nil {
log.Errorln(err)
return "", err
}
defer out.Close()
// Get the installer script
resp, err := http.Get(installer)
if err != nil {
return "", err
}
defer resp.Body.Close()
// Write the installer to file
_, err = io.Copy(out, resp.Body)
if err != nil {
return "", err
}
return out.Name(), nil
}
// Check to see if owncast is listed as a running service under systemd.
func isRunningUnderSystemD() bool {
// Our current PID
ppid := os.Getppid()
// A randomized, unique 128-bit ID identifying each runtime cycle of the unit.
invocationID, hasInvocationID := os.LookupEnv("INVOCATION_ID")
// systemd's pid should be 1, so if our process' parent pid is 1
// then we are running under systemd.
return ppid == 1 || (hasInvocationID && invocationID != "")
}
// Taken from https://stackoverflow.com/questions/23513045/how-to-check-if-a-process-is-running-inside-docker-container
func getContainerID() string {
pid := os.Getppid()
cgroupPath := fmt.Sprintf("/proc/%s/cgroup", strconv.Itoa(pid))
containerID := ""
content, err := os.ReadFile(cgroupPath) //nolint:gosec
if err != nil {
return containerID
}
lines := strings.Split(string(content), "\n")
for _, line := range lines {
field := strings.Split(line, ":")
if len(field) < 3 {
continue
}
cgroupPath := field[2]
if len(cgroupPath) < 64 {
continue
}
// Non-systemd Docker
// 5:net_prio,net_cls:/docker/de630f22746b9c06c412858f26ca286c6cdfed086d3b302998aa403d9dcedc42
// 3:net_cls:/kubepods/burstable/pod5f399c1a-f9fc-11e8-bf65-246e9659ebfc/9170559b8aadd07d99978d9460cf8d1c71552f3c64fefc7e9906ab3fb7e18f69
pos := strings.LastIndex(cgroupPath, "/")
if pos > 0 {
idLen := len(cgroupPath) - pos - 1
if idLen == 64 {
// docker id
containerID = cgroupPath[pos+1 : pos+1+64]
return containerID
}
}
// systemd Docker
// 5:net_cls:/system.slice/docker-afd862d2ed48ef5dc0ce8f1863e4475894e331098c9a512789233ca9ca06fc62.scope
dockerStr := "docker-"
pos = strings.Index(cgroupPath, dockerStr)
if pos > 0 {
posScope := strings.Index(cgroupPath, ".scope")
idLen := posScope - pos - len(dockerStr)
if posScope > 0 && idLen == 64 {
containerID = cgroupPath[pos+len(dockerStr) : pos+len(dockerStr)+64]
return containerID
}
}
}
return containerID
}
type flushWriter struct {
f http.Flusher
w io.Writer
}
func (fw *flushWriter) Write(p []byte) (n int, err error) {
n, err = fw.w.Write(p)
if fw.f != nil {
fw.f.Flush()
}
return
}

View File

@@ -0,0 +1,87 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/metrics"
log "github.com/sirupsen/logrus"
)
// GetVideoPlaybackMetrics returns video playback metrics.
func GetVideoPlaybackMetrics(w http.ResponseWriter, r *http.Request) {
type response struct {
Errors []metrics.TimestampedValue `json:"errors"`
QualityVariantChanges []metrics.TimestampedValue `json:"qualityVariantChanges"`
HighestLatency []metrics.TimestampedValue `json:"highestLatency"`
MedianLatency []metrics.TimestampedValue `json:"medianLatency"`
LowestLatency []metrics.TimestampedValue `json:"lowestLatency"`
MedianDownloadDuration []metrics.TimestampedValue `json:"medianSegmentDownloadDuration"`
MaximumDownloadDuration []metrics.TimestampedValue `json:"maximumSegmentDownloadDuration"`
MinimumDownloadDuration []metrics.TimestampedValue `json:"minimumSegmentDownloadDuration"`
SlowestDownloadRate []metrics.TimestampedValue `json:"minPlayerBitrate"`
MedianDownloadRate []metrics.TimestampedValue `json:"medianPlayerBitrate"`
HighestDownloadRater []metrics.TimestampedValue `json:"maxPlayerBitrate"`
AvailableBitrates []int `json:"availableBitrates"`
SegmentLength int `json:"segmentLength"`
Representation int `json:"representation"`
}
availableBitrates := []int{}
var segmentLength int
if core.GetCurrentBroadcast() != nil {
segmentLength = core.GetCurrentBroadcast().LatencyLevel.SecondsPerSegment
for _, variants := range core.GetCurrentBroadcast().OutputSettings {
availableBitrates = append(availableBitrates, variants.VideoBitrate)
}
} else {
segmentLength = data.GetStreamLatencyLevel().SecondsPerSegment
for _, variants := range data.GetStreamOutputVariants() {
availableBitrates = append(availableBitrates, variants.VideoBitrate)
}
}
errors := metrics.GetPlaybackErrorCountOverTime()
medianLatency := metrics.GetMedianLatencyOverTime()
minimumLatency := metrics.GetMinimumLatencyOverTime()
maximumLatency := metrics.GetMaximumLatencyOverTime()
medianDurations := metrics.GetMedianDownloadDurationsOverTime()
maximumDurations := metrics.GetMaximumDownloadDurationsOverTime()
minimumDurations := metrics.GetMinimumDownloadDurationsOverTime()
minPlayerBitrate := metrics.GetSlowestDownloadRateOverTime()
medianPlayerBitrate := metrics.GetMedianDownloadRateOverTime()
maxPlayerBitrate := metrics.GetMaxDownloadRateOverTime()
qualityVariantChanges := metrics.GetQualityVariantChangesOverTime()
representation := metrics.GetPlaybackMetricsRepresentation()
resp := response{
AvailableBitrates: availableBitrates,
Errors: errors,
MedianLatency: medianLatency,
HighestLatency: maximumLatency,
LowestLatency: minimumLatency,
SegmentLength: segmentLength,
MedianDownloadDuration: medianDurations,
MaximumDownloadDuration: maximumDurations,
MinimumDownloadDuration: minimumDurations,
SlowestDownloadRate: minPlayerBitrate,
MedianDownloadRate: medianPlayerBitrate,
HighestDownloadRater: maxPlayerBitrate,
QualityVariantChanges: qualityVariantChanges,
Representation: representation,
}
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(resp)
if err != nil {
log.Errorln(err)
}
}

View File

@@ -0,0 +1,54 @@
package admin
import (
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/metrics"
"github.com/owncast/owncast/models"
webutils "github.com/owncast/owncast/webserver/utils"
log "github.com/sirupsen/logrus"
)
// GetViewersOverTime will return the number of viewers at points in time.
func GetViewersOverTime(w http.ResponseWriter, r *http.Request) {
windowStartAtStr := r.URL.Query().Get("windowStart")
windowStartAtUnix, err := strconv.Atoi(windowStartAtStr)
if err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
windowStartAt := time.Unix(int64(windowStartAtUnix), 0)
windowEnd := time.Now()
viewersOverTime := metrics.GetViewersOverTime(windowStartAt, windowEnd)
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(viewersOverTime)
if err != nil {
log.Errorln(err)
}
}
// GetActiveViewers returns currently connected clients.
func GetActiveViewers(w http.ResponseWriter, r *http.Request) {
c := core.GetActiveViewers()
viewers := make([]models.Viewer, 0, len(c))
for _, v := range c {
viewers = append(viewers, *v)
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(viewers); err != nil {
webutils.InternalErrorHandler(w, err)
}
}
// ExternalGetActiveViewers returns currently connected clients.
func ExternalGetActiveViewers(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
GetConnectedChatClients(w, r)
}

View File

@@ -0,0 +1,84 @@
package admin
import (
"encoding/json"
"errors"
"net/http"
"time"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
webutils "github.com/owncast/owncast/webserver/utils"
)
type deleteWebhookRequest struct {
ID int `json:"id"`
}
type createWebhookRequest struct {
URL string `json:"url"`
Events []models.EventType `json:"events"`
}
// CreateWebhook will add a single webhook.
func CreateWebhook(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var request createWebhookRequest
if err := decoder.Decode(&request); err != nil {
webutils.BadRequestHandler(w, err)
return
}
// Verify all the scopes provided are valid
if !models.HasValidEvents(request.Events) {
webutils.BadRequestHandler(w, errors.New("one or more invalid event provided"))
return
}
newWebhookID, err := data.InsertWebhook(request.URL, request.Events)
if err != nil {
webutils.InternalErrorHandler(w, err)
return
}
webutils.WriteResponse(w, models.Webhook{
ID: newWebhookID,
URL: request.URL,
Events: request.Events,
Timestamp: time.Now(),
LastUsed: nil,
})
}
// GetWebhooks will return all webhooks.
func GetWebhooks(w http.ResponseWriter, r *http.Request) {
webhooks, err := data.GetWebhooks()
if err != nil {
webutils.InternalErrorHandler(w, err)
return
}
webutils.WriteResponse(w, webhooks)
}
// DeleteWebhook will delete a single webhook.
func DeleteWebhook(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
webutils.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var request deleteWebhookRequest
if err := decoder.Decode(&request); err != nil {
webutils.BadRequestHandler(w, err)
return
}
if err := data.DeleteWebhook(request.ID); err != nil {
webutils.InternalErrorHandler(w, err)
return
}
webutils.WriteSimpleResponse(w, true, "deleted webhook")
}

View File

@@ -0,0 +1,20 @@
package admin
import (
"net/http"
"github.com/owncast/owncast/core/data"
webutils "github.com/owncast/owncast/webserver/utils"
log "github.com/sirupsen/logrus"
)
// ResetYPRegistration will clear the YP protocol registration key.
func ResetYPRegistration(w http.ResponseWriter, r *http.Request) {
log.Traceln("Resetting YP registration key")
if err := data.SetDirectoryRegistrationKey(""); err != nil {
log.Errorln(err)
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
webutils.WriteSimpleResponse(w, true, "reset")
}

View File

@@ -3,8 +3,8 @@ package handlers
import (
"net/http"
"github.com/owncast/owncast/controllers/auth/fediverse"
"github.com/owncast/owncast/controllers/auth/indieauth"
"github.com/owncast/owncast/webserver/handlers/auth/fediverse"
"github.com/owncast/owncast/webserver/handlers/auth/indieauth"
"github.com/owncast/owncast/webserver/handlers/generated"
"github.com/owncast/owncast/webserver/router/middleware"
)

View File

@@ -0,0 +1,110 @@
package fediverse
import (
"encoding/json"
"fmt"
"net/http"
"github.com/owncast/owncast/activitypub"
fediverseauth "github.com/owncast/owncast/auth/fediverse"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/persistence/userrepository"
webutils "github.com/owncast/owncast/webserver/utils"
log "github.com/sirupsen/logrus"
)
// RegisterFediverseOTPRequest registers a new OTP request for the given access token.
func RegisterFediverseOTPRequest(u models.User, w http.ResponseWriter, r *http.Request) {
type request struct {
FediverseAccount string `json:"account"`
}
var req request
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&req); err != nil {
webutils.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 {
webutils.WriteSimpleResponse(w, false, "Could not register auth request: "+err.Error())
return
}
if !success {
webutils.WriteSimpleResponse(w, false, "Could not register auth request. One may already be pending. Try again later.")
return
}
msg := fmt.Sprintf("<p>One-time code from %s: %s. If you did not request this message please ignore or block.</p>", data.GetServerName(), reg.Code)
if err := activitypub.SendDirectFederatedMessage(msg, reg.Account); err != nil {
webutils.WriteSimpleResponse(w, false, "Could not send code to fediverse: "+err.Error())
return
}
webutils.WriteSimpleResponse(w, true, "")
}
// VerifyFediverseOTPRequest verifies the given OTP code for the given access token.
func VerifyFediverseOTPRequest(w http.ResponseWriter, r *http.Request) {
type request struct {
Code string `json:"code"`
}
var req request
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&req); err != nil {
webutils.WriteSimpleResponse(w, false, "Could not decode request: "+err.Error())
return
}
accessToken := r.URL.Query().Get("accessToken")
valid, authRegistration := fediverseauth.ValidateFediverseOTP(accessToken, req.Code)
if !valid {
w.WriteHeader(http.StatusForbidden)
return
}
userRepository := userrepository.Get()
// Check if a user with this auth already exists, if so, log them in.
if u := userRepository.GetUserByAuth(authRegistration.Account, models.Fediverse); u != nil {
// Handle existing auth.
log.Debugln("user with provided fedvierse identity already exists, logging them in")
// Update the current user's access token to point to the existing user id.
userID := u.ID
if err := userRepository.SetAccessTokenToOwner(accessToken, userID); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
if authRegistration.UserDisplayName != u.DisplayName {
loginMessage := fmt.Sprintf("**%s** is now authenticated as **%s**", authRegistration.UserDisplayName, u.DisplayName)
if err := chat.SendSystemAction(loginMessage, true); err != nil {
log.Errorln(err)
}
}
webutils.WriteSimpleResponse(w, true, "")
return
}
// 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 := userRepository.AddAuth(authRegistration.UserID, authRegistration.Account, models.Fediverse); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
// Update the current user's authenticated flag so we can show it in
// the chat UI.
if err := userRepository.SetUserAsAuthenticated(authRegistration.UserID); err != nil {
log.Errorln(err)
}
webutils.WriteSimpleResponse(w, true, "")
}

View File

@@ -0,0 +1,107 @@
package indieauth
import (
"encoding/json"
"fmt"
"io"
"net/http"
ia "github.com/owncast/owncast/auth/indieauth"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/persistence/userrepository"
webutils "github.com/owncast/owncast/webserver/utils"
log "github.com/sirupsen/logrus"
)
// StartAuthFlow will begin the IndieAuth flow for the current user.
func StartAuthFlow(u models.User, w http.ResponseWriter, r *http.Request) {
type request struct {
AuthHost string `json:"authHost"`
}
type response struct {
Redirect string `json:"redirect"`
}
var authRequest request
p, err := io.ReadAll(r.Body)
if err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
if err := json.Unmarshal(p, &authRequest); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
accessToken := r.URL.Query().Get("accessToken")
redirectURL, err := ia.StartAuthFlow(authRequest.AuthHost, u.ID, accessToken, u.DisplayName)
if err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
redirectResponse := response{
Redirect: redirectURL.String(),
}
webutils.WriteResponse(w, redirectResponse)
}
// HandleRedirect will handle the redirect from an IndieAuth server to
// continue the auth flow.
func HandleRedirect(w http.ResponseWriter, r *http.Request) {
state := r.URL.Query().Get("state")
code := r.URL.Query().Get("code")
request, response, err := ia.HandleCallbackCode(code, state)
if err != nil {
log.Debugln(err)
msg := `Unable to complete authentication. <a href="/">Go back.</a><hr/>`
_ = webutils.WriteString(w, msg, http.StatusBadRequest)
return
}
userRepository := userrepository.Get()
// Check if a user with this auth already exists, if so, log them in.
if u := userRepository.GetUserByAuth(response.Me, models.IndieAuth); u != nil {
// Handle existing auth.
log.Debugln("user with provided indieauth already exists, logging them in")
// Update the current user's access token to point to the existing user id.
accessToken := request.CurrentAccessToken
userID := u.ID
if err := userRepository.SetAccessTokenToOwner(accessToken, userID); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
if request.DisplayName != u.DisplayName {
loginMessage := fmt.Sprintf("**%s** is now authenticated as **%s**", request.DisplayName, u.DisplayName)
if err := chat.SendSystemAction(loginMessage, true); err != nil {
log.Errorln(err)
}
}
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
// 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 := userRepository.AddAuth(request.UserID, response.Me, models.IndieAuth); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
// Update the current user's authenticated flag so we can show it in
// the chat UI.
if err := userRepository.SetUserAsAuthenticated(request.UserID); err != nil {
log.Errorln(err)
}
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}

View File

@@ -0,0 +1,83 @@
package indieauth
import (
"net/http"
"net/url"
ia "github.com/owncast/owncast/auth/indieauth"
"github.com/owncast/owncast/webserver/router/middleware"
webutils "github.com/owncast/owncast/webserver/utils"
)
// HandleAuthEndpoint will handle the IndieAuth auth endpoint.
func HandleAuthEndpoint(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
// Require the GET request for IndieAuth to be behind admin login.
f := middleware.RequireAdminAuth(HandleAuthEndpointGet)
f(w, r)
return
} else if r.Method == http.MethodPost {
HandleAuthEndpointPost(w, r)
} else {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
}
func HandleAuthEndpointGet(w http.ResponseWriter, r *http.Request) {
clientID := r.URL.Query().Get("client_id")
redirectURI := r.URL.Query().Get("redirect_uri")
codeChallenge := r.URL.Query().Get("code_challenge")
state := r.URL.Query().Get("state")
me := r.URL.Query().Get("me")
request, err := ia.StartServerAuth(clientID, redirectURI, codeChallenge, state, me)
if err != nil {
_ = webutils.WriteString(w, err.Error(), http.StatusInternalServerError)
return
}
// Redirect the client browser with the values we generated to continue
// the IndieAuth flow.
// If the URL is invalid then return with specific "invalid_request" error.
u, err := url.Parse(redirectURI)
if err != nil {
webutils.WriteResponse(w, ia.Response{
Error: "invalid_request",
ErrorDescription: err.Error(),
})
return
}
redirectParams := u.Query()
redirectParams.Set("code", request.Code)
redirectParams.Set("state", request.State)
u.RawQuery = redirectParams.Encode()
http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect)
}
func HandleAuthEndpointPost(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
code := r.PostForm.Get("code")
redirectURI := r.PostForm.Get("redirect_uri")
clientID := r.PostForm.Get("client_id")
codeVerifier := r.PostForm.Get("code_verifier")
// If the server auth flow cannot be completed then return with specific
// "invalid_client" error.
response, err := ia.CompleteServerAuth(code, redirectURI, clientID, codeVerifier)
if err != nil {
webutils.WriteResponse(w, ia.Response{
Error: "invalid_client",
ErrorDescription: err.Error(),
})
return
}
webutils.WriteResponse(w, response)
}

120
webserver/handlers/chat.go Normal file
View File

@@ -0,0 +1,120 @@
package handlers
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/persistence/userrepository"
"github.com/owncast/owncast/utils"
"github.com/owncast/owncast/webserver/router/middleware"
webutils "github.com/owncast/owncast/webserver/utils"
log "github.com/sirupsen/logrus"
)
// ExternalGetChatMessages gets all of the chat messages.
func ExternalGetChatMessages(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
middleware.EnableCors(w)
getChatMessages(w, r)
}
// GetChatMessages gets all of the chat messages.
func GetChatMessages(u models.User, w http.ResponseWriter, r *http.Request) {
middleware.EnableCors(w)
getChatMessages(w, r)
}
func getChatMessages(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
messages := chat.GetChatHistory()
if err := json.NewEncoder(w).Encode(messages); err != nil {
log.Debugln(err)
}
default:
w.WriteHeader(http.StatusNotImplemented)
if err := json.NewEncoder(w).Encode(webutils.J{"error": "method not implemented (PRs are accepted)"}); err != nil {
webutils.InternalErrorHandler(w, err)
}
}
}
// RegisterAnonymousChatUser will register a new user.
func RegisterAnonymousChatUser(w http.ResponseWriter, r *http.Request) {
middleware.EnableCors(w)
userRepository := userrepository.Get()
if r.Method == http.MethodOptions {
// All OPTIONS requests should have a wildcard CORS header.
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(http.StatusNoContent)
return
}
if r.Method != http.MethodPost {
// nolint:goconst
webutils.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
type registerAnonymousUserRequest struct {
DisplayName string `json:"displayName"`
}
type registerAnonymousUserResponse struct {
ID string `json:"id"`
AccessToken string `json:"accessToken"`
DisplayName string `json:"displayName"`
}
decoder := json.NewDecoder(r.Body)
var request registerAnonymousUserRequest
if err := decoder.Decode(&request); err != nil { //nolint
// this is fine. register a new user anyway.
}
proposedNewDisplayName := r.Header.Get("X-Forwarded-User")
if proposedNewDisplayName == "" {
proposedNewDisplayName = request.DisplayName
}
if proposedNewDisplayName == "" {
proposedNewDisplayName = generateDisplayName()
}
proposedNewDisplayName = utils.MakeSafeStringOfLength(proposedNewDisplayName, config.MaxChatDisplayNameLength)
newUser, accessToken, err := userRepository.CreateAnonymousUser(proposedNewDisplayName)
if err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
response := registerAnonymousUserResponse{
ID: newUser.ID,
AccessToken: accessToken,
DisplayName: newUser.DisplayName,
}
w.Header().Set("Content-Type", "application/json")
middleware.DisableCache(w)
webutils.WriteResponse(w, response)
}
func generateDisplayName() string {
suggestedUsernamesList := data.GetSuggestedUsernamesList()
minSuggestedUsernamePoolLength := 10
if len(suggestedUsernamesList) >= minSuggestedUsernamePoolLength {
index := utils.RandomIndex(len(suggestedUsernamesList))
return suggestedUsernamesList[index]
} else {
return utils.GeneratePhrase()
}
}

View File

@@ -1,368 +1,156 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/owncast/owncast/controllers/admin"
"github.com/owncast/owncast/activitypub"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
"github.com/owncast/owncast/webserver/router/middleware"
webutils "github.com/owncast/owncast/webserver/utils"
log "github.com/sirupsen/logrus"
)
func (*ServerInterfaceImpl) SetAdminPassword(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetAdminPassword)(w, r)
}
func (*ServerInterfaceImpl) SetAdminPasswordOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetAdminPassword)(w, r)
}
func (*ServerInterfaceImpl) SetStreamKeys(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamKeys)(w, r)
}
func (*ServerInterfaceImpl) SetStreamKeysOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamKeys)(w, r)
}
func (*ServerInterfaceImpl) SetExtraPageContent(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetExtraPageContent)(w, r)
}
func (*ServerInterfaceImpl) SetExtraPageContentOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetExtraPageContent)(w, r)
}
func (*ServerInterfaceImpl) SetStreamTitle(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamTitle)(w, r)
}
func (*ServerInterfaceImpl) SetStreamTitleOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamTitle)(w, r)
}
func (*ServerInterfaceImpl) SetServerName(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerName)(w, r)
}
func (*ServerInterfaceImpl) SetServerNameOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerName)(w, r)
}
func (*ServerInterfaceImpl) SetServerSummary(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerSummary)(w, r)
}
func (*ServerInterfaceImpl) SetServerSummaryOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerSummary)(w, r)
}
func (*ServerInterfaceImpl) SetCustomOfflineMessage(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomOfflineMessage)(w, r)
}
func (*ServerInterfaceImpl) SetCustomOfflineMessageOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomOfflineMessage)(w, r)
}
func (*ServerInterfaceImpl) SetServerWelcomeMessage(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerWelcomeMessage)(w, r)
}
func (*ServerInterfaceImpl) SetServerWelcomeMessageOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerWelcomeMessage)(w, r)
}
func (*ServerInterfaceImpl) SetChatDisabled(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatDisabled)(w, r)
}
func (*ServerInterfaceImpl) SetChatDisabledOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatDisabled)(w, r)
}
func (*ServerInterfaceImpl) SetChatJoinMessagesEnabled(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatJoinMessagesEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetChatJoinMessagesEnabledOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatJoinMessagesEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetEnableEstablishedChatUserMode(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetEnableEstablishedChatUserMode)(w, r)
}
func (*ServerInterfaceImpl) SetEnableEstablishedChatUserModeOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetEnableEstablishedChatUserMode)(w, r)
}
func (*ServerInterfaceImpl) SetForbiddenUsernameList(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetForbiddenUsernameList)(w, r)
}
func (*ServerInterfaceImpl) SetForbiddenUsernameListOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetForbiddenUsernameList)(w, r)
}
func (*ServerInterfaceImpl) SetSuggestedUsernameList(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetSuggestedUsernameList)(w, r)
}
func (*ServerInterfaceImpl) SetSuggestedUsernameListOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetSuggestedUsernameList)(w, r)
}
func (*ServerInterfaceImpl) SetChatSpamProtectionEnabled(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatSpamProtectionEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetChatSpamProtectionEnabledOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatSpamProtectionEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetChatSlurFilterEnabled(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatSlurFilterEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetChatSlurFilterEnabledOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatSlurFilterEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetVideoCodec(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetVideoCodec)(w, r)
}
func (*ServerInterfaceImpl) SetVideoCodecOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetVideoCodec)(w, r)
}
func (*ServerInterfaceImpl) SetStreamLatencyLevel(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamLatencyLevel)(w, r)
}
func (*ServerInterfaceImpl) SetStreamLatencyLevelOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamLatencyLevel)(w, r)
}
func (*ServerInterfaceImpl) SetStreamOutputVariants(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamOutputVariants)(w, r)
}
func (*ServerInterfaceImpl) SetStreamOutputVariantsOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamOutputVariants)(w, r)
}
func (*ServerInterfaceImpl) SetCustomColorVariableValues(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomColorVariableValues)(w, r)
}
func (*ServerInterfaceImpl) SetCustomColorVariableValuesOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomColorVariableValues)(w, r)
}
func (*ServerInterfaceImpl) SetLogo(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetLogo)(w, r)
}
func (*ServerInterfaceImpl) SetLogoOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetLogo)(w, r)
}
func (*ServerInterfaceImpl) SetTags(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetTags)(w, r)
}
func (*ServerInterfaceImpl) SetTagsOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetTags)(w, r)
}
func (*ServerInterfaceImpl) SetFfmpegPath(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFfmpegPath)(w, r)
}
func (*ServerInterfaceImpl) SetFfmpegPathOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFfmpegPath)(w, r)
}
func (*ServerInterfaceImpl) SetWebServerPort(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetWebServerPort)(w, r)
}
func (*ServerInterfaceImpl) SetWebServerPortOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetWebServerPort)(w, r)
}
func (*ServerInterfaceImpl) SetWebServerIP(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetWebServerIP)(w, r)
}
func (*ServerInterfaceImpl) SetWebServerIPOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetWebServerIP)(w, r)
}
func (*ServerInterfaceImpl) SetRTMPServerPort(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetRTMPServerPort)(w, r)
}
func (*ServerInterfaceImpl) SetRTMPServerPortOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetRTMPServerPort)(w, r)
}
func (*ServerInterfaceImpl) SetSocketHostOverride(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetSocketHostOverride)(w, r)
}
func (*ServerInterfaceImpl) SetSocketHostOverrideOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetSocketHostOverride)(w, r)
}
func (*ServerInterfaceImpl) SetVideoServingEndpoint(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetVideoServingEndpoint)(w, r)
}
func (*ServerInterfaceImpl) SetVideoServingEndpointOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetVideoServingEndpoint)(w, r)
}
func (*ServerInterfaceImpl) SetNSFW(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetNSFW)(w, r)
}
func (*ServerInterfaceImpl) SetNSFWOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetNSFW)(w, r)
}
func (*ServerInterfaceImpl) SetDirectoryEnabled(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetDirectoryEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetDirectoryEnabledOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetDirectoryEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetSocialHandles(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetSocialHandles)(w, r)
}
func (*ServerInterfaceImpl) SetSocialHandlesOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetSocialHandles)(w, r)
}
func (*ServerInterfaceImpl) SetS3Configuration(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetS3Configuration)(w, r)
}
func (*ServerInterfaceImpl) SetS3ConfigurationOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetS3Configuration)(w, r)
}
func (*ServerInterfaceImpl) SetServerURL(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerURL)(w, r)
}
func (*ServerInterfaceImpl) SetServerURLOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerURL)(w, r)
}
func (*ServerInterfaceImpl) SetExternalActions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetExternalActions)(w, r)
}
func (*ServerInterfaceImpl) SetExternalActionsOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetExternalActions)(w, r)
}
func (*ServerInterfaceImpl) SetCustomStyles(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomStyles)(w, r)
}
func (*ServerInterfaceImpl) SetCustomStylesOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomStyles)(w, r)
}
func (*ServerInterfaceImpl) SetCustomJavascript(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomJavascript)(w, r)
}
func (*ServerInterfaceImpl) SetCustomJavascriptOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomJavascript)(w, r)
}
func (*ServerInterfaceImpl) SetHideViewerCount(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetHideViewerCount)(w, r)
}
func (*ServerInterfaceImpl) SetHideViewerCountOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetHideViewerCount)(w, r)
}
func (*ServerInterfaceImpl) SetDisableSearchIndexing(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetDisableSearchIndexing)(w, r)
}
func (*ServerInterfaceImpl) SetDisableSearchIndexingOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetDisableSearchIndexing)(w, r)
}
func (*ServerInterfaceImpl) SetFederationEnabled(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetFederationEnabledOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetFederationActivityPrivate(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationActivityPrivate)(w, r)
}
func (*ServerInterfaceImpl) SetFederationActivityPrivateOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationActivityPrivate)(w, r)
}
func (*ServerInterfaceImpl) SetFederationShowEngagement(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationShowEngagement)(w, r)
}
func (*ServerInterfaceImpl) SetFederationShowEngagementOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationShowEngagement)(w, r)
}
func (*ServerInterfaceImpl) SetFederationUsername(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationUsername)(w, r)
}
func (*ServerInterfaceImpl) SetFederationUsernameOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationUsername)(w, r)
}
func (*ServerInterfaceImpl) SetFederationGoLiveMessage(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationGoLiveMessage)(w, r)
}
func (*ServerInterfaceImpl) SetFederationGoLiveMessageOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationGoLiveMessage)(w, r)
}
func (*ServerInterfaceImpl) SetFederationBlockDomains(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationBlockDomains)(w, r)
}
func (*ServerInterfaceImpl) SetFederationBlockDomainsOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationBlockDomains)(w, r)
}
func (*ServerInterfaceImpl) SetDiscordNotificationConfiguration(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetDiscordNotificationConfiguration)(w, r)
}
func (*ServerInterfaceImpl) SetDiscordNotificationConfigurationOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetDiscordNotificationConfiguration)(w, r)
}
func (*ServerInterfaceImpl) SetBrowserNotificationConfiguration(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetBrowserNotificationConfiguration)(w, r)
}
func (*ServerInterfaceImpl) SetBrowserNotificationConfigurationOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetBrowserNotificationConfiguration)(w, r)
type webConfigResponse struct {
AppearanceVariables map[string]string `json:"appearanceVariables"`
Name string `json:"name"`
CustomStyles string `json:"customStyles"`
StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream
OfflineMessage string `json:"offlineMessage"`
Logo string `json:"logo"`
Version string `json:"version"`
SocketHostOverride string `json:"socketHostOverride,omitempty"`
ExtraPageContent string `json:"extraPageContent"`
Summary string `json:"summary"`
Tags []string `json:"tags"`
SocialHandles []models.SocialHandle `json:"socialHandles"`
ExternalActions []models.ExternalAction `json:"externalActions"`
Notifications notificationsConfigResponse `json:"notifications"`
Federation federationConfigResponse `json:"federation"`
MaxSocketPayloadSize int `json:"maxSocketPayloadSize"`
HideViewerCount bool `json:"hideViewerCount"`
ChatDisabled bool `json:"chatDisabled"`
ChatSpamProtectionDisabled bool `json:"chatSpamProtectionDisabled"`
NSFW bool `json:"nsfw"`
Authentication authenticationConfigResponse `json:"authentication"`
}
type federationConfigResponse struct {
Account string `json:"account,omitempty"`
FollowerCount int `json:"followerCount,omitempty"`
Enabled bool `json:"enabled"`
}
type browserNotificationsConfigResponse struct {
PublicKey string `json:"publicKey,omitempty"`
Enabled bool `json:"enabled"`
}
type notificationsConfigResponse struct {
Browser browserNotificationsConfigResponse `json:"browser"`
}
type authenticationConfigResponse struct {
IndieAuthEnabled bool `json:"indieAuthEnabled"`
}
// GetWebConfig gets the status of the server.
func GetWebConfig(w http.ResponseWriter, r *http.Request) {
middleware.EnableCors(w)
middleware.DisableCache(w)
w.Header().Set("Content-Type", "application/json")
configuration := getConfigResponse()
if err := json.NewEncoder(w).Encode(configuration); err != nil {
webutils.BadRequestHandler(w, err)
}
}
func getConfigResponse() webConfigResponse {
pageContent := utils.RenderPageContentMarkdown(data.GetExtraPageBodyContent())
offlineMessage := utils.RenderSimpleMarkdown(data.GetCustomOfflineMessage())
socialHandles := data.GetSocialHandles()
for i, handle := range socialHandles {
platform := models.GetSocialHandle(handle.Platform)
if platform != nil {
handle.Icon = platform.Icon
socialHandles[i] = handle
}
}
serverSummary := data.GetServerSummary()
var federationResponse federationConfigResponse
federationEnabled := data.GetFederationEnabled()
followerCount, _ := activitypub.GetFollowerCount()
if federationEnabled {
serverURLString := data.GetServerURL()
serverURL, _ := url.Parse(serverURLString)
account := fmt.Sprintf("%s@%s", data.GetDefaultFederationUsername(), serverURL.Host)
federationResponse = federationConfigResponse{
Enabled: federationEnabled,
FollowerCount: int(followerCount),
Account: account,
}
}
browserPushEnabled := data.GetBrowserPushConfig().Enabled
browserPushPublicKey, err := data.GetBrowserPushPublicKey()
if err != nil {
log.Errorln("unable to fetch browser push notifications public key", err)
browserPushEnabled = false
}
notificationsResponse := notificationsConfigResponse{
Browser: browserNotificationsConfigResponse{
Enabled: browserPushEnabled,
PublicKey: browserPushPublicKey,
},
}
authenticationResponse := authenticationConfigResponse{
IndieAuthEnabled: data.GetServerURL() != "",
}
return webConfigResponse{
Name: data.GetServerName(),
Summary: serverSummary,
OfflineMessage: offlineMessage,
Logo: "/logo",
Tags: data.GetServerMetadataTags(),
Version: config.GetReleaseString(),
NSFW: data.GetNSFW(),
SocketHostOverride: data.GetWebsocketOverrideHost(),
ExtraPageContent: pageContent,
StreamTitle: data.GetStreamTitle(),
SocialHandles: socialHandles,
ChatDisabled: data.GetChatDisabled(),
ChatSpamProtectionDisabled: data.GetChatSpamProtectionEnabled(),
ExternalActions: data.GetExternalActions(),
CustomStyles: data.GetCustomStyles(),
MaxSocketPayloadSize: config.MaxSocketPayloadSize,
Federation: federationResponse,
Notifications: notificationsResponse,
Authentication: authenticationResponse,
AppearanceVariables: data.GetCustomColorVariableValues(),
HideViewerCount: data.GetHideViewerCount(),
}
}
// GetAllSocialPlatforms will return a list of all social platform types.
func 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 {
webutils.InternalErrorHandler(w, err)
}
}

View File

@@ -0,0 +1,368 @@
package handlers
import (
"net/http"
"github.com/owncast/owncast/webserver/handlers/admin"
"github.com/owncast/owncast/webserver/router/middleware"
)
func (*ServerInterfaceImpl) SetAdminPassword(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetAdminPassword)(w, r)
}
func (*ServerInterfaceImpl) SetAdminPasswordOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetAdminPassword)(w, r)
}
func (*ServerInterfaceImpl) SetStreamKeys(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamKeys)(w, r)
}
func (*ServerInterfaceImpl) SetStreamKeysOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamKeys)(w, r)
}
func (*ServerInterfaceImpl) SetExtraPageContent(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetExtraPageContent)(w, r)
}
func (*ServerInterfaceImpl) SetExtraPageContentOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetExtraPageContent)(w, r)
}
func (*ServerInterfaceImpl) SetStreamTitle(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamTitle)(w, r)
}
func (*ServerInterfaceImpl) SetStreamTitleOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamTitle)(w, r)
}
func (*ServerInterfaceImpl) SetServerName(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerName)(w, r)
}
func (*ServerInterfaceImpl) SetServerNameOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerName)(w, r)
}
func (*ServerInterfaceImpl) SetServerSummary(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerSummary)(w, r)
}
func (*ServerInterfaceImpl) SetServerSummaryOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerSummary)(w, r)
}
func (*ServerInterfaceImpl) SetCustomOfflineMessage(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomOfflineMessage)(w, r)
}
func (*ServerInterfaceImpl) SetCustomOfflineMessageOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomOfflineMessage)(w, r)
}
func (*ServerInterfaceImpl) SetServerWelcomeMessage(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerWelcomeMessage)(w, r)
}
func (*ServerInterfaceImpl) SetServerWelcomeMessageOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerWelcomeMessage)(w, r)
}
func (*ServerInterfaceImpl) SetChatDisabled(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatDisabled)(w, r)
}
func (*ServerInterfaceImpl) SetChatDisabledOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatDisabled)(w, r)
}
func (*ServerInterfaceImpl) SetChatJoinMessagesEnabled(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatJoinMessagesEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetChatJoinMessagesEnabledOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatJoinMessagesEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetEnableEstablishedChatUserMode(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetEnableEstablishedChatUserMode)(w, r)
}
func (*ServerInterfaceImpl) SetEnableEstablishedChatUserModeOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetEnableEstablishedChatUserMode)(w, r)
}
func (*ServerInterfaceImpl) SetForbiddenUsernameList(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetForbiddenUsernameList)(w, r)
}
func (*ServerInterfaceImpl) SetForbiddenUsernameListOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetForbiddenUsernameList)(w, r)
}
func (*ServerInterfaceImpl) SetSuggestedUsernameList(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetSuggestedUsernameList)(w, r)
}
func (*ServerInterfaceImpl) SetSuggestedUsernameListOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetSuggestedUsernameList)(w, r)
}
func (*ServerInterfaceImpl) SetChatSpamProtectionEnabled(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatSpamProtectionEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetChatSpamProtectionEnabledOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatSpamProtectionEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetChatSlurFilterEnabled(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatSlurFilterEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetChatSlurFilterEnabledOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetChatSlurFilterEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetVideoCodec(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetVideoCodec)(w, r)
}
func (*ServerInterfaceImpl) SetVideoCodecOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetVideoCodec)(w, r)
}
func (*ServerInterfaceImpl) SetStreamLatencyLevel(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamLatencyLevel)(w, r)
}
func (*ServerInterfaceImpl) SetStreamLatencyLevelOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamLatencyLevel)(w, r)
}
func (*ServerInterfaceImpl) SetStreamOutputVariants(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamOutputVariants)(w, r)
}
func (*ServerInterfaceImpl) SetStreamOutputVariantsOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetStreamOutputVariants)(w, r)
}
func (*ServerInterfaceImpl) SetCustomColorVariableValues(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomColorVariableValues)(w, r)
}
func (*ServerInterfaceImpl) SetCustomColorVariableValuesOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomColorVariableValues)(w, r)
}
func (*ServerInterfaceImpl) SetLogo(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetLogo)(w, r)
}
func (*ServerInterfaceImpl) SetLogoOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetLogo)(w, r)
}
func (*ServerInterfaceImpl) SetTags(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetTags)(w, r)
}
func (*ServerInterfaceImpl) SetTagsOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetTags)(w, r)
}
func (*ServerInterfaceImpl) SetFfmpegPath(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFfmpegPath)(w, r)
}
func (*ServerInterfaceImpl) SetFfmpegPathOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFfmpegPath)(w, r)
}
func (*ServerInterfaceImpl) SetWebServerPort(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetWebServerPort)(w, r)
}
func (*ServerInterfaceImpl) SetWebServerPortOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetWebServerPort)(w, r)
}
func (*ServerInterfaceImpl) SetWebServerIP(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetWebServerIP)(w, r)
}
func (*ServerInterfaceImpl) SetWebServerIPOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetWebServerIP)(w, r)
}
func (*ServerInterfaceImpl) SetRTMPServerPort(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetRTMPServerPort)(w, r)
}
func (*ServerInterfaceImpl) SetRTMPServerPortOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetRTMPServerPort)(w, r)
}
func (*ServerInterfaceImpl) SetSocketHostOverride(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetSocketHostOverride)(w, r)
}
func (*ServerInterfaceImpl) SetSocketHostOverrideOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetSocketHostOverride)(w, r)
}
func (*ServerInterfaceImpl) SetVideoServingEndpoint(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetVideoServingEndpoint)(w, r)
}
func (*ServerInterfaceImpl) SetVideoServingEndpointOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetVideoServingEndpoint)(w, r)
}
func (*ServerInterfaceImpl) SetNSFW(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetNSFW)(w, r)
}
func (*ServerInterfaceImpl) SetNSFWOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetNSFW)(w, r)
}
func (*ServerInterfaceImpl) SetDirectoryEnabled(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetDirectoryEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetDirectoryEnabledOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetDirectoryEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetSocialHandles(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetSocialHandles)(w, r)
}
func (*ServerInterfaceImpl) SetSocialHandlesOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetSocialHandles)(w, r)
}
func (*ServerInterfaceImpl) SetS3Configuration(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetS3Configuration)(w, r)
}
func (*ServerInterfaceImpl) SetS3ConfigurationOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetS3Configuration)(w, r)
}
func (*ServerInterfaceImpl) SetServerURL(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerURL)(w, r)
}
func (*ServerInterfaceImpl) SetServerURLOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetServerURL)(w, r)
}
func (*ServerInterfaceImpl) SetExternalActions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetExternalActions)(w, r)
}
func (*ServerInterfaceImpl) SetExternalActionsOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetExternalActions)(w, r)
}
func (*ServerInterfaceImpl) SetCustomStyles(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomStyles)(w, r)
}
func (*ServerInterfaceImpl) SetCustomStylesOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomStyles)(w, r)
}
func (*ServerInterfaceImpl) SetCustomJavascript(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomJavascript)(w, r)
}
func (*ServerInterfaceImpl) SetCustomJavascriptOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetCustomJavascript)(w, r)
}
func (*ServerInterfaceImpl) SetHideViewerCount(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetHideViewerCount)(w, r)
}
func (*ServerInterfaceImpl) SetHideViewerCountOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetHideViewerCount)(w, r)
}
func (*ServerInterfaceImpl) SetDisableSearchIndexing(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetDisableSearchIndexing)(w, r)
}
func (*ServerInterfaceImpl) SetDisableSearchIndexingOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetDisableSearchIndexing)(w, r)
}
func (*ServerInterfaceImpl) SetFederationEnabled(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetFederationEnabledOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationEnabled)(w, r)
}
func (*ServerInterfaceImpl) SetFederationActivityPrivate(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationActivityPrivate)(w, r)
}
func (*ServerInterfaceImpl) SetFederationActivityPrivateOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationActivityPrivate)(w, r)
}
func (*ServerInterfaceImpl) SetFederationShowEngagement(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationShowEngagement)(w, r)
}
func (*ServerInterfaceImpl) SetFederationShowEngagementOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationShowEngagement)(w, r)
}
func (*ServerInterfaceImpl) SetFederationUsername(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationUsername)(w, r)
}
func (*ServerInterfaceImpl) SetFederationUsernameOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationUsername)(w, r)
}
func (*ServerInterfaceImpl) SetFederationGoLiveMessage(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationGoLiveMessage)(w, r)
}
func (*ServerInterfaceImpl) SetFederationGoLiveMessageOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationGoLiveMessage)(w, r)
}
func (*ServerInterfaceImpl) SetFederationBlockDomains(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationBlockDomains)(w, r)
}
func (*ServerInterfaceImpl) SetFederationBlockDomainsOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetFederationBlockDomains)(w, r)
}
func (*ServerInterfaceImpl) SetDiscordNotificationConfiguration(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetDiscordNotificationConfiguration)(w, r)
}
func (*ServerInterfaceImpl) SetDiscordNotificationConfigurationOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetDiscordNotificationConfiguration)(w, r)
}
func (*ServerInterfaceImpl) SetBrowserNotificationConfiguration(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetBrowserNotificationConfiguration)(w, r)
}
func (*ServerInterfaceImpl) SetBrowserNotificationConfigurationOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireAdminAuth(admin.SetBrowserNotificationConfiguration)(w, r)
}

View File

@@ -0,0 +1,7 @@
package handlers
// POST is the HTTP POST method.
const POST = "POST"
// GET is the HTTP GET method.
const GET = "GET"

View File

@@ -0,0 +1,15 @@
package handlers
import (
"net/http"
"github.com/owncast/owncast/core/data"
)
// ServeCustomJavascript will serve optional custom Javascript.
func ServeCustomJavascript(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
js := data.GetCustomJavascript()
_, _ = w.Write([]byte(js))
}

View File

@@ -0,0 +1,33 @@
package handlers
import (
"encoding/json"
"net/http"
"os"
"strings"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/webserver/router/middleware"
webutils "github.com/owncast/owncast/webserver/utils"
)
// GetCustomEmojiList returns a list of emoji via the API.
func GetCustomEmojiList(w http.ResponseWriter, r *http.Request) {
emojiList := data.GetEmojiList()
middleware.SetCachingHeaders(w, r)
if err := json.NewEncoder(w).Encode(emojiList); err != nil {
webutils.InternalErrorHandler(w, err)
}
}
// GetCustomEmojiImage returns a single emoji image.
func GetCustomEmojiImage(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/img/emoji/")
r.URL.Path = path
emojiFS := os.DirFS(config.CustomEmojiPath)
middleware.SetCachingHeaders(w, r)
http.FileServer(http.FS(emojiFS)).ServeHTTP(w, r)
}

View File

@@ -0,0 +1,23 @@
package handlers
import (
"net/http"
"github.com/owncast/owncast/activitypub/persistence"
webutils "github.com/owncast/owncast/webserver/utils"
)
// 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) {
followers, total, err := persistence.GetFederationFollowers(limit, offset)
if err != nil {
webutils.WriteSimpleResponse(w, false, "unable to fetch followers")
return
}
response := webutils.PaginatedResponse{
Total: total,
Results: followers,
}
webutils.WriteResponse(w, response)
}

View File

@@ -3,8 +3,7 @@ package handlers
import (
"net/http"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/controllers/admin"
"github.com/owncast/owncast/webserver/handlers/admin"
"github.com/owncast/owncast/webserver/handlers/generated"
"github.com/owncast/owncast/webserver/router/middleware"
@@ -25,23 +24,23 @@ func (s *ServerInterfaceImpl) Handler() http.Handler {
}
func (*ServerInterfaceImpl) GetStatus(w http.ResponseWriter, r *http.Request) {
controllers.GetStatus(w, r)
GetStatus(w, r)
}
func (*ServerInterfaceImpl) GetCustomEmojiList(w http.ResponseWriter, r *http.Request) {
controllers.GetCustomEmojiList(w, r)
GetCustomEmojiList(w, r)
}
func (*ServerInterfaceImpl) GetChatMessages(w http.ResponseWriter, r *http.Request, params generated.GetChatMessagesParams) {
middleware.RequireUserAccessToken(controllers.GetChatMessages)(w, r)
middleware.RequireUserAccessToken(GetChatMessages)(w, r)
}
func (*ServerInterfaceImpl) RegisterAnonymousChatUser(w http.ResponseWriter, r *http.Request, params generated.RegisterAnonymousChatUserParams) {
controllers.RegisterAnonymousChatUser(w, r)
RegisterAnonymousChatUser(w, r)
}
func (*ServerInterfaceImpl) RegisterAnonymousChatUserOptions(w http.ResponseWriter, r *http.Request) {
controllers.RegisterAnonymousChatUser(w, r)
RegisterAnonymousChatUser(w, r)
}
func (*ServerInterfaceImpl) UpdateMessageVisibility(w http.ResponseWriter, r *http.Request, params generated.UpdateMessageVisibilityParams) {
@@ -53,7 +52,7 @@ func (*ServerInterfaceImpl) UpdateUserEnabled(w http.ResponseWriter, r *http.Req
}
func (*ServerInterfaceImpl) GetWebConfig(w http.ResponseWriter, r *http.Request) {
controllers.GetWebConfig(w, r)
GetWebConfig(w, r)
}
func (*ServerInterfaceImpl) GetYPResponse(w http.ResponseWriter, r *http.Request) {
@@ -61,29 +60,29 @@ func (*ServerInterfaceImpl) GetYPResponse(w http.ResponseWriter, r *http.Request
}
func (*ServerInterfaceImpl) GetAllSocialPlatforms(w http.ResponseWriter, r *http.Request) {
controllers.GetAllSocialPlatforms(w, r)
GetAllSocialPlatforms(w, r)
}
func (*ServerInterfaceImpl) GetVideoStreamOutputVariants(w http.ResponseWriter, r *http.Request) {
controllers.GetVideoStreamOutputVariants(w, r)
GetVideoStreamOutputVariants(w, r)
}
func (*ServerInterfaceImpl) Ping(w http.ResponseWriter, r *http.Request) {
controllers.Ping(w, r)
Ping(w, r)
}
func (*ServerInterfaceImpl) RemoteFollow(w http.ResponseWriter, r *http.Request) {
controllers.RemoteFollow(w, r)
RemoteFollow(w, r)
}
func (*ServerInterfaceImpl) GetFollowers(w http.ResponseWriter, r *http.Request, params generated.GetFollowersParams) {
middleware.HandlePagination(controllers.GetFollowers)(w, r)
middleware.HandlePagination(GetFollowers)(w, r)
}
func (*ServerInterfaceImpl) ReportPlaybackMetrics(w http.ResponseWriter, r *http.Request) {
controllers.ReportPlaybackMetrics(w, r)
ReportPlaybackMetrics(w, r)
}
func (*ServerInterfaceImpl) RegisterForLiveNotifications(w http.ResponseWriter, r *http.Request, params generated.RegisterForLiveNotificationsParams) {
middleware.RequireUserAccessToken(controllers.RegisterForLiveNotifications)(w, r)
middleware.RequireUserAccessToken(RegisterForLiveNotifications)(w, r)
}

55
webserver/handlers/hls.go Normal file
View File

@@ -0,0 +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/router/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)
}

View File

@@ -0,0 +1,80 @@
package handlers
import (
"net/http"
"path/filepath"
"time"
"github.com/jellydator/ttlcache/v3"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/utils"
)
const (
contentTypeJPEG = "image/jpeg"
contentTypeGIF = "image/gif"
)
var previewThumbCache = ttlcache.New(
ttlcache.WithTTL[string, []byte](15),
ttlcache.WithCapacity[string, []byte](1),
ttlcache.WithDisableTouchOnHit[string, []byte](),
)
// GetThumbnail will return the thumbnail image as a response.
func GetThumbnail(w http.ResponseWriter, r *http.Request) {
imageFilename := "thumbnail.jpg"
imagePath := filepath.Join(config.TempDir, imageFilename)
httpCacheTime := utils.GetCacheDurationSecondsForPath(imagePath)
inMemoryCacheTime := time.Duration(15) * time.Second
var imageBytes []byte
var err error
if previewThumbCache.Get(imagePath) != nil {
ci := previewThumbCache.Get(imagePath)
imageBytes = ci.Value()
} else if utils.DoesFileExists(imagePath) {
imageBytes, err = getImage(imagePath)
previewThumbCache.Set(imagePath, imageBytes, inMemoryCacheTime)
} else {
GetLogo(w, r)
return
}
if err != nil {
GetLogo(w, r)
return
}
writeBytesAsImage(imageBytes, contentTypeJPEG, w, httpCacheTime)
}
// GetPreview will return the preview gif as a response.
func GetPreview(w http.ResponseWriter, r *http.Request) {
imageFilename := "preview.gif"
imagePath := filepath.Join(config.TempDir, imageFilename)
httpCacheTime := utils.GetCacheDurationSecondsForPath(imagePath)
inMemoryCacheTime := time.Duration(15) * time.Second
var imageBytes []byte
var err error
if previewThumbCache.Get(imagePath) != nil {
ci := previewThumbCache.Get(imagePath)
imageBytes = ci.Value()
} else if utils.DoesFileExists(imagePath) {
imageBytes, err = getImage(imagePath)
previewThumbCache.Set(imagePath, imageBytes, inMemoryCacheTime)
} else {
GetLogo(w, r)
return
}
if err != nil {
GetLogo(w, r)
return
}
writeBytesAsImage(imageBytes, contentTypeGIF, w, httpCacheTime)
}

206
webserver/handlers/index.go Normal file
View File

@@ -0,0 +1,206 @@
package handlers
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"path/filepath"
"strings"
"time"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/cache"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/static"
"github.com/owncast/owncast/utils"
"github.com/owncast/owncast/webserver/router/middleware"
log "github.com/sirupsen/logrus"
)
var gc = cache.GetGlobalCache()
// IndexHandler handles the default index route.
func 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) == ""
if utils.IsUserAgentAPlayer(r.UserAgent()) && isIndexRequest {
http.Redirect(w, r, "/hls/stream.m3u8", http.StatusTemporaryRedirect)
return
}
// For search engine bots and social scrapers return a special
// server-rendered page.
if utils.IsUserAgentABot(r.UserAgent()) && isIndexRequest {
handleScraperMetadataPage(w, r)
return
}
// Set a cache control max-age header
middleware.SetCachingHeaders(w, r)
nonceRandom, _ := utils.GenerateRandomString(5)
// Set our global HTTP headers
middleware.SetHeaders(w, fmt.Sprintf("nonce-%s", nonceRandom))
if isIndexRequest {
renderIndexHtml(w, nonceRandom)
return
}
serveWeb(w, r)
}
func renderIndexHtml(w http.ResponseWriter, nonce string) {
type serverSideContent struct {
Name string
Summary string
RequestedURL string
TagsString string
ThumbnailURL string
Thumbnail string
Image string
StatusJSON string
ServerConfigJSON string
EmbedVideo string
Nonce string
}
status := getStatusResponse()
sb, err := json.Marshal(status)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
config := getConfigResponse()
cb, err := json.Marshal(config)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
content := serverSideContent{
Name: data.GetServerName(),
Summary: data.GetServerSummary(),
RequestedURL: fmt.Sprintf("%s%s", data.GetServerURL(), "/"),
TagsString: strings.Join(data.GetServerMetadataTags(), ","),
ThumbnailURL: "thumbnail.jpg",
Thumbnail: "thumbnail.jpg",
Image: "logo/external",
StatusJSON: string(sb),
ServerConfigJSON: string(cb),
EmbedVideo: "embed/video",
Nonce: nonce,
}
index, err := static.GetWebIndexTemplate()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := index.Execute(w, content); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// MetadataPage represents a server-rendered web page for bots and web scrapers.
type MetadataPage struct {
RequestedURL string
Image string
Thumbnail string
TagsString string
Summary string
Name string
Tags []string
SocialHandles []models.SocialHandle
}
// Return a basic HTML page with server-rendered metadata from the config
// to give to Opengraph clients and web scrapers (bots, web crawlers, etc).
func handleScraperMetadataPage(w http.ResponseWriter, r *http.Request) {
cacheKey := "bot-scraper-html"
cacheHtmlExpiration := time.Duration(10) * time.Second
c := gc.GetOrCreateCache(cacheKey, cacheHtmlExpiration)
cachedHtml := c.GetValueForKey(cacheKey)
if cachedHtml != nil {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write(cachedHtml)
return
}
tmpl, err := static.GetBotMetadataTemplate()
if err != nil {
log.Errorln(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
scheme := "http"
if siteURL := data.GetServerURL(); siteURL != "" {
if parsed, err := url.Parse(siteURL); err == nil && parsed.Scheme != "" {
scheme = parsed.Scheme
}
}
fullURL, err := url.Parse(fmt.Sprintf("%s://%s%s", scheme, r.Host, r.URL.Path))
if err != nil {
log.Errorln(err)
}
imageURL, err := url.Parse(fmt.Sprintf("%s://%s%s", scheme, r.Host, "/logo/external"))
if err != nil {
log.Errorln(err)
}
status := core.GetStatus()
// If the thumbnail does not exist or we're offline then just use the logo image
var thumbnailURL string
if status.Online && utils.DoesFileExists(filepath.Join(config.DataDirectory, "tmp", "thumbnail.jpg")) {
thumbnail, err := url.Parse(fmt.Sprintf("%s://%s%s", scheme, r.Host, "/thumbnail.jpg"))
if err != nil {
log.Errorln(err)
thumbnailURL = imageURL.String()
} else {
thumbnailURL = thumbnail.String()
}
} else {
thumbnailURL = imageURL.String()
}
tagsString := strings.Join(data.GetServerMetadataTags(), ",")
metadata := MetadataPage{
Name: data.GetServerName(),
RequestedURL: fullURL.String(),
Image: imageURL.String(),
Summary: data.GetServerSummary(),
Thumbnail: thumbnailURL,
TagsString: tagsString,
Tags: data.GetServerMetadataTags(),
SocialHandles: data.GetSocialHandles(),
}
// Cache the rendered HTML
var b bytes.Buffer
if err := tmpl.Execute(&b, metadata); err != nil {
log.Errorln(err)
}
c.Set(cacheKey, b.Bytes())
// Set a cache header
middleware.SetCachingHeaders(w, r)
w.Header().Set("Content-Type", "text/html")
if _, err = w.Write(b.Bytes()); err != nil {
log.Errorln(err)
}
}

View File

@@ -3,9 +3,8 @@ package handlers
import (
"net/http"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/controllers/admin"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/webserver/handlers/admin"
"github.com/owncast/owncast/webserver/router/middleware"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
@@ -69,11 +68,11 @@ func (*ServerInterfaceImpl) ExternalSetStreamTitleOptions(w http.ResponseWriter,
}
func (*ServerInterfaceImpl) ExternalGetChatMessages(w http.ResponseWriter, r *http.Request) {
middleware.RequireExternalAPIAccessToken(models.ScopeHasAdminAccess, controllers.ExternalGetChatMessages)(w, r)
middleware.RequireExternalAPIAccessToken(models.ScopeHasAdminAccess, ExternalGetChatMessages)(w, r)
}
func (*ServerInterfaceImpl) ExternalGetChatMessagesOptions(w http.ResponseWriter, r *http.Request) {
middleware.RequireExternalAPIAccessToken(models.ScopeHasAdminAccess, controllers.ExternalGetChatMessages)(w, r)
middleware.RequireExternalAPIAccessToken(models.ScopeHasAdminAccess, ExternalGetChatMessages)(w, r)
}
func (*ServerInterfaceImpl) ExternalGetConnectedChatClients(w http.ResponseWriter, r *http.Request) {

View File

@@ -0,0 +1,95 @@
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"
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) {
imageFilename := data.GetLogoPath()
if imageFilename == "" {
returnDefault(w)
return
}
imagePath := filepath.Join(config.DataDirectory, imageFilename)
imageBytes, err := getImage(imagePath)
if err != nil {
returnDefault(w)
return
}
contentType := "image/jpeg"
if filepath.Ext(imageFilename) == ".svg" {
contentType = "image/svg+xml"
} else if filepath.Ext(imageFilename) == ".gif" {
contentType = "image/gif"
} else if filepath.Ext(imageFilename) == ".png" {
contentType = "image/png"
}
cacheTime := utils.GetCacheDurationSecondsForPath(imagePath)
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) {
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)
return
}
// Otherwise use a fallback logo.png.
imagePath := filepath.Join(config.DataDirectory, "logo.png")
contentType := "image/png"
imageBytes, err := getImage(imagePath)
if err != nil {
returnDefault(w)
return
}
cacheTime := utils.GetCacheDurationSecondsForPath(imagePath)
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.")
_hasWarnedSVGLogo = true
}
}
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.")
}
}
func getImage(path string) ([]byte, error) {
return os.ReadFile(path) // nolint
}

View File

@@ -3,8 +3,8 @@ package handlers
import (
"net/http"
"github.com/owncast/owncast/controllers/moderation"
"github.com/owncast/owncast/webserver/handlers/generated"
"github.com/owncast/owncast/webserver/handlers/moderation"
"github.com/owncast/owncast/webserver/router/middleware"
)

View File

@@ -0,0 +1,76 @@
package moderation
import (
"encoding/json"
"net/http"
"strings"
"time"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/persistence/userrepository"
"github.com/owncast/owncast/webserver/utils"
log "github.com/sirupsen/logrus"
)
// GetUserDetails returns the details of a chat user for moderators.
func GetUserDetails(w http.ResponseWriter, r *http.Request) {
type connectedClient struct {
ConnectedAt time.Time `json:"connectedAt"`
UserAgent string `json:"userAgent"`
Geo string `json:"geo,omitempty"`
Id uint `json:"id"`
MessageCount int `json:"messageCount"`
}
type response struct {
User *models.User `json:"user"`
ConnectedClients []connectedClient `json:"connectedClients"`
Messages []events.UserMessageEvent `json:"messages"`
}
pathComponents := strings.Split(r.URL.Path, "/")
uid := pathComponents[len(pathComponents)-1]
userRepository := userrepository.Get()
u := userRepository.GetUserByID(uid)
if u == nil {
w.WriteHeader(http.StatusNotFound)
return
}
c, _ := chat.GetClientsForUser(uid)
clients := make([]connectedClient, len(c))
for i, c := range c {
client := connectedClient{
Id: c.Id,
MessageCount: c.MessageCount,
UserAgent: c.UserAgent,
ConnectedAt: c.ConnectedAt,
}
if c.Geo != nil {
client.Geo = c.Geo.CountryCode
}
clients[i] = client
}
messages, err := chat.GetMessagesFromUser(uid)
if err != nil {
log.Errorln(err)
}
res := response{
User: u,
ConnectedClients: clients,
Messages: messages,
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(res); err != nil {
utils.InternalErrorHandler(w, err)
}
}

View File

@@ -0,0 +1,52 @@
package handlers
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/notifications"
"github.com/owncast/owncast/utils"
webutils "github.com/owncast/owncast/webserver/utils"
log "github.com/sirupsen/logrus"
)
// RegisterForLiveNotifications will register a channel + destination to be
// notified when a stream goes live.
func RegisterForLiveNotifications(u models.User, w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
webutils.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
type request struct {
// Channel is the notification channel (browser, sms, etc)
Channel string `json:"channel"`
// Destination is the target of the notification in the above channel.
Destination string `json:"destination"`
}
decoder := json.NewDecoder(r.Body)
var req request
if err := decoder.Decode(&req); err != nil {
log.Errorln(err)
webutils.WriteSimpleResponse(w, false, "unable to register for notifications")
return
}
// Make sure the requested channel is one we want to handle.
validTypes := []string{notifications.BrowserPushNotification}
_, validChannel := utils.FindInSlice(validTypes, req.Channel)
if !validChannel {
webutils.WriteSimpleResponse(w, false, "invalid notification channel: "+req.Channel)
return
}
if err := notifications.AddNotification(req.Channel, req.Destination); err != nil {
log.Errorln(err)
webutils.WriteSimpleResponse(w, false, "unable to save notification")
return
}
}

View File

@@ -0,0 +1,15 @@
package handlers
import (
"net/http"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/models"
)
// Ping is fired by a client to show they are still an active viewer.
func Ping(w http.ResponseWriter, r *http.Request) {
viewer := models.GenerateViewerFromRequest(r)
core.SetViewerActive(&viewer)
w.WriteHeader(http.StatusOK)
}

View File

@@ -0,0 +1,53 @@
package handlers
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/metrics"
"github.com/owncast/owncast/utils"
webutils "github.com/owncast/owncast/webserver/utils"
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 {
webutils.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
type reportPlaybackMetricsRequest struct {
Bandwidth float64 `json:"bandwidth"`
Latency float64 `json:"latency"`
Errors float64 `json:"errors"`
DownloadDuration float64 `json:"downloadDuration"`
QualityVariantChanges float64 `json:"qualityVariantChanges"`
}
decoder := json.NewDecoder(r.Body)
var request reportPlaybackMetricsRequest
if err := decoder.Decode(&request); err != nil {
log.Errorln("error decoding playback metrics payload:", err)
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
clientID := utils.GenerateClientIDFromRequest(r)
metrics.RegisterPlaybackErrorCount(clientID, request.Errors)
if request.Bandwidth != 0.0 {
metrics.RegisterPlayerBandwidth(clientID, request.Bandwidth)
}
if request.Latency != 0.0 {
metrics.RegisterPlayerLatency(clientID, request.Latency)
}
if request.DownloadDuration != 0.0 {
metrics.RegisterPlayerSegmentDownloadDuration(clientID, request.DownloadDuration)
}
metrics.RegisterQualityVariantChangesCount(clientID, request.QualityVariantChanges)
}

View File

@@ -0,0 +1,66 @@
package handlers
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/owncast/owncast/activitypub/webfinger"
"github.com/owncast/owncast/core/data"
webutils "github.com/owncast/owncast/webserver/utils"
)
// RemoteFollow handles a request to begin the remote follow redirect flow.
func RemoteFollow(w http.ResponseWriter, r *http.Request) {
type followRequest struct {
Account string `json:"account"`
}
type followResponse struct {
RedirectURL string `json:"redirectUrl"`
}
var request followRequest
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&request); err != nil {
webutils.WriteSimpleResponse(w, false, "unable to parse request")
return
}
if request.Account == "" {
webutils.WriteSimpleResponse(w, false, "Remote Fediverse account is required to follow.")
return
}
localActorPath, _ := url.Parse(data.GetServerURL())
localActorPath.Path = fmt.Sprintf("/federation/user/%s", data.GetDefaultFederationUsername())
var template string
links, err := webfinger.GetWebfingerLinks(request.Account)
if err != nil {
webutils.WriteSimpleResponse(w, false, err.Error())
return
}
// Acquire the remote follow redirect template.
for _, link := range links {
for k, v := range link {
if k == "rel" && v == "http://ostatus.org/schema/1.0/subscribe" && link["template"] != nil {
template = link["template"].(string)
}
}
}
if localActorPath.String() == "" || template == "" {
webutils.WriteSimpleResponse(w, false, "unable to determine remote follow information for "+request.Account)
return
}
redirectURL := strings.Replace(template, "{uri}", localActorPath.String(), 1)
response := followResponse{
RedirectURL: redirectURL,
}
webutils.WriteResponse(w, response)
}

View File

@@ -0,0 +1,28 @@
package handlers
import (
"net/http"
"strings"
"github.com/owncast/owncast/core/data"
)
// GetRobotsDotTxt returns the contents of our robots.txt.
func GetRobotsDotTxt(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
contents := []string{
"User-agent: *",
"Disallow: /admin",
"Disallow: /api",
}
if data.GetDisableSearchIndexing() {
contents = append(contents, "Disallow: /")
}
txt := []byte(strings.Join(contents, "\n"))
if _, err := w.Write(txt); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

View File

@@ -0,0 +1,52 @@
package handlers
import (
"encoding/json"
"net/http"
"time"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/utils"
"github.com/owncast/owncast/webserver/router/middleware"
webutils "github.com/owncast/owncast/webserver/utils"
)
// GetStatus gets the status of the server.
func 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 {
webutils.InternalErrorHandler(w, err)
}
}
func getStatusResponse() webStatusResponse {
status := core.GetStatus()
response := webStatusResponse{
Online: status.Online,
ServerTime: time.Now(),
LastConnectTime: status.LastConnectTime,
LastDisconnectTime: status.LastDisconnectTime,
VersionNumber: status.VersionNumber,
StreamTitle: status.StreamTitle,
}
if !data.GetHideViewerCount() {
response.ViewerCount = status.ViewerCount
}
return response
}
type webStatusResponse struct {
ServerTime time.Time `json:"serverTime"`
LastConnectTime *utils.NullTime `json:"lastConnectTime"`
LastDisconnectTime *utils.NullTime `json:"lastDisconnectTime"`
VersionNumber string `json:"versionNumber"`
StreamTitle string `json:"streamTitle"`
ViewerCount int `json:"viewerCount,omitempty"`
Online bool `json:"online"`
}

View File

@@ -0,0 +1,60 @@
package handlers
import (
"net/http"
"sort"
"github.com/owncast/owncast/core/data"
webutils "github.com/owncast/owncast/webserver/utils"
)
type variantsSort struct {
Name string
Index int
VideoBitrate int
IsVideoPassthrough bool
}
type variantsResponse struct {
Name string `json:"name"`
Index int `json:"index"`
}
// GetVideoStreamOutputVariants will return the video variants available.
func GetVideoStreamOutputVariants(w http.ResponseWriter, r *http.Request) {
outputVariants := data.GetStreamOutputVariants()
streamSortVariants := make([]variantsSort, len(outputVariants))
for i, variant := range outputVariants {
variantSort := variantsSort{
Index: i,
Name: variant.GetName(),
IsVideoPassthrough: variant.IsVideoPassthrough,
VideoBitrate: variant.VideoBitrate,
}
streamSortVariants[i] = variantSort
}
sort.Slice(streamSortVariants, func(i, j int) bool {
if streamSortVariants[i].IsVideoPassthrough && !streamSortVariants[j].IsVideoPassthrough {
return true
}
if !streamSortVariants[i].IsVideoPassthrough && streamSortVariants[j].IsVideoPassthrough {
return false
}
return streamSortVariants[i].VideoBitrate > streamSortVariants[j].VideoBitrate
})
response := make([]variantsResponse, len(streamSortVariants))
for i, variant := range streamSortVariants {
variantResponse := variantsResponse{
Index: variant.Index,
Name: variant.Name,
}
response[i] = variantResponse
}
webutils.WriteResponse(w, response)
}

14
webserver/handlers/web.go Normal file
View File

@@ -0,0 +1,14 @@
package handlers
import (
"net/http"
"github.com/owncast/owncast/static"
)
var staticServer = http.FileServer(http.FS(static.GetWeb()))
// serveWeb will serve web assets.
func serveWeb(w http.ResponseWriter, r *http.Request) {
staticServer.ServeHTTP(w, r)
}

View File

@@ -14,9 +14,8 @@ import (
"golang.org/x/net/http2/h2c"
"github.com/owncast/owncast/activitypub"
apControllers "github.com/owncast/owncast/activitypub/controllers"
aphandlers "github.com/owncast/owncast/activitypub/controllers"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/webserver/handlers"
@@ -44,19 +43,19 @@ func Start(enableVerboseLogging bool) error {
r.Handle("/public/*", http.StripPrefix("/public/", fs))
// Return HLS video
r.HandleFunc("/hls/*", controllers.HandleHLSRequest)
r.HandleFunc("/hls/*", handlers.HandleHLSRequest)
// The admin web app.
r.HandleFunc("/admin/*", middleware.RequireAdminAuth(controllers.IndexHandler))
r.HandleFunc("/admin/*", middleware.RequireAdminAuth(handlers.IndexHandler))
// Single ActivityPub Actor
r.HandleFunc("/federation/user/*", middleware.RequireActivityPubOrRedirect(apControllers.ActorHandler))
r.HandleFunc("/federation/user/*", middleware.RequireActivityPubOrRedirect(aphandlers.ActorHandler))
// Single AP object
r.HandleFunc("/federation/*", middleware.RequireActivityPubOrRedirect(apControllers.ObjectHandler))
r.HandleFunc("/federation/*", middleware.RequireActivityPubOrRedirect(aphandlers.ObjectHandler))
// The primary web app.
r.HandleFunc("/*", controllers.IndexHandler)
r.HandleFunc("/*", handlers.IndexHandler)
// mount the api
r.Mount("/api/", handlers.New().Handler())
@@ -105,40 +104,40 @@ func Start(enableVerboseLogging bool) error {
func addStaticFileEndpoints(r chi.Router) {
// Images
r.HandleFunc("/thumbnail.jpg", controllers.GetThumbnail)
r.HandleFunc("/preview.gif", controllers.GetPreview)
r.HandleFunc("/logo", controllers.GetLogo)
r.HandleFunc("/thumbnail.jpg", handlers.GetThumbnail)
r.HandleFunc("/preview.gif", handlers.GetPreview)
r.HandleFunc("/logo", handlers.GetLogo)
// return a logo that's compatible with external social networks
r.HandleFunc("/logo/external", controllers.GetCompatibleLogo)
r.HandleFunc("/logo/external", handlers.GetCompatibleLogo)
// Custom Javascript
r.HandleFunc("/customjavascript", controllers.ServeCustomJavascript)
r.HandleFunc("/customjavascript", handlers.ServeCustomJavascript)
// robots.txt
r.HandleFunc("/robots.txt", controllers.GetRobotsDotTxt)
r.HandleFunc("/robots.txt", handlers.GetRobotsDotTxt)
// Return a single emoji image.
emojiDir := config.EmojiDir
if !strings.HasSuffix(emojiDir, "*") {
emojiDir += "*"
}
r.HandleFunc(emojiDir, controllers.GetCustomEmojiImage)
r.HandleFunc(emojiDir, handlers.GetCustomEmojiImage)
// WebFinger
r.HandleFunc("/.well-known/webfinger", apControllers.WebfingerHandler)
r.HandleFunc("/.well-known/webfinger", aphandlers.WebfingerHandler)
// Host Metadata
r.HandleFunc("/.well-known/host-meta", apControllers.HostMetaController)
r.HandleFunc("/.well-known/host-meta", aphandlers.HostMetaController)
// Nodeinfo v1
r.HandleFunc("/.well-known/nodeinfo", apControllers.NodeInfoController)
r.HandleFunc("/.well-known/nodeinfo", aphandlers.NodeInfoController)
// x-nodeinfo v2
r.HandleFunc("/.well-known/x-nodeinfo2", apControllers.XNodeInfo2Controller)
r.HandleFunc("/.well-known/x-nodeinfo2", aphandlers.XNodeInfo2Controller)
// Nodeinfo v2
r.HandleFunc("/nodeinfo/2.0", apControllers.NodeInfoV2Controller)
r.HandleFunc("/nodeinfo/2.0", aphandlers.NodeInfoV2Controller)
// Instance details
r.HandleFunc("/api/v1/instance", apControllers.InstanceV1Controller)
r.HandleFunc("/api/v1/instance", aphandlers.InstanceV1Controller)
}

View File

@@ -0,0 +1,7 @@
package utils
// PaginatedResponse is a structure for returning a total count with results.
type PaginatedResponse struct {
Results interface{} `json:"results"`
Total int `json:"total"`
}

View File

@@ -0,0 +1,77 @@
package utils
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/webserver/router/middleware"
log "github.com/sirupsen/logrus"
)
type J map[string]interface{}
// InternalErrorHandler will return an error message as an HTTP response.
func InternalErrorHandler(w http.ResponseWriter, err error) {
if err == nil {
return
}
if err := json.NewEncoder(w).Encode(J{"error": err.Error()}); err != nil {
InternalErrorHandler(w, err)
}
}
// BadRequestHandler will return an HTTP 500 as an HTTP response.
func BadRequestHandler(w http.ResponseWriter, err error) {
if err == nil {
return
}
log.Debugln(err)
w.WriteHeader(http.StatusBadRequest)
if err := json.NewEncoder(w).Encode(J{"error": err.Error()}); err != nil {
InternalErrorHandler(w, err)
}
}
// WriteSimpleResponse will return a message as a response.
func WriteSimpleResponse(w http.ResponseWriter, success bool, message string) {
response := models.BaseAPIResponse{
Success: success,
Message: message,
}
w.Header().Set("Content-Type", "application/json")
if success {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusBadRequest)
}
if err := json.NewEncoder(w).Encode(response); err != nil {
InternalErrorHandler(w, err)
}
}
// WriteResponse will return an object as a JSON encoded uncacheable response.
func WriteResponse(w http.ResponseWriter, response interface{}) {
w.Header().Set("Content-Type", "application/json")
middleware.DisableCache(w)
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(response); err != nil {
InternalErrorHandler(w, err)
}
}
// WriteString will return a basic string and a status code to the client.
func WriteString(w http.ResponseWriter, text string, status int) error {
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(status)
_, err := w.Write([]byte(text))
return err
}