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:
199
core/data/accessTokens.go
Normal file
199
core/data/accessTokens.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/models"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func createAccessTokensTable() {
|
||||
log.Traceln("Creating access_tokens table...")
|
||||
|
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS access_tokens (
|
||||
"token" string NOT NULL PRIMARY KEY,
|
||||
"name" string,
|
||||
"scopes" TEXT,
|
||||
"timestamp" DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
"last_used" DATETIME
|
||||
);`
|
||||
|
||||
stmt, err := _db.Prepare(createTableSQL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec()
|
||||
if err != nil {
|
||||
log.Warnln(err)
|
||||
}
|
||||
}
|
||||
|
||||
// InsertToken will add a new token to the database.
|
||||
func InsertToken(token string, name string, scopes []string) error {
|
||||
log.Println("Adding new access token:", name)
|
||||
|
||||
scopesString := strings.Join(scopes, ",")
|
||||
|
||||
tx, err := _db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt, err := tx.Prepare("INSERT INTO access_tokens(token, name, scopes) values(?, ?, ?)")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if _, err = stmt.Exec(token, name, scopesString); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteToken will delete a token from the database.
|
||||
func DeleteToken(token string) error {
|
||||
log.Println("Deleting access token:", token)
|
||||
|
||||
tx, err := _db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt, err := tx.Prepare("DELETE FROM access_tokens WHERE token = ?")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
result, err := stmt.Exec(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsDeleted, _ := result.RowsAffected(); rowsDeleted == 0 {
|
||||
tx.Rollback() //nolint
|
||||
return errors.New(token + " not found")
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DoesTokenSupportScope will determine if a specific token has access to perform a scoped action.
|
||||
func DoesTokenSupportScope(token string, scope string) (bool, error) {
|
||||
// This will split the scopes from comma separated to individual rows
|
||||
// so we can efficiently find if a token supports a single scope.
|
||||
// This is SQLite specific, so if we ever support other database
|
||||
// backends we need to support other methods.
|
||||
var query = `SELECT count(*) FROM (
|
||||
WITH RECURSIVE split(token, scope, rest) AS (
|
||||
SELECT token, '', scopes || ',' FROM access_tokens
|
||||
UNION ALL
|
||||
SELECT token,
|
||||
substr(rest, 0, instr(rest, ',')),
|
||||
substr(rest, instr(rest, ',')+1)
|
||||
FROM split
|
||||
WHERE rest <> '')
|
||||
SELECT token, scope
|
||||
FROM split
|
||||
WHERE scope <> ''
|
||||
ORDER BY token, scope
|
||||
) AS token WHERE token.token = ? AND token.scope = ?;`
|
||||
|
||||
row := _db.QueryRow(query, token, scope)
|
||||
|
||||
var count = 0
|
||||
err := row.Scan(&count)
|
||||
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
// GetAccessTokens will return all access tokens.
|
||||
func GetAccessTokens() ([]models.AccessToken, error) { //nolint
|
||||
tokens := make([]models.AccessToken, 0)
|
||||
|
||||
// Get all messages sent within the past day
|
||||
var query = "SELECT * FROM access_tokens"
|
||||
|
||||
rows, err := _db.Query(query)
|
||||
if err != nil {
|
||||
return tokens, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var token string
|
||||
var name string
|
||||
var scopes string
|
||||
var timestampString string
|
||||
var lastUsedString *string
|
||||
|
||||
if err := rows.Scan(&token, &name, &scopes, ×tampString, &lastUsedString); err != nil {
|
||||
log.Error("There is a problem reading the database.", err)
|
||||
return tokens, err
|
||||
}
|
||||
|
||||
timestamp, err := time.Parse(time.RFC3339, timestampString)
|
||||
if err != nil {
|
||||
return tokens, err
|
||||
}
|
||||
|
||||
var lastUsed *time.Time = nil
|
||||
if lastUsedString != nil {
|
||||
lastUsedTime, _ := time.Parse(time.RFC3339, *lastUsedString)
|
||||
lastUsed = &lastUsedTime
|
||||
}
|
||||
|
||||
singleToken := models.AccessToken{
|
||||
Name: name,
|
||||
Token: token,
|
||||
Scopes: strings.Split(scopes, ","),
|
||||
Timestamp: timestamp,
|
||||
LastUsed: lastUsed,
|
||||
}
|
||||
|
||||
tokens = append(tokens, singleToken)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return tokens, err
|
||||
}
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
// SetAccessTokenAsUsed will update the last used timestamp for a token.
|
||||
func SetAccessTokenAsUsed(token string) error {
|
||||
tx, err := _db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt, err := tx.Prepare("UPDATE access_tokens SET last_used = CURRENT_TIMESTAMP WHERE token = ?")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if _, err := stmt.Exec(token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
18
core/data/cache.go
Normal file
18
core/data/cache.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package data
|
||||
|
||||
import "errors"
|
||||
|
||||
// GetCachedValue will return a value for key from the cache.
|
||||
func (ds *Datastore) GetCachedValue(key string) ([]byte, error) {
|
||||
// Check for a cached value
|
||||
if val, ok := ds.cache[key]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
return nil, errors.New(key + " not found in cache")
|
||||
}
|
||||
|
||||
// SetCachedValue will set a value for key in the cache.
|
||||
func (ds *Datastore) SetCachedValue(key string, b []byte) {
|
||||
ds.cache[key] = b
|
||||
}
|
||||
450
core/data/config.go
Normal file
450
core/data/config.go
Normal file
@@ -0,0 +1,450 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/models"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const extraContentKey = "extra_page_content"
|
||||
const streamTitleKey = "stream_title"
|
||||
const streamKeyKey = "stream_key"
|
||||
const logoPathKey = "logo_path"
|
||||
const serverSummaryKey = "server_summary"
|
||||
const serverNameKey = "server_name"
|
||||
const serverURLKey = "server_url"
|
||||
const httpPortNumberKey = "http_port_number"
|
||||
const rtmpPortNumberKey = "rtmp_port_number"
|
||||
const serverMetadataTagsKey = "server_metadata_tags"
|
||||
const directoryEnabledKey = "directory_enabled"
|
||||
const directoryRegistrationKeyKey = "directory_registration_key"
|
||||
const socialHandlesKey = "social_handles"
|
||||
const peakViewersSessionKey = "peak_viewers_session"
|
||||
const peakViewersOverallKey = "peak_viewers_overall"
|
||||
const lastDisconnectTimeKey = "last_disconnect_time"
|
||||
const ffmpegPathKey = "ffmpeg_path"
|
||||
const nsfwKey = "nsfw"
|
||||
const s3StorageEnabledKey = "s3_storage_enabled"
|
||||
const s3StorageConfigKey = "s3_storage_config"
|
||||
const videoLatencyLevel = "video_latency_level"
|
||||
const videoStreamOutputVariantsKey = "video_stream_output_variants"
|
||||
|
||||
// GetExtraPageBodyContent will return the user-supplied body content.
|
||||
func GetExtraPageBodyContent() string {
|
||||
content, err := _datastore.GetString(extraContentKey)
|
||||
if err != nil {
|
||||
log.Errorln(extraContentKey, err)
|
||||
return config.GetDefaults().PageBodyContent
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
// SetExtraPageBodyContent will set the user-supplied body content.
|
||||
func SetExtraPageBodyContent(content string) error {
|
||||
return _datastore.SetString(extraContentKey, content)
|
||||
}
|
||||
|
||||
// GetStreamTitle will return the name of the current stream.
|
||||
func GetStreamTitle() string {
|
||||
title, err := _datastore.GetString(streamTitleKey)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return title
|
||||
}
|
||||
|
||||
// SetStreamTitle will set the name of the current stream.
|
||||
func SetStreamTitle(title string) error {
|
||||
return _datastore.SetString(streamTitleKey, title)
|
||||
}
|
||||
|
||||
// GetStreamKey will return the inbound streaming password.
|
||||
func GetStreamKey() string {
|
||||
key, err := _datastore.GetString(streamKeyKey)
|
||||
if err != nil {
|
||||
log.Errorln(streamKeyKey, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
// SetStreamKey will set the inbound streaming password.
|
||||
func SetStreamKey(key string) error {
|
||||
return _datastore.SetString(streamKeyKey, key)
|
||||
}
|
||||
|
||||
// GetLogoPath will return the path for the logo, relative to webroot.
|
||||
func GetLogoPath() string {
|
||||
logo, err := _datastore.GetString(logoPathKey)
|
||||
if err != nil {
|
||||
log.Errorln(logoPathKey, err)
|
||||
return config.GetDefaults().Logo
|
||||
}
|
||||
|
||||
if logo == "" {
|
||||
return config.GetDefaults().Logo
|
||||
}
|
||||
|
||||
return logo
|
||||
}
|
||||
|
||||
// SetLogoPath will set the path for the logo, relative to webroot.
|
||||
func SetLogoPath(logo string) error {
|
||||
return _datastore.SetString(logoPathKey, logo)
|
||||
}
|
||||
|
||||
// GetServerSummary will return the server summary text.
|
||||
func GetServerSummary() string {
|
||||
summary, err := _datastore.GetString(serverSummaryKey)
|
||||
if err != nil {
|
||||
log.Errorln(serverSummaryKey, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return summary
|
||||
}
|
||||
|
||||
// SetServerSummary will set the server summary text.
|
||||
func SetServerSummary(summary string) error {
|
||||
return _datastore.SetString(serverSummaryKey, summary)
|
||||
}
|
||||
|
||||
// GetServerName will return the server name text.
|
||||
func GetServerName() string {
|
||||
name, err := _datastore.GetString(serverNameKey)
|
||||
if err != nil {
|
||||
log.Errorln(serverNameKey, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// SetServerName will set the server name text.
|
||||
func SetServerName(name string) error {
|
||||
return _datastore.SetString(serverNameKey, name)
|
||||
}
|
||||
|
||||
// GetServerURL will return the server URL.
|
||||
func GetServerURL() string {
|
||||
url, err := _datastore.GetString(serverURLKey)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
// SetServerURL will set the server URL.
|
||||
func SetServerURL(url string) error {
|
||||
return _datastore.SetString(serverURLKey, url)
|
||||
}
|
||||
|
||||
// GetHTTPPortNumber will return the server HTTP port.
|
||||
func GetHTTPPortNumber() int {
|
||||
port, err := _datastore.GetNumber(httpPortNumberKey)
|
||||
if err != nil {
|
||||
log.Errorln(httpPortNumberKey, err)
|
||||
return config.GetDefaults().WebServerPort
|
||||
}
|
||||
|
||||
if port == 0 {
|
||||
return config.GetDefaults().WebServerPort
|
||||
}
|
||||
return int(port)
|
||||
}
|
||||
|
||||
// SetHTTPPortNumber will set the server HTTP port.
|
||||
func SetHTTPPortNumber(port float64) error {
|
||||
return _datastore.SetNumber(httpPortNumberKey, port)
|
||||
}
|
||||
|
||||
// GetRTMPPortNumber will return the server RTMP port.
|
||||
func GetRTMPPortNumber() int {
|
||||
port, err := _datastore.GetNumber(rtmpPortNumberKey)
|
||||
if err != nil {
|
||||
log.Errorln(rtmpPortNumberKey, err)
|
||||
return config.GetDefaults().RTMPServerPort
|
||||
}
|
||||
|
||||
if port == 0 {
|
||||
return config.GetDefaults().RTMPServerPort
|
||||
}
|
||||
|
||||
return int(port)
|
||||
}
|
||||
|
||||
// SetRTMPPortNumber will set the server RTMP port.
|
||||
func SetRTMPPortNumber(port float64) error {
|
||||
return _datastore.SetNumber(rtmpPortNumberKey, port)
|
||||
}
|
||||
|
||||
// GetServerMetadataTags will return the metadata tags.
|
||||
func GetServerMetadataTags() []string {
|
||||
tagsString, err := _datastore.GetString(serverMetadataTagsKey)
|
||||
if err != nil {
|
||||
log.Errorln(serverMetadataTagsKey, err)
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return strings.Split(tagsString, ",")
|
||||
}
|
||||
|
||||
// SetServerMetadataTags will return the metadata tags.
|
||||
func SetServerMetadataTags(tags []string) error {
|
||||
tagString := strings.Join(tags, ",")
|
||||
return _datastore.SetString(serverMetadataTagsKey, tagString)
|
||||
}
|
||||
|
||||
// GetDirectoryEnabled will return if this server should register to YP.
|
||||
func GetDirectoryEnabled() bool {
|
||||
enabled, err := _datastore.GetBool(directoryEnabledKey)
|
||||
if err != nil {
|
||||
return config.GetDefaults().YPEnabled
|
||||
}
|
||||
|
||||
return enabled
|
||||
}
|
||||
|
||||
// SetDirectoryEnabled will set if this server should register to YP.
|
||||
func SetDirectoryEnabled(enabled bool) error {
|
||||
return _datastore.SetBool(directoryEnabledKey, enabled)
|
||||
}
|
||||
|
||||
// SetDirectoryRegistrationKey will set the YP protocol registration key.
|
||||
func SetDirectoryRegistrationKey(key string) error {
|
||||
return _datastore.SetString(directoryRegistrationKeyKey, key)
|
||||
}
|
||||
|
||||
// GetDirectoryRegistrationKey will return the YP protocol registration key.
|
||||
func GetDirectoryRegistrationKey() string {
|
||||
key, _ := _datastore.GetString(directoryRegistrationKeyKey)
|
||||
return key
|
||||
}
|
||||
|
||||
// GetSocialHandles will return the external social links.
|
||||
func GetSocialHandles() []models.SocialHandle {
|
||||
var socialHandles []models.SocialHandle
|
||||
|
||||
configEntry, err := _datastore.Get(socialHandlesKey)
|
||||
if err != nil {
|
||||
log.Errorln(socialHandlesKey, err)
|
||||
return socialHandles
|
||||
}
|
||||
|
||||
if err := configEntry.getObject(&socialHandles); err != nil {
|
||||
log.Errorln(err)
|
||||
return socialHandles
|
||||
}
|
||||
|
||||
return socialHandles
|
||||
}
|
||||
|
||||
// SetSocialHandles will set the external social links.
|
||||
func SetSocialHandles(socialHandles []models.SocialHandle) error {
|
||||
var configEntry = ConfigEntry{Key: socialHandlesKey, Value: socialHandles}
|
||||
return _datastore.Save(configEntry)
|
||||
}
|
||||
|
||||
// GetPeakSessionViewerCount will return the max number of viewers for this stream.
|
||||
func GetPeakSessionViewerCount() int {
|
||||
count, err := _datastore.GetNumber(peakViewersSessionKey)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return int(count)
|
||||
}
|
||||
|
||||
// SetPeakSessionViewerCount will set the max number of viewers for this stream.
|
||||
func SetPeakSessionViewerCount(count int) error {
|
||||
return _datastore.SetNumber(peakViewersSessionKey, float64(count))
|
||||
}
|
||||
|
||||
// GetPeakOverallViewerCount will return the overall max number of viewers.
|
||||
func GetPeakOverallViewerCount() int {
|
||||
count, err := _datastore.GetNumber(peakViewersOverallKey)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return int(count)
|
||||
}
|
||||
|
||||
// SetPeakOverallViewerCount will set the overall max number of viewers.
|
||||
func SetPeakOverallViewerCount(count int) error {
|
||||
return _datastore.SetNumber(peakViewersOverallKey, float64(count))
|
||||
}
|
||||
|
||||
// GetLastDisconnectTime will return the time the last stream ended.
|
||||
func GetLastDisconnectTime() (time.Time, error) {
|
||||
var disconnectTime time.Time
|
||||
configEntry, err := _datastore.Get(lastDisconnectTimeKey)
|
||||
if err != nil {
|
||||
return disconnectTime, err
|
||||
}
|
||||
|
||||
if err := configEntry.getObject(disconnectTime); err != nil {
|
||||
return disconnectTime, err
|
||||
}
|
||||
|
||||
return disconnectTime, nil
|
||||
}
|
||||
|
||||
// SetLastDisconnectTime will set the time the last stream ended.
|
||||
func SetLastDisconnectTime(disconnectTime time.Time) error {
|
||||
var configEntry = ConfigEntry{Key: lastDisconnectTimeKey, Value: disconnectTime}
|
||||
return _datastore.Save(configEntry)
|
||||
}
|
||||
|
||||
// SetNSFW will set if this stream has NSFW content.
|
||||
func SetNSFW(isNSFW bool) error {
|
||||
return _datastore.SetBool(nsfwKey, isNSFW)
|
||||
}
|
||||
|
||||
// GetNSFW will return if this stream has NSFW content.
|
||||
func GetNSFW() bool {
|
||||
nsfw, err := _datastore.GetBool(nsfwKey)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return nsfw
|
||||
}
|
||||
|
||||
// SetFfmpegPath will set the custom ffmpeg path.
|
||||
func SetFfmpegPath(path string) error {
|
||||
return _datastore.SetString(ffmpegPathKey, path)
|
||||
}
|
||||
|
||||
// GetFfMpegPath will return the ffmpeg path.
|
||||
func GetFfMpegPath() string {
|
||||
path, err := _datastore.GetString(ffmpegPathKey)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// GetS3Config will return the external storage configuration.
|
||||
func GetS3Config() models.S3 {
|
||||
configEntry, err := _datastore.Get(s3StorageConfigKey)
|
||||
if err != nil {
|
||||
return models.S3{Enabled: false}
|
||||
}
|
||||
|
||||
var s3Config models.S3
|
||||
if err := configEntry.getObject(&s3Config); err != nil {
|
||||
return models.S3{Enabled: false}
|
||||
}
|
||||
|
||||
return s3Config
|
||||
}
|
||||
|
||||
// SetS3Config will set the external storage configuration.
|
||||
func SetS3Config(config models.S3) error {
|
||||
var configEntry = ConfigEntry{Key: s3StorageConfigKey, Value: config}
|
||||
return _datastore.Save(configEntry)
|
||||
}
|
||||
|
||||
// GetS3StorageEnabled will return if external storage is enabled.
|
||||
func GetS3StorageEnabled() bool {
|
||||
enabled, err := _datastore.GetBool(s3StorageEnabledKey)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return false
|
||||
}
|
||||
|
||||
return enabled
|
||||
}
|
||||
|
||||
// SetS3StorageEnabled will enable or disable external storage.
|
||||
func SetS3StorageEnabled(enabled bool) error {
|
||||
return _datastore.SetBool(s3StorageEnabledKey, enabled)
|
||||
}
|
||||
|
||||
// GetStreamLatencyLevel will return the stream latency level.
|
||||
func GetStreamLatencyLevel() models.LatencyLevel {
|
||||
level, err := _datastore.GetNumber(videoLatencyLevel)
|
||||
if err != nil || level == 0 {
|
||||
level = 4
|
||||
}
|
||||
|
||||
return models.GetLatencyLevel(int(level))
|
||||
}
|
||||
|
||||
// SetStreamLatencyLevel will set the stream latency level.
|
||||
func SetStreamLatencyLevel(level float64) error {
|
||||
return _datastore.SetNumber(videoLatencyLevel, level)
|
||||
}
|
||||
|
||||
// GetStreamOutputVariants will return all of the stream output variants.
|
||||
func GetStreamOutputVariants() []models.StreamOutputVariant {
|
||||
configEntry, err := _datastore.Get(videoStreamOutputVariantsKey)
|
||||
if err != nil {
|
||||
return config.GetDefaults().StreamVariants
|
||||
}
|
||||
|
||||
var streamOutputVariants []models.StreamOutputVariant
|
||||
if err := configEntry.getObject(&streamOutputVariants); err != nil {
|
||||
return config.GetDefaults().StreamVariants
|
||||
}
|
||||
|
||||
if len(streamOutputVariants) == 0 {
|
||||
return config.GetDefaults().StreamVariants
|
||||
}
|
||||
|
||||
return streamOutputVariants
|
||||
}
|
||||
|
||||
// SetStreamOutputVariants will set the stream output variants.
|
||||
func SetStreamOutputVariants(variants []models.StreamOutputVariant) error {
|
||||
var configEntry = ConfigEntry{Key: videoStreamOutputVariantsKey, Value: variants}
|
||||
return _datastore.Save(configEntry)
|
||||
}
|
||||
|
||||
// VerifySettings will perform a sanity check for specific settings values.
|
||||
func VerifySettings() error {
|
||||
if GetStreamKey() == "" {
|
||||
return errors.New("no stream key set. Please set one in your config file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindHighestVideoQualityIndex will return the highest quality from a slice of variants.
|
||||
func FindHighestVideoQualityIndex(qualities []models.StreamOutputVariant) int {
|
||||
type IndexedQuality struct {
|
||||
index int
|
||||
quality models.StreamOutputVariant
|
||||
}
|
||||
|
||||
if len(qualities) < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
indexedQualities := make([]IndexedQuality, 0)
|
||||
for index, quality := range qualities {
|
||||
indexedQuality := IndexedQuality{index, quality}
|
||||
indexedQualities = append(indexedQualities, indexedQuality)
|
||||
}
|
||||
|
||||
sort.Slice(indexedQualities, func(a, b int) bool {
|
||||
if indexedQualities[a].quality.IsVideoPassthrough && !indexedQualities[b].quality.IsVideoPassthrough {
|
||||
return true
|
||||
}
|
||||
|
||||
if !indexedQualities[a].quality.IsVideoPassthrough && indexedQualities[b].quality.IsVideoPassthrough {
|
||||
return false
|
||||
}
|
||||
|
||||
return indexedQualities[a].quality.VideoBitrate > indexedQualities[b].quality.VideoBitrate
|
||||
})
|
||||
|
||||
return indexedQualities[0].index
|
||||
}
|
||||
46
core/data/configEntry.go
Normal file
46
core/data/configEntry.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
)
|
||||
|
||||
// ConfigEntry is the actual object saved to the database.
|
||||
// The Value is encoded using encoding/gob.
|
||||
type ConfigEntry struct {
|
||||
Key string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
func (c *ConfigEntry) getString() (string, error) {
|
||||
decoder := c.getDecoder()
|
||||
var result string
|
||||
err := decoder.Decode(&result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (c *ConfigEntry) getNumber() (float64, error) {
|
||||
decoder := c.getDecoder()
|
||||
var result float64
|
||||
err := decoder.Decode(&result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (c *ConfigEntry) getBool() (bool, error) {
|
||||
decoder := c.getDecoder()
|
||||
var result bool
|
||||
err := decoder.Decode(&result)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (c *ConfigEntry) getObject(result interface{}) error {
|
||||
decoder := c.getDecoder()
|
||||
err := decoder.Decode(result)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ConfigEntry) getDecoder() *gob.Decoder {
|
||||
valueBytes := c.Value.([]byte)
|
||||
decoder := gob.NewDecoder(bytes.NewBuffer(valueBytes))
|
||||
return decoder
|
||||
}
|
||||
@@ -8,25 +8,32 @@ import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
schemaVersion = 0
|
||||
backupFile = "backup/owncastdb.bak"
|
||||
)
|
||||
|
||||
var _db *sql.DB
|
||||
var _datastore *Datastore
|
||||
|
||||
// GetDatabase will return the shared instance of the actual database.
|
||||
func GetDatabase() *sql.DB {
|
||||
return _db
|
||||
}
|
||||
|
||||
func SetupPersistence() error {
|
||||
file := config.Config.DatabaseFilePath
|
||||
// GetStore will return the shared instance of the read/write datastore.
|
||||
func GetStore() *Datastore {
|
||||
return _datastore
|
||||
}
|
||||
|
||||
// SetupPersistence will open the datastore and make it available.
|
||||
func SetupPersistence(file string) error {
|
||||
// Create empty DB file if it doesn't exist.
|
||||
if !utils.DoesFileExists(file) {
|
||||
log.Traceln("Creating new database at", file)
|
||||
@@ -79,11 +86,26 @@ func SetupPersistence() error {
|
||||
}
|
||||
|
||||
_db = db
|
||||
|
||||
createWebhooksTable()
|
||||
createAccessTokensTable()
|
||||
|
||||
_datastore = &Datastore{}
|
||||
_datastore.Setup()
|
||||
|
||||
dbBackupTicker := time.NewTicker(1 * time.Hour)
|
||||
go func() {
|
||||
for range dbBackupTicker.C {
|
||||
utils.Backup(_db, backupFile)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func migrateDatabase(db *sql.DB, from, to int) error {
|
||||
log.Printf("Migrating database from version %d to %d\n", from, to)
|
||||
utils.Backup(db, fmt.Sprintf("backup/owncast-v%d.bak", from))
|
||||
for v := from; v < to; v++ {
|
||||
switch v {
|
||||
case 0:
|
||||
|
||||
115
core/data/data_test.go
Normal file
115
core/data/data_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
dbFile := "../../test/test.db"
|
||||
|
||||
SetupPersistence(dbFile)
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
const testKey = "test string key"
|
||||
const testValue = "test string value"
|
||||
|
||||
err := _datastore.SetString(testKey, testValue)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Get the config entry from the database
|
||||
stringTestResult, err := _datastore.GetString(testKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if stringTestResult != testValue {
|
||||
t.Error("expected", testValue, "but test returned", stringTestResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumber(t *testing.T) {
|
||||
const testKey = "test number key"
|
||||
const testValue = 42
|
||||
|
||||
err := _datastore.SetNumber(testKey, testValue)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Get the config entry from the database
|
||||
numberTestResult, err := _datastore.GetNumber(testKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(numberTestResult)
|
||||
|
||||
if numberTestResult != testValue {
|
||||
t.Error("expected", testValue, "but test returned", numberTestResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBool(t *testing.T) {
|
||||
const testKey = "test bool key"
|
||||
const testValue = true
|
||||
|
||||
err := _datastore.SetBool(testKey, testValue)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Get the config entry from the database
|
||||
numberTestResult, err := _datastore.GetBool(testKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println(numberTestResult)
|
||||
|
||||
if numberTestResult != testValue {
|
||||
t.Error("expected", testValue, "but test returned", numberTestResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomType(t *testing.T) {
|
||||
const testKey = "test custom type key"
|
||||
|
||||
// Test an example struct with a slice
|
||||
testStruct := TestStruct{
|
||||
Test: "Test string 123 in test struct",
|
||||
TestSlice: []string{"test string 1", "test string 2"},
|
||||
}
|
||||
|
||||
// Save config entry to the database
|
||||
if err := _datastore.Save(ConfigEntry{testKey, &testStruct}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Get the config entry from the database
|
||||
entryResult, err := _datastore.Get(testKey)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// Get a typed struct out of it
|
||||
var testResult TestStruct
|
||||
if err := entryResult.getObject(&testResult); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%+v", testResult)
|
||||
|
||||
if testResult.TestSlice[0] != testStruct.TestSlice[0] {
|
||||
t.Error("expected", testStruct.TestSlice[0], "but test returned", testResult.TestSlice[0])
|
||||
}
|
||||
}
|
||||
|
||||
// Custom type for testing
|
||||
type TestStruct struct {
|
||||
Test string
|
||||
TestSlice []string
|
||||
privateProperty string
|
||||
}
|
||||
43
core/data/defaults.go
Normal file
43
core/data/defaults.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/models"
|
||||
)
|
||||
|
||||
// HasPopulatedDefaults will determine if the defaults have been inserted into the database.
|
||||
func HasPopulatedDefaults() bool {
|
||||
hasPopulated, err := _datastore.GetBool("HAS_POPULATED_DEFAULTS")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return hasPopulated
|
||||
}
|
||||
|
||||
// PopulateDefaults will set default values in the database.
|
||||
func PopulateDefaults() {
|
||||
defaults := config.GetDefaults()
|
||||
|
||||
if HasPopulatedDefaults() {
|
||||
return
|
||||
}
|
||||
|
||||
_ = SetStreamKey(defaults.StreamKey)
|
||||
_ = SetHTTPPortNumber(float64(defaults.WebServerPort))
|
||||
_ = SetRTMPPortNumber(float64(defaults.RTMPServerPort))
|
||||
_ = SetLogoPath(defaults.Logo)
|
||||
_ = SetServerMetadataTags([]string{"owncast", "streaming"})
|
||||
_ = SetServerSummary("Welcome to your new Owncast server! This description can be changed in the admin")
|
||||
_ = SetServerName("Owncast")
|
||||
_ = SetStreamKey(defaults.StreamKey)
|
||||
_ = SetExtraPageBodyContent("This is your page's content that can be edited in the admin.")
|
||||
_ = SetSocialHandles([]models.SocialHandle{
|
||||
{
|
||||
Platform: "github",
|
||||
URL: "https://github.com/owncast/owncast",
|
||||
},
|
||||
})
|
||||
|
||||
_datastore.warmCache()
|
||||
_ = _datastore.SetBool("HAS_POPULATED_DEFAULTS", true)
|
||||
}
|
||||
266
core/data/migrator.go
Normal file
266
core/data/migrator.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// RunMigrations will start the migration process from the config file.
|
||||
func RunMigrations() {
|
||||
if !utils.DoesFileExists(config.BackupDirectory) {
|
||||
if err := os.Mkdir(config.BackupDirectory, 0700); err != nil {
|
||||
log.Errorln("Unable to create backup directory", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
migrateConfigFile()
|
||||
migrateStatsFile()
|
||||
migrateYPKey()
|
||||
}
|
||||
|
||||
func migrateStatsFile() {
|
||||
oldStats := models.Stats{
|
||||
Clients: make(map[string]models.Client),
|
||||
}
|
||||
|
||||
if !utils.DoesFileExists(config.StatsFile) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Infoln("Migrating", config.StatsFile, "to new datastore")
|
||||
|
||||
jsonFile, err := ioutil.ReadFile(config.StatsFile)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(jsonFile, &oldStats); err != nil {
|
||||
log.Errorln(err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = SetPeakSessionViewerCount(oldStats.SessionMaxViewerCount)
|
||||
_ = SetPeakOverallViewerCount(oldStats.OverallMaxViewerCount)
|
||||
|
||||
if err := utils.Move(config.StatsFile, "backup/stats.old"); err != nil {
|
||||
log.Warnln(err)
|
||||
}
|
||||
}
|
||||
|
||||
func migrateYPKey() {
|
||||
filePath := ".yp.key"
|
||||
|
||||
if !utils.DoesFileExists(filePath) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Infoln("Migrating", filePath, "to new datastore")
|
||||
|
||||
keyFile, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
log.Errorln("Unable to migrate", keyFile, "there may be issues registering with the directory")
|
||||
}
|
||||
|
||||
if err := SetDirectoryRegistrationKey(string(keyFile)); err != nil {
|
||||
log.Errorln("Unable to migrate", keyFile, "there may be issues registering with the directory")
|
||||
return
|
||||
}
|
||||
|
||||
if err := utils.Move(filePath, "backup/yp.key.old"); err != nil {
|
||||
log.Warnln(err)
|
||||
}
|
||||
}
|
||||
|
||||
func migrateConfigFile() {
|
||||
filePath := config.ConfigFilePath
|
||||
|
||||
if !utils.DoesFileExists(filePath) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Infoln("Migrating", filePath, "to new datastore")
|
||||
|
||||
var oldConfig configFile
|
||||
|
||||
yamlFile, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
log.Errorln("config file err", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(yamlFile, &oldConfig); err != nil {
|
||||
log.Errorln("Error reading the config file.", err)
|
||||
return
|
||||
}
|
||||
|
||||
_ = SetServerName(oldConfig.InstanceDetails.Name)
|
||||
_ = SetServerSummary(oldConfig.InstanceDetails.Summary)
|
||||
_ = SetServerMetadataTags(oldConfig.InstanceDetails.Tags)
|
||||
_ = SetStreamKey(oldConfig.VideoSettings.StreamingKey)
|
||||
_ = SetNSFW(oldConfig.InstanceDetails.NSFW)
|
||||
_ = SetServerURL(oldConfig.YP.InstanceURL)
|
||||
_ = SetDirectoryEnabled(oldConfig.YP.Enabled)
|
||||
_ = SetSocialHandles(oldConfig.InstanceDetails.SocialHandles)
|
||||
_ = SetFfmpegPath(oldConfig.FFMpegPath)
|
||||
_ = SetHTTPPortNumber(float64(oldConfig.WebServerPort))
|
||||
_ = SetRTMPPortNumber(float64(oldConfig.RTMPServerPort))
|
||||
|
||||
// Migrate logo
|
||||
if logo := oldConfig.InstanceDetails.Logo; logo != "" {
|
||||
filename := filepath.Base(logo)
|
||||
newPath := filepath.Join("data", filename)
|
||||
err := utils.Copy(filepath.Join("webroot", logo), newPath)
|
||||
log.Traceln("Copying logo from", logo, "to", newPath)
|
||||
if err != nil {
|
||||
log.Errorln("Error moving logo", logo, err)
|
||||
} else {
|
||||
_ = SetLogoPath(filename)
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate video variants
|
||||
variants := []models.StreamOutputVariant{}
|
||||
for _, variant := range oldConfig.VideoSettings.StreamQualities {
|
||||
migratedVariant := models.StreamOutputVariant{}
|
||||
migratedVariant.IsAudioPassthrough = true
|
||||
migratedVariant.IsVideoPassthrough = variant.IsVideoPassthrough
|
||||
migratedVariant.Framerate = variant.Framerate
|
||||
migratedVariant.VideoBitrate = variant.VideoBitrate
|
||||
migratedVariant.ScaledHeight = variant.ScaledHeight
|
||||
migratedVariant.ScaledWidth = variant.ScaledWidth
|
||||
|
||||
presetMapping := map[string]int{
|
||||
"ultrafast": 1,
|
||||
"superfast": 2,
|
||||
"veryfast": 3,
|
||||
"faster": 4,
|
||||
"fast": 5,
|
||||
}
|
||||
migratedVariant.CPUUsageLevel = presetMapping[variant.EncoderPreset]
|
||||
variants = append(variants, migratedVariant)
|
||||
}
|
||||
_ = SetStreamOutputVariants(variants)
|
||||
|
||||
// Migrate latency level
|
||||
level := 4
|
||||
oldSegmentLength := oldConfig.VideoSettings.ChunkLengthInSeconds
|
||||
oldNumberOfSegments := oldConfig.Files.MaxNumberInPlaylist
|
||||
latencyLevels := models.GetLatencyConfigs()
|
||||
|
||||
if oldSegmentLength == latencyLevels[1].SecondsPerSegment && oldNumberOfSegments == latencyLevels[1].SegmentCount {
|
||||
level = 1
|
||||
} else if oldSegmentLength == latencyLevels[2].SecondsPerSegment && oldNumberOfSegments == latencyLevels[2].SegmentCount {
|
||||
level = 2
|
||||
} else if oldSegmentLength == latencyLevels[3].SecondsPerSegment && oldNumberOfSegments == latencyLevels[3].SegmentCount {
|
||||
level = 3
|
||||
} else if oldSegmentLength == latencyLevels[5].SecondsPerSegment && oldNumberOfSegments == latencyLevels[5].SegmentCount {
|
||||
level = 5
|
||||
} else if oldSegmentLength >= latencyLevels[6].SecondsPerSegment && oldNumberOfSegments >= latencyLevels[6].SegmentCount {
|
||||
level = 6
|
||||
}
|
||||
|
||||
_ = SetStreamLatencyLevel(float64(level))
|
||||
|
||||
// Migrate storage config
|
||||
_ = SetS3Config(models.S3(oldConfig.Storage))
|
||||
|
||||
// Migrate the old content.md file
|
||||
content, err := ioutil.ReadFile(config.ExtraInfoFile)
|
||||
if err == nil && len(content) > 0 {
|
||||
_ = SetExtraPageBodyContent(string(content))
|
||||
}
|
||||
|
||||
if err := utils.Move(filePath, "backup/config.old"); err != nil {
|
||||
log.Warnln(err)
|
||||
}
|
||||
|
||||
log.Infoln("Your old config file can be found in the backup directory for reference. For all future configuration use the web admin.")
|
||||
}
|
||||
|
||||
type configFile struct {
|
||||
DatabaseFilePath string `yaml:"databaseFile"`
|
||||
EnableDebugFeatures bool `yaml:"-"`
|
||||
FFMpegPath string
|
||||
Files files `yaml:"files"`
|
||||
InstanceDetails instanceDetails `yaml:"instanceDetails"`
|
||||
VersionInfo string `yaml:"-"` // For storing the version/build number
|
||||
VersionNumber string `yaml:"-"`
|
||||
VideoSettings videoSettings `yaml:"videoSettings"`
|
||||
WebServerPort int
|
||||
RTMPServerPort int
|
||||
YP yp `yaml:"yp"`
|
||||
Storage s3 `yaml:"s3"`
|
||||
}
|
||||
|
||||
// instanceDetails defines the user-visible information about this particular instance.
|
||||
type instanceDetails struct {
|
||||
Name string `yaml:"name"`
|
||||
Title string `yaml:"title"`
|
||||
Summary string `yaml:"summary"`
|
||||
Logo string `yaml:"logo"`
|
||||
Tags []string `yaml:"tags"`
|
||||
Version string `yaml:"version"`
|
||||
NSFW bool `yaml:"nsfw"`
|
||||
ExtraPageContent string `yaml:"extraPageContent"`
|
||||
StreamTitle string `yaml:"streamTitle"`
|
||||
SocialHandles []models.SocialHandle `yaml:"socialHandles"`
|
||||
}
|
||||
|
||||
type videoSettings struct {
|
||||
ChunkLengthInSeconds int `yaml:"chunkLengthInSeconds"`
|
||||
StreamingKey string `yaml:"streamingKey"`
|
||||
StreamQualities []streamQuality `yaml:"streamQualities"`
|
||||
HighestQualityStreamIndex int `yaml:"-"`
|
||||
}
|
||||
|
||||
// yp allows registration to the central Owncast yp (Yellow pages) service operating as a directory.
|
||||
type yp struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
InstanceURL string `yaml:"instanceUrl"` // The public URL the directory should link to
|
||||
}
|
||||
|
||||
// streamQuality defines the specifics of a single HLS stream variant.
|
||||
type streamQuality struct {
|
||||
// Enable passthrough to copy the video and/or audio directly from the
|
||||
// incoming stream and disable any transcoding. It will ignore any of
|
||||
// the below settings.
|
||||
IsVideoPassthrough bool `yaml:"videoPassthrough" json:"videoPassthrough"`
|
||||
IsAudioPassthrough bool `yaml:"audioPassthrough" json:"audioPassthrough"`
|
||||
|
||||
VideoBitrate int `yaml:"videoBitrate" json:"videoBitrate"`
|
||||
AudioBitrate int `yaml:"audioBitrate" json:"audioBitrate"`
|
||||
|
||||
// Set only one of these in order to keep your current aspect ratio.
|
||||
// Or set neither to not scale the video.
|
||||
ScaledWidth int `yaml:"scaledWidth" json:"scaledWidth,omitempty"`
|
||||
ScaledHeight int `yaml:"scaledHeight" json:"scaledHeight,omitempty"`
|
||||
|
||||
Framerate int `yaml:"framerate" json:"framerate"`
|
||||
EncoderPreset string `yaml:"encoderPreset" json:"encoderPreset"`
|
||||
}
|
||||
|
||||
type files struct {
|
||||
MaxNumberInPlaylist int `yaml:"maxNumberInPlaylist"`
|
||||
}
|
||||
|
||||
// s3 is for configuring the s3 integration.
|
||||
type s3 struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
Endpoint string `yaml:"endpoint" json:"endpoint,omitempty"`
|
||||
ServingEndpoint string `yaml:"servingEndpoint" json:"servingEndpoint,omitempty"`
|
||||
AccessKey string `yaml:"accessKey" json:"accessKey,omitempty"`
|
||||
Secret string `yaml:"secret" json:"secret,omitempty"`
|
||||
Bucket string `yaml:"bucket" json:"bucket,omitempty"`
|
||||
Region string `yaml:"region" json:"region,omitempty"`
|
||||
ACL string `yaml:"acl" json:"acl,omitempty"`
|
||||
}
|
||||
152
core/data/persistence.go
Normal file
152
core/data/persistence.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/gob"
|
||||
|
||||
// sqlite requires a blank import.
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Datastore is the global key/value store for configuration values.
|
||||
type Datastore struct {
|
||||
db *sql.DB
|
||||
cache map[string][]byte
|
||||
}
|
||||
|
||||
func (ds *Datastore) warmCache() {
|
||||
log.Traceln("Warming config value cache")
|
||||
|
||||
res, err := ds.db.Query("SELECT key, value FROM datastore")
|
||||
if err != nil || res.Err() != nil {
|
||||
log.Errorln("error warming config cache", err, res.Err())
|
||||
}
|
||||
defer res.Close()
|
||||
|
||||
for res.Next() {
|
||||
var rowKey string
|
||||
var rowValue []byte
|
||||
if err := res.Scan(&rowKey, &rowValue); err != nil {
|
||||
log.Errorln("error pre-caching config row", err)
|
||||
}
|
||||
ds.cache[rowKey] = rowValue
|
||||
}
|
||||
}
|
||||
|
||||
// Get will query the database for the key and return the entry.
|
||||
func (ds *Datastore) Get(key string) (ConfigEntry, error) {
|
||||
cachedValue, err := ds.GetCachedValue(key)
|
||||
if err == nil {
|
||||
return ConfigEntry{
|
||||
Key: key,
|
||||
Value: cachedValue,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var resultKey string
|
||||
var resultValue []byte
|
||||
|
||||
row := ds.db.QueryRow("SELECT key, value FROM datastore WHERE key = ? LIMIT 1", key)
|
||||
if err := row.Scan(&resultKey, &resultValue); err != nil {
|
||||
return ConfigEntry{}, err
|
||||
}
|
||||
|
||||
result := ConfigEntry{
|
||||
Key: resultKey,
|
||||
Value: resultValue,
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Save will save the ConfigEntry to the database.
|
||||
func (ds *Datastore) Save(e ConfigEntry) error {
|
||||
var dataGob bytes.Buffer
|
||||
enc := gob.NewEncoder(&dataGob)
|
||||
if err := enc.Encode(e.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := ds.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var stmt *sql.Stmt
|
||||
var count int
|
||||
row := ds.db.QueryRow("SELECT COUNT(*) FROM datastore WHERE key = ? LIMIT 1", e.Key)
|
||||
if err := row.Scan(&count); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
stmt, err = tx.Prepare("INSERT INTO datastore(key, value) values(?, ?)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = stmt.Exec(e.Key, dataGob.Bytes())
|
||||
} else {
|
||||
stmt, err = tx.Prepare("UPDATE datastore SET value=? WHERE key=?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = stmt.Exec(dataGob.Bytes(), e.Key)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
ds.SetCachedValue(e.Key, dataGob.Bytes())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Setup will create the datastore table and perform initial initialization.
|
||||
func (ds *Datastore) Setup() {
|
||||
ds.cache = make(map[string][]byte)
|
||||
ds.db = GetDatabase()
|
||||
|
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS datastore (
|
||||
"key" string NOT NULL PRIMARY KEY,
|
||||
"value" BLOB,
|
||||
"timestamp" DATE DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);`
|
||||
|
||||
stmt, err := ds.db.Prepare(createTableSQL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
_, err = stmt.Exec()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
if !HasPopulatedDefaults() {
|
||||
PopulateDefaults()
|
||||
}
|
||||
}
|
||||
|
||||
// Reset will delete all config entries in the datastore and start over.
|
||||
func (ds *Datastore) Reset() {
|
||||
sql := "DELETE FROM datastore"
|
||||
stmt, err := ds.db.Prepare(sql)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
|
||||
if _, err = stmt.Exec(); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
PopulateDefaults()
|
||||
}
|
||||
46
core/data/types.go
Normal file
46
core/data/types.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package data
|
||||
|
||||
// GetString will return the string value for a key.
|
||||
func (ds *Datastore) GetString(key string) (string, error) {
|
||||
configEntry, err := ds.Get(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return configEntry.getString()
|
||||
}
|
||||
|
||||
// SetString will set the string value for a key.
|
||||
func (ds *Datastore) SetString(key string, value string) error {
|
||||
configEntry := ConfigEntry{key, value}
|
||||
return ds.Save(configEntry)
|
||||
}
|
||||
|
||||
// GetNumber will return the numeric value for a key.
|
||||
func (ds *Datastore) GetNumber(key string) (float64, error) {
|
||||
configEntry, err := ds.Get(key)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return configEntry.getNumber()
|
||||
}
|
||||
|
||||
// SetNumber will set the numeric value for a key.
|
||||
func (ds *Datastore) SetNumber(key string, value float64) error {
|
||||
configEntry := ConfigEntry{key, value}
|
||||
return ds.Save(configEntry)
|
||||
}
|
||||
|
||||
// GetBool will return the boolean value for a key.
|
||||
func (ds *Datastore) GetBool(key string) (bool, error) {
|
||||
configEntry, err := ds.Get(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return configEntry.getBool()
|
||||
}
|
||||
|
||||
// SetBool will set the boolean value for a key.
|
||||
func (ds *Datastore) SetBool(key string, value bool) error {
|
||||
configEntry := ConfigEntry{key, value}
|
||||
return ds.Save(configEntry)
|
||||
}
|
||||
220
core/data/webhooks.go
Normal file
220
core/data/webhooks.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/models"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func createWebhooksTable() {
|
||||
log.Traceln("Creating webhooks table...")
|
||||
|
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS webhooks (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"url" string NOT NULL,
|
||||
"events" TEXT NOT NULL,
|
||||
"timestamp" DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
"last_used" DATETIME
|
||||
);`
|
||||
|
||||
stmt, err := _db.Prepare(createTableSQL)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.Exec()
|
||||
if err != nil {
|
||||
log.Warnln(err)
|
||||
}
|
||||
}
|
||||
|
||||
// InsertWebhook will add a new webhook to the database.
|
||||
func InsertWebhook(url string, events []models.EventType) (int, error) {
|
||||
log.Println("Adding new webhook:", url)
|
||||
|
||||
eventsString := strings.Join(events, ",")
|
||||
|
||||
tx, err := _db.Begin()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
stmt, err := tx.Prepare("INSERT INTO webhooks(url, events) values(?, ?)")
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
insertResult, err := stmt.Exec(url, eventsString)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
newID, err := insertResult.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return int(newID), err
|
||||
}
|
||||
|
||||
// DeleteWebhook will delete a webhook from the database.
|
||||
func DeleteWebhook(id int) error {
|
||||
log.Println("Deleting webhook:", id)
|
||||
|
||||
tx, err := _db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt, err := tx.Prepare("DELETE FROM webhooks WHERE id = ?")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
result, err := stmt.Exec(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsDeleted, _ := result.RowsAffected(); rowsDeleted == 0 {
|
||||
tx.Rollback() //nolint
|
||||
return errors.New(fmt.Sprint(id) + " not found")
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetWebhooksForEvent will return all of the webhooks that want to be notified about an event type.
|
||||
func GetWebhooksForEvent(event models.EventType) []models.Webhook {
|
||||
webhooks := make([]models.Webhook, 0)
|
||||
|
||||
var query = `SELECT * FROM (
|
||||
WITH RECURSIVE split(url, event, rest) AS (
|
||||
SELECT url, '', events || ',' FROM webhooks
|
||||
UNION ALL
|
||||
SELECT url,
|
||||
substr(rest, 0, instr(rest, ',')),
|
||||
substr(rest, instr(rest, ',')+1)
|
||||
FROM split
|
||||
WHERE rest <> '')
|
||||
SELECT url, event
|
||||
FROM split
|
||||
WHERE event <> ''
|
||||
) AS webhook WHERE event IS "` + event + `"`
|
||||
|
||||
rows, err := _db.Query(query)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var url string
|
||||
|
||||
err = rows.Scan(&url, &event)
|
||||
if err != nil {
|
||||
log.Debugln(err)
|
||||
log.Error("There is a problem with the database.")
|
||||
break
|
||||
}
|
||||
|
||||
singleWebhook := models.Webhook{
|
||||
URL: url,
|
||||
}
|
||||
|
||||
webhooks = append(webhooks, singleWebhook)
|
||||
}
|
||||
return webhooks
|
||||
}
|
||||
|
||||
// GetWebhooks will return all the webhooks.
|
||||
func GetWebhooks() ([]models.Webhook, error) { //nolint
|
||||
webhooks := make([]models.Webhook, 0)
|
||||
|
||||
var query = "SELECT * FROM webhooks"
|
||||
|
||||
rows, err := _db.Query(query)
|
||||
if err != nil {
|
||||
return webhooks, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var id int
|
||||
var url string
|
||||
var events string
|
||||
var timestampString string
|
||||
var lastUsedString *string
|
||||
|
||||
if err := rows.Scan(&id, &url, &events, ×tampString, &lastUsedString); err != nil {
|
||||
log.Error("There is a problem reading the database.", err)
|
||||
return webhooks, err
|
||||
}
|
||||
|
||||
timestamp, err := time.Parse(time.RFC3339, timestampString)
|
||||
if err != nil {
|
||||
return webhooks, err
|
||||
}
|
||||
|
||||
var lastUsed *time.Time = nil
|
||||
if lastUsedString != nil {
|
||||
lastUsedTime, _ := time.Parse(time.RFC3339, *lastUsedString)
|
||||
lastUsed = &lastUsedTime
|
||||
}
|
||||
|
||||
singleWebhook := models.Webhook{
|
||||
ID: id,
|
||||
URL: url,
|
||||
Events: strings.Split(events, ","),
|
||||
Timestamp: timestamp,
|
||||
LastUsed: lastUsed,
|
||||
}
|
||||
|
||||
webhooks = append(webhooks, singleWebhook)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return webhooks, err
|
||||
}
|
||||
|
||||
return webhooks, nil
|
||||
}
|
||||
|
||||
// SetWebhookAsUsed will update the last used time for a webhook.
|
||||
func SetWebhookAsUsed(id string) error {
|
||||
tx, err := _db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt, err := tx.Prepare("UPDATE webhooks SET last_used = CURRENT_TIMESTAMP WHERE id = ?")
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if _, err := stmt.Exec(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user