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:
Gabe Kangas
2021-02-18 23:05:52 -08:00
committed by GitHub
parent 05ec74a1e3
commit bc2caadb74
125 changed files with 5544 additions and 1510 deletions

199
core/data/accessTokens.go Normal file
View 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, &timestampString, &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
View 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
View 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
View 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
}

View File

@@ -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
View 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
View 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
View 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
View 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
View 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
View 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, &timestampString, &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
}