chore(api): reorganize handlers into webserver package

This commit is contained in:
Gabe Kangas
2024-07-01 21:44:51 -07:00
parent e200692502
commit 93c0a20935
54 changed files with 904 additions and 933 deletions
+35
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")
}
+384
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")
}
+915
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")
}
@@ -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)
}
+21
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")
}
+96
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))
}
@@ -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")
}
+179
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)
}
+82
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)
}
+20
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)
}
}
+58
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,
}
}
+60
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")
}
+174
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"`
}
+52
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"`
}
+202
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
}
+87
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)
}
}
+54
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)
}
+84
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")
}
+20
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")
}