Brenden Ehlers b2582f409b
Spec-based API (#3653)
* chore(deps): update dependency sass to v1.74.1

* Bundle embedded web app

* chore(deps): update dependency typescript to v5.4.4

* Bundle embedded web app

* Commit screenshots

* fix(deps): update module github.com/yuin/goldmark to v1.7.1

* chore(deps): lock file maintenance (#3662)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Bundle embedded web app

* Commit screenshots

* chore(deps): update dependency @types/node to v20.12.5

* Bundle embedded web app

* fix(deps): update module github.com/shirou/gopsutil/v3 to v3.24.3

* fix(deps): update module github.com/aws/aws-sdk-go to v1.51.17

* fix(deps): update module golang.org/x/net to v0.24.0

* fix(deps): update module golang.org/x/mod to v0.17.0

* add get admin chat clients

* comment out old code

* add get admin logs

* add get admin warnings

* add get admin chat

* add post admin chat messageVisibility

* add post admin chat users enabled

* add post admin chat users ipban create

* add post admin chat users ipban remove

* add get admin chat users ipbans

* chore(deps): update dependency sass-loader to v14 (#3674)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update typescript-eslint monorepo to v7 (#3680)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency less-loader to v12 (#3673)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency chromatic to v11 (#3669)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency knip to v5 (#3672)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update nick-fields/retry action to v3 (#3678)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update dependency @types/markdown-it to v14 (#3668)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update chromaui/action action to v11 (#3666)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Bundle embedded web app

* Bump version number

* Bundle embedded web app

* Bundle embedded web app

* Bundle embedded web app

* fix(deps): update dependency react-virtuoso to v4.7.8

* Commit screenshots

* Bundle embedded web app

* Optionally disable chat rate limiter and add optional chat slur/language filter (#3681)

* feat(chat): basic profanity filter. For #3139

* feat(chat): add setting for disabling chat spam protection. Closes #3523

* feat(chat): wire up the new chat slur filter to admin and chat. Closes #3139

* Bundle embedded web app

* chore(deps): update peter-evans/create-or-update-comment digest to 48bb05b

* fix(deps): update dependency @codemirror/language-data to v6.5.0

* Bundle embedded web app

* chore(deps): update dependency css-loader to v7 (#3670)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Bundle embedded web app

* add get admin chat users disabled

* add get admin chat users setModerator

* add get admin chat users moderators

* add get admin followers

* chore(deps): update tj-actions/changed-files action to v44 (#3679)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Fix for copying stream key without revealing it only copies asterisks (#3663)

* asterisk copy fix for #3460

* rename text to keys for consistency + linting fix

* make onCopy spawn an antd message directly, remove redundant copyText function

---------

Co-authored-by: Muaz Ahmad <mahmad2000@protonmail.com>

* Commit updated Storybook stories

* Bundle embedded web app

* Commit screenshots

* chore(deps): update dependency css-loader to v7.1.0

* Bundle embedded web app

* chore(deps): update typescript-eslint monorepo to v7.6.0

* Bundle embedded web app

* chore(deps): update dependency @types/react to v18.2.75 (#3686)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Bundle embedded web app

* stream offline status embed updated (#3684)

* Commit screenshots

* add get admin followers pending

* add get admin followers blocked

* add post admin followers approve

* add 400 error to admin requests

* add post admin emoji upload

* add post admin emoji delete

* add post admin config adminpass

* add post admin config streamkeys

* add post admin config pagecontent

* add post admin config streamtitle

* add post admin config streamtitle

* add post admin config serversummary

* add post admin config offlinemessage

* add post admin config welcomemessage

* add post admin config chat disable

* add post admin config chat joinmessageenabled

* add post admin config chat establishedusermode

* add post admin config chat forbiddenusernames

* add post admin config chat suggestedusernames

* add post admin config video codec

* add post admin config appearance

* Allow react/no-danger

* Commit screenshots

* Change tag to self-closing since the linter complains otherwise (#3690)

Co-authored-by: Muaz Ahmad <mahmad2000@protonmail.com>

* Bundle embedded web app

* chore(deps): update dependency css-loader to v7.1.1 (#3691)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Bundle embedded web app

* chore(deps): update dependency @types/node to v20.12.7 (#3688)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Bundle embedded web app

* Commit screenshots

* chore(deps): update dependency npm to v10.5.2

* Bundle embedded web app

* chore(deps): update dependency knip to v5.9.4

* Bundle embedded web app

* chore(deps): update dependency typescript to v5.4.5

* Bundle embedded web app

* prevent css-loader v7 renaming default to _default, needed for storybook (#3692)

Co-authored-by: Muaz Ahmad <mahmad2000@protonmail.com>

* Bundle embedded web app

* chore(deps): update dependency cypress to v13.7.3

* Bundle embedded web app

* Fix custom runtimeCaching rules, default runtimeCaching rule order (#3685)

Co-authored-by: Muaz Ahmad <mahmad2000@protonmail.com>

* Bundle embedded web app

* Fixes for aria-live bugs (#3694)

* make the aria-live text adhere to the last message's username

* Wrap lastMessage in an Interweave to handle pre-encoded characters properly

---------

Co-authored-by: Muaz Ahmad <mahmad2000@protonmail.com>

* Bundle embedded web app

* chore(deps): update dependency @types/react to v18.2.77

* Bundle embedded web app

* chore(deps): update dependency @storybook/addon-styling-webpack to v1 (#3667)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Bundle embedded web app

* chore(deps): update dependency storybook-addon-fetch-mock to v2 (#3675)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Bundle embedded web app

* chore(deps): update dependency sass to v1.75.0

* Bundle embedded web app

* Commit screenshots

* chore(deps): update dependency sass-loader to v14.2.0

* Bundle embedded web app

* fix(deps): update codemirror

* add get admin webhooks

* add create and delete webhooks

* add get, delete, create access token endpoints

* add update endpoints

* Bundle embedded web app

* fix(deps): update nextjs monorepo to v14.2.0

* Bundle embedded web app

* fix(deps): update nextjs monorepo to v14.2.1

* Bundle embedded web app

* Commit screenshots

* chore(deps): update dependency @types/markdown-it to v14.0.1

* Bundle embedded web app

* chore(deps): update dependency @types/react to v18.2.78

* Bundle embedded web app

* add send message to connected client

* add deprecated send user message

* add send message to 3rd party

* add send user action

* add update external message visibility

* add update external stream title

* add external get chat messages and fixed error with chat

* add external get clients

* remove options request

* add update logo

* add update tags

* add update ffmpeg path

* add update server port

* add update server ip

* add update rtmp port

* add update websocket host override

* add update custom video serving endpoint

* add update nsfw

* add update directory enabled

* Commit screenshots

* add update social handles

* add update stream latency level

* add update stream output variants

* add update s3 config

* add update server url

* add reset yp registration

* add update external action links

* add update custom styles

* add update custom javascript

* add get video metrics

* add update hide viewer count

* add update search indexing

* Change fediAuth message to be more succinct (#3696)

* Change fediAuth message to be more succint

* Update controllers/auth/fediverse/fediverse.go

for -> from

---------

Co-authored-by: Gabe Kangas <gabek@real-ity.com>

* Mute the 'stream ended' clip's audio (#3630)

* Mute the 'stream ended' clip's audio

The 'stream ended' clip plays at the at the end of every stream broadcast using owncast.
It currently contains audio that peaks at -7.1db. (according to ffmpeg's volumedetect audio filter)
This can result in a bad experience for viewers if the stream that they were just watching had a much lower average volume, and they had turned up their speakers or headphones to compensate. In extreme cases this could theoretically cause harm to viewers and/or their equipment.
As an admin running owncast, there is no way to remove this audio *except* for patching the file. Even if you do patch the file, you need to notify your viewers to clear their browser caches if they have ever seen the clip, because offline.ts has a cache-control max-age header specifying 365 days. The caching of the previous version of this clip is out of scope of this PR.

This issue is discussed in more detail in #1965.

Unlike my previous attempt in #3332, which removed the audio track, this PR *mutes* the audio.

Specifically, I used this ffmpeg command:
```
ffmpeg -i offline.ts -filter:a "volume=0.0" output.ts
```

There are no other modifications to the clip.

* Commit updated API documentation

* feat(video): make compatible muted offline clip. Rename clip as a v2 so it is not cached

* Fix conflict

* force add new offline file

---------

Co-authored-by: vivlim <vivlim@vivl.im>
Co-authored-by: Owncast <owncast@owncast.online>
Co-authored-by: Gabe Kangas <gabek@real-ity.com>

* Commit screenshots

* chore(tests): retry cypress tests on failure

* fix(deps): update module github.com/aws/aws-sdk-go to v1.51.23

* chore(deps): update dependency @types/video.js to v7.3.58 (#3705)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Bundle embedded web app

* fix(deps): update dependency @codemirror/language-data to v6.5.1 (#3706)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Bundle embedded web app

* add update message visibility

* add update user status

* add get moderation user details

* create file for /admin/config endpoints

* chore(deps): update dependency style-loader to v4 (#3703)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* Bundle embedded web app

* chore(deps): update typescript-eslint monorepo to v7.7.0

* Bundle embedded web app

* add non-api endpoints to new router

* add set federation enabled

* add set federation private

* chore(tests): attempt to clean up test flakiness (#3710)

* chore(deps): update dependency @types/react to v18.2.79

* add show federation engagement

* add set federation username

* add set federation go live message

* add set federation blocked domains

* add set discord notification configuration

* add set browser notification configuration

* add prometheus endpoint

* add send federation message

* add get federation activities

* add indie auth documentation

* add changes to indie auth server

* Bundle embedded web app

* add fediverse auth

* add spam protection and slur filter

* Commit screenshots

* fix bug with hls endpoint

* Add effect to set media session metadata (#3713)

* Bundle embedded web app

* chore(deps): update dependency sass-loader to v14.2.1

* Bundle embedded web app

* Commit screenshots

* fix(deps): update nextjs monorepo to v14.2.2

* Bundle embedded web app

* Commit screenshots

* fix(deps): update dependency react-virtuoso to v4.7.9

* Bundle embedded web app

* chore(deps): update dependency cypress to v13.8.0

* Bundle embedded web app

* Commit screenshots

* chore(deps): lock file maintenance

* Bundle embedded web app

* add options requests and reformat operationIds

* chore(go): update to go 1.22 (#3708)

* chore(go): update to go 1.22

* fix: install go before running codeql to fix autobild

* chore(test): explicitly set base crosscompile image

* fix(test): do not point at image with incorrect version of Go

* chore: troubleshoot js autoformating in workflow

* Commit screenshots

* add wildcard to index path

* give /admin/ routes precedence

* fix(deps): update dependency @fontsource/poppins to v5.0.14

* Bundle embedded web app

* Bump formidable and artillery in /test/load (#3718)

Removes [formidable](https://github.com/node-formidable/formidable). It's no longer used after updating ancestor dependency [artillery](https://github.com/artilleryio/artillery). These dependencies need to be updated together.


Removes `formidable`

Updates `artillery` from 2.0.2 to 2.0.10
- [Release notes](https://github.com/artilleryio/artillery/releases)
- [Commits](https://github.com/artilleryio/artillery/compare/artillery-2.0.2...artillery-2.0.10)

---
updated-dependencies:
- dependency-name: formidable
  dependency-type: indirect
- dependency-name: artillery
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Commit screenshots

* chore(deps): update peter-evans/create-or-update-comment digest to da12db3

* fix(deps): update dependency react-virtuoso to v4.7.10

* add verbose logging and update handler precedence

* Bundle embedded web app

* Commit screenshots

* chore(deps): update golangci/golangci-lint-action action to v5 (#3722)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update typescript-eslint monorepo to v7.7.1

* Bundle embedded web app

* Commit screenshots

* chore(deps): update dependency knip to v5.10.0

* Bundle embedded web app

* add federation routers

* updated comments to pass linter

* fix bug with mount

* fix linting errors

* add router logs to log files

* update emoji dir

* fixed linting error

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Owncast <owncast@owncast.online>
Co-authored-by: Gabe Kangas <gabek@real-ity.com>
Co-authored-by: mahmed2000 <49453542+mahmed2000@users.noreply.github.com>
Co-authored-by: Muaz Ahmad <mahmad2000@protonmail.com>
Co-authored-by: Jeet Chawda <68128367+Jeetch8@users.noreply.github.com>
Co-authored-by: Vivian Lim  <1565930+vivlim@users.noreply.github.com>
Co-authored-by: vivlim <vivlim@vivl.im>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-01 18:50:20 -07:00

917 lines
25 KiB
Go

package admin
import (
"encoding/json"
"fmt"
"net"
"net/http"
"net/netip"
"os"
"path/filepath"
"reflect"
"strings"
"github.com/owncast/owncast/activitypub/outbox"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/core/webhooks"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
"github.com/teris-io/shortid"
)
// ConfigValue is a container object that holds a value, is encoded, and saved to the database.
type ConfigValue struct {
Value interface{} `json:"value"`
}
// SetTags will handle the web config request to set tags.
func SetTags(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValues, success := getValuesFromRequest(w, r)
if !success {
return
}
tagStrings := make([]string, 0)
for _, tag := range configValues {
tagStrings = append(tagStrings, strings.TrimLeft(tag.Value.(string), "#"))
}
if err := data.SetServerMetadataTags(tagStrings); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
// Update Fediverse followers about this change.
if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetStreamTitle will handle the web config request to set the current stream title.
func SetStreamTitle(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
value := configValue.Value.(string)
if err := data.SetStreamTitle(value); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
if value != "" {
sendSystemChatAction(fmt.Sprintf("Stream title changed to **%s**", value), true)
go webhooks.SendStreamStatusEvent(models.StreamTitleUpdated)
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// ExternalSetStreamTitle will change the stream title on behalf of an external integration API request.
func ExternalSetStreamTitle(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
SetStreamTitle(w, r)
}
func sendSystemChatAction(messageText string, ephemeral bool) {
if err := chat.SendSystemAction(messageText, ephemeral); err != nil {
log.Errorln(err)
}
}
// SetServerName will handle the web config request to set the server's name.
func SetServerName(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetServerName(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
// Update Fediverse followers about this change.
if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetServerSummary will handle the web config request to set the about/summary text.
func SetServerSummary(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetServerSummary(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
// Update Fediverse followers about this change.
if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetCustomOfflineMessage will set a message to display when the server is offline.
func SetCustomOfflineMessage(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetCustomOfflineMessage(strings.TrimSpace(configValue.Value.(string))); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetServerWelcomeMessage will handle the web config request to set the welcome message text.
func SetServerWelcomeMessage(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetServerWelcomeMessage(strings.TrimSpace(configValue.Value.(string))); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetExtraPageContent will handle the web config request to set the page markdown content.
func SetExtraPageContent(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetExtraPageBodyContent(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetAdminPassword will handle the web config request to set the server admin password.
func SetAdminPassword(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetAdminPassword(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetLogo will handle a new logo image file being uploaded.
func SetLogo(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
value, ok := configValue.Value.(string)
if !ok {
controllers.WriteSimpleResponse(w, false, "unable to find image data")
return
}
bytes, extension, err := utils.DecodeBase64Image(value)
if err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
imgPath := filepath.Join("data", "logo"+extension)
if err := os.WriteFile(imgPath, bytes, 0o600); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
if err := data.SetLogoPath("logo" + extension); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
if err := data.SetLogoUniquenessString(shortid.MustGenerate()); err != nil {
log.Error("Error saving logo uniqueness string: ", err)
}
// Update Fediverse followers about this change.
if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetNSFW will handle the web config request to set the NSFW flag.
func SetNSFW(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetNSFW(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetFfmpegPath will handle the web config request to validate and set an updated copy of ffmpg.
func SetFfmpegPath(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
path := configValue.Value.(string)
if err := utils.VerifyFFMpegPath(path); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
if err := data.SetFfmpegPath(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// SetWebServerPort will handle the web config request to set the server's HTTP port.
func SetWebServerPort(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if port, ok := configValue.Value.(float64); ok {
if (port < 1) || (port > 65535) {
controllers.WriteSimpleResponse(w, false, "Port number must be between 1 and 65535")
return
}
if err := data.SetHTTPPortNumber(port); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "HTTP port set")
return
}
controllers.WriteSimpleResponse(w, false, "Invalid type or value, port must be a number")
}
// SetWebServerIP will handle the web config request to set the server's HTTP listen address.
func SetWebServerIP(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if input, ok := configValue.Value.(string); ok {
if ip := net.ParseIP(input); ip != nil {
if err := data.SetHTTPListenAddress(ip.String()); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "HTTP listen address set")
return
}
controllers.WriteSimpleResponse(w, false, "Invalid IP address")
return
}
controllers.WriteSimpleResponse(w, false, "Invalid type or value, IP address must be a string")
}
// SetRTMPServerPort will handle the web config request to set the inbound RTMP port.
func SetRTMPServerPort(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetRTMPPortNumber(configValue.Value.(float64)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "rtmp port set")
}
// SetServerURL will handle the web config request to set the full server URL.
func SetServerURL(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
rawValue, ok := configValue.Value.(string)
if !ok {
controllers.WriteSimpleResponse(w, false, "could not read server url")
return
}
serverHostString := utils.GetHostnameFromURLString(rawValue)
if serverHostString == "" {
controllers.WriteSimpleResponse(w, false, "server url value invalid")
return
}
// Block Private IP URLs
ipAddr, ipErr := netip.ParseAddr(utils.GetHostnameWithoutPortFromURLString(rawValue))
if ipErr == nil && ipAddr.IsPrivate() {
controllers.WriteSimpleResponse(w, false, "Server URL cannot be private")
return
}
// Trim any trailing slash
serverURL := strings.TrimRight(rawValue, "/")
if err := data.SetServerURL(serverURL); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "server url set")
}
// SetSocketHostOverride will set the host override for the websocket.
func SetSocketHostOverride(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetWebsocketOverrideHost(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "websocket host override set")
}
// SetDirectoryEnabled will handle the web config request to enable or disable directory registration.
func SetDirectoryEnabled(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetDirectoryEnabled(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "directory state changed")
}
// SetStreamLatencyLevel will handle the web config request to set the stream latency level.
func SetStreamLatencyLevel(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetStreamLatencyLevel(configValue.Value.(float64)); err != nil {
controllers.WriteSimpleResponse(w, false, "error setting stream latency "+err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "set stream latency")
}
// SetS3Configuration will handle the web config request to set the storage configuration.
func SetS3Configuration(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type s3ConfigurationRequest struct {
Value models.S3 `json:"value"`
}
decoder := json.NewDecoder(r.Body)
var newS3Config s3ConfigurationRequest
if err := decoder.Decode(&newS3Config); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update s3 config with provided values")
return
}
if newS3Config.Value.Enabled {
if newS3Config.Value.Endpoint == "" || !utils.IsValidURL((newS3Config.Value.Endpoint)) {
controllers.WriteSimpleResponse(w, false, "s3 support requires an endpoint")
return
}
if newS3Config.Value.AccessKey == "" || newS3Config.Value.Secret == "" {
controllers.WriteSimpleResponse(w, false, "s3 support requires an access key and secret")
return
}
if newS3Config.Value.Region == "" {
controllers.WriteSimpleResponse(w, false, "s3 support requires a region and endpoint")
return
}
if newS3Config.Value.Bucket == "" {
controllers.WriteSimpleResponse(w, false, "s3 support requires a bucket created for storing public video segments")
return
}
}
if err := data.SetS3Config(newS3Config.Value); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "storage configuration changed")
}
// SetStreamOutputVariants will handle the web config request to set the video output stream variants.
func SetStreamOutputVariants(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type streamOutputVariantRequest struct {
Value []models.StreamOutputVariant `json:"value"`
}
decoder := json.NewDecoder(r.Body)
var videoVariants streamOutputVariantRequest
if err := decoder.Decode(&videoVariants); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update video config with provided values "+err.Error())
return
}
if err := data.SetStreamOutputVariants(videoVariants.Value); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update video config with provided values "+err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "stream output variants updated")
}
// SetSocialHandles will handle the web config request to set the external social profile links.
func SetSocialHandles(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type socialHandlesRequest struct {
Value []models.SocialHandle `json:"value"`
}
decoder := json.NewDecoder(r.Body)
var socialHandles socialHandlesRequest
if err := decoder.Decode(&socialHandles); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update social handles with provided values")
return
}
if err := data.SetSocialHandles(socialHandles.Value); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update social handles with provided values")
return
}
// Update Fediverse followers about this change.
if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "social handles updated")
}
// SetChatDisabled will disable chat functionality.
func SetChatDisabled(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to update chat disabled")
return
}
if err := data.SetChatDisabled(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "chat disabled status updated")
}
// SetVideoCodec will change the codec used for video encoding.
func SetVideoCodec(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to change video codec")
return
}
if err := data.SetVideoCodec(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update codec")
return
}
controllers.WriteSimpleResponse(w, true, "video codec updated")
}
// SetExternalActions will set the 3rd party actions for the web interface.
func SetExternalActions(w http.ResponseWriter, r *http.Request) {
type externalActionsRequest struct {
Value []models.ExternalAction `json:"value"`
}
decoder := json.NewDecoder(r.Body)
var actions externalActionsRequest
if err := decoder.Decode(&actions); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update external actions with provided values")
return
}
if err := data.SetExternalActions(actions.Value); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update external actions with provided values")
return
}
controllers.WriteSimpleResponse(w, true, "external actions update")
}
// SetCustomStyles will set the CSS string we insert into the page.
func SetCustomStyles(w http.ResponseWriter, r *http.Request) {
customStyles, success := getValueFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to update custom styles")
return
}
if err := data.SetCustomStyles(customStyles.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "custom styles updated")
}
// SetCustomJavascript will set the Javascript string we insert into the page.
func SetCustomJavascript(w http.ResponseWriter, r *http.Request) {
customJavascript, success := getValueFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to update custom javascript")
return
}
if err := data.SetCustomJavascript(customJavascript.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "custom styles updated")
}
// SetForbiddenUsernameList will set the list of usernames we do not allow to use.
func SetForbiddenUsernameList(w http.ResponseWriter, r *http.Request) {
type forbiddenUsernameListRequest struct {
Value []string `json:"value"`
}
decoder := json.NewDecoder(r.Body)
var request forbiddenUsernameListRequest
if err := decoder.Decode(&request); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update forbidden usernames with provided values")
return
}
if err := data.SetForbiddenUsernameList(request.Value); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "forbidden username list updated")
}
// SetSuggestedUsernameList will set the list of suggested usernames that newly registered users are assigned if it isn't inferred otherwise (i.e. through a proxy).
func SetSuggestedUsernameList(w http.ResponseWriter, r *http.Request) {
type suggestedUsernameListRequest struct {
Value []string `json:"value"`
}
decoder := json.NewDecoder(r.Body)
var request suggestedUsernameListRequest
if err := decoder.Decode(&request); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update suggested usernames with provided values")
return
}
if err := data.SetSuggestedUsernamesList(request.Value); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "suggested username list updated")
}
// SetChatJoinMessagesEnabled will enable or disable the chat join messages.
func SetChatJoinMessagesEnabled(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to update chat join messages enabled")
return
}
if err := data.SetChatJoinMessagesEnabled(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "chat join message status updated")
}
// SetHideViewerCount will enable or disable hiding the viewer count.
func SetHideViewerCount(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to update hiding viewer count")
return
}
if err := data.SetHideViewerCount(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "hide viewer count setting updated")
}
// SetDisableSearchIndexing will set search indexing support.
func SetDisableSearchIndexing(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to update search indexing")
return
}
if err := data.SetDisableSearchIndexing(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "search indexing support updated")
}
// SetVideoServingEndpoint will save the video serving endpoint.
func SetVideoServingEndpoint(w http.ResponseWriter, r *http.Request) {
endpoint, success := getValueFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to update custom video serving endpoint")
return
}
value, ok := endpoint.Value.(string)
if !ok {
controllers.WriteSimpleResponse(w, false, "unable to update custom video serving endpoint")
return
}
if err := data.SetVideoServingEndpoint(value); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "custom video serving endpoint updated")
}
// 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 {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.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 {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "chat message slur filter changed")
}
func requirePOST(w http.ResponseWriter, r *http.Request) bool {
if r.Method != controllers.POST {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return false
}
return true
}
func getValueFromRequest(w http.ResponseWriter, r *http.Request) (ConfigValue, bool) {
decoder := json.NewDecoder(r.Body)
var configValue ConfigValue
if err := decoder.Decode(&configValue); err != nil {
log.Warnln(err)
controllers.WriteSimpleResponse(w, false, "unable to parse new value")
return configValue, false
}
return configValue, true
}
func getValuesFromRequest(w http.ResponseWriter, r *http.Request) ([]ConfigValue, bool) {
var values []ConfigValue
decoder := json.NewDecoder(r.Body)
var configValue ConfigValue
if err := decoder.Decode(&configValue); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to parse array of values")
return values, false
}
object := reflect.ValueOf(configValue.Value)
for i := 0; i < object.Len(); i++ {
values = append(values, ConfigValue{Value: object.Index(i).Interface()})
}
return values, true
}
// SetStreamKeys will set the valid stream keys.
func SetStreamKeys(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type streamKeysRequest struct {
Value []models.StreamKey `json:"value"`
}
decoder := json.NewDecoder(r.Body)
var streamKeys streamKeysRequest
if err := decoder.Decode(&streamKeys); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update stream keys with provided values")
return
}
if len(streamKeys.Value) == 0 {
controllers.WriteSimpleResponse(w, false, "must provide at least one valid stream key")
return
}
for _, streamKey := range streamKeys.Value {
if streamKey.Key == "" {
controllers.WriteSimpleResponse(w, false, "stream key cannot be empty")
return
}
}
if err := data.SetStreamKeys(streamKeys.Value); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}