chore(api): reorganize handlers into webserver package
This commit is contained in:
@@ -3,8 +3,8 @@ package handlers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/controllers"
|
||||
"github.com/owncast/owncast/controllers/admin"
|
||||
"github.com/owncast/owncast/core/rtmp"
|
||||
"github.com/owncast/owncast/webserver/handlers/admin"
|
||||
"github.com/owncast/owncast/webserver/handlers/generated"
|
||||
"github.com/owncast/owncast/webserver/router/middleware"
|
||||
)
|
||||
@@ -154,11 +154,11 @@ func (*ServerInterfaceImpl) GetWarningsOptions(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) GetFollowersAdmin(w http.ResponseWriter, r *http.Request, params generated.GetFollowersAdminParams) {
|
||||
middleware.RequireAdminAuth(middleware.HandlePagination(controllers.GetFollowers))(w, r)
|
||||
middleware.RequireAdminAuth(middleware.HandlePagination(GetFollowers))(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) GetFollowersAdminOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(middleware.HandlePagination(controllers.GetFollowers))(w, r)
|
||||
middleware.RequireAdminAuth(middleware.HandlePagination(GetFollowers))(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) GetPendingFollowRequests(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -304,3 +304,9 @@ func (*ServerInterfaceImpl) GetFederatedActions(w http.ResponseWriter, r *http.R
|
||||
func (*ServerInterfaceImpl) GetFederatedActionsOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(middleware.HandlePagination(admin.GetFederatedActions))(w, r)
|
||||
}
|
||||
|
||||
// DisconnectInboundConnection will force-disconnect an inbound stream.
|
||||
func DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) {
|
||||
rtmp.Disconnect()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
35
webserver/handlers/admin/appearance.go
Normal file
35
webserver/handlers/admin/appearance.go
Normal 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
webserver/handlers/admin/chat.go
Normal file
384
webserver/handlers/admin/chat.go
Normal 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
webserver/handlers/admin/config.go
Normal file
915
webserver/handlers/admin/config.go
Normal 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")
|
||||
}
|
||||
25
webserver/handlers/admin/connectedClients.go
Normal file
25
webserver/handlers/admin/connectedClients.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/core/chat"
|
||||
"github.com/owncast/owncast/models"
|
||||
webutils "github.com/owncast/owncast/webserver/utils"
|
||||
)
|
||||
|
||||
// GetConnectedChatClients returns currently connected clients.
|
||||
func GetConnectedChatClients(w http.ResponseWriter, r *http.Request) {
|
||||
clients := chat.GetClients()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(clients); err != nil {
|
||||
webutils.InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ExternalGetConnectedChatClients returns currently connected clients.
|
||||
func ExternalGetConnectedChatClients(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
||||
GetConnectedChatClients(w, r)
|
||||
}
|
||||
21
webserver/handlers/admin/disconnect.go
Normal file
21
webserver/handlers/admin/disconnect.go
Normal 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
webserver/handlers/admin/emoji.go
Normal file
96
webserver/handlers/admin/emoji.go
Normal 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))
|
||||
}
|
||||
109
webserver/handlers/admin/externalAPIUsers.go
Normal file
109
webserver/handlers/admin/externalAPIUsers.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/persistence/userrepository"
|
||||
"github.com/owncast/owncast/utils"
|
||||
webutils "github.com/owncast/owncast/webserver/utils"
|
||||
)
|
||||
|
||||
type deleteExternalAPIUserRequest struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type createExternalAPIUserRequest struct {
|
||||
Name string `json:"name"`
|
||||
Scopes []string `json:"scopes"`
|
||||
}
|
||||
|
||||
// CreateExternalAPIUser will generate a 3rd party access token.
|
||||
func CreateExternalAPIUser(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request createExternalAPIUserRequest
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
webutils.BadRequestHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
userRepository := userrepository.Get()
|
||||
|
||||
// Verify all the scopes provided are valid
|
||||
if !userRepository.HasValidScopes(request.Scopes) {
|
||||
webutils.BadRequestHandler(w, errors.New("one or more invalid scopes provided"))
|
||||
return
|
||||
}
|
||||
|
||||
token, err := utils.GenerateAccessToken()
|
||||
if err != nil {
|
||||
webutils.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
color := utils.GenerateRandomDisplayColor(config.MaxUserColor)
|
||||
|
||||
if err := userRepository.InsertExternalAPIUser(token, request.Name, color, request.Scopes); err != nil {
|
||||
webutils.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
webutils.WriteResponse(w, models.ExternalAPIUser{
|
||||
AccessToken: token,
|
||||
DisplayName: request.Name,
|
||||
DisplayColor: color,
|
||||
Scopes: request.Scopes,
|
||||
CreatedAt: time.Now(),
|
||||
LastUsedAt: nil,
|
||||
})
|
||||
}
|
||||
|
||||
// GetExternalAPIUsers will return all 3rd party access tokens.
|
||||
func GetExternalAPIUsers(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
userRepository := userrepository.Get()
|
||||
|
||||
tokens, err := userRepository.GetExternalAPIUser()
|
||||
if err != nil {
|
||||
webutils.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
webutils.WriteResponse(w, tokens)
|
||||
}
|
||||
|
||||
// DeleteExternalAPIUser will return a single 3rd party access token.
|
||||
func DeleteExternalAPIUser(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
webutils.WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||
return
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request deleteExternalAPIUserRequest
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
webutils.BadRequestHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if request.Token == "" {
|
||||
webutils.BadRequestHandler(w, errors.New("must provide a token"))
|
||||
return
|
||||
}
|
||||
|
||||
userRepository := userrepository.Get()
|
||||
|
||||
if err := userRepository.DeleteExternalAPIUser(request.Token); err != nil {
|
||||
webutils.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
webutils.WriteSimpleResponse(w, true, "deleted token")
|
||||
}
|
||||
179
webserver/handlers/admin/federation.go
Normal file
179
webserver/handlers/admin/federation.go
Normal 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
webserver/handlers/admin/followers.go
Normal file
82
webserver/handlers/admin/followers.go
Normal 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
webserver/handlers/admin/hardware.go
Normal file
20
webserver/handlers/admin/hardware.go
Normal 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
webserver/handlers/admin/logs.go
Normal file
58
webserver/handlers/admin/logs.go
Normal 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
webserver/handlers/admin/notifications.go
Normal file
60
webserver/handlers/admin/notifications.go
Normal 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
webserver/handlers/admin/serverConfig.go
Normal file
174
webserver/handlers/admin/serverConfig.go
Normal 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
webserver/handlers/admin/status.go
Normal file
52
webserver/handlers/admin/status.go
Normal 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
webserver/handlers/admin/update.go
Normal file
202
webserver/handlers/admin/update.go
Normal 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
webserver/handlers/admin/video.go
Normal file
87
webserver/handlers/admin/video.go
Normal 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
webserver/handlers/admin/viewers.go
Normal file
54
webserver/handlers/admin/viewers.go
Normal 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
webserver/handlers/admin/webhooks.go
Normal file
84
webserver/handlers/admin/webhooks.go
Normal 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
webserver/handlers/admin/yp.go
Normal file
20
webserver/handlers/admin/yp.go
Normal 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")
|
||||
}
|
||||
@@ -3,8 +3,8 @@ package handlers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/controllers/auth/fediverse"
|
||||
"github.com/owncast/owncast/controllers/auth/indieauth"
|
||||
"github.com/owncast/owncast/webserver/handlers/auth/fediverse"
|
||||
"github.com/owncast/owncast/webserver/handlers/auth/indieauth"
|
||||
"github.com/owncast/owncast/webserver/handlers/generated"
|
||||
"github.com/owncast/owncast/webserver/router/middleware"
|
||||
)
|
||||
|
||||
110
webserver/handlers/auth/fediverse/fediverse.go
Normal file
110
webserver/handlers/auth/fediverse/fediverse.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package fediverse
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/activitypub"
|
||||
fediverseauth "github.com/owncast/owncast/auth/fediverse"
|
||||
"github.com/owncast/owncast/core/chat"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/persistence/userrepository"
|
||||
webutils "github.com/owncast/owncast/webserver/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// RegisterFediverseOTPRequest registers a new OTP request for the given access token.
|
||||
func RegisterFediverseOTPRequest(u models.User, w http.ResponseWriter, r *http.Request) {
|
||||
type request struct {
|
||||
FediverseAccount string `json:"account"`
|
||||
}
|
||||
var req request
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
if err := decoder.Decode(&req); err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, "Could not decode request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
accessToken := r.URL.Query().Get("accessToken")
|
||||
reg, success, err := fediverseauth.RegisterFediverseOTP(accessToken, u.ID, u.DisplayName, req.FediverseAccount)
|
||||
if err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, "Could not register auth request: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !success {
|
||||
webutils.WriteSimpleResponse(w, false, "Could not register auth request. One may already be pending. Try again later.")
|
||||
return
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("<p>One-time code from %s: %s. If you did not request this message please ignore or block.</p>", data.GetServerName(), reg.Code)
|
||||
if err := activitypub.SendDirectFederatedMessage(msg, reg.Account); err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, "Could not send code to fediverse: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
webutils.WriteSimpleResponse(w, true, "")
|
||||
}
|
||||
|
||||
// VerifyFediverseOTPRequest verifies the given OTP code for the given access token.
|
||||
func VerifyFediverseOTPRequest(w http.ResponseWriter, r *http.Request) {
|
||||
type request struct {
|
||||
Code string `json:"code"`
|
||||
}
|
||||
|
||||
var req request
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
if err := decoder.Decode(&req); err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, "Could not decode request: "+err.Error())
|
||||
return
|
||||
}
|
||||
accessToken := r.URL.Query().Get("accessToken")
|
||||
valid, authRegistration := fediverseauth.ValidateFediverseOTP(accessToken, req.Code)
|
||||
if !valid {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
userRepository := userrepository.Get()
|
||||
|
||||
// Check if a user with this auth already exists, if so, log them in.
|
||||
if u := userRepository.GetUserByAuth(authRegistration.Account, models.Fediverse); u != nil {
|
||||
// Handle existing auth.
|
||||
log.Debugln("user with provided fedvierse identity already exists, logging them in")
|
||||
|
||||
// Update the current user's access token to point to the existing user id.
|
||||
userID := u.ID
|
||||
if err := userRepository.SetAccessTokenToOwner(accessToken, userID); err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if authRegistration.UserDisplayName != u.DisplayName {
|
||||
loginMessage := fmt.Sprintf("**%s** is now authenticated as **%s**", authRegistration.UserDisplayName, u.DisplayName)
|
||||
if err := chat.SendSystemAction(loginMessage, true); err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
}
|
||||
|
||||
webutils.WriteSimpleResponse(w, true, "")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, save this as new auth.
|
||||
log.Debug("fediverse account does not already exist, saving it as a new one for the current user")
|
||||
if err := userRepository.AddAuth(authRegistration.UserID, authRegistration.Account, models.Fediverse); err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Update the current user's authenticated flag so we can show it in
|
||||
// the chat UI.
|
||||
if err := userRepository.SetUserAsAuthenticated(authRegistration.UserID); err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
|
||||
webutils.WriteSimpleResponse(w, true, "")
|
||||
}
|
||||
107
webserver/handlers/auth/indieauth/client.go
Normal file
107
webserver/handlers/auth/indieauth/client.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package indieauth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
ia "github.com/owncast/owncast/auth/indieauth"
|
||||
"github.com/owncast/owncast/core/chat"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/persistence/userrepository"
|
||||
webutils "github.com/owncast/owncast/webserver/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// StartAuthFlow will begin the IndieAuth flow for the current user.
|
||||
func StartAuthFlow(u models.User, w http.ResponseWriter, r *http.Request) {
|
||||
type request struct {
|
||||
AuthHost string `json:"authHost"`
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Redirect string `json:"redirect"`
|
||||
}
|
||||
|
||||
var authRequest request
|
||||
p, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(p, &authRequest); err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
accessToken := r.URL.Query().Get("accessToken")
|
||||
|
||||
redirectURL, err := ia.StartAuthFlow(authRequest.AuthHost, u.ID, accessToken, u.DisplayName)
|
||||
if err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
redirectResponse := response{
|
||||
Redirect: redirectURL.String(),
|
||||
}
|
||||
webutils.WriteResponse(w, redirectResponse)
|
||||
}
|
||||
|
||||
// HandleRedirect will handle the redirect from an IndieAuth server to
|
||||
// continue the auth flow.
|
||||
func HandleRedirect(w http.ResponseWriter, r *http.Request) {
|
||||
state := r.URL.Query().Get("state")
|
||||
code := r.URL.Query().Get("code")
|
||||
request, response, err := ia.HandleCallbackCode(code, state)
|
||||
if err != nil {
|
||||
log.Debugln(err)
|
||||
msg := `Unable to complete authentication. <a href="/">Go back.</a><hr/>`
|
||||
_ = webutils.WriteString(w, msg, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
userRepository := userrepository.Get()
|
||||
|
||||
// Check if a user with this auth already exists, if so, log them in.
|
||||
if u := userRepository.GetUserByAuth(response.Me, models.IndieAuth); u != nil {
|
||||
// Handle existing auth.
|
||||
log.Debugln("user with provided indieauth already exists, logging them in")
|
||||
|
||||
// Update the current user's access token to point to the existing user id.
|
||||
accessToken := request.CurrentAccessToken
|
||||
userID := u.ID
|
||||
if err := userRepository.SetAccessTokenToOwner(accessToken, userID); err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if request.DisplayName != u.DisplayName {
|
||||
loginMessage := fmt.Sprintf("**%s** is now authenticated as **%s**", request.DisplayName, u.DisplayName)
|
||||
if err := chat.SendSystemAction(loginMessage, true); err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, save this as new auth.
|
||||
log.Debug("indieauth token does not already exist, saving it as a new one for the current user")
|
||||
if err := userRepository.AddAuth(request.UserID, response.Me, models.IndieAuth); err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Update the current user's authenticated flag so we can show it in
|
||||
// the chat UI.
|
||||
if err := userRepository.SetUserAsAuthenticated(request.UserID); err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
|
||||
}
|
||||
83
webserver/handlers/auth/indieauth/server.go
Normal file
83
webserver/handlers/auth/indieauth/server.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package indieauth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
ia "github.com/owncast/owncast/auth/indieauth"
|
||||
"github.com/owncast/owncast/webserver/router/middleware"
|
||||
webutils "github.com/owncast/owncast/webserver/utils"
|
||||
)
|
||||
|
||||
// HandleAuthEndpoint will handle the IndieAuth auth endpoint.
|
||||
func HandleAuthEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodGet {
|
||||
// Require the GET request for IndieAuth to be behind admin login.
|
||||
f := middleware.RequireAdminAuth(HandleAuthEndpointGet)
|
||||
f(w, r)
|
||||
return
|
||||
} else if r.Method == http.MethodPost {
|
||||
HandleAuthEndpointPost(w, r)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HandleAuthEndpointGet(w http.ResponseWriter, r *http.Request) {
|
||||
clientID := r.URL.Query().Get("client_id")
|
||||
redirectURI := r.URL.Query().Get("redirect_uri")
|
||||
codeChallenge := r.URL.Query().Get("code_challenge")
|
||||
state := r.URL.Query().Get("state")
|
||||
me := r.URL.Query().Get("me")
|
||||
|
||||
request, err := ia.StartServerAuth(clientID, redirectURI, codeChallenge, state, me)
|
||||
if err != nil {
|
||||
_ = webutils.WriteString(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Redirect the client browser with the values we generated to continue
|
||||
// the IndieAuth flow.
|
||||
// If the URL is invalid then return with specific "invalid_request" error.
|
||||
u, err := url.Parse(redirectURI)
|
||||
if err != nil {
|
||||
webutils.WriteResponse(w, ia.Response{
|
||||
Error: "invalid_request",
|
||||
ErrorDescription: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
redirectParams := u.Query()
|
||||
redirectParams.Set("code", request.Code)
|
||||
redirectParams.Set("state", request.State)
|
||||
u.RawQuery = redirectParams.Encode()
|
||||
|
||||
http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
func HandleAuthEndpointPost(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
code := r.PostForm.Get("code")
|
||||
redirectURI := r.PostForm.Get("redirect_uri")
|
||||
clientID := r.PostForm.Get("client_id")
|
||||
codeVerifier := r.PostForm.Get("code_verifier")
|
||||
|
||||
// If the server auth flow cannot be completed then return with specific
|
||||
// "invalid_client" error.
|
||||
response, err := ia.CompleteServerAuth(code, redirectURI, clientID, codeVerifier)
|
||||
if err != nil {
|
||||
webutils.WriteResponse(w, ia.Response{
|
||||
Error: "invalid_client",
|
||||
ErrorDescription: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
webutils.WriteResponse(w, response)
|
||||
}
|
||||
120
webserver/handlers/chat.go
Normal file
120
webserver/handlers/chat.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/chat"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/persistence/userrepository"
|
||||
"github.com/owncast/owncast/utils"
|
||||
"github.com/owncast/owncast/webserver/router/middleware"
|
||||
webutils "github.com/owncast/owncast/webserver/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ExternalGetChatMessages gets all of the chat messages.
|
||||
func ExternalGetChatMessages(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
||||
middleware.EnableCors(w)
|
||||
getChatMessages(w, r)
|
||||
}
|
||||
|
||||
// GetChatMessages gets all of the chat messages.
|
||||
func GetChatMessages(u models.User, w http.ResponseWriter, r *http.Request) {
|
||||
middleware.EnableCors(w)
|
||||
getChatMessages(w, r)
|
||||
}
|
||||
|
||||
func getChatMessages(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
messages := chat.GetChatHistory()
|
||||
|
||||
if err := json.NewEncoder(w).Encode(messages); err != nil {
|
||||
log.Debugln(err)
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
if err := json.NewEncoder(w).Encode(webutils.J{"error": "method not implemented (PRs are accepted)"}); err != nil {
|
||||
webutils.InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterAnonymousChatUser will register a new user.
|
||||
func RegisterAnonymousChatUser(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.EnableCors(w)
|
||||
|
||||
userRepository := userrepository.Get()
|
||||
|
||||
if r.Method == http.MethodOptions {
|
||||
// All OPTIONS requests should have a wildcard CORS header.
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
// nolint:goconst
|
||||
webutils.WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||
return
|
||||
}
|
||||
|
||||
type registerAnonymousUserRequest struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
|
||||
type registerAnonymousUserResponse struct {
|
||||
ID string `json:"id"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request registerAnonymousUserRequest
|
||||
if err := decoder.Decode(&request); err != nil { //nolint
|
||||
// this is fine. register a new user anyway.
|
||||
}
|
||||
|
||||
proposedNewDisplayName := r.Header.Get("X-Forwarded-User")
|
||||
if proposedNewDisplayName == "" {
|
||||
proposedNewDisplayName = request.DisplayName
|
||||
}
|
||||
if proposedNewDisplayName == "" {
|
||||
proposedNewDisplayName = generateDisplayName()
|
||||
}
|
||||
|
||||
proposedNewDisplayName = utils.MakeSafeStringOfLength(proposedNewDisplayName, config.MaxChatDisplayNameLength)
|
||||
newUser, accessToken, err := userRepository.CreateAnonymousUser(proposedNewDisplayName)
|
||||
if err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := registerAnonymousUserResponse{
|
||||
ID: newUser.ID,
|
||||
AccessToken: accessToken,
|
||||
DisplayName: newUser.DisplayName,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
middleware.DisableCache(w)
|
||||
|
||||
webutils.WriteResponse(w, response)
|
||||
}
|
||||
|
||||
func generateDisplayName() string {
|
||||
suggestedUsernamesList := data.GetSuggestedUsernamesList()
|
||||
minSuggestedUsernamePoolLength := 10
|
||||
|
||||
if len(suggestedUsernamesList) >= minSuggestedUsernamePoolLength {
|
||||
index := utils.RandomIndex(len(suggestedUsernamesList))
|
||||
return suggestedUsernamesList[index]
|
||||
} else {
|
||||
return utils.GeneratePhrase()
|
||||
}
|
||||
}
|
||||
@@ -1,368 +1,156 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/owncast/owncast/controllers/admin"
|
||||
"github.com/owncast/owncast/activitypub"
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/utils"
|
||||
"github.com/owncast/owncast/webserver/router/middleware"
|
||||
webutils "github.com/owncast/owncast/webserver/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (*ServerInterfaceImpl) SetAdminPassword(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetAdminPassword)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetAdminPasswordOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetAdminPassword)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamKeys(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamKeys)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamKeysOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamKeys)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetExtraPageContent(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetExtraPageContent)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetExtraPageContentOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetExtraPageContent)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamTitle(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamTitle)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamTitleOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamTitle)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerName(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerName)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerNameOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerName)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerSummary(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerSummary)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerSummaryOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerSummary)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomOfflineMessage(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomOfflineMessage)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomOfflineMessageOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomOfflineMessage)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerWelcomeMessage(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerWelcomeMessage)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerWelcomeMessageOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerWelcomeMessage)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatDisabled(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatDisabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatDisabledOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatDisabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatJoinMessagesEnabled(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatJoinMessagesEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatJoinMessagesEnabledOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatJoinMessagesEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetEnableEstablishedChatUserMode(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetEnableEstablishedChatUserMode)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetEnableEstablishedChatUserModeOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetEnableEstablishedChatUserMode)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetForbiddenUsernameList(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetForbiddenUsernameList)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetForbiddenUsernameListOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetForbiddenUsernameList)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetSuggestedUsernameList(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetSuggestedUsernameList)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetSuggestedUsernameListOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetSuggestedUsernameList)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatSpamProtectionEnabled(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatSpamProtectionEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatSpamProtectionEnabledOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatSpamProtectionEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatSlurFilterEnabled(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatSlurFilterEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatSlurFilterEnabledOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatSlurFilterEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetVideoCodec(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetVideoCodec)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetVideoCodecOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetVideoCodec)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamLatencyLevel(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamLatencyLevel)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamLatencyLevelOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamLatencyLevel)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamOutputVariants(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamOutputVariants)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamOutputVariantsOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamOutputVariants)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomColorVariableValues(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomColorVariableValues)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomColorVariableValuesOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomColorVariableValues)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetLogo(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetLogo)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetLogoOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetLogo)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetTags(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetTags)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetTagsOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetTags)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFfmpegPath(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFfmpegPath)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFfmpegPathOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFfmpegPath)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetWebServerPort(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetWebServerPort)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetWebServerPortOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetWebServerPort)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetWebServerIP(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetWebServerIP)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetWebServerIPOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetWebServerIP)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetRTMPServerPort(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetRTMPServerPort)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetRTMPServerPortOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetRTMPServerPort)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetSocketHostOverride(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetSocketHostOverride)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetSocketHostOverrideOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetSocketHostOverride)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetVideoServingEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetVideoServingEndpoint)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetVideoServingEndpointOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetVideoServingEndpoint)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetNSFW(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetNSFW)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetNSFWOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetNSFW)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetDirectoryEnabled(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetDirectoryEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetDirectoryEnabledOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetDirectoryEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetSocialHandles(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetSocialHandles)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetSocialHandlesOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetSocialHandles)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetS3Configuration(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetS3Configuration)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetS3ConfigurationOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetS3Configuration)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerURL(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerURL)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerURLOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerURL)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetExternalActions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetExternalActions)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetExternalActionsOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetExternalActions)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomStyles(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomStyles)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomStylesOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomStyles)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomJavascript(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomJavascript)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomJavascriptOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomJavascript)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetHideViewerCount(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetHideViewerCount)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetHideViewerCountOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetHideViewerCount)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetDisableSearchIndexing(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetDisableSearchIndexing)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetDisableSearchIndexingOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetDisableSearchIndexing)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationEnabled(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationEnabledOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationActivityPrivate(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationActivityPrivate)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationActivityPrivateOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationActivityPrivate)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationShowEngagement(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationShowEngagement)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationShowEngagementOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationShowEngagement)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationUsername(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationUsername)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationUsernameOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationUsername)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationGoLiveMessage(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationGoLiveMessage)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationGoLiveMessageOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationGoLiveMessage)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationBlockDomains(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationBlockDomains)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationBlockDomainsOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationBlockDomains)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetDiscordNotificationConfiguration(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetDiscordNotificationConfiguration)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetDiscordNotificationConfigurationOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetDiscordNotificationConfiguration)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetBrowserNotificationConfiguration(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetBrowserNotificationConfiguration)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetBrowserNotificationConfigurationOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetBrowserNotificationConfiguration)(w, r)
|
||||
type webConfigResponse struct {
|
||||
AppearanceVariables map[string]string `json:"appearanceVariables"`
|
||||
Name string `json:"name"`
|
||||
CustomStyles string `json:"customStyles"`
|
||||
StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream
|
||||
OfflineMessage string `json:"offlineMessage"`
|
||||
Logo string `json:"logo"`
|
||||
Version string `json:"version"`
|
||||
SocketHostOverride string `json:"socketHostOverride,omitempty"`
|
||||
ExtraPageContent string `json:"extraPageContent"`
|
||||
Summary string `json:"summary"`
|
||||
Tags []string `json:"tags"`
|
||||
SocialHandles []models.SocialHandle `json:"socialHandles"`
|
||||
ExternalActions []models.ExternalAction `json:"externalActions"`
|
||||
Notifications notificationsConfigResponse `json:"notifications"`
|
||||
Federation federationConfigResponse `json:"federation"`
|
||||
MaxSocketPayloadSize int `json:"maxSocketPayloadSize"`
|
||||
HideViewerCount bool `json:"hideViewerCount"`
|
||||
ChatDisabled bool `json:"chatDisabled"`
|
||||
ChatSpamProtectionDisabled bool `json:"chatSpamProtectionDisabled"`
|
||||
NSFW bool `json:"nsfw"`
|
||||
Authentication authenticationConfigResponse `json:"authentication"`
|
||||
}
|
||||
|
||||
type federationConfigResponse struct {
|
||||
Account string `json:"account,omitempty"`
|
||||
FollowerCount int `json:"followerCount,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type browserNotificationsConfigResponse struct {
|
||||
PublicKey string `json:"publicKey,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type notificationsConfigResponse struct {
|
||||
Browser browserNotificationsConfigResponse `json:"browser"`
|
||||
}
|
||||
|
||||
type authenticationConfigResponse struct {
|
||||
IndieAuthEnabled bool `json:"indieAuthEnabled"`
|
||||
}
|
||||
|
||||
// GetWebConfig gets the status of the server.
|
||||
func GetWebConfig(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.EnableCors(w)
|
||||
middleware.DisableCache(w)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
configuration := getConfigResponse()
|
||||
|
||||
if err := json.NewEncoder(w).Encode(configuration); err != nil {
|
||||
webutils.BadRequestHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
func getConfigResponse() webConfigResponse {
|
||||
pageContent := utils.RenderPageContentMarkdown(data.GetExtraPageBodyContent())
|
||||
offlineMessage := utils.RenderSimpleMarkdown(data.GetCustomOfflineMessage())
|
||||
socialHandles := data.GetSocialHandles()
|
||||
for i, handle := range socialHandles {
|
||||
platform := models.GetSocialHandle(handle.Platform)
|
||||
if platform != nil {
|
||||
handle.Icon = platform.Icon
|
||||
socialHandles[i] = handle
|
||||
}
|
||||
}
|
||||
|
||||
serverSummary := data.GetServerSummary()
|
||||
|
||||
var federationResponse federationConfigResponse
|
||||
federationEnabled := data.GetFederationEnabled()
|
||||
|
||||
followerCount, _ := activitypub.GetFollowerCount()
|
||||
if federationEnabled {
|
||||
serverURLString := data.GetServerURL()
|
||||
serverURL, _ := url.Parse(serverURLString)
|
||||
account := fmt.Sprintf("%s@%s", data.GetDefaultFederationUsername(), serverURL.Host)
|
||||
federationResponse = federationConfigResponse{
|
||||
Enabled: federationEnabled,
|
||||
FollowerCount: int(followerCount),
|
||||
Account: account,
|
||||
}
|
||||
}
|
||||
|
||||
browserPushEnabled := data.GetBrowserPushConfig().Enabled
|
||||
browserPushPublicKey, err := data.GetBrowserPushPublicKey()
|
||||
if err != nil {
|
||||
log.Errorln("unable to fetch browser push notifications public key", err)
|
||||
browserPushEnabled = false
|
||||
}
|
||||
|
||||
notificationsResponse := notificationsConfigResponse{
|
||||
Browser: browserNotificationsConfigResponse{
|
||||
Enabled: browserPushEnabled,
|
||||
PublicKey: browserPushPublicKey,
|
||||
},
|
||||
}
|
||||
|
||||
authenticationResponse := authenticationConfigResponse{
|
||||
IndieAuthEnabled: data.GetServerURL() != "",
|
||||
}
|
||||
|
||||
return webConfigResponse{
|
||||
Name: data.GetServerName(),
|
||||
Summary: serverSummary,
|
||||
OfflineMessage: offlineMessage,
|
||||
Logo: "/logo",
|
||||
Tags: data.GetServerMetadataTags(),
|
||||
Version: config.GetReleaseString(),
|
||||
NSFW: data.GetNSFW(),
|
||||
SocketHostOverride: data.GetWebsocketOverrideHost(),
|
||||
ExtraPageContent: pageContent,
|
||||
StreamTitle: data.GetStreamTitle(),
|
||||
SocialHandles: socialHandles,
|
||||
ChatDisabled: data.GetChatDisabled(),
|
||||
ChatSpamProtectionDisabled: data.GetChatSpamProtectionEnabled(),
|
||||
ExternalActions: data.GetExternalActions(),
|
||||
CustomStyles: data.GetCustomStyles(),
|
||||
MaxSocketPayloadSize: config.MaxSocketPayloadSize,
|
||||
Federation: federationResponse,
|
||||
Notifications: notificationsResponse,
|
||||
Authentication: authenticationResponse,
|
||||
AppearanceVariables: data.GetCustomColorVariableValues(),
|
||||
HideViewerCount: data.GetHideViewerCount(),
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllSocialPlatforms will return a list of all social platform types.
|
||||
func GetAllSocialPlatforms(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.EnableCors(w)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
platforms := models.GetAllSocialHandles()
|
||||
if err := json.NewEncoder(w).Encode(platforms); err != nil {
|
||||
webutils.InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
368
webserver/handlers/configInterface.go
Normal file
368
webserver/handlers/configInterface.go
Normal file
@@ -0,0 +1,368 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/webserver/handlers/admin"
|
||||
"github.com/owncast/owncast/webserver/router/middleware"
|
||||
)
|
||||
|
||||
func (*ServerInterfaceImpl) SetAdminPassword(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetAdminPassword)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetAdminPasswordOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetAdminPassword)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamKeys(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamKeys)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamKeysOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamKeys)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetExtraPageContent(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetExtraPageContent)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetExtraPageContentOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetExtraPageContent)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamTitle(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamTitle)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamTitleOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamTitle)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerName(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerName)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerNameOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerName)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerSummary(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerSummary)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerSummaryOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerSummary)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomOfflineMessage(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomOfflineMessage)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomOfflineMessageOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomOfflineMessage)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerWelcomeMessage(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerWelcomeMessage)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerWelcomeMessageOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerWelcomeMessage)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatDisabled(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatDisabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatDisabledOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatDisabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatJoinMessagesEnabled(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatJoinMessagesEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatJoinMessagesEnabledOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatJoinMessagesEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetEnableEstablishedChatUserMode(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetEnableEstablishedChatUserMode)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetEnableEstablishedChatUserModeOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetEnableEstablishedChatUserMode)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetForbiddenUsernameList(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetForbiddenUsernameList)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetForbiddenUsernameListOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetForbiddenUsernameList)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetSuggestedUsernameList(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetSuggestedUsernameList)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetSuggestedUsernameListOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetSuggestedUsernameList)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatSpamProtectionEnabled(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatSpamProtectionEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatSpamProtectionEnabledOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatSpamProtectionEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatSlurFilterEnabled(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatSlurFilterEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetChatSlurFilterEnabledOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetChatSlurFilterEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetVideoCodec(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetVideoCodec)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetVideoCodecOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetVideoCodec)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamLatencyLevel(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamLatencyLevel)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamLatencyLevelOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamLatencyLevel)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamOutputVariants(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamOutputVariants)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetStreamOutputVariantsOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetStreamOutputVariants)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomColorVariableValues(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomColorVariableValues)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomColorVariableValuesOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomColorVariableValues)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetLogo(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetLogo)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetLogoOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetLogo)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetTags(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetTags)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetTagsOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetTags)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFfmpegPath(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFfmpegPath)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFfmpegPathOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFfmpegPath)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetWebServerPort(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetWebServerPort)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetWebServerPortOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetWebServerPort)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetWebServerIP(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetWebServerIP)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetWebServerIPOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetWebServerIP)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetRTMPServerPort(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetRTMPServerPort)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetRTMPServerPortOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetRTMPServerPort)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetSocketHostOverride(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetSocketHostOverride)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetSocketHostOverrideOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetSocketHostOverride)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetVideoServingEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetVideoServingEndpoint)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetVideoServingEndpointOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetVideoServingEndpoint)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetNSFW(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetNSFW)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetNSFWOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetNSFW)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetDirectoryEnabled(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetDirectoryEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetDirectoryEnabledOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetDirectoryEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetSocialHandles(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetSocialHandles)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetSocialHandlesOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetSocialHandles)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetS3Configuration(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetS3Configuration)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetS3ConfigurationOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetS3Configuration)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerURL(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerURL)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetServerURLOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetServerURL)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetExternalActions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetExternalActions)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetExternalActionsOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetExternalActions)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomStyles(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomStyles)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomStylesOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomStyles)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomJavascript(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomJavascript)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetCustomJavascriptOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetCustomJavascript)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetHideViewerCount(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetHideViewerCount)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetHideViewerCountOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetHideViewerCount)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetDisableSearchIndexing(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetDisableSearchIndexing)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetDisableSearchIndexingOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetDisableSearchIndexing)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationEnabled(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationEnabledOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationEnabled)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationActivityPrivate(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationActivityPrivate)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationActivityPrivateOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationActivityPrivate)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationShowEngagement(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationShowEngagement)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationShowEngagementOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationShowEngagement)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationUsername(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationUsername)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationUsernameOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationUsername)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationGoLiveMessage(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationGoLiveMessage)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationGoLiveMessageOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationGoLiveMessage)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationBlockDomains(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationBlockDomains)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetFederationBlockDomainsOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetFederationBlockDomains)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetDiscordNotificationConfiguration(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetDiscordNotificationConfiguration)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetDiscordNotificationConfigurationOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetDiscordNotificationConfiguration)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetBrowserNotificationConfiguration(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetBrowserNotificationConfiguration)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) SetBrowserNotificationConfigurationOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireAdminAuth(admin.SetBrowserNotificationConfiguration)(w, r)
|
||||
}
|
||||
7
webserver/handlers/constants.go
Normal file
7
webserver/handlers/constants.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package handlers
|
||||
|
||||
// POST is the HTTP POST method.
|
||||
const POST = "POST"
|
||||
|
||||
// GET is the HTTP GET method.
|
||||
const GET = "GET"
|
||||
15
webserver/handlers/customJavascript.go
Normal file
15
webserver/handlers/customJavascript.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/core/data"
|
||||
)
|
||||
|
||||
// ServeCustomJavascript will serve optional custom Javascript.
|
||||
func ServeCustomJavascript(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/javascript; charset=utf-8")
|
||||
|
||||
js := data.GetCustomJavascript()
|
||||
_, _ = w.Write([]byte(js))
|
||||
}
|
||||
33
webserver/handlers/emoji.go
Normal file
33
webserver/handlers/emoji.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/webserver/router/middleware"
|
||||
webutils "github.com/owncast/owncast/webserver/utils"
|
||||
)
|
||||
|
||||
// GetCustomEmojiList returns a list of emoji via the API.
|
||||
func GetCustomEmojiList(w http.ResponseWriter, r *http.Request) {
|
||||
emojiList := data.GetEmojiList()
|
||||
middleware.SetCachingHeaders(w, r)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(emojiList); err != nil {
|
||||
webutils.InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCustomEmojiImage returns a single emoji image.
|
||||
func GetCustomEmojiImage(w http.ResponseWriter, r *http.Request) {
|
||||
path := strings.TrimPrefix(r.URL.Path, "/img/emoji/")
|
||||
r.URL.Path = path
|
||||
|
||||
emojiFS := os.DirFS(config.CustomEmojiPath)
|
||||
middleware.SetCachingHeaders(w, r)
|
||||
http.FileServer(http.FS(emojiFS)).ServeHTTP(w, r)
|
||||
}
|
||||
23
webserver/handlers/followers.go
Normal file
23
webserver/handlers/followers.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/activitypub/persistence"
|
||||
webutils "github.com/owncast/owncast/webserver/utils"
|
||||
)
|
||||
|
||||
// GetFollowers will handle an API request to fetch the list of followers (non-activitypub response).
|
||||
func GetFollowers(offset int, limit int, w http.ResponseWriter, r *http.Request) {
|
||||
followers, total, err := persistence.GetFederationFollowers(limit, offset)
|
||||
if err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, "unable to fetch followers")
|
||||
return
|
||||
}
|
||||
|
||||
response := webutils.PaginatedResponse{
|
||||
Total: total,
|
||||
Results: followers,
|
||||
}
|
||||
webutils.WriteResponse(w, response)
|
||||
}
|
||||
@@ -3,8 +3,7 @@ package handlers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/controllers"
|
||||
"github.com/owncast/owncast/controllers/admin"
|
||||
"github.com/owncast/owncast/webserver/handlers/admin"
|
||||
"github.com/owncast/owncast/webserver/handlers/generated"
|
||||
"github.com/owncast/owncast/webserver/router/middleware"
|
||||
|
||||
@@ -25,23 +24,23 @@ func (s *ServerInterfaceImpl) Handler() http.Handler {
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) GetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
controllers.GetStatus(w, r)
|
||||
GetStatus(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) GetCustomEmojiList(w http.ResponseWriter, r *http.Request) {
|
||||
controllers.GetCustomEmojiList(w, r)
|
||||
GetCustomEmojiList(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) GetChatMessages(w http.ResponseWriter, r *http.Request, params generated.GetChatMessagesParams) {
|
||||
middleware.RequireUserAccessToken(controllers.GetChatMessages)(w, r)
|
||||
middleware.RequireUserAccessToken(GetChatMessages)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) RegisterAnonymousChatUser(w http.ResponseWriter, r *http.Request, params generated.RegisterAnonymousChatUserParams) {
|
||||
controllers.RegisterAnonymousChatUser(w, r)
|
||||
RegisterAnonymousChatUser(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) RegisterAnonymousChatUserOptions(w http.ResponseWriter, r *http.Request) {
|
||||
controllers.RegisterAnonymousChatUser(w, r)
|
||||
RegisterAnonymousChatUser(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) UpdateMessageVisibility(w http.ResponseWriter, r *http.Request, params generated.UpdateMessageVisibilityParams) {
|
||||
@@ -53,7 +52,7 @@ func (*ServerInterfaceImpl) UpdateUserEnabled(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) GetWebConfig(w http.ResponseWriter, r *http.Request) {
|
||||
controllers.GetWebConfig(w, r)
|
||||
GetWebConfig(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) GetYPResponse(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -61,29 +60,29 @@ func (*ServerInterfaceImpl) GetYPResponse(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) GetAllSocialPlatforms(w http.ResponseWriter, r *http.Request) {
|
||||
controllers.GetAllSocialPlatforms(w, r)
|
||||
GetAllSocialPlatforms(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) GetVideoStreamOutputVariants(w http.ResponseWriter, r *http.Request) {
|
||||
controllers.GetVideoStreamOutputVariants(w, r)
|
||||
GetVideoStreamOutputVariants(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) Ping(w http.ResponseWriter, r *http.Request) {
|
||||
controllers.Ping(w, r)
|
||||
Ping(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) RemoteFollow(w http.ResponseWriter, r *http.Request) {
|
||||
controllers.RemoteFollow(w, r)
|
||||
RemoteFollow(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) GetFollowers(w http.ResponseWriter, r *http.Request, params generated.GetFollowersParams) {
|
||||
middleware.HandlePagination(controllers.GetFollowers)(w, r)
|
||||
middleware.HandlePagination(GetFollowers)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) ReportPlaybackMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
controllers.ReportPlaybackMetrics(w, r)
|
||||
ReportPlaybackMetrics(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) RegisterForLiveNotifications(w http.ResponseWriter, r *http.Request, params generated.RegisterForLiveNotificationsParams) {
|
||||
middleware.RequireUserAccessToken(controllers.RegisterForLiveNotifications)(w, r)
|
||||
middleware.RequireUserAccessToken(RegisterForLiveNotifications)(w, r)
|
||||
}
|
||||
|
||||
55
webserver/handlers/hls.go
Normal file
55
webserver/handlers/hls.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/utils"
|
||||
"github.com/owncast/owncast/webserver/router/middleware"
|
||||
)
|
||||
|
||||
// HandleHLSRequest will manage all requests to HLS content.
|
||||
func HandleHLSRequest(w http.ResponseWriter, r *http.Request) {
|
||||
// Sanity check to limit requests to HLS file types.
|
||||
if filepath.Ext(r.URL.Path) != ".m3u8" && filepath.Ext(r.URL.Path) != ".ts" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
requestedPath := r.URL.Path
|
||||
relativePath := strings.Replace(requestedPath, "/hls/", "", 1)
|
||||
fullPath := filepath.Join(config.HLSStoragePath, relativePath)
|
||||
|
||||
// If using external storage then only allow requests for the
|
||||
// master playlist at stream.m3u8, no variants or segments.
|
||||
if data.GetS3Config().Enabled && relativePath != "stream.m3u8" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle playlists
|
||||
if path.Ext(r.URL.Path) == ".m3u8" {
|
||||
// Playlists should never be cached.
|
||||
middleware.DisableCache(w)
|
||||
|
||||
// Force the correct content type
|
||||
w.Header().Set("Content-Type", "application/x-mpegURL")
|
||||
|
||||
// Use this as an opportunity to mark this viewer as active.
|
||||
viewer := models.GenerateViewerFromRequest(r)
|
||||
core.SetViewerActive(&viewer)
|
||||
} else {
|
||||
cacheTime := utils.GetCacheDurationSecondsForPath(relativePath)
|
||||
w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(cacheTime))
|
||||
}
|
||||
|
||||
middleware.EnableCors(w)
|
||||
http.ServeFile(w, r, fullPath)
|
||||
}
|
||||
80
webserver/handlers/images.go
Normal file
80
webserver/handlers/images.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
contentTypeJPEG = "image/jpeg"
|
||||
contentTypeGIF = "image/gif"
|
||||
)
|
||||
|
||||
var previewThumbCache = ttlcache.New(
|
||||
ttlcache.WithTTL[string, []byte](15),
|
||||
ttlcache.WithCapacity[string, []byte](1),
|
||||
ttlcache.WithDisableTouchOnHit[string, []byte](),
|
||||
)
|
||||
|
||||
// GetThumbnail will return the thumbnail image as a response.
|
||||
func GetThumbnail(w http.ResponseWriter, r *http.Request) {
|
||||
imageFilename := "thumbnail.jpg"
|
||||
imagePath := filepath.Join(config.TempDir, imageFilename)
|
||||
httpCacheTime := utils.GetCacheDurationSecondsForPath(imagePath)
|
||||
inMemoryCacheTime := time.Duration(15) * time.Second
|
||||
|
||||
var imageBytes []byte
|
||||
var err error
|
||||
|
||||
if previewThumbCache.Get(imagePath) != nil {
|
||||
ci := previewThumbCache.Get(imagePath)
|
||||
imageBytes = ci.Value()
|
||||
} else if utils.DoesFileExists(imagePath) {
|
||||
imageBytes, err = getImage(imagePath)
|
||||
previewThumbCache.Set(imagePath, imageBytes, inMemoryCacheTime)
|
||||
} else {
|
||||
GetLogo(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
GetLogo(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
writeBytesAsImage(imageBytes, contentTypeJPEG, w, httpCacheTime)
|
||||
}
|
||||
|
||||
// GetPreview will return the preview gif as a response.
|
||||
func GetPreview(w http.ResponseWriter, r *http.Request) {
|
||||
imageFilename := "preview.gif"
|
||||
imagePath := filepath.Join(config.TempDir, imageFilename)
|
||||
httpCacheTime := utils.GetCacheDurationSecondsForPath(imagePath)
|
||||
inMemoryCacheTime := time.Duration(15) * time.Second
|
||||
|
||||
var imageBytes []byte
|
||||
var err error
|
||||
|
||||
if previewThumbCache.Get(imagePath) != nil {
|
||||
ci := previewThumbCache.Get(imagePath)
|
||||
imageBytes = ci.Value()
|
||||
} else if utils.DoesFileExists(imagePath) {
|
||||
imageBytes, err = getImage(imagePath)
|
||||
previewThumbCache.Set(imagePath, imageBytes, inMemoryCacheTime)
|
||||
} else {
|
||||
GetLogo(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
GetLogo(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
writeBytesAsImage(imageBytes, contentTypeGIF, w, httpCacheTime)
|
||||
}
|
||||
206
webserver/handlers/index.go
Normal file
206
webserver/handlers/index.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core"
|
||||
"github.com/owncast/owncast/core/cache"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/static"
|
||||
"github.com/owncast/owncast/utils"
|
||||
"github.com/owncast/owncast/webserver/router/middleware"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var gc = cache.GetGlobalCache()
|
||||
|
||||
// IndexHandler handles the default index route.
|
||||
func IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.EnableCors(w)
|
||||
|
||||
isIndexRequest := r.URL.Path == "/" || filepath.Base(r.URL.Path) == "index.html" || filepath.Base(r.URL.Path) == ""
|
||||
|
||||
if utils.IsUserAgentAPlayer(r.UserAgent()) && isIndexRequest {
|
||||
http.Redirect(w, r, "/hls/stream.m3u8", http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
|
||||
// For search engine bots and social scrapers return a special
|
||||
// server-rendered page.
|
||||
if utils.IsUserAgentABot(r.UserAgent()) && isIndexRequest {
|
||||
handleScraperMetadataPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Set a cache control max-age header
|
||||
middleware.SetCachingHeaders(w, r)
|
||||
|
||||
nonceRandom, _ := utils.GenerateRandomString(5)
|
||||
|
||||
// Set our global HTTP headers
|
||||
middleware.SetHeaders(w, fmt.Sprintf("nonce-%s", nonceRandom))
|
||||
|
||||
if isIndexRequest {
|
||||
renderIndexHtml(w, nonceRandom)
|
||||
return
|
||||
}
|
||||
|
||||
serveWeb(w, r)
|
||||
}
|
||||
|
||||
func renderIndexHtml(w http.ResponseWriter, nonce string) {
|
||||
type serverSideContent struct {
|
||||
Name string
|
||||
Summary string
|
||||
RequestedURL string
|
||||
TagsString string
|
||||
ThumbnailURL string
|
||||
Thumbnail string
|
||||
Image string
|
||||
StatusJSON string
|
||||
ServerConfigJSON string
|
||||
EmbedVideo string
|
||||
Nonce string
|
||||
}
|
||||
|
||||
status := getStatusResponse()
|
||||
sb, err := json.Marshal(status)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
config := getConfigResponse()
|
||||
cb, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
content := serverSideContent{
|
||||
Name: data.GetServerName(),
|
||||
Summary: data.GetServerSummary(),
|
||||
RequestedURL: fmt.Sprintf("%s%s", data.GetServerURL(), "/"),
|
||||
TagsString: strings.Join(data.GetServerMetadataTags(), ","),
|
||||
ThumbnailURL: "thumbnail.jpg",
|
||||
Thumbnail: "thumbnail.jpg",
|
||||
Image: "logo/external",
|
||||
StatusJSON: string(sb),
|
||||
ServerConfigJSON: string(cb),
|
||||
EmbedVideo: "embed/video",
|
||||
Nonce: nonce,
|
||||
}
|
||||
|
||||
index, err := static.GetWebIndexTemplate()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := index.Execute(w, content); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// MetadataPage represents a server-rendered web page for bots and web scrapers.
|
||||
type MetadataPage struct {
|
||||
RequestedURL string
|
||||
Image string
|
||||
Thumbnail string
|
||||
TagsString string
|
||||
Summary string
|
||||
Name string
|
||||
Tags []string
|
||||
SocialHandles []models.SocialHandle
|
||||
}
|
||||
|
||||
// Return a basic HTML page with server-rendered metadata from the config
|
||||
// to give to Opengraph clients and web scrapers (bots, web crawlers, etc).
|
||||
func handleScraperMetadataPage(w http.ResponseWriter, r *http.Request) {
|
||||
cacheKey := "bot-scraper-html"
|
||||
cacheHtmlExpiration := time.Duration(10) * time.Second
|
||||
c := gc.GetOrCreateCache(cacheKey, cacheHtmlExpiration)
|
||||
|
||||
cachedHtml := c.GetValueForKey(cacheKey)
|
||||
if cachedHtml != nil {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
_, _ = w.Write(cachedHtml)
|
||||
return
|
||||
}
|
||||
|
||||
tmpl, err := static.GetBotMetadataTemplate()
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
|
||||
if siteURL := data.GetServerURL(); siteURL != "" {
|
||||
if parsed, err := url.Parse(siteURL); err == nil && parsed.Scheme != "" {
|
||||
scheme = parsed.Scheme
|
||||
}
|
||||
}
|
||||
|
||||
fullURL, err := url.Parse(fmt.Sprintf("%s://%s%s", scheme, r.Host, r.URL.Path))
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
imageURL, err := url.Parse(fmt.Sprintf("%s://%s%s", scheme, r.Host, "/logo/external"))
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
|
||||
status := core.GetStatus()
|
||||
|
||||
// If the thumbnail does not exist or we're offline then just use the logo image
|
||||
var thumbnailURL string
|
||||
if status.Online && utils.DoesFileExists(filepath.Join(config.DataDirectory, "tmp", "thumbnail.jpg")) {
|
||||
thumbnail, err := url.Parse(fmt.Sprintf("%s://%s%s", scheme, r.Host, "/thumbnail.jpg"))
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
thumbnailURL = imageURL.String()
|
||||
} else {
|
||||
thumbnailURL = thumbnail.String()
|
||||
}
|
||||
} else {
|
||||
thumbnailURL = imageURL.String()
|
||||
}
|
||||
|
||||
tagsString := strings.Join(data.GetServerMetadataTags(), ",")
|
||||
metadata := MetadataPage{
|
||||
Name: data.GetServerName(),
|
||||
RequestedURL: fullURL.String(),
|
||||
Image: imageURL.String(),
|
||||
Summary: data.GetServerSummary(),
|
||||
Thumbnail: thumbnailURL,
|
||||
TagsString: tagsString,
|
||||
Tags: data.GetServerMetadataTags(),
|
||||
SocialHandles: data.GetSocialHandles(),
|
||||
}
|
||||
|
||||
// Cache the rendered HTML
|
||||
var b bytes.Buffer
|
||||
if err := tmpl.Execute(&b, metadata); err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
c.Set(cacheKey, b.Bytes())
|
||||
|
||||
// Set a cache header
|
||||
middleware.SetCachingHeaders(w, r)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
if _, err = w.Write(b.Bytes()); err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,8 @@ package handlers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/controllers"
|
||||
"github.com/owncast/owncast/controllers/admin"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/webserver/handlers/admin"
|
||||
"github.com/owncast/owncast/webserver/router/middleware"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
@@ -69,11 +68,11 @@ func (*ServerInterfaceImpl) ExternalSetStreamTitleOptions(w http.ResponseWriter,
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) ExternalGetChatMessages(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireExternalAPIAccessToken(models.ScopeHasAdminAccess, controllers.ExternalGetChatMessages)(w, r)
|
||||
middleware.RequireExternalAPIAccessToken(models.ScopeHasAdminAccess, ExternalGetChatMessages)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) ExternalGetChatMessagesOptions(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.RequireExternalAPIAccessToken(models.ScopeHasAdminAccess, controllers.ExternalGetChatMessages)(w, r)
|
||||
middleware.RequireExternalAPIAccessToken(models.ScopeHasAdminAccess, ExternalGetChatMessages)(w, r)
|
||||
}
|
||||
|
||||
func (*ServerInterfaceImpl) ExternalGetConnectedChatClients(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
95
webserver/handlers/logo.go
Normal file
95
webserver/handlers/logo.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/static"
|
||||
"github.com/owncast/owncast/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var _hasWarnedSVGLogo = false
|
||||
|
||||
// GetLogo will return the logo image as a response.
|
||||
func GetLogo(w http.ResponseWriter, r *http.Request) {
|
||||
imageFilename := data.GetLogoPath()
|
||||
if imageFilename == "" {
|
||||
returnDefault(w)
|
||||
return
|
||||
}
|
||||
imagePath := filepath.Join(config.DataDirectory, imageFilename)
|
||||
imageBytes, err := getImage(imagePath)
|
||||
if err != nil {
|
||||
returnDefault(w)
|
||||
return
|
||||
}
|
||||
|
||||
contentType := "image/jpeg"
|
||||
if filepath.Ext(imageFilename) == ".svg" {
|
||||
contentType = "image/svg+xml"
|
||||
} else if filepath.Ext(imageFilename) == ".gif" {
|
||||
contentType = "image/gif"
|
||||
} else if filepath.Ext(imageFilename) == ".png" {
|
||||
contentType = "image/png"
|
||||
}
|
||||
|
||||
cacheTime := utils.GetCacheDurationSecondsForPath(imagePath)
|
||||
writeBytesAsImage(imageBytes, contentType, w, cacheTime)
|
||||
}
|
||||
|
||||
// GetCompatibleLogo will return the logo unless it's a SVG
|
||||
// and in that case will return a default placeholder.
|
||||
// Used for sharing to external social networks that generally
|
||||
// don't support SVG.
|
||||
func GetCompatibleLogo(w http.ResponseWriter, r *http.Request) {
|
||||
imageFilename := data.GetLogoPath()
|
||||
|
||||
// If the logo image is not a SVG then we can return it
|
||||
// without any problems.
|
||||
if imageFilename != "" && filepath.Ext(imageFilename) != ".svg" {
|
||||
GetLogo(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise use a fallback logo.png.
|
||||
imagePath := filepath.Join(config.DataDirectory, "logo.png")
|
||||
contentType := "image/png"
|
||||
imageBytes, err := getImage(imagePath)
|
||||
if err != nil {
|
||||
returnDefault(w)
|
||||
return
|
||||
}
|
||||
|
||||
cacheTime := utils.GetCacheDurationSecondsForPath(imagePath)
|
||||
writeBytesAsImage(imageBytes, contentType, w, cacheTime)
|
||||
|
||||
if !_hasWarnedSVGLogo {
|
||||
log.Warnf("an external site requested your logo. because many social networks do not support SVGs we returned a placeholder instead. change your current logo to a png or jpeg to be most compatible with external social networking sites.")
|
||||
_hasWarnedSVGLogo = true
|
||||
}
|
||||
}
|
||||
|
||||
func returnDefault(w http.ResponseWriter) {
|
||||
imageBytes := static.GetLogo()
|
||||
cacheTime := utils.GetCacheDurationSecondsForPath("logo.png")
|
||||
writeBytesAsImage(imageBytes, "image/png", w, cacheTime)
|
||||
}
|
||||
|
||||
func writeBytesAsImage(data []byte, contentType string, w http.ResponseWriter, cacheSeconds int) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
||||
w.Header().Set("Cache-Control", "public, max-age="+strconv.Itoa(cacheSeconds))
|
||||
|
||||
if _, err := w.Write(data); err != nil {
|
||||
log.Println("unable to write image.")
|
||||
}
|
||||
}
|
||||
|
||||
func getImage(path string) ([]byte, error) {
|
||||
return os.ReadFile(path) // nolint
|
||||
}
|
||||
@@ -3,8 +3,8 @@ package handlers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/controllers/moderation"
|
||||
"github.com/owncast/owncast/webserver/handlers/generated"
|
||||
"github.com/owncast/owncast/webserver/handlers/moderation"
|
||||
"github.com/owncast/owncast/webserver/router/middleware"
|
||||
)
|
||||
|
||||
|
||||
76
webserver/handlers/moderation/moderation.go
Normal file
76
webserver/handlers/moderation/moderation.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package moderation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/core/chat"
|
||||
"github.com/owncast/owncast/core/chat/events"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/persistence/userrepository"
|
||||
"github.com/owncast/owncast/webserver/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GetUserDetails returns the details of a chat user for moderators.
|
||||
func GetUserDetails(w http.ResponseWriter, r *http.Request) {
|
||||
type connectedClient struct {
|
||||
ConnectedAt time.Time `json:"connectedAt"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
Geo string `json:"geo,omitempty"`
|
||||
Id uint `json:"id"`
|
||||
MessageCount int `json:"messageCount"`
|
||||
}
|
||||
|
||||
type response struct {
|
||||
User *models.User `json:"user"`
|
||||
ConnectedClients []connectedClient `json:"connectedClients"`
|
||||
Messages []events.UserMessageEvent `json:"messages"`
|
||||
}
|
||||
|
||||
pathComponents := strings.Split(r.URL.Path, "/")
|
||||
uid := pathComponents[len(pathComponents)-1]
|
||||
|
||||
userRepository := userrepository.Get()
|
||||
|
||||
u := userRepository.GetUserByID(uid)
|
||||
|
||||
if u == nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
c, _ := chat.GetClientsForUser(uid)
|
||||
clients := make([]connectedClient, len(c))
|
||||
for i, c := range c {
|
||||
client := connectedClient{
|
||||
Id: c.Id,
|
||||
MessageCount: c.MessageCount,
|
||||
UserAgent: c.UserAgent,
|
||||
ConnectedAt: c.ConnectedAt,
|
||||
}
|
||||
if c.Geo != nil {
|
||||
client.Geo = c.Geo.CountryCode
|
||||
}
|
||||
|
||||
clients[i] = client
|
||||
}
|
||||
|
||||
messages, err := chat.GetMessagesFromUser(uid)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
|
||||
res := response{
|
||||
User: u,
|
||||
ConnectedClients: clients,
|
||||
Messages: messages,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||
utils.InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
52
webserver/handlers/notifications.go
Normal file
52
webserver/handlers/notifications.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/notifications"
|
||||
|
||||
"github.com/owncast/owncast/utils"
|
||||
|
||||
webutils "github.com/owncast/owncast/webserver/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// RegisterForLiveNotifications will register a channel + destination to be
|
||||
// notified when a stream goes live.
|
||||
func RegisterForLiveNotifications(u models.User, w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
webutils.WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||
return
|
||||
}
|
||||
|
||||
type request struct {
|
||||
// Channel is the notification channel (browser, sms, etc)
|
||||
Channel string `json:"channel"`
|
||||
// Destination is the target of the notification in the above channel.
|
||||
Destination string `json:"destination"`
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var req request
|
||||
if err := decoder.Decode(&req); err != nil {
|
||||
log.Errorln(err)
|
||||
webutils.WriteSimpleResponse(w, false, "unable to register for notifications")
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the requested channel is one we want to handle.
|
||||
validTypes := []string{notifications.BrowserPushNotification}
|
||||
_, validChannel := utils.FindInSlice(validTypes, req.Channel)
|
||||
if !validChannel {
|
||||
webutils.WriteSimpleResponse(w, false, "invalid notification channel: "+req.Channel)
|
||||
return
|
||||
}
|
||||
|
||||
if err := notifications.AddNotification(req.Channel, req.Destination); err != nil {
|
||||
log.Errorln(err)
|
||||
webutils.WriteSimpleResponse(w, false, "unable to save notification")
|
||||
return
|
||||
}
|
||||
}
|
||||
15
webserver/handlers/ping.go
Normal file
15
webserver/handlers/ping.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/core"
|
||||
"github.com/owncast/owncast/models"
|
||||
)
|
||||
|
||||
// Ping is fired by a client to show they are still an active viewer.
|
||||
func Ping(w http.ResponseWriter, r *http.Request) {
|
||||
viewer := models.GenerateViewerFromRequest(r)
|
||||
core.SetViewerActive(&viewer)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
53
webserver/handlers/playbackMetrics.go
Normal file
53
webserver/handlers/playbackMetrics.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/metrics"
|
||||
"github.com/owncast/owncast/utils"
|
||||
webutils "github.com/owncast/owncast/webserver/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ReportPlaybackMetrics will accept playback metrics from a client and save
|
||||
// them for future video health reporting.
|
||||
func ReportPlaybackMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != POST {
|
||||
webutils.WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||
return
|
||||
}
|
||||
|
||||
type reportPlaybackMetricsRequest struct {
|
||||
Bandwidth float64 `json:"bandwidth"`
|
||||
Latency float64 `json:"latency"`
|
||||
Errors float64 `json:"errors"`
|
||||
DownloadDuration float64 `json:"downloadDuration"`
|
||||
QualityVariantChanges float64 `json:"qualityVariantChanges"`
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request reportPlaybackMetricsRequest
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
log.Errorln("error decoding playback metrics payload:", err)
|
||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
clientID := utils.GenerateClientIDFromRequest(r)
|
||||
|
||||
metrics.RegisterPlaybackErrorCount(clientID, request.Errors)
|
||||
if request.Bandwidth != 0.0 {
|
||||
metrics.RegisterPlayerBandwidth(clientID, request.Bandwidth)
|
||||
}
|
||||
|
||||
if request.Latency != 0.0 {
|
||||
metrics.RegisterPlayerLatency(clientID, request.Latency)
|
||||
}
|
||||
|
||||
if request.DownloadDuration != 0.0 {
|
||||
metrics.RegisterPlayerSegmentDownloadDuration(clientID, request.DownloadDuration)
|
||||
}
|
||||
|
||||
metrics.RegisterQualityVariantChangesCount(clientID, request.QualityVariantChanges)
|
||||
}
|
||||
66
webserver/handlers/remoteFollow.go
Normal file
66
webserver/handlers/remoteFollow.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/owncast/owncast/activitypub/webfinger"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
webutils "github.com/owncast/owncast/webserver/utils"
|
||||
)
|
||||
|
||||
// RemoteFollow handles a request to begin the remote follow redirect flow.
|
||||
func RemoteFollow(w http.ResponseWriter, r *http.Request) {
|
||||
type followRequest struct {
|
||||
Account string `json:"account"`
|
||||
}
|
||||
|
||||
type followResponse struct {
|
||||
RedirectURL string `json:"redirectUrl"`
|
||||
}
|
||||
|
||||
var request followRequest
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, "unable to parse request")
|
||||
return
|
||||
}
|
||||
|
||||
if request.Account == "" {
|
||||
webutils.WriteSimpleResponse(w, false, "Remote Fediverse account is required to follow.")
|
||||
return
|
||||
}
|
||||
|
||||
localActorPath, _ := url.Parse(data.GetServerURL())
|
||||
localActorPath.Path = fmt.Sprintf("/federation/user/%s", data.GetDefaultFederationUsername())
|
||||
var template string
|
||||
links, err := webfinger.GetWebfingerLinks(request.Account)
|
||||
if err != nil {
|
||||
webutils.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Acquire the remote follow redirect template.
|
||||
for _, link := range links {
|
||||
for k, v := range link {
|
||||
if k == "rel" && v == "http://ostatus.org/schema/1.0/subscribe" && link["template"] != nil {
|
||||
template = link["template"].(string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if localActorPath.String() == "" || template == "" {
|
||||
webutils.WriteSimpleResponse(w, false, "unable to determine remote follow information for "+request.Account)
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL := strings.Replace(template, "{uri}", localActorPath.String(), 1)
|
||||
response := followResponse{
|
||||
RedirectURL: redirectURL,
|
||||
}
|
||||
|
||||
webutils.WriteResponse(w, response)
|
||||
}
|
||||
28
webserver/handlers/robots.go
Normal file
28
webserver/handlers/robots.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/owncast/owncast/core/data"
|
||||
)
|
||||
|
||||
// GetRobotsDotTxt returns the contents of our robots.txt.
|
||||
func GetRobotsDotTxt(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
contents := []string{
|
||||
"User-agent: *",
|
||||
"Disallow: /admin",
|
||||
"Disallow: /api",
|
||||
}
|
||||
|
||||
if data.GetDisableSearchIndexing() {
|
||||
contents = append(contents, "Disallow: /")
|
||||
}
|
||||
|
||||
txt := []byte(strings.Join(contents, "\n"))
|
||||
|
||||
if _, err := w.Write(txt); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
52
webserver/handlers/status.go
Normal file
52
webserver/handlers/status.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/core"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/utils"
|
||||
"github.com/owncast/owncast/webserver/router/middleware"
|
||||
webutils "github.com/owncast/owncast/webserver/utils"
|
||||
)
|
||||
|
||||
// GetStatus gets the status of the server.
|
||||
func GetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
response := getStatusResponse()
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
middleware.DisableCache(w)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
webutils.InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
func getStatusResponse() webStatusResponse {
|
||||
status := core.GetStatus()
|
||||
response := webStatusResponse{
|
||||
Online: status.Online,
|
||||
ServerTime: time.Now(),
|
||||
LastConnectTime: status.LastConnectTime,
|
||||
LastDisconnectTime: status.LastDisconnectTime,
|
||||
VersionNumber: status.VersionNumber,
|
||||
StreamTitle: status.StreamTitle,
|
||||
}
|
||||
if !data.GetHideViewerCount() {
|
||||
response.ViewerCount = status.ViewerCount
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
type webStatusResponse struct {
|
||||
ServerTime time.Time `json:"serverTime"`
|
||||
LastConnectTime *utils.NullTime `json:"lastConnectTime"`
|
||||
LastDisconnectTime *utils.NullTime `json:"lastDisconnectTime"`
|
||||
|
||||
VersionNumber string `json:"versionNumber"`
|
||||
StreamTitle string `json:"streamTitle"`
|
||||
ViewerCount int `json:"viewerCount,omitempty"`
|
||||
Online bool `json:"online"`
|
||||
}
|
||||
60
webserver/handlers/video.go
Normal file
60
webserver/handlers/video.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/owncast/owncast/core/data"
|
||||
webutils "github.com/owncast/owncast/webserver/utils"
|
||||
)
|
||||
|
||||
type variantsSort struct {
|
||||
Name string
|
||||
Index int
|
||||
VideoBitrate int
|
||||
IsVideoPassthrough bool
|
||||
}
|
||||
|
||||
type variantsResponse struct {
|
||||
Name string `json:"name"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
// GetVideoStreamOutputVariants will return the video variants available.
|
||||
func GetVideoStreamOutputVariants(w http.ResponseWriter, r *http.Request) {
|
||||
outputVariants := data.GetStreamOutputVariants()
|
||||
|
||||
streamSortVariants := make([]variantsSort, len(outputVariants))
|
||||
for i, variant := range outputVariants {
|
||||
variantSort := variantsSort{
|
||||
Index: i,
|
||||
Name: variant.GetName(),
|
||||
IsVideoPassthrough: variant.IsVideoPassthrough,
|
||||
VideoBitrate: variant.VideoBitrate,
|
||||
}
|
||||
streamSortVariants[i] = variantSort
|
||||
}
|
||||
|
||||
sort.Slice(streamSortVariants, func(i, j int) bool {
|
||||
if streamSortVariants[i].IsVideoPassthrough && !streamSortVariants[j].IsVideoPassthrough {
|
||||
return true
|
||||
}
|
||||
|
||||
if !streamSortVariants[i].IsVideoPassthrough && streamSortVariants[j].IsVideoPassthrough {
|
||||
return false
|
||||
}
|
||||
|
||||
return streamSortVariants[i].VideoBitrate > streamSortVariants[j].VideoBitrate
|
||||
})
|
||||
|
||||
response := make([]variantsResponse, len(streamSortVariants))
|
||||
for i, variant := range streamSortVariants {
|
||||
variantResponse := variantsResponse{
|
||||
Index: variant.Index,
|
||||
Name: variant.Name,
|
||||
}
|
||||
response[i] = variantResponse
|
||||
}
|
||||
|
||||
webutils.WriteResponse(w, response)
|
||||
}
|
||||
14
webserver/handlers/web.go
Normal file
14
webserver/handlers/web.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/static"
|
||||
)
|
||||
|
||||
var staticServer = http.FileServer(http.FS(static.GetWeb()))
|
||||
|
||||
// serveWeb will serve web assets.
|
||||
func serveWeb(w http.ResponseWriter, r *http.Request) {
|
||||
staticServer.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -14,9 +14,8 @@ import (
|
||||
"golang.org/x/net/http2/h2c"
|
||||
|
||||
"github.com/owncast/owncast/activitypub"
|
||||
apControllers "github.com/owncast/owncast/activitypub/controllers"
|
||||
aphandlers "github.com/owncast/owncast/activitypub/controllers"
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/controllers"
|
||||
"github.com/owncast/owncast/core/chat"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/webserver/handlers"
|
||||
@@ -44,19 +43,19 @@ func Start(enableVerboseLogging bool) error {
|
||||
r.Handle("/public/*", http.StripPrefix("/public/", fs))
|
||||
|
||||
// Return HLS video
|
||||
r.HandleFunc("/hls/*", controllers.HandleHLSRequest)
|
||||
r.HandleFunc("/hls/*", handlers.HandleHLSRequest)
|
||||
|
||||
// The admin web app.
|
||||
r.HandleFunc("/admin/*", middleware.RequireAdminAuth(controllers.IndexHandler))
|
||||
r.HandleFunc("/admin/*", middleware.RequireAdminAuth(handlers.IndexHandler))
|
||||
|
||||
// Single ActivityPub Actor
|
||||
r.HandleFunc("/federation/user/*", middleware.RequireActivityPubOrRedirect(apControllers.ActorHandler))
|
||||
r.HandleFunc("/federation/user/*", middleware.RequireActivityPubOrRedirect(aphandlers.ActorHandler))
|
||||
|
||||
// Single AP object
|
||||
r.HandleFunc("/federation/*", middleware.RequireActivityPubOrRedirect(apControllers.ObjectHandler))
|
||||
r.HandleFunc("/federation/*", middleware.RequireActivityPubOrRedirect(aphandlers.ObjectHandler))
|
||||
|
||||
// The primary web app.
|
||||
r.HandleFunc("/*", controllers.IndexHandler)
|
||||
r.HandleFunc("/*", handlers.IndexHandler)
|
||||
|
||||
// mount the api
|
||||
r.Mount("/api/", handlers.New().Handler())
|
||||
@@ -105,40 +104,40 @@ func Start(enableVerboseLogging bool) error {
|
||||
|
||||
func addStaticFileEndpoints(r chi.Router) {
|
||||
// Images
|
||||
r.HandleFunc("/thumbnail.jpg", controllers.GetThumbnail)
|
||||
r.HandleFunc("/preview.gif", controllers.GetPreview)
|
||||
r.HandleFunc("/logo", controllers.GetLogo)
|
||||
r.HandleFunc("/thumbnail.jpg", handlers.GetThumbnail)
|
||||
r.HandleFunc("/preview.gif", handlers.GetPreview)
|
||||
r.HandleFunc("/logo", handlers.GetLogo)
|
||||
// return a logo that's compatible with external social networks
|
||||
r.HandleFunc("/logo/external", controllers.GetCompatibleLogo)
|
||||
r.HandleFunc("/logo/external", handlers.GetCompatibleLogo)
|
||||
|
||||
// Custom Javascript
|
||||
r.HandleFunc("/customjavascript", controllers.ServeCustomJavascript)
|
||||
r.HandleFunc("/customjavascript", handlers.ServeCustomJavascript)
|
||||
|
||||
// robots.txt
|
||||
r.HandleFunc("/robots.txt", controllers.GetRobotsDotTxt)
|
||||
r.HandleFunc("/robots.txt", handlers.GetRobotsDotTxt)
|
||||
|
||||
// Return a single emoji image.
|
||||
emojiDir := config.EmojiDir
|
||||
if !strings.HasSuffix(emojiDir, "*") {
|
||||
emojiDir += "*"
|
||||
}
|
||||
r.HandleFunc(emojiDir, controllers.GetCustomEmojiImage)
|
||||
r.HandleFunc(emojiDir, handlers.GetCustomEmojiImage)
|
||||
|
||||
// WebFinger
|
||||
r.HandleFunc("/.well-known/webfinger", apControllers.WebfingerHandler)
|
||||
r.HandleFunc("/.well-known/webfinger", aphandlers.WebfingerHandler)
|
||||
|
||||
// Host Metadata
|
||||
r.HandleFunc("/.well-known/host-meta", apControllers.HostMetaController)
|
||||
r.HandleFunc("/.well-known/host-meta", aphandlers.HostMetaController)
|
||||
|
||||
// Nodeinfo v1
|
||||
r.HandleFunc("/.well-known/nodeinfo", apControllers.NodeInfoController)
|
||||
r.HandleFunc("/.well-known/nodeinfo", aphandlers.NodeInfoController)
|
||||
|
||||
// x-nodeinfo v2
|
||||
r.HandleFunc("/.well-known/x-nodeinfo2", apControllers.XNodeInfo2Controller)
|
||||
r.HandleFunc("/.well-known/x-nodeinfo2", aphandlers.XNodeInfo2Controller)
|
||||
|
||||
// Nodeinfo v2
|
||||
r.HandleFunc("/nodeinfo/2.0", apControllers.NodeInfoV2Controller)
|
||||
r.HandleFunc("/nodeinfo/2.0", aphandlers.NodeInfoV2Controller)
|
||||
|
||||
// Instance details
|
||||
r.HandleFunc("/api/v1/instance", apControllers.InstanceV1Controller)
|
||||
r.HandleFunc("/api/v1/instance", aphandlers.InstanceV1Controller)
|
||||
}
|
||||
|
||||
7
webserver/utils/pagination.go
Normal file
7
webserver/utils/pagination.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package utils
|
||||
|
||||
// PaginatedResponse is a structure for returning a total count with results.
|
||||
type PaginatedResponse struct {
|
||||
Results interface{} `json:"results"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
77
webserver/utils/responses.go
Normal file
77
webserver/utils/responses.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/webserver/router/middleware"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type J map[string]interface{}
|
||||
|
||||
// InternalErrorHandler will return an error message as an HTTP response.
|
||||
func InternalErrorHandler(w http.ResponseWriter, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(J{"error": err.Error()}); err != nil {
|
||||
InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
// BadRequestHandler will return an HTTP 500 as an HTTP response.
|
||||
func BadRequestHandler(w http.ResponseWriter, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugln(err)
|
||||
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
if err := json.NewEncoder(w).Encode(J{"error": err.Error()}); err != nil {
|
||||
InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteSimpleResponse will return a message as a response.
|
||||
func WriteSimpleResponse(w http.ResponseWriter, success bool, message string) {
|
||||
response := models.BaseAPIResponse{
|
||||
Success: success,
|
||||
Message: message,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if success {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteResponse will return an object as a JSON encoded uncacheable response.
|
||||
func WriteResponse(w http.ResponseWriter, response interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
middleware.DisableCache(w)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteString will return a basic string and a status code to the client.
|
||||
func WriteString(w http.ResponseWriter, text string, status int) error {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(status)
|
||||
_, err := w.Write([]byte(text))
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user