chore(api): reorganize handlers into webserver package
This commit is contained in:
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user