mirror of
https://github.com/owncast/owncast.git
synced 2025-11-02 20:23:29 +08:00
0.0.6 -> Master (#731)
* Implement webhook events for external integrations (#574) * Implement webhook events for external integrations Reference #556 * move message type to models and remove duplicate * add json header so content type can be determined * Pass at migrating webhooks to datastore + management apis (#589) * Pass at migrating webhooks to datastore + management apis * Support nil lastUsed timestamps and return back the new webhook on create * Cleanup from review feedback * Simplify a bit Co-authored-by: Aaron Ogle <aaron@geekgonecrazy.com> Co-authored-by: Gabe Kangas <gabek@real-ity.com> * Webhook query cleanup * Access tokens + Send system message external API (#585) * New add, get and delete access token APIs * Create auth token middleware * Update last_used timestamp when using an access token * Add auth'ed endpoint for sending system messages * Cleanup * Update api spec for new apis * Commit updated API documentation * Add auth'ed endpoint for sending user chat messages * Return access token string * Commit updated API documentation * Fix route * Support nil lastUsed time * Commit updated Javascript packages * Remove duplicate function post rebase * Fix msg id generation * Update controllers/admin/chat.go Co-authored-by: Aaron Ogle <geekgonecrazy@users.noreply.github.com> * Webhook query cleanup * Add SystemMessageSent to EventType Co-authored-by: Owncast <owncast@owncast.online> Co-authored-by: Aaron Ogle <geekgonecrazy@users.noreply.github.com> * Set webhook as used on completion. Closes #610 * Display webhook errors as errors * Commit updated API documentation * Add user joined chat event * Change integration API paths. Update API spec * Update development version of admin that supports integration apis * Commit updated API documentation * Add automated tests for external integration APIs * check error * quiet this test for now * Route up some additional 3rd party apis. #638 * Commit updated API documentation * Save username on user joined event * Add missing scope to valid scopes list * Add generic chat action event API for 3rd parties. Closes #666 * Commit updated API documentation * First pass at moving WIP config framework into project for #234 * Only support exported fields in custom types * Using YP get/set key as a first pass at using the data layer. Fixes + integration. * Ignore test db * Start adding getters and setters for config values * More get/set config work. Starting to populate api with data * Wire up some config edit endpoints * More endpoints * Disable cors middleware * Add more endpoints and add test to test them * Remove the in-memory change APIs * Add endpoint for changing tags * Add more config endpoints * Starting to point more things away from config file and to the datastore * Populate YP with db data * Create new util method for parsing page body markdown and return it in api * Verify proposed path to ffmpeg * For development purposes show the config key in logs * Move stats values to datastore * Moving over more values to the datastore * Move S3 config to datastore * First pass the config -> db migrator * Add the start of the video config apis * It builds pointing everything away from the config * Tweak ffmpeg path error message * Backup database every hour. Closes #549 * Config + defaults + migration work for db * Cleanup logging * Remove all the old config structs * Add descriptive info about migration * Tweak ffmpeg validation logic * Fix db backup path. backup on db version migration * Set video and s3 configurations * Update api spec with new config endpoints * Add migrator for stats file * Commit updated API documentation * Use a dynamic system port for internal HLS writes. Closes #577 (#626) * Use a dynamic system port for internal HLS writes. Closes #577 * Cleanup * YP key migration to datastore * Create a backup directory if needed before migrations * Remove config test that no longer makes sense. Cleanup. * Change number types from float32 to float64 * Update automated test suite * Allow restoring a database backup via command line flags. Closes #549 * Add new hls segment config api * Commit updated API documentation * Update apis to require a value container property * add socialHandles api * Commit updated API documentation * Add new latancy level setting to replace segment settings * Commit updated API documentation * Fix spelling * Commit updated API documentation * hardcode a json api of available social platforms * Add additional icons * Return social handles in server config api * Add socialhandles validation to test * Move list of hard coded social platforms to an api * Remove audio only code from transcoder since we do not use it * Add latency levels api + snapshot of video settings as current broadcast * Add config/serverurl endpoint * Return 404 on YP api if disabled * Surface stream title in YP response * Add stream title to web ui * Cleanup log message. Closes #520 * Rename ffmpeg package to transcoder * Add ws package for testing * Reduce chat backlog to past 5hrs, max 50. Closes #548 * Fix error formatting * Add endpoint for resetting yp registration * Add yp/reset to api spec. return status in response * Return zero viewer count if stream is offline. Closes #422 * Post-rebase fixes * Fix merge conflict in openapi file * Commit updated API documentation * Standardize controller names * Support setting the stream key via the command line. Closes #665 * Return social handles with YP data. First half of https://github.com/owncast/owncast-yp/issues/28 * Give the YP package access to server status regardless if enabled or not * Change delay in automated tests * Add stream title integration API. For #638 * Commit updated API documentation * Add storage to the migrator * Missing returning NSFW value in server config * Add flag to ignore websocket client. Closes #537 * Add error for parsing broadcaster metadata * Add support for a cli specified http server port. Closes #674 * Add cpu usage levels and a temporary mapping between it and libx264 presets * Test for valid url endpoint when saving s3 config * Re-configure storage on every stream to allow changing storage providers * After 5 minutes of a stream being stopped clear the stream title * Hide viewer count once stream goes offline instead of when player stops * Pull steamTitle from the status that gets updated instead of the config * Commit updated API documentation * Optionally show stream title in the header * Reset stream title when server starts * Show chat action when stream title is updated * Allow system messages to come back in persistence * Split out getting chat history for moderation + fix tests * Remove server title and standardize on name only * Commit updated API documentation * Bump github.com/aws/aws-sdk-go from 1.37.1 to 1.37.2 (#680) Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.37.1 to 1.37.2. - [Release notes](https://github.com/aws/aws-sdk-go/releases) - [Changelog](https://github.com/aws/aws-sdk-go/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-go/compare/v1.37.1...v1.37.2) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Add video variant and stream latency config file migrator * Remove mostly unused disable upgrade check bool * Commit updated API documentation * Allow bundling the admin from the 0.0.6 branch * Fix saving port numbers * Use name instead of old title on window focus * Work on latency levels. Fix test to use levels. Clean up transcoder to only reference levels * Another place where title -> name * Fix test * Bump github.com/aws/aws-sdk-go from 1.37.2 to 1.37.3 (#690) Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.37.2 to 1.37.3. - [Release notes](https://github.com/aws/aws-sdk-go/releases) - [Changelog](https://github.com/aws/aws-sdk-go/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-go/compare/v1.37.2...v1.37.3) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Update dependabot config * Bump github.com/aws/aws-sdk-go from 1.37.3 to 1.37.5 (#693) Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.37.3 to 1.37.5. - [Release notes](https://github.com/aws/aws-sdk-go/releases) - [Changelog](https://github.com/aws/aws-sdk-go/blob/master/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-go/compare/v1.37.3...v1.37.5) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump video.js from 7.10.2 to 7.11.4 in /build/javascript (#694) * Bump video.js from 7.10.2 to 7.11.4 in /build/javascript Bumps [video.js](https://github.com/videojs/video.js) from 7.10.2 to 7.11.4. - [Release notes](https://github.com/videojs/video.js/releases) - [Changelog](https://github.com/videojs/video.js/blob/main/CHANGELOG.md) - [Commits](https://github.com/videojs/video.js/compare/v7.10.2...v7.11.4) Signed-off-by: dependabot[bot] <support@github.com> * Commit updated Javascript packages Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Owncast <owncast@owncast.online> * Make the latency migrator dynamic so I can tweak values easier * Split out fetching ffmpeg path from validating the path so it can be changed in the admin * Some commenting and linter cleanup * Validate the path for a logo change and throw an error if it does not exist * Logo change requests have to be a real file now * Cleanup, making linter happy * Format javascript on push * Only format js in master * Tweak latency level values * Remove unused config file examples * Fix thumbnail generation after messing with the ffmpeg path getter * Reduce how often we report high hardware utilization warnings * Bundle the 0.0.6 branch version of the admin * Return validated ffmpeg path in admin server config * Change the logo to be stored in the data directory instead of webroot * Bump postcss from 8.2.4 to 8.2.5 in /build/javascript (#702) Bumps [postcss](https://github.com/postcss/postcss) from 8.2.4 to 8.2.5. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.2.4...8.2.5) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Default config file no longer used * don't show stream title when offline addresses https://github.com/owncast/owncast/issues/677 * Remove auto-clearing stream title. #677 * webroot -> data when using logo as thumbnail * Do not list websocket/access token create/delete as integration APIs * Commit updated API documentation * Bundle updated admin * Remove pointing to the 0.0.6 admin branch * Linter cleanup * Linter cleanup * Add donations and follow links to show up under social handles * Prettified Code! * More linter cleanup * Update admin bundle * Remove use of platforms.js and return icons with social handles. Closes #732 * Update admin bundle * Support custom config path for use in migration * Remove unused platform-logos.gif * Reduce log level of message * Remove unused logo files in static dir * Handle dev vs. release build info * Restore logo.png for initial thumbnail * Cleanup some files from the build process that are not needed * Fix incorrect build-time injection var * Fix missing file getting copied to the build * Remove console directory message. * Update admin bundle * Fix comment * Report storage setup error * add some value set error checking * Use validated dynamic ffmpeg path for animated gif preview * Make chat message links be white so they don't hide in the bg. Closes #599 * Restore conditional that was accidentally removed Co-authored-by: Aaron Ogle <geekgonecrazy@users.noreply.github.com> Co-authored-by: Owncast <owncast@owncast.online> Co-authored-by: Ginger Wong <omqmail@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: nebunez <uoj2y7wak869@opayq.net> Co-authored-by: gabek <gabek@users.noreply.github.com>
This commit is contained in:
100
controllers/admin/accessToken.go
Normal file
100
controllers/admin/accessToken.go
Normal file
@ -0,0 +1,100 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/controllers"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/utils"
|
||||
)
|
||||
|
||||
type deleteTokenRequest struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type createTokenRequest struct {
|
||||
Name string `json:"name"`
|
||||
Scopes []string `json:"scopes"`
|
||||
}
|
||||
|
||||
// CreateAccessToken will generate a 3rd party access token.
|
||||
func CreateAccessToken(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request createTokenRequest
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
controllers.BadRequestHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify all the scopes provided are valid
|
||||
if !models.HasValidScopes(request.Scopes) {
|
||||
controllers.BadRequestHandler(w, errors.New("one or more invalid scopes provided"))
|
||||
return
|
||||
}
|
||||
|
||||
token, err := utils.GenerateAccessToken()
|
||||
if err != nil {
|
||||
controllers.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := data.InsertToken(token, request.Name, request.Scopes); err != nil {
|
||||
controllers.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
controllers.WriteResponse(w, models.AccessToken{
|
||||
Token: token,
|
||||
Name: request.Name,
|
||||
Scopes: request.Scopes,
|
||||
Timestamp: time.Now(),
|
||||
LastUsed: nil,
|
||||
})
|
||||
}
|
||||
|
||||
// GetAccessTokens will return all 3rd party access tokens.
|
||||
func GetAccessTokens(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
tokens, err := data.GetAccessTokens()
|
||||
if err != nil {
|
||||
controllers.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
controllers.WriteResponse(w, tokens)
|
||||
}
|
||||
|
||||
// DeleteAccessToken will return a single 3rd party access token.
|
||||
func DeleteAccessToken(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if r.Method != controllers.POST {
|
||||
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||
return
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request deleteTokenRequest
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
controllers.BadRequestHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if request.Token == "" {
|
||||
controllers.BadRequestHandler(w, errors.New("must provide a token"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := data.DeleteToken(request.Token); err != nil {
|
||||
controllers.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
controllers.WriteSimpleResponse(w, true, "deleted token")
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/controllers"
|
||||
"github.com/owncast/owncast/utils"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ChangeExtraPageContent will change the optional page content.
|
||||
func ChangeExtraPageContent(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||
return
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request changeExtraPageContentRequest
|
||||
err := decoder.Decode(&request)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
controllers.WriteSimpleResponse(w, false, "")
|
||||
return
|
||||
}
|
||||
|
||||
config.Config.InstanceDetails.ExtraPageContent = utils.RenderSimpleMarkdown(request.Key)
|
||||
controllers.WriteSimpleResponse(w, true, "changed")
|
||||
}
|
||||
|
||||
type changeExtraPageContentRequest struct {
|
||||
Key string `json:"content"`
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/controllers"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ChangeStreamKey will change the stream key (in memory).
|
||||
func ChangeStreamKey(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||
return
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request changeStreamKeyRequest
|
||||
err := decoder.Decode(&request)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
controllers.WriteSimpleResponse(w, false, "")
|
||||
return
|
||||
}
|
||||
|
||||
config.Config.VideoSettings.StreamingKey = request.Key
|
||||
controllers.WriteSimpleResponse(w, true, "changed")
|
||||
}
|
||||
|
||||
type changeStreamKeyRequest struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/controllers"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ChangeStreamName will change the stream key (in memory).
|
||||
func ChangeStreamName(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||
return
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request changeStreamNameRequest
|
||||
err := decoder.Decode(&request)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
controllers.WriteSimpleResponse(w, false, "")
|
||||
return
|
||||
}
|
||||
|
||||
config.Config.InstanceDetails.Name = request.Name
|
||||
controllers.WriteSimpleResponse(w, true, "changed")
|
||||
}
|
||||
|
||||
type changeStreamNameRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/controllers"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ChangeStreamTags will change the stream key (in memory).
|
||||
func ChangeStreamTags(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||
return
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request changeStreamTagsRequest
|
||||
err := decoder.Decode(&request)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
controllers.WriteSimpleResponse(w, false, "")
|
||||
return
|
||||
}
|
||||
|
||||
config.Config.InstanceDetails.Tags = request.Tags
|
||||
controllers.WriteSimpleResponse(w, true, "changed")
|
||||
}
|
||||
|
||||
type changeStreamTagsRequest struct {
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/controllers"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ChangeStreamTitle will change the stream key (in memory).
|
||||
func ChangeStreamTitle(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||
return
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request changeStreamTitleRequest
|
||||
err := decoder.Decode(&request)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
controllers.WriteSimpleResponse(w, false, "")
|
||||
return
|
||||
}
|
||||
|
||||
config.Config.InstanceDetails.Title = request.Title
|
||||
controllers.WriteSimpleResponse(w, true, "changed")
|
||||
}
|
||||
|
||||
type changeStreamTitleRequest struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
@ -4,36 +4,36 @@ package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/controllers"
|
||||
"github.com/owncast/owncast/core"
|
||||
"github.com/owncast/owncast/core/chat"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/models"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/teris-io/shortid"
|
||||
)
|
||||
|
||||
// UpdateMessageVisibility updates an array of message IDs to have the same visiblity.
|
||||
func UpdateMessageVisibility(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
if r.Method != controllers.POST {
|
||||
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||
return
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request messageVisibilityUpdateRequest // creates an empty struc
|
||||
var request messageVisibilityUpdateRequest
|
||||
|
||||
err := decoder.Decode(&request) // decode the json into `request`
|
||||
err := decoder.Decode(&request)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
controllers.WriteSimpleResponse(w, false, "")
|
||||
return
|
||||
}
|
||||
|
||||
// // make sql update call here.
|
||||
// // := means create a new var
|
||||
// _db := data.GetDatabase()
|
||||
// updateMessageVisibility(_db, request)
|
||||
|
||||
if err := chat.SetMessagesVisibility(request.IDArray, request.Visible); err != nil {
|
||||
controllers.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
@ -49,12 +49,99 @@ type messageVisibilityUpdateRequest struct {
|
||||
|
||||
// GetChatMessages returns all of the chat messages, unfiltered.
|
||||
func GetChatMessages(w http.ResponseWriter, r *http.Request) {
|
||||
// middleware.EnableCors(&w)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
messages := core.GetAllChatMessages(false)
|
||||
messages := core.GetModerationChatMessages()
|
||||
|
||||
if err := json.NewEncoder(w).Encode(messages); err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
}
|
||||
|
||||
// SendSystemMessage will send an official "SYSTEM" message
|
||||
// to chat on behalf of your server.
|
||||
func SendSystemMessage(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
var message models.ChatEvent
|
||||
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
|
||||
controllers.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
message.MessageType = models.SystemMessageSent
|
||||
message.Author = data.GetServerName()
|
||||
message.ClientID = "owncast-server"
|
||||
message.ID = shortid.MustGenerate()
|
||||
message.Visible = true
|
||||
|
||||
message.SetDefaults()
|
||||
message.RenderAndSanitizeMessageBody()
|
||||
|
||||
if err := core.SendMessageToChat(message); err != nil {
|
||||
controllers.BadRequestHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
controllers.WriteSimpleResponse(w, true, "sent")
|
||||
}
|
||||
|
||||
// SendUserMessage will send a message to chat on behalf of a user.
|
||||
func SendUserMessage(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
var message models.ChatEvent
|
||||
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
|
||||
controllers.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !message.Valid() {
|
||||
controllers.BadRequestHandler(w, errors.New("invalid chat message; id, author, and body are required"))
|
||||
return
|
||||
}
|
||||
|
||||
message.MessageType = models.MessageSent
|
||||
message.ClientID = "external-request"
|
||||
message.ID = shortid.MustGenerate()
|
||||
message.Visible = true
|
||||
|
||||
message.SetDefaults()
|
||||
message.RenderAndSanitizeMessageBody()
|
||||
|
||||
if err := core.SendMessageToChat(message); err != nil {
|
||||
controllers.BadRequestHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
controllers.WriteSimpleResponse(w, true, "sent")
|
||||
}
|
||||
|
||||
// SendChatAction will send a generic chat action.
|
||||
func SendChatAction(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
var message models.ChatEvent
|
||||
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
|
||||
controllers.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
message.MessageType = models.ChatActionSent
|
||||
message.ClientID = "external-request"
|
||||
message.ID = shortid.MustGenerate()
|
||||
message.Visible = true
|
||||
|
||||
if message.Author != "" {
|
||||
message.Body = fmt.Sprintf("%s %s", message.Author, message.Body)
|
||||
}
|
||||
|
||||
message.SetDefaults()
|
||||
|
||||
if err := core.SendMessageToChat(message); err != nil {
|
||||
controllers.BadRequestHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
controllers.WriteSimpleResponse(w, true, "sent")
|
||||
}
|
||||
|
||||
475
controllers/admin/config.go
Normal file
475
controllers/admin/config.go
Normal file
@ -0,0 +1,475 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
||||
"github.com/owncast/owncast/controllers"
|
||||
"github.com/owncast/owncast/core"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
var tagStrings []string
|
||||
for _, tag := range configValues {
|
||||
tagStrings = append(tagStrings, tag.Value.(string))
|
||||
}
|
||||
|
||||
if err := data.SetServerMetadataTags(tagStrings); 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))
|
||||
}
|
||||
controllers.WriteSimpleResponse(w, true, "changed")
|
||||
}
|
||||
|
||||
func sendSystemChatAction(messageText string) {
|
||||
message := models.ChatEvent{}
|
||||
message.Body = messageText
|
||||
message.MessageType = models.ChatActionSent
|
||||
message.ClientID = "internal-server"
|
||||
message.SetDefaults()
|
||||
|
||||
if err := core.SendMessageToChat(message); 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
// SetStreamKey will handle the web config request to set the server stream key.
|
||||
func SetStreamKey(w http.ResponseWriter, r *http.Request) {
|
||||
if !requirePOST(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
configValue, success := getValueFromRequest(w, r)
|
||||
if !success {
|
||||
return
|
||||
}
|
||||
|
||||
if err := data.SetStreamKey(configValue.Value.(string)); err != nil {
|
||||
controllers.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
controllers.WriteSimpleResponse(w, true, "changed")
|
||||
}
|
||||
|
||||
// SetLogoPath will handle the web config request to validate and set the logo path.
|
||||
func SetLogoPath(w http.ResponseWriter, r *http.Request) {
|
||||
if !requirePOST(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
configValue, success := getValueFromRequest(w, r)
|
||||
if !success {
|
||||
return
|
||||
}
|
||||
|
||||
imgPath := configValue.Value.(string)
|
||||
fullPath := filepath.Join("data", imgPath)
|
||||
if !utils.DoesFileExists(fullPath) {
|
||||
controllers.WriteSimpleResponse(w, false, fmt.Sprintf("%s does not exist", fullPath))
|
||||
return
|
||||
}
|
||||
|
||||
if err := data.SetLogoPath(imgPath); 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 err := data.SetHTTPPortNumber(configValue.Value.(float64)); err != nil {
|
||||
controllers.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
controllers.WriteSimpleResponse(w, true, "http port set")
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
if err := data.SetServerURL(configValue.Value.(string)); err != nil {
|
||||
controllers.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
controllers.WriteSimpleResponse(w, true, "server url 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
|
||||
}
|
||||
}
|
||||
|
||||
data.SetS3Config(newS3Config.Value)
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
// Temporary: Convert the cpuUsageLevel to a preset. In the future we will have
|
||||
// different codec models that will handle this for us and we won't
|
||||
// be keeping track of presets at all. But for now...
|
||||
presetMapping := []string{
|
||||
"ultrafast",
|
||||
"superfast",
|
||||
"veryfast",
|
||||
"faster",
|
||||
"fast",
|
||||
}
|
||||
|
||||
for i, variant := range videoVariants.Value {
|
||||
preset := "superfast"
|
||||
if variant.CPUUsageLevel > 0 && variant.CPUUsageLevel <= len(presetMapping) {
|
||||
preset = presetMapping[variant.CPUUsageLevel-1]
|
||||
}
|
||||
variant.EncoderPreset = preset
|
||||
videoVariants.Value[i] = variant
|
||||
}
|
||||
|
||||
if err := data.SetStreamOutputVariants(videoVariants.Value); err != nil {
|
||||
controllers.WriteSimpleResponse(w, false, "unable to update video config with provided values")
|
||||
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
|
||||
}
|
||||
|
||||
controllers.WriteSimpleResponse(w, true, "social handles updated")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@ -5,35 +5,50 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GetServerConfig gets the config details of the server.
|
||||
func GetServerConfig(w http.ResponseWriter, r *http.Request) {
|
||||
var videoQualityVariants = make([]config.StreamQuality, 0)
|
||||
for _, variant := range config.Config.GetVideoStreamQualities() {
|
||||
videoQualityVariants = append(videoQualityVariants, config.StreamQuality{
|
||||
var videoQualityVariants = make([]models.StreamOutputVariant, 0)
|
||||
for _, variant := range data.GetStreamOutputVariants() {
|
||||
videoQualityVariants = append(videoQualityVariants, models.StreamOutputVariant{
|
||||
IsAudioPassthrough: variant.GetIsAudioPassthrough(),
|
||||
IsVideoPassthrough: variant.IsVideoPassthrough,
|
||||
Framerate: variant.GetFramerate(),
|
||||
EncoderPreset: variant.GetEncoderPreset(),
|
||||
VideoBitrate: variant.VideoBitrate,
|
||||
AudioBitrate: variant.AudioBitrate,
|
||||
CPUUsageLevel: variant.GetCPUUsageLevel(),
|
||||
})
|
||||
}
|
||||
response := serverConfigAdminResponse{
|
||||
InstanceDetails: config.Config.InstanceDetails,
|
||||
FFmpegPath: config.Config.GetFFMpegPath(),
|
||||
StreamKey: config.Config.VideoSettings.StreamingKey,
|
||||
WebServerPort: config.Config.GetPublicWebServerPort(),
|
||||
RTMPServerPort: config.Config.GetRTMPServerPort(),
|
||||
VideoSettings: videoSettings{
|
||||
VideoQualityVariants: videoQualityVariants,
|
||||
SegmentLengthSeconds: config.Config.GetVideoSegmentSecondsLength(),
|
||||
NumberOfPlaylistItems: config.Config.GetMaxNumberOfReferencedSegmentsInPlaylist(),
|
||||
InstanceDetails: webConfigResponse{
|
||||
Name: data.GetServerName(),
|
||||
Summary: data.GetServerSummary(),
|
||||
Tags: data.GetServerMetadataTags(),
|
||||
ExtraPageContent: data.GetExtraPageBodyContent(),
|
||||
StreamTitle: data.GetStreamTitle(),
|
||||
Logo: data.GetLogoPath(),
|
||||
SocialHandles: data.GetSocialHandles(),
|
||||
NSFW: data.GetNSFW(),
|
||||
},
|
||||
YP: config.Config.YP,
|
||||
S3: config.Config.S3,
|
||||
FFmpegPath: utils.ValidatedFfmpegPath(data.GetFfMpegPath()),
|
||||
StreamKey: data.GetStreamKey(),
|
||||
WebServerPort: config.WebServerPort,
|
||||
RTMPServerPort: data.GetRTMPPortNumber(),
|
||||
VideoSettings: videoSettings{
|
||||
VideoQualityVariants: videoQualityVariants,
|
||||
LatencyLevel: data.GetStreamLatencyLevel().Level,
|
||||
},
|
||||
YP: yp{
|
||||
Enabled: data.GetDirectoryEnabled(),
|
||||
InstanceURL: data.GetServerURL(),
|
||||
},
|
||||
S3: data.GetS3Config(),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@ -44,18 +59,36 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type serverConfigAdminResponse struct {
|
||||
InstanceDetails config.InstanceDetails `json:"instanceDetails"`
|
||||
FFmpegPath string `json:"ffmpegPath"`
|
||||
StreamKey string `json:"streamKey"`
|
||||
WebServerPort int `json:"webServerPort"`
|
||||
RTMPServerPort int `json:"rtmpServerPort"`
|
||||
S3 config.S3 `json:"s3"`
|
||||
VideoSettings videoSettings `json:"videoSettings"`
|
||||
YP config.YP `json:"yp"`
|
||||
InstanceDetails webConfigResponse `json:"instanceDetails"`
|
||||
FFmpegPath string `json:"ffmpegPath"`
|
||||
StreamKey string `json:"streamKey"`
|
||||
WebServerPort int `json:"webServerPort"`
|
||||
RTMPServerPort int `json:"rtmpServerPort"`
|
||||
S3 models.S3 `json:"s3"`
|
||||
VideoSettings videoSettings `json:"videoSettings"`
|
||||
LatencyLevel int `json:"latencyLevel"`
|
||||
YP yp `json:"yp"`
|
||||
}
|
||||
|
||||
type videoSettings struct {
|
||||
VideoQualityVariants []config.StreamQuality `json:"videoQualityVariants"`
|
||||
SegmentLengthSeconds int `json:"segmentLengthSeconds"`
|
||||
NumberOfPlaylistItems int `json:"numberOfPlaylistItems"`
|
||||
VideoQualityVariants []models.StreamOutputVariant `json:"videoQualityVariants"`
|
||||
LatencyLevel int `json:"latencyLevel"`
|
||||
}
|
||||
|
||||
type webConfigResponse struct {
|
||||
Name string `json:"name"`
|
||||
Summary string `json:"summary"`
|
||||
Logo string `json:"logo"`
|
||||
Tags []string `json:"tags"`
|
||||
Version string `json:"version"`
|
||||
NSFW bool `json:"nsfw"`
|
||||
ExtraPageContent string `json:"extraPageContent"`
|
||||
StreamTitle string `json:"streamTitle"` // What's going on with the current stream
|
||||
SocialHandles []models.SocialHandle `json:"socialHandles"`
|
||||
}
|
||||
|
||||
type yp struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
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)
|
||||
}
|
||||
|
||||
@ -4,8 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/models"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -14,15 +14,17 @@ import (
|
||||
func Status(w http.ResponseWriter, r *http.Request) {
|
||||
broadcaster := core.GetBroadcaster()
|
||||
status := core.GetStatus()
|
||||
currentBroadcast := core.GetCurrentBroadcast()
|
||||
|
||||
response := adminStatusResponse{
|
||||
Broadcaster: broadcaster,
|
||||
CurrentBroadcast: currentBroadcast,
|
||||
Online: status.Online,
|
||||
ViewerCount: status.ViewerCount,
|
||||
OverallPeakViewerCount: status.OverallMaxViewerCount,
|
||||
SessionPeakViewerCount: status.SessionMaxViewerCount,
|
||||
VersionNumber: status.VersionNumber,
|
||||
DisableUpgradeChecks: config.Config.DisableUpgradeChecks,
|
||||
StreamTitle: data.GetStreamTitle(),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@ -33,12 +35,12 @@ func Status(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type adminStatusResponse struct {
|
||||
Broadcaster *models.Broadcaster `json:"broadcaster"`
|
||||
Online bool `json:"online"`
|
||||
ViewerCount int `json:"viewerCount"`
|
||||
OverallPeakViewerCount int `json:"overallPeakViewerCount"`
|
||||
SessionPeakViewerCount int `json:"sessionPeakViewerCount"`
|
||||
|
||||
VersionNumber string `json:"versionNumber"`
|
||||
DisableUpgradeChecks bool `json:"disableUpgradeChecks"`
|
||||
Broadcaster *models.Broadcaster `json:"broadcaster"`
|
||||
CurrentBroadcast *models.CurrentBroadcast `json:"currentBroadcast"`
|
||||
Online bool `json:"online"`
|
||||
ViewerCount int `json:"viewerCount"`
|
||||
OverallPeakViewerCount int `json:"overallPeakViewerCount"`
|
||||
SessionPeakViewerCount int `json:"sessionPeakViewerCount"`
|
||||
StreamTitle string `json:"streamTitle"`
|
||||
VersionNumber string `json:"versionNumber"`
|
||||
}
|
||||
|
||||
84
controllers/admin/webhooks.go
Normal file
84
controllers/admin/webhooks.go
Normal file
@ -0,0 +1,84 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/controllers"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/models"
|
||||
)
|
||||
|
||||
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 {
|
||||
controllers.BadRequestHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify all the scopes provided are valid
|
||||
if !models.HasValidEvents(request.Events) {
|
||||
controllers.BadRequestHandler(w, errors.New("one or more invalid event provided"))
|
||||
return
|
||||
}
|
||||
|
||||
newWebhookID, err := data.InsertWebhook(request.URL, request.Events)
|
||||
if err != nil {
|
||||
controllers.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
controllers.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 {
|
||||
controllers.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
controllers.WriteResponse(w, webhooks)
|
||||
}
|
||||
|
||||
// DeleteWebhook will delete a single webhook.
|
||||
func DeleteWebhook(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != controllers.POST {
|
||||
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||
return
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request deleteWebhookRequest
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
controllers.BadRequestHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := data.DeleteWebhook(request.ID); err != nil {
|
||||
controllers.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
controllers.WriteSimpleResponse(w, true, "deleted webhook")
|
||||
}
|
||||
20
controllers/admin/yp.go
Normal file
20
controllers/admin/yp.go
Normal file
@ -0,0 +1,20 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/controllers"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
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)
|
||||
controllers.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
controllers.WriteSimpleResponse(w, true, "reset")
|
||||
}
|
||||
@ -5,7 +5,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/core"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/router/middleware"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@ -17,32 +16,16 @@ func GetChatMessages(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
messages := core.GetAllChatMessages(true)
|
||||
messages := core.GetAllChatMessages()
|
||||
|
||||
err := json.NewEncoder(w).Encode(messages)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
case http.MethodPost:
|
||||
var message models.ChatEvent
|
||||
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
|
||||
internalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := core.SendMessageToChat(message); err != nil {
|
||||
badRequestHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(j{"success": true}); err != nil {
|
||||
internalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
if err := json.NewEncoder(w).Encode(j{"error": "method not implemented (PRs are accepted)"}); err != nil {
|
||||
internalErrorHandler(w, err)
|
||||
InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,17 +5,63 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/router/middleware"
|
||||
"github.com/owncast/owncast/utils"
|
||||
)
|
||||
|
||||
type webConfigResponse struct {
|
||||
Name string `json:"name"`
|
||||
Summary string `json:"summary"`
|
||||
Logo string `json:"logo"`
|
||||
Tags []string `json:"tags"`
|
||||
Version string `json:"version"`
|
||||
NSFW bool `json:"nsfw"`
|
||||
ExtraPageContent string `json:"extraPageContent"`
|
||||
StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream
|
||||
SocialHandles []models.SocialHandle `json:"socialHandles"`
|
||||
}
|
||||
|
||||
// GetWebConfig gets the status of the server.
|
||||
func GetWebConfig(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.EnableCors(&w)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
configuration := config.Config.InstanceDetails
|
||||
configuration.Version = config.Config.VersionInfo
|
||||
pageContent := utils.RenderPageContentMarkdown(data.GetExtraPageBodyContent())
|
||||
socialHandles := data.GetSocialHandles()
|
||||
for i, handle := range socialHandles {
|
||||
platform := models.GetSocialHandle(handle.Platform)
|
||||
if platform != nil {
|
||||
handle.Icon = platform.Icon
|
||||
socialHandles[i] = handle
|
||||
}
|
||||
}
|
||||
|
||||
configuration := webConfigResponse{
|
||||
Name: data.GetServerName(),
|
||||
Summary: data.GetServerSummary(),
|
||||
Logo: "/logo",
|
||||
Tags: data.GetServerMetadataTags(),
|
||||
Version: config.GetReleaseString(),
|
||||
NSFW: data.GetNSFW(),
|
||||
ExtraPageContent: pageContent,
|
||||
StreamTitle: data.GetStreamTitle(),
|
||||
SocialHandles: socialHandles,
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(configuration); err != nil {
|
||||
badRequestHandler(w, err)
|
||||
BadRequestHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,6 @@ func GetConnectedClients(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(clients); err != nil {
|
||||
internalErrorHandler(w, err)
|
||||
InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
7
controllers/constants.go
Normal file
7
controllers/constants.go
Normal file
@ -0,0 +1,7 @@
|
||||
package controllers
|
||||
|
||||
// POST is the HTTP POST method.
|
||||
const POST = "POST"
|
||||
|
||||
// GET is the HTTP GET method.
|
||||
const GET = "GET"
|
||||
@ -5,39 +5,65 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/models"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type j map[string]interface{}
|
||||
|
||||
func internalErrorHandler(w http.ResponseWriter, err error) {
|
||||
// InternalErrorHandler will return an error message as an HTTP response.
|
||||
func InternalErrorHandler(w http.ResponseWriter, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.Errorln(err)
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
if err := json.NewEncoder(w).Encode(j{"error": err.Error()}); err != nil {
|
||||
internalErrorHandler(w, err)
|
||||
InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
func badRequestHandler(w http.ResponseWriter, err error) {
|
||||
// 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)
|
||||
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.WriteHeader(http.StatusOK)
|
||||
|
||||
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)
|
||||
InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
// WriteResponse will return an object as a JSON encoded response.
|
||||
func WriteResponse(w http.ResponseWriter, response interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,6 +40,6 @@ func GetCustomEmoji(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(emojiList); err != nil {
|
||||
internalErrorHandler(w, err)
|
||||
InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,16 +14,23 @@ import (
|
||||
|
||||
"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/router/middleware"
|
||||
"github.com/owncast/owncast/utils"
|
||||
)
|
||||
|
||||
// MetadataPage represents a server-rendered web page for bots and web scrapers.
|
||||
type MetadataPage struct {
|
||||
Config config.InstanceDetails
|
||||
RequestedURL string
|
||||
Image string
|
||||
Thumbnail string
|
||||
TagsString string
|
||||
RequestedURL string
|
||||
Image string
|
||||
Thumbnail string
|
||||
TagsString string
|
||||
Title string
|
||||
Summary string
|
||||
Name string
|
||||
Tags []string
|
||||
SocialHandles []models.SocialHandle
|
||||
}
|
||||
|
||||
// IndexHandler handles the default index route.
|
||||
@ -70,7 +77,7 @@ func handleScraperMetadataPage(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
imageURL, err := url.Parse(fmt.Sprintf("http://%s%s", r.Host, config.Config.InstanceDetails.Logo))
|
||||
imageURL, err := url.Parse(fmt.Sprintf("http://%s%s", r.Host, data.GetLogoPath()))
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
@ -91,8 +98,15 @@ func handleScraperMetadataPage(w http.ResponseWriter, r *http.Request) {
|
||||
thumbnailURL = imageURL.String()
|
||||
}
|
||||
|
||||
tagsString := strings.Join(config.Config.InstanceDetails.Tags, ",")
|
||||
metadata := MetadataPage{config.Config.InstanceDetails, fullURL.String(), imageURL.String(), thumbnailURL, tagsString}
|
||||
tagsString := strings.Join(data.GetServerMetadataTags(), ",")
|
||||
metadata := MetadataPage{
|
||||
RequestedURL: fullURL.String(),
|
||||
Image: imageURL.String(),
|
||||
Thumbnail: thumbnailURL,
|
||||
TagsString: tagsString,
|
||||
Tags: data.GetServerMetadataTags(),
|
||||
SocialHandles: data.GetSocialHandles(),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
err = tmpl.Execute(w, metadata)
|
||||
|
||||
65
controllers/logo.go
Normal file
65
controllers/logo.go
Normal file
@ -0,0 +1,65 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 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("data", 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)
|
||||
}
|
||||
|
||||
func returnDefault(w http.ResponseWriter) {
|
||||
imagePath := filepath.Join(config.WebRoot, "img", "logo.svg")
|
||||
imageBytes, err := getImage(imagePath)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return
|
||||
}
|
||||
cacheTime := utils.GetCacheDurationSecondsForPath(imagePath)
|
||||
writeBytesAsImage(imageBytes, "image/svg+xml", 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 ioutil.ReadFile(path)
|
||||
}
|
||||
@ -16,6 +16,6 @@ func GetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(status); err != nil {
|
||||
internalErrorHandler(w, err)
|
||||
InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user