mirror of
https://github.com/owncast/owncast.git
synced 2024-11-21 20:28:15 +03:00
WIP refactored all storage into repos. Tests pass.
This commit is contained in:
parent
dcc09edba1
commit
32b1dbeaf3
126 changed files with 1913 additions and 1607 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -46,3 +46,5 @@ web/style-definitions/build/
|
||||||
|
|
||||||
web/public/sw.js
|
web/public/sw.js
|
||||||
web/public/workbox-*.js
|
web/public/workbox-*.js
|
||||||
|
|
||||||
|
!storage/data
|
||||||
|
|
|
@ -8,23 +8,27 @@ import (
|
||||||
"github.com/owncast/owncast/activitypub/outbox"
|
"github.com/owncast/owncast/activitypub/outbox"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
"github.com/owncast/owncast/activitypub/workerpool"
|
"github.com/owncast/owncast/activitypub/workerpool"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
|
"github.com/owncast/owncast/storage/data"
|
||||||
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var configRepository = configrepository.Get()
|
||||||
|
|
||||||
// Start will initialize and start the federation support.
|
// Start will initialize and start the federation support.
|
||||||
func Start(datastore *data.Datastore, router *http.ServeMux) {
|
func Start(datastore *data.Store, router *http.ServeMux) {
|
||||||
persistence.Setup(datastore)
|
persistence.Setup(datastore)
|
||||||
workerpool.InitOutboundWorkerPool()
|
workerpool.InitOutboundWorkerPool()
|
||||||
inbox.InitInboxWorkerPool()
|
inbox.InitInboxWorkerPool()
|
||||||
StartRouter(router)
|
StartRouter(router)
|
||||||
|
|
||||||
// Generate the keys for signing federated activity if needed.
|
// Generate the keys for signing federated activity if needed.
|
||||||
if data.GetPrivateKey() == "" {
|
if configRepository.GetPrivateKey() == "" {
|
||||||
privateKey, publicKey, err := crypto.GenerateKeys()
|
privateKey, publicKey, err := crypto.GenerateKeys()
|
||||||
_ = data.SetPrivateKey(string(privateKey))
|
_ = configRepository.SetPrivateKey(string(privateKey))
|
||||||
_ = data.SetPublicKey(string(publicKey))
|
_ = configRepository.SetPublicKey(string(publicKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("Unable to get private key", err)
|
log.Errorln("Unable to get private key", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PrivacyAudience represents the audience for an activity.
|
// PrivacyAudience represents the audience for an activity.
|
||||||
|
@ -87,8 +87,10 @@ func MakeActivityDirect(activity vocab.ActivityStreamsCreate, toIRI *url.URL) vo
|
||||||
// MakeActivityPublic sets the required properties to make this activity
|
// MakeActivityPublic sets the required properties to make this activity
|
||||||
// seen as public.
|
// seen as public.
|
||||||
func MakeActivityPublic(activity vocab.ActivityStreamsCreate) vocab.ActivityStreamsCreate {
|
func MakeActivityPublic(activity vocab.ActivityStreamsCreate) vocab.ActivityStreamsCreate {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
// TO the public if we're not treating ActivityPub as "private".
|
// TO the public if we're not treating ActivityPub as "private".
|
||||||
if !data.GetFederationIsPrivate() {
|
if !configRepository.GetFederationIsPrivate() {
|
||||||
public, _ := url.Parse(PUBLIC)
|
public, _ := url.Parse(PUBLIC)
|
||||||
|
|
||||||
to := streams.NewActivityStreamsToProperty()
|
to := streams.NewActivityStreamsToProperty()
|
||||||
|
@ -115,13 +117,15 @@ func MakeCreateActivity(activityID *url.URL) vocab.ActivityStreamsCreate {
|
||||||
|
|
||||||
// MakeUpdateActivity will return a new Update activity with the provided aID.
|
// MakeUpdateActivity will return a new Update activity with the provided aID.
|
||||||
func MakeUpdateActivity(activityID *url.URL) vocab.ActivityStreamsUpdate {
|
func MakeUpdateActivity(activityID *url.URL) vocab.ActivityStreamsUpdate {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
activity := streams.NewActivityStreamsUpdate()
|
activity := streams.NewActivityStreamsUpdate()
|
||||||
id := streams.NewJSONLDIdProperty()
|
id := streams.NewJSONLDIdProperty()
|
||||||
id.Set(activityID)
|
id.Set(activityID)
|
||||||
activity.SetJSONLDId(id)
|
activity.SetJSONLDId(id)
|
||||||
|
|
||||||
// CC the public if we're not treating ActivityPub as "private".
|
// CC the public if we're not treating ActivityPub as "private".
|
||||||
if !data.GetFederationIsPrivate() {
|
if !configRepository.GetFederationIsPrivate() {
|
||||||
public, _ := url.Parse(PUBLIC)
|
public, _ := url.Parse(PUBLIC)
|
||||||
cc := streams.NewActivityStreamsCcProperty()
|
cc := streams.NewActivityStreamsCcProperty()
|
||||||
cc.AppendIRI(public)
|
cc.AppendIRI(public)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/owncast/owncast/activitypub/crypto"
|
"github.com/owncast/owncast/activitypub/crypto"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -100,11 +101,13 @@ func MakeActorPropertyWithID(idIRI *url.URL) vocab.ActivityStreamsActorProperty
|
||||||
|
|
||||||
// MakeServiceForAccount will create a new local actor service with the the provided username.
|
// MakeServiceForAccount will create a new local actor service with the the provided username.
|
||||||
func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
actorIRI := MakeLocalIRIForAccount(accountName)
|
actorIRI := MakeLocalIRIForAccount(accountName)
|
||||||
|
|
||||||
person := streams.NewActivityStreamsService()
|
person := streams.NewActivityStreamsService()
|
||||||
nameProperty := streams.NewActivityStreamsNameProperty()
|
nameProperty := streams.NewActivityStreamsNameProperty()
|
||||||
nameProperty.AppendXMLSchemaString(data.GetServerName())
|
nameProperty.AppendXMLSchemaString(configRepository.GetServerName())
|
||||||
person.SetActivityStreamsName(nameProperty)
|
person.SetActivityStreamsName(nameProperty)
|
||||||
|
|
||||||
preferredUsernameProperty := streams.NewActivityStreamsPreferredUsernameProperty()
|
preferredUsernameProperty := streams.NewActivityStreamsPreferredUsernameProperty()
|
||||||
|
@ -118,7 +121,7 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
||||||
person.SetActivityStreamsInbox(inboxProp)
|
person.SetActivityStreamsInbox(inboxProp)
|
||||||
|
|
||||||
needsFollowApprovalProperty := streams.NewActivityStreamsManuallyApprovesFollowersProperty()
|
needsFollowApprovalProperty := streams.NewActivityStreamsManuallyApprovesFollowersProperty()
|
||||||
needsFollowApprovalProperty.Set(data.GetFederationIsPrivate())
|
needsFollowApprovalProperty.Set(configRepository.GetFederationIsPrivate())
|
||||||
person.SetActivityStreamsManuallyApprovesFollowers(needsFollowApprovalProperty)
|
person.SetActivityStreamsManuallyApprovesFollowers(needsFollowApprovalProperty)
|
||||||
|
|
||||||
outboxIRI := MakeLocalIRIForResource("/user/" + accountName + "/outbox")
|
outboxIRI := MakeLocalIRIForResource("/user/" + accountName + "/outbox")
|
||||||
|
@ -151,7 +154,7 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
||||||
publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKeyType)
|
publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKeyType)
|
||||||
person.SetW3IDSecurityV1PublicKey(publicKeyProp)
|
person.SetW3IDSecurityV1PublicKey(publicKeyProp)
|
||||||
|
|
||||||
if t, err := data.GetServerInitTime(); t != nil {
|
if t, err := configRepository.GetServerInitTime(); t != nil {
|
||||||
publishedDateProp := streams.NewActivityStreamsPublishedProperty()
|
publishedDateProp := streams.NewActivityStreamsPublishedProperty()
|
||||||
publishedDateProp.Set(t.Time)
|
publishedDateProp.Set(t.Time)
|
||||||
person.SetActivityStreamsPublished(publishedDateProp)
|
person.SetActivityStreamsPublished(publishedDateProp)
|
||||||
|
@ -162,8 +165,8 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
||||||
// Profile properties
|
// Profile properties
|
||||||
|
|
||||||
// Avatar
|
// Avatar
|
||||||
uniquenessString := data.GetLogoUniquenessString()
|
uniquenessString := configRepository.GetLogoUniquenessString()
|
||||||
userAvatarURLString := data.GetServerURL() + "/logo/external"
|
userAvatarURLString := configRepository.GetServerURL() + "/logo/external"
|
||||||
userAvatarURL, err := url.Parse(userAvatarURLString)
|
userAvatarURL, err := url.Parse(userAvatarURLString)
|
||||||
userAvatarURL.RawQuery = "uc=" + uniquenessString
|
userAvatarURL.RawQuery = "uc=" + uniquenessString
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -194,14 +197,14 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
||||||
|
|
||||||
// Profile bio
|
// Profile bio
|
||||||
summaryProperty := streams.NewActivityStreamsSummaryProperty()
|
summaryProperty := streams.NewActivityStreamsSummaryProperty()
|
||||||
summaryProperty.AppendXMLSchemaString(data.GetServerSummary())
|
summaryProperty.AppendXMLSchemaString(configRepository.GetServerSummary())
|
||||||
person.SetActivityStreamsSummary(summaryProperty)
|
person.SetActivityStreamsSummary(summaryProperty)
|
||||||
|
|
||||||
// Links
|
// Links
|
||||||
if serverURL := data.GetServerURL(); serverURL != "" {
|
if serverURL := configRepository.GetServerURL(); serverURL != "" {
|
||||||
addMetadataLinkToProfile(person, "Stream", serverURL)
|
addMetadataLinkToProfile(person, "Stream", serverURL)
|
||||||
}
|
}
|
||||||
for _, link := range data.GetSocialHandles() {
|
for _, link := range configRepository.GetSocialHandles() {
|
||||||
addMetadataLinkToProfile(person, link.Platform, link.URL)
|
addMetadataLinkToProfile(person, link.Platform, link.URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +222,7 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
||||||
|
|
||||||
// Tags
|
// Tags
|
||||||
tagProp := streams.NewActivityStreamsTagProperty()
|
tagProp := streams.NewActivityStreamsTagProperty()
|
||||||
for _, tagString := range data.GetServerMetadataTags() {
|
for _, tagString := range configRepository.GetServerMetadataTags() {
|
||||||
hashtag := MakeHashtag(tagString)
|
hashtag := MakeHashtag(tagString)
|
||||||
tagProp.AppendTootHashtag(hashtag)
|
tagProp.AppendTootHashtag(hashtag)
|
||||||
}
|
}
|
||||||
|
@ -228,7 +231,7 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
|
||||||
|
|
||||||
// Work around an issue where a single attachment will not serialize
|
// Work around an issue where a single attachment will not serialize
|
||||||
// as an array, so add another item to the mix.
|
// as an array, so add another item to the mix.
|
||||||
if len(data.GetSocialHandles()) == 1 {
|
if len(configRepository.GetSocialHandles()) == 1 {
|
||||||
addMetadataLinkToProfile(person, "Owncast", "https://owncast.online")
|
addMetadataLinkToProfile(person, "Owncast", "https://owncast.online")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package apmodels
|
package apmodels
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
|
"github.com/owncast/owncast/storage/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
func makeFakeService() vocab.ActivityStreamsService {
|
func makeFakeService() vocab.ActivityStreamsService {
|
||||||
|
@ -50,13 +50,14 @@ func makeFakeService() vocab.ActivityStreamsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
dbFile, err := ioutil.TempFile(os.TempDir(), "owncast-test-db.db")
|
ds, err := data.NewStore(":memory:")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
data.SetupPersistence(dbFile.Name())
|
configRepository := configrepository.New(ds)
|
||||||
data.SetServerURL("https://my.cool.site.biz")
|
configRepository.PopulateDefaults()
|
||||||
|
configRepository.SetServerURL("https://my.cool.site.biz")
|
||||||
|
|
||||||
m.Run()
|
m.Run()
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/go-fed/activity/streams"
|
"github.com/go-fed/activity/streams"
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -26,7 +27,9 @@ func MakeRemoteIRIForResource(resourcePath string, host string) (*url.URL, error
|
||||||
|
|
||||||
// MakeLocalIRIForResource will create an IRI for the local server.
|
// MakeLocalIRIForResource will create an IRI for the local server.
|
||||||
func MakeLocalIRIForResource(resourcePath string) *url.URL {
|
func MakeLocalIRIForResource(resourcePath string) *url.URL {
|
||||||
host := data.GetServerURL()
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
host := configRepository.GetServerURL()
|
||||||
u, err := url.Parse(host)
|
u, err := url.Parse(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("unable to parse local IRI url", host, err)
|
log.Errorln("unable to parse local IRI url", host, err)
|
||||||
|
@ -40,7 +43,8 @@ func MakeLocalIRIForResource(resourcePath string) *url.URL {
|
||||||
|
|
||||||
// MakeLocalIRIForAccount will return a full IRI for the local server account username.
|
// MakeLocalIRIForAccount will return a full IRI for the local server account username.
|
||||||
func MakeLocalIRIForAccount(account string) *url.URL {
|
func MakeLocalIRIForAccount(account string) *url.URL {
|
||||||
host := data.GetServerURL()
|
configRepository := configrepository.Get()
|
||||||
|
host := configRepository.GetServerURL()
|
||||||
u, err := url.Parse(host)
|
u, err := url.Parse(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("unable to parse local IRI account server url", err)
|
log.Errorln("unable to parse local IRI account server url", err)
|
||||||
|
@ -63,7 +67,8 @@ func Serialize(obj vocab.Type) ([]byte, error) {
|
||||||
|
|
||||||
// MakeLocalIRIForStreamURL will return a full IRI for the local server stream url.
|
// MakeLocalIRIForStreamURL will return a full IRI for the local server stream url.
|
||||||
func MakeLocalIRIForStreamURL() *url.URL {
|
func MakeLocalIRIForStreamURL() *url.URL {
|
||||||
host := data.GetServerURL()
|
configRepository := configrepository.Get()
|
||||||
|
host := configRepository.GetServerURL()
|
||||||
u, err := url.Parse(host)
|
u, err := url.Parse(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("unable to parse local IRI stream url", err)
|
log.Errorln("unable to parse local IRI stream url", err)
|
||||||
|
@ -77,7 +82,8 @@ func MakeLocalIRIForStreamURL() *url.URL {
|
||||||
|
|
||||||
// MakeLocalIRIforLogo will return a full IRI for the local server logo.
|
// MakeLocalIRIforLogo will return a full IRI for the local server logo.
|
||||||
func MakeLocalIRIforLogo() *url.URL {
|
func MakeLocalIRIforLogo() *url.URL {
|
||||||
host := data.GetServerURL()
|
configRepository := configrepository.Get()
|
||||||
|
host := configRepository.GetServerURL()
|
||||||
u, err := url.Parse(host)
|
u, err := url.Parse(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("unable to parse local IRI stream url", err)
|
log.Errorln("unable to parse local IRI stream url", err)
|
||||||
|
@ -92,7 +98,8 @@ func MakeLocalIRIforLogo() *url.URL {
|
||||||
// GetLogoType will return the rel value for the webfinger response and
|
// GetLogoType will return the rel value for the webfinger response and
|
||||||
// the default static image is of type png.
|
// the default static image is of type png.
|
||||||
func GetLogoType() string {
|
func GetLogoType() string {
|
||||||
imageFilename := data.GetLogoPath()
|
configRepository := configrepository.Get()
|
||||||
|
imageFilename := configRepository.GetLogoPath()
|
||||||
if imageFilename == "" {
|
if imageFilename == "" {
|
||||||
return "image/png"
|
return "image/png"
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,12 +9,14 @@ import (
|
||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/activitypub/crypto"
|
"github.com/owncast/owncast/activitypub/crypto"
|
||||||
"github.com/owncast/owncast/activitypub/requests"
|
"github.com/owncast/owncast/activitypub/requests"
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var configRepository = configrepository.Get()
|
||||||
|
|
||||||
// ActorHandler handles requests for a single actor.
|
// ActorHandler handles requests for a single actor.
|
||||||
func ActorHandler(w http.ResponseWriter, r *http.Request) {
|
func ActorHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if !data.GetFederationEnabled() {
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -22,7 +24,7 @@ func ActorHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
pathComponents := strings.Split(r.URL.Path, "/")
|
pathComponents := strings.Split(r.URL.Path, "/")
|
||||||
accountName := pathComponents[3]
|
accountName := pathComponents[3]
|
||||||
|
|
||||||
if _, valid := data.GetFederatedInboxMap()[accountName]; !valid {
|
if _, valid := configRepository.GetFederatedInboxMap()[accountName]; !valid {
|
||||||
// User is not valid
|
// User is not valid
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
|
|
@ -16,7 +16,6 @@ import (
|
||||||
"github.com/owncast/owncast/activitypub/crypto"
|
"github.com/owncast/owncast/activitypub/crypto"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
"github.com/owncast/owncast/activitypub/requests"
|
"github.com/owncast/owncast/activitypub/requests"
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -145,7 +144,7 @@ func getFollowersPage(page string, r *http.Request) (vocab.ActivityStreamsOrdere
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPageURL(r *http.Request, page *string) (*url.URL, error) {
|
func createPageURL(r *http.Request, page *string) (*url.URL, error) {
|
||||||
domain := data.GetServerURL()
|
domain := configRepository.GetServerURL()
|
||||||
if domain == "" {
|
if domain == "" {
|
||||||
return nil, errors.New("unable to get server URL")
|
return nil, errors.New("unable to get server URL")
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ func InboxHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func acceptInboxRequest(w http.ResponseWriter, r *http.Request) {
|
func acceptInboxRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
if !data.GetFederationEnabled() {
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ func acceptInboxRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// The account this request is for must match the account name we have set
|
// The account this request is for must match the account name we have set
|
||||||
// for federation.
|
// for federation.
|
||||||
if forLocalAccount != data.GetFederationUsername() {
|
if forLocalAccount != configRepository.GetFederationUsername() {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,12 +24,12 @@ func NodeInfoController(w http.ResponseWriter, r *http.Request) {
|
||||||
Links []links `json:"links"`
|
Links []links `json:"links"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.GetFederationEnabled() {
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
serverURL := data.GetServerURL()
|
serverURL := configRepository.GetServerURL()
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
@ -88,7 +88,7 @@ func NodeInfoV2Controller(w http.ResponseWriter, r *http.Request) {
|
||||||
Metadata metadata `json:"metadata"`
|
Metadata metadata `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.GetFederationEnabled() {
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -117,7 +117,7 @@ func NodeInfoV2Controller(w http.ResponseWriter, r *http.Request) {
|
||||||
OpenRegistrations: false,
|
OpenRegistrations: false,
|
||||||
Protocols: []string{"activitypub"},
|
Protocols: []string{"activitypub"},
|
||||||
Metadata: metadata{
|
Metadata: metadata{
|
||||||
ChatEnabled: !data.GetChatDisabled(),
|
ChatEnabled: !configRepository.GetChatDisabled(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,12 +163,12 @@ func XNodeInfo2Controller(w http.ResponseWriter, r *http.Request) {
|
||||||
OpenRegistrations bool `json:"openRegistrations"`
|
OpenRegistrations bool `json:"openRegistrations"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.GetFederationEnabled() {
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
serverURL := data.GetServerURL()
|
serverURL := configRepository.GetServerURL()
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
@ -179,7 +179,7 @@ func XNodeInfo2Controller(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
res := &response{
|
res := &response{
|
||||||
Organization: Organization{
|
Organization: Organization{
|
||||||
Name: data.GetServerName(),
|
Name: configRepository.GetServerName(),
|
||||||
Contact: serverURL,
|
Contact: serverURL,
|
||||||
},
|
},
|
||||||
Server: Server{
|
Server: Server{
|
||||||
|
@ -233,12 +233,12 @@ func InstanceV1Controller(w http.ResponseWriter, r *http.Request) {
|
||||||
InvitesEnabled bool `json:"invites_enabled"`
|
InvitesEnabled bool `json:"invites_enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.GetFederationEnabled() {
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
serverURL := data.GetServerURL()
|
serverURL := configRepository.GetServerURL()
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
@ -256,9 +256,9 @@ func InstanceV1Controller(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
res := response{
|
res := response{
|
||||||
URI: serverURL,
|
URI: serverURL,
|
||||||
Title: data.GetServerName(),
|
Title: configRepository.GetServerName(),
|
||||||
ShortDescription: data.GetServerSummary(),
|
ShortDescription: configRepository.GetServerSummary(),
|
||||||
Description: data.GetServerSummary(),
|
Description: configRepository.GetServerSummary(),
|
||||||
Version: c.GetReleaseString(),
|
Version: c.GetReleaseString(),
|
||||||
Stats: Stats{
|
Stats: Stats{
|
||||||
UserCount: 1,
|
UserCount: 1,
|
||||||
|
@ -277,7 +277,7 @@ func InstanceV1Controller(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeResponse(payload interface{}, w http.ResponseWriter) error {
|
func writeResponse(payload interface{}, w http.ResponseWriter) error {
|
||||||
accountName := data.GetDefaultFederationUsername()
|
accountName := configRepository.GetDefaultFederationUsername()
|
||||||
actorIRI := apmodels.MakeLocalIRIForAccount(accountName)
|
actorIRI := apmodels.MakeLocalIRIForAccount(accountName)
|
||||||
publicKey := crypto.GetPublicKey(actorIRI)
|
publicKey := crypto.GetPublicKey(actorIRI)
|
||||||
|
|
||||||
|
@ -286,7 +286,7 @@ func writeResponse(payload interface{}, w http.ResponseWriter) error {
|
||||||
|
|
||||||
// HostMetaController points to webfinger.
|
// HostMetaController points to webfinger.
|
||||||
func HostMetaController(w http.ResponseWriter, r *http.Request) {
|
func HostMetaController(w http.ResponseWriter, r *http.Request) {
|
||||||
serverURL := data.GetServerURL()
|
serverURL := configRepository.GetServerURL()
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
|
|
@ -13,25 +13,25 @@ import (
|
||||||
|
|
||||||
// ObjectHandler handles requests for a single federated ActivityPub object.
|
// ObjectHandler handles requests for a single federated ActivityPub object.
|
||||||
func ObjectHandler(w http.ResponseWriter, r *http.Request) {
|
func ObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if !data.GetFederationEnabled() {
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If private federation mode is enabled do not allow access to objects.
|
// If private federation mode is enabled do not allow access to objects.
|
||||||
if data.GetFederationIsPrivate() {
|
if configRepository.GetFederationIsPrivate() {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
iri := strings.Join([]string{strings.TrimSuffix(data.GetServerURL(), "/"), r.URL.Path}, "")
|
iri := strings.Join([]string{strings.TrimSuffix(configRepository.GetServerURL(), "/"), r.URL.Path}, "")
|
||||||
object, _, _, err := persistence.GetObjectByIRI(iri)
|
object, _, _, err := persistence.GetObjectByIRI(iri)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accountName := data.GetDefaultFederationUsername()
|
accountName := configRepository.GetDefaultFederationUsername()
|
||||||
actorIRI := apmodels.MakeLocalIRIForAccount(accountName)
|
actorIRI := apmodels.MakeLocalIRIForAccount(accountName)
|
||||||
publicKey := crypto.GetPublicKey(actorIRI)
|
publicKey := crypto.GetPublicKey(actorIRI)
|
||||||
|
|
||||||
|
|
|
@ -6,20 +6,19 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebfingerHandler will handle webfinger lookup requests.
|
// WebfingerHandler will handle webfinger lookup requests.
|
||||||
func WebfingerHandler(w http.ResponseWriter, r *http.Request) {
|
func WebfingerHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if !data.GetFederationEnabled() {
|
if !configRepository.GetFederationEnabled() {
|
||||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
log.Debugln("webfinger request rejected! Federation is not enabled")
|
log.Debugln("webfinger request rejected! Federation is not enabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
instanceHostURL := data.GetServerURL()
|
instanceHostURL := configRepository.GetServerURL()
|
||||||
if instanceHostURL == "" {
|
if instanceHostURL == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
log.Warnln("webfinger request rejected! Federation is enabled but server URL is empty.")
|
log.Warnln("webfinger request rejected! Federation is enabled but server URL is empty.")
|
||||||
|
@ -29,7 +28,7 @@ func WebfingerHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
instanceHostString := utils.GetHostnameFromURLString(instanceHostURL)
|
instanceHostString := utils.GetHostnameFromURLString(instanceHostURL)
|
||||||
if instanceHostString == "" {
|
if instanceHostString == "" {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
log.Warnln("webfinger request rejected! Federation is enabled but server URL is not set properly. data.GetServerURL(): " + data.GetServerURL())
|
log.Warnln("webfinger request rejected! Federation is enabled but server URL is not set properly. configRepository.GetServerURL(): " + configRepository.GetServerURL())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +50,7 @@ func WebfingerHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
host := userComponents[1]
|
host := userComponents[1]
|
||||||
user := userComponents[0]
|
user := userComponents[0]
|
||||||
|
|
||||||
if _, valid := data.GetFederatedInboxMap()[user]; !valid {
|
if _, valid := configRepository.GetFederatedInboxMap()[user]; !valid {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
log.Debugln("webfinger request rejected! Invalid user: " + user)
|
log.Debugln("webfinger request rejected! Invalid user: " + user)
|
||||||
return
|
return
|
||||||
|
|
|
@ -8,12 +8,14 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetPublicKey will return the public key for the provided actor.
|
// GetPublicKey will return the public key for the provided actor.
|
||||||
func GetPublicKey(actorIRI *url.URL) PublicKey {
|
func GetPublicKey(actorIRI *url.URL) PublicKey {
|
||||||
key := data.GetPublicKey()
|
configRepository := configrepository.Get()
|
||||||
|
key := configRepository.GetPublicKey()
|
||||||
idURL, err := url.Parse(actorIRI.String() + "#main-key")
|
idURL, err := url.Parse(actorIRI.String() + "#main-key")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("unable to parse actor iri string", idURL, err)
|
log.Errorln("unable to parse actor iri string", idURL, err)
|
||||||
|
@ -28,7 +30,8 @@ func GetPublicKey(actorIRI *url.URL) PublicKey {
|
||||||
|
|
||||||
// GetPrivateKey will return the internal server private key.
|
// GetPrivateKey will return the internal server private key.
|
||||||
func GetPrivateKey() *rsa.PrivateKey {
|
func GetPrivateKey() *rsa.PrivateKey {
|
||||||
key := data.GetPrivateKey()
|
configRepository := configrepository.Get()
|
||||||
|
key := configRepository.GetPrivateKey()
|
||||||
|
|
||||||
block, _ := pem.Decode([]byte(key))
|
block, _ := pem.Decode([]byte(key))
|
||||||
if block == nil {
|
if block == nil {
|
||||||
|
|
|
@ -7,16 +7,19 @@ import (
|
||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
"github.com/owncast/owncast/core/chat"
|
"github.com/owncast/owncast/core/chat"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var configRepository = configrepository.Get()
|
||||||
|
|
||||||
func handleEngagementActivity(eventType events.EventType, isLiveNotification bool, actorReference vocab.ActivityStreamsActorProperty, action string) error {
|
func handleEngagementActivity(eventType events.EventType, isLiveNotification bool, actorReference vocab.ActivityStreamsActorProperty, action string) error {
|
||||||
// Do nothing if displaying engagement actions has been turned off.
|
// Do nothing if displaying engagement actions has been turned off.
|
||||||
if !data.GetFederationShowEngagement() {
|
if !configRepository.GetFederationShowEngagement() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do nothing if chat is disabled
|
// Do nothing if chat is disabled
|
||||||
if data.GetChatDisabled() {
|
if configRepository.GetChatDisabled() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,11 +38,11 @@ func handleEngagementActivity(eventType events.EventType, isLiveNotification boo
|
||||||
if isLiveNotification && action == events.FediverseEngagementLike {
|
if isLiveNotification && action == events.FediverseEngagementLike {
|
||||||
suffix = "liked that this stream went live."
|
suffix = "liked that this stream went live."
|
||||||
} else if action == events.FediverseEngagementLike {
|
} else if action == events.FediverseEngagementLike {
|
||||||
suffix = fmt.Sprintf("liked a post from %s.", data.GetServerName())
|
suffix = fmt.Sprintf("liked a post from %s.", configRepository.GetServerName())
|
||||||
} else if isLiveNotification && action == events.FediverseEngagementRepost {
|
} else if isLiveNotification && action == events.FediverseEngagementRepost {
|
||||||
suffix = "shared this stream with their followers."
|
suffix = "shared this stream with their followers."
|
||||||
} else if action == events.FediverseEngagementRepost {
|
} else if action == events.FediverseEngagementRepost {
|
||||||
suffix = fmt.Sprintf("shared a post from %s.", data.GetServerName())
|
suffix = fmt.Sprintf("shared a post from %s.", configRepository.GetServerName())
|
||||||
} else if action == events.FediverseEngagementFollow {
|
} else if action == events.FediverseEngagementFollow {
|
||||||
suffix = "followed this stream."
|
suffix = "followed this stream."
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -26,7 +26,7 @@ func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsF
|
||||||
return fmt.Errorf("unable to handle request")
|
return fmt.Errorf("unable to handle request")
|
||||||
}
|
}
|
||||||
|
|
||||||
approved := !data.GetFederationIsPrivate()
|
approved := !configRepository.GetFederationIsPrivate()
|
||||||
|
|
||||||
followRequest := *follow
|
followRequest := *follow
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@ func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsF
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
localAccountName := data.GetDefaultFederationUsername()
|
localAccountName := configRepository.GetDefaultFederationUsername()
|
||||||
|
|
||||||
if approved {
|
if approved {
|
||||||
if err := requests.SendFollowAccept(follow.Inbox, activity, localAccountName); err != nil {
|
if err := requests.SendFollowAccept(follow.Inbox, activity, localAccountName); err != nil {
|
||||||
|
|
|
@ -130,7 +130,7 @@ func Verify(request *http.Request) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func isBlockedDomain(domain string) bool {
|
func isBlockedDomain(domain string) bool {
|
||||||
blockedDomains := data.GetBlockedFederatedDomains()
|
blockedDomains := configRepository.GetBlockedFederatedDomains()
|
||||||
|
|
||||||
for _, blockedDomain := range blockedDomains {
|
for _, blockedDomain := range blockedDomains {
|
||||||
if strings.Contains(domain, blockedDomain) {
|
if strings.Contains(domain, blockedDomain) {
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
|
"github.com/owncast/owncast/storage/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
func makeFakePerson() vocab.ActivityStreamsPerson {
|
func makeFakePerson() vocab.ActivityStreamsPerson {
|
||||||
|
@ -47,22 +49,30 @@ func makeFakePerson() vocab.ActivityStreamsPerson {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
data.SetupPersistence(":memory:")
|
ds, err := data.NewStore(":memory:")
|
||||||
data.SetServerURL("https://my.cool.site.biz")
|
if err != nil {
|
||||||
persistence.Setup(data.GetDatastore())
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configRepository := configrepository.New(ds)
|
||||||
|
configRepository.PopulateDefaults()
|
||||||
|
configRepository.SetServerURL("https://my.cool.site.biz")
|
||||||
|
|
||||||
m.Run()
|
m.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBlockedDomains(t *testing.T) {
|
func TestBlockedDomains(t *testing.T) {
|
||||||
|
cr := configrepository.Get()
|
||||||
|
|
||||||
person := makeFakePerson()
|
person := makeFakePerson()
|
||||||
|
|
||||||
data.SetBlockedFederatedDomains([]string{"freedom.eagle", "guns.life"})
|
cr.SetBlockedFederatedDomains([]string{"freedom.eagle", "guns.life"})
|
||||||
|
|
||||||
if len(data.GetBlockedFederatedDomains()) != 2 {
|
if len(cr.GetBlockedFederatedDomains()) != 2 {
|
||||||
t.Error("Blocked federated domains is not set correctly")
|
t.Error("Blocked federated domains is not set correctly")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, domain := range data.GetBlockedFederatedDomains() {
|
for _, domain := range cr.GetBlockedFederatedDomains() {
|
||||||
if domain == person.GetJSONLDId().GetIRI().Host {
|
if domain == person.GetJSONLDId().GetIRI().Host {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@ import (
|
||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
"github.com/owncast/owncast/activitypub/webfinger"
|
"github.com/owncast/owncast/activitypub/webfinger"
|
||||||
"github.com/owncast/owncast/activitypub/workerpool"
|
"github.com/owncast/owncast/activitypub/workerpool"
|
||||||
|
"github.com/owncast/owncast/core/data"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/owncast/owncast/services/config"
|
"github.com/owncast/owncast/services/config"
|
||||||
|
@ -24,9 +26,11 @@ import (
|
||||||
"github.com/teris-io/shortid"
|
"github.com/teris-io/shortid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var configRepository = configrepository.Get()
|
||||||
|
|
||||||
// SendLive will send all followers the message saying you started a live stream.
|
// SendLive will send all followers the message saying you started a live stream.
|
||||||
func SendLive() error {
|
func SendLive() error {
|
||||||
textContent := data.GetFederationGoLiveMessage()
|
textContent := configRepository.GetFederationGoLiveMessage()
|
||||||
|
|
||||||
// If the message is empty then do not send it.
|
// If the message is empty then do not send it.
|
||||||
if textContent == "" {
|
if textContent == "" {
|
||||||
|
@ -37,7 +41,7 @@ func SendLive() error {
|
||||||
reg := regexp.MustCompile("[^a-zA-Z0-9]+")
|
reg := regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||||
|
|
||||||
tagProp := streams.NewActivityStreamsTagProperty()
|
tagProp := streams.NewActivityStreamsTagProperty()
|
||||||
for _, tagString := range data.GetServerMetadataTags() {
|
for _, tagString := range configRepository.GetServerMetadataTags() {
|
||||||
tagWithoutSpecialCharacters := reg.ReplaceAllString(tagString, "")
|
tagWithoutSpecialCharacters := reg.ReplaceAllString(tagString, "")
|
||||||
hashtag := apmodels.MakeHashtag(tagWithoutSpecialCharacters)
|
hashtag := apmodels.MakeHashtag(tagWithoutSpecialCharacters)
|
||||||
tagProp.AppendTootHashtag(hashtag)
|
tagProp.AppendTootHashtag(hashtag)
|
||||||
|
@ -56,7 +60,7 @@ func SendLive() error {
|
||||||
tagsString := strings.Join(tagStrings, " ")
|
tagsString := strings.Join(tagStrings, " ")
|
||||||
|
|
||||||
var streamTitle string
|
var streamTitle string
|
||||||
if title := data.GetStreamTitle(); title != "" {
|
if title := configRepository.GetStreamTitle(); title != "" {
|
||||||
streamTitle = fmt.Sprintf("<p>%s</p>", title)
|
streamTitle = fmt.Sprintf("<p>%s</p>", title)
|
||||||
}
|
}
|
||||||
textContent = fmt.Sprintf("<p>%s</p>%s<p>%s</p><p><a href=\"%s\">%s</a></p>", textContent, streamTitle, tagsString, data.GetServerURL(), data.GetServerURL())
|
textContent = fmt.Sprintf("<p>%s</p>%s<p>%s</p><p><a href=\"%s\">%s</a></p>", textContent, streamTitle, tagsString, data.GetServerURL(), data.GetServerURL())
|
||||||
|
@ -64,7 +68,7 @@ func SendLive() error {
|
||||||
activity, _, note, noteID := createBaseOutboundMessage(textContent)
|
activity, _, note, noteID := createBaseOutboundMessage(textContent)
|
||||||
|
|
||||||
// To the public if we're not treating ActivityPub as "private".
|
// To the public if we're not treating ActivityPub as "private".
|
||||||
if !data.GetFederationIsPrivate() {
|
if !configRepository.GetFederationIsPrivate() {
|
||||||
note = apmodels.MakeNotePublic(note)
|
note = apmodels.MakeNotePublic(note)
|
||||||
activity = apmodels.MakeActivityPublic(activity)
|
activity = apmodels.MakeActivityPublic(activity)
|
||||||
}
|
}
|
||||||
|
@ -72,7 +76,7 @@ func SendLive() error {
|
||||||
note.SetActivityStreamsTag(tagProp)
|
note.SetActivityStreamsTag(tagProp)
|
||||||
|
|
||||||
// Attach an image along with the Federated message.
|
// Attach an image along with the Federated message.
|
||||||
previewURL, err := url.Parse(data.GetServerURL())
|
previewURL, err := url.Parse(configRepository.GetServerURL())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var imageToAttach string
|
var imageToAttach string
|
||||||
var mediaType string
|
var mediaType string
|
||||||
|
@ -94,7 +98,7 @@ func SendLive() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.GetNSFW() {
|
if configRepository.GetNSFW() {
|
||||||
// Mark content as sensitive.
|
// Mark content as sensitive.
|
||||||
sensitive := streams.NewActivityStreamsSensitiveProperty()
|
sensitive := streams.NewActivityStreamsSensitiveProperty()
|
||||||
sensitive.AppendXMLSchemaBoolean(true)
|
sensitive.AppendXMLSchemaBoolean(true)
|
||||||
|
@ -173,7 +177,7 @@ func SendPublicMessage(textContent string) error {
|
||||||
activity, _, note, noteID := createBaseOutboundMessage(textContent)
|
activity, _, note, noteID := createBaseOutboundMessage(textContent)
|
||||||
note.SetActivityStreamsTag(tagProp)
|
note.SetActivityStreamsTag(tagProp)
|
||||||
|
|
||||||
if !data.GetFederationIsPrivate() {
|
if !configRepository.GetFederationIsPrivate() {
|
||||||
note = apmodels.MakeNotePublic(note)
|
note = apmodels.MakeNotePublic(note)
|
||||||
activity = apmodels.MakeActivityPublic(activity)
|
activity = apmodels.MakeActivityPublic(activity)
|
||||||
}
|
}
|
||||||
|
@ -197,7 +201,7 @@ func SendPublicMessage(textContent string) error {
|
||||||
|
|
||||||
// nolint: unparam
|
// nolint: unparam
|
||||||
func createBaseOutboundMessage(textContent string) (vocab.ActivityStreamsCreate, string, vocab.ActivityStreamsNote, string) {
|
func createBaseOutboundMessage(textContent string) (vocab.ActivityStreamsCreate, string, vocab.ActivityStreamsNote, string) {
|
||||||
localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
|
localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
|
||||||
noteID := shortid.MustGenerate()
|
noteID := shortid.MustGenerate()
|
||||||
noteIRI := apmodels.MakeLocalIRIForResource(noteID)
|
noteIRI := apmodels.MakeLocalIRIForResource(noteID)
|
||||||
id := shortid.MustGenerate()
|
id := shortid.MustGenerate()
|
||||||
|
@ -218,7 +222,7 @@ func getHashtagLinkHTMLFromTagString(baseHashtag string) string {
|
||||||
|
|
||||||
// SendToFollowers will send an arbitrary payload to all follower inboxes.
|
// SendToFollowers will send an arbitrary payload to all follower inboxes.
|
||||||
func SendToFollowers(payload []byte) error {
|
func SendToFollowers(payload []byte) error {
|
||||||
localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
|
localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
|
||||||
|
|
||||||
followers, _, err := persistence.GetFederationFollowers(-1, 0)
|
followers, _, err := persistence.GetFederationFollowers(-1, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -241,7 +245,7 @@ func SendToFollowers(payload []byte) error {
|
||||||
|
|
||||||
// SendToUser will send a payload to a single specific inbox.
|
// SendToUser will send a payload to a single specific inbox.
|
||||||
func SendToUser(inbox *url.URL, payload []byte) error {
|
func SendToUser(inbox *url.URL, payload []byte) error {
|
||||||
localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
|
localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
|
||||||
|
|
||||||
req, err := requests.CreateSignedRequest(payload, inbox, localActor)
|
req, err := requests.CreateSignedRequest(payload, inbox, localActor)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -256,7 +260,7 @@ func SendToUser(inbox *url.URL, payload []byte) error {
|
||||||
// UpdateFollowersWithAccountUpdates will send an update to all followers alerting of a profile update.
|
// UpdateFollowersWithAccountUpdates will send an update to all followers alerting of a profile update.
|
||||||
func UpdateFollowersWithAccountUpdates() error {
|
func UpdateFollowersWithAccountUpdates() error {
|
||||||
// Don't do anything if federation is disabled.
|
// Don't do anything if federation is disabled.
|
||||||
if !data.GetFederationEnabled() {
|
if !configRepository.GetFederationEnabled() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -265,7 +269,7 @@ func UpdateFollowersWithAccountUpdates() error {
|
||||||
activity := apmodels.MakeUpdateActivity(objectID)
|
activity := apmodels.MakeUpdateActivity(objectID)
|
||||||
|
|
||||||
actor := streams.NewActivityStreamsPerson()
|
actor := streams.NewActivityStreamsPerson()
|
||||||
actorID := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
|
actorID := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
|
||||||
actorIDProperty := streams.NewJSONLDIdProperty()
|
actorIDProperty := streams.NewJSONLDIdProperty()
|
||||||
actorIDProperty.Set(actorID)
|
actorIDProperty.Set(actorID)
|
||||||
actor.SetJSONLDId(actorIDProperty)
|
actor.SetJSONLDId(actorIDProperty)
|
||||||
|
|
|
@ -1,98 +0,0 @@
|
||||||
package persistence
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/db"
|
|
||||||
"github.com/owncast/owncast/models"
|
|
||||||
"github.com/owncast/owncast/utils"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetFollowerCount will return the number of followers we're keeping track of.
|
|
||||||
func GetFollowerCount() (int64, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
return _datastore.GetQueries().GetFollowerCount(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFederationFollowers will return a slice of the followers we keep track of locally.
|
|
||||||
func GetFederationFollowers(limit int, offset int) ([]models.Follower, int, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
total, err := _datastore.GetQueries().GetFollowerCount(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, errors.Wrap(err, "unable to fetch total number of followers")
|
|
||||||
}
|
|
||||||
|
|
||||||
followersResult, err := _datastore.GetQueries().GetFederationFollowersWithOffset(ctx, db.GetFederationFollowersWithOffsetParams{
|
|
||||||
Limit: int32(limit),
|
|
||||||
Offset: int32(offset),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
followers := make([]models.Follower, 0)
|
|
||||||
|
|
||||||
for _, row := range followersResult {
|
|
||||||
singleFollower := models.Follower{
|
|
||||||
Name: row.Name.String,
|
|
||||||
Username: row.Username,
|
|
||||||
Image: row.Image.String,
|
|
||||||
ActorIRI: row.Iri,
|
|
||||||
Inbox: row.Inbox,
|
|
||||||
Timestamp: utils.NullTime(row.CreatedAt),
|
|
||||||
}
|
|
||||||
|
|
||||||
followers = append(followers, singleFollower)
|
|
||||||
}
|
|
||||||
|
|
||||||
return followers, int(total), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPendingFollowRequests will return pending follow requests.
|
|
||||||
func GetPendingFollowRequests() ([]models.Follower, error) {
|
|
||||||
pendingFollowersResult, err := _datastore.GetQueries().GetFederationFollowerApprovalRequests(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
followers := make([]models.Follower, 0)
|
|
||||||
|
|
||||||
for _, row := range pendingFollowersResult {
|
|
||||||
singleFollower := models.Follower{
|
|
||||||
Name: row.Name.String,
|
|
||||||
Username: row.Username,
|
|
||||||
Image: row.Image.String,
|
|
||||||
ActorIRI: row.Iri,
|
|
||||||
Inbox: row.Inbox,
|
|
||||||
Timestamp: utils.NullTime{Time: row.CreatedAt.Time, Valid: true},
|
|
||||||
}
|
|
||||||
followers = append(followers, singleFollower)
|
|
||||||
}
|
|
||||||
|
|
||||||
return followers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetBlockedAndRejectedFollowers will return blocked and rejected followers.
|
|
||||||
func GetBlockedAndRejectedFollowers() ([]models.Follower, error) {
|
|
||||||
pendingFollowersResult, err := _datastore.GetQueries().GetRejectedAndBlockedFollowers(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
followers := make([]models.Follower, 0)
|
|
||||||
|
|
||||||
for _, row := range pendingFollowersResult {
|
|
||||||
singleFollower := models.Follower{
|
|
||||||
Name: row.Name.String,
|
|
||||||
Username: row.Username,
|
|
||||||
Image: row.Image.String,
|
|
||||||
ActorIRI: row.Iri,
|
|
||||||
DisabledAt: utils.NullTime{Time: row.DisabledAt.Time, Valid: true},
|
|
||||||
Timestamp: utils.NullTime{Time: row.CreatedAt.Time, Valid: true},
|
|
||||||
}
|
|
||||||
followers = append(followers, singleFollower)
|
|
||||||
}
|
|
||||||
|
|
||||||
return followers, nil
|
|
||||||
}
|
|
|
@ -1,319 +0,0 @@
|
||||||
package persistence
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-fed/activity/streams"
|
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
|
||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
|
||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
|
||||||
"github.com/owncast/owncast/db"
|
|
||||||
"github.com/owncast/owncast/models"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _datastore *data.Datastore
|
|
||||||
|
|
||||||
// Setup will initialize the ActivityPub persistence layer with the provided datastore.
|
|
||||||
func Setup(datastore *data.Datastore) {
|
|
||||||
_datastore = datastore
|
|
||||||
createFederationFollowersTable()
|
|
||||||
createFederationOutboxTable()
|
|
||||||
createFederatedActivitiesTable()
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddFollow will save a follow to the datastore.
|
|
||||||
func AddFollow(follow apmodels.ActivityPubActor, approved bool) error {
|
|
||||||
log.Traceln("Saving", follow.ActorIri, "as a follower.")
|
|
||||||
var image string
|
|
||||||
if follow.Image != nil {
|
|
||||||
image = follow.Image.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
followRequestObject, err := apmodels.Serialize(follow.RequestObject)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "error serializing follow request object")
|
|
||||||
}
|
|
||||||
|
|
||||||
return createFollow(follow.ActorIri.String(), follow.Inbox.String(), follow.FollowRequestIri.String(), follow.Name, follow.Username, image, followRequestObject, approved)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveFollow will remove a follow from the datastore.
|
|
||||||
func RemoveFollow(unfollow apmodels.ActivityPubActor) error {
|
|
||||||
log.Traceln("Removing", unfollow.ActorIri, "as a follower.")
|
|
||||||
return removeFollow(unfollow.ActorIri)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFollower will return a single follower/request given an IRI.
|
|
||||||
func GetFollower(iri string) (*apmodels.ActivityPubActor, error) {
|
|
||||||
result, err := _datastore.GetQueries().GetFollowerByIRI(context.Background(), iri)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
followIRI, err := url.Parse(result.Request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error parsing follow request IRI")
|
|
||||||
}
|
|
||||||
|
|
||||||
iriURL, err := url.Parse(result.Iri)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error parsing actor IRI")
|
|
||||||
}
|
|
||||||
|
|
||||||
inbox, err := url.Parse(result.Inbox)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "error parsing acting inbox")
|
|
||||||
}
|
|
||||||
|
|
||||||
image, _ := url.Parse(result.Image.String)
|
|
||||||
|
|
||||||
var disabledAt *time.Time
|
|
||||||
if result.DisabledAt.Valid {
|
|
||||||
disabledAt = &result.DisabledAt.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
follower := apmodels.ActivityPubActor{
|
|
||||||
ActorIri: iriURL,
|
|
||||||
Inbox: inbox,
|
|
||||||
Name: result.Name.String,
|
|
||||||
Username: result.Username,
|
|
||||||
Image: image,
|
|
||||||
FollowRequestIri: followIRI,
|
|
||||||
DisabledAt: disabledAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &follower, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ApprovePreviousFollowRequest will approve a follow request.
|
|
||||||
func ApprovePreviousFollowRequest(iri string) error {
|
|
||||||
return _datastore.GetQueries().ApproveFederationFollower(context.Background(), db.ApproveFederationFollowerParams{
|
|
||||||
Iri: iri,
|
|
||||||
ApprovedAt: sql.NullTime{
|
|
||||||
Time: time.Now(),
|
|
||||||
Valid: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlockOrRejectFollower will block an existing follower or reject a follow request.
|
|
||||||
func BlockOrRejectFollower(iri string) error {
|
|
||||||
return _datastore.GetQueries().RejectFederationFollower(context.Background(), db.RejectFederationFollowerParams{
|
|
||||||
Iri: iri,
|
|
||||||
DisabledAt: sql.NullTime{
|
|
||||||
Time: time.Now(),
|
|
||||||
Valid: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func createFollow(actor, inbox, request, name, username, image string, requestObject []byte, approved bool) error {
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
log.Debugln(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
}()
|
|
||||||
|
|
||||||
var approvedAt sql.NullTime
|
|
||||||
if approved {
|
|
||||||
approvedAt = sql.NullTime{
|
|
||||||
Time: time.Now(),
|
|
||||||
Valid: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = _datastore.GetQueries().WithTx(tx).AddFollower(context.Background(), db.AddFollowerParams{
|
|
||||||
Iri: actor,
|
|
||||||
Inbox: inbox,
|
|
||||||
Name: sql.NullString{String: name, Valid: true},
|
|
||||||
Username: username,
|
|
||||||
Image: sql.NullString{String: image, Valid: true},
|
|
||||||
ApprovedAt: approvedAt,
|
|
||||||
Request: request,
|
|
||||||
RequestObject: requestObject,
|
|
||||||
}); err != nil {
|
|
||||||
log.Errorln("error creating new federation follow: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateFollower will update the details of a stored follower given an IRI.
|
|
||||||
func UpdateFollower(actorIRI string, inbox string, name string, username string, image string) error {
|
|
||||||
_datastore.DbLock.Lock()
|
|
||||||
defer _datastore.DbLock.Unlock()
|
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
log.Debugln(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = _datastore.GetQueries().WithTx(tx).UpdateFollowerByIRI(context.Background(), db.UpdateFollowerByIRIParams{
|
|
||||||
Inbox: inbox,
|
|
||||||
Name: sql.NullString{String: name, Valid: true},
|
|
||||||
Username: username,
|
|
||||||
Image: sql.NullString{String: image, Valid: true},
|
|
||||||
Iri: actorIRI,
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("error updating follower %s %s", actorIRI, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeFollow(actor *url.URL) error {
|
|
||||||
_datastore.DbLock.Lock()
|
|
||||||
defer _datastore.DbLock.Unlock()
|
|
||||||
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := _datastore.GetQueries().WithTx(tx).RemoveFollowerByIRI(context.Background(), actor.String()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOutboxPostCount will return the number of posts in the outbox.
|
|
||||||
func GetOutboxPostCount() (int64, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
return _datastore.GetQueries().GetLocalPostCount(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetOutbox will return an instance of the outbox populated by stored items.
|
|
||||||
func GetOutbox(limit int, offset int) (vocab.ActivityStreamsOrderedCollection, error) {
|
|
||||||
collection := streams.NewActivityStreamsOrderedCollection()
|
|
||||||
orderedItems := streams.NewActivityStreamsOrderedItemsProperty()
|
|
||||||
rows, err := _datastore.GetQueries().GetOutboxWithOffset(
|
|
||||||
context.Background(),
|
|
||||||
db.GetOutboxWithOffsetParams{Limit: int32(limit), Offset: int32(offset)},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return collection, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, value := range rows {
|
|
||||||
createCallback := func(c context.Context, activity vocab.ActivityStreamsCreate) error {
|
|
||||||
orderedItems.AppendActivityStreamsCreate(activity)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := resolvers.Resolve(context.Background(), value, createCallback); err != nil {
|
|
||||||
return collection, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return collection, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddToOutbox will store a single payload to the persistence layer.
|
|
||||||
func AddToOutbox(iri string, itemData []byte, typeString string, isLiveNotification bool) error {
|
|
||||||
tx, err := _datastore.DB.Begin()
|
|
||||||
if err != nil {
|
|
||||||
log.Debugln(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err = _datastore.GetQueries().WithTx(tx).AddToOutbox(context.Background(), db.AddToOutboxParams{
|
|
||||||
Iri: iri,
|
|
||||||
Value: itemData,
|
|
||||||
Type: typeString,
|
|
||||||
LiveNotification: sql.NullBool{Bool: isLiveNotification, Valid: true},
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("error creating new item in federation outbox %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetObjectByIRI will return a string representation of a single object by the IRI.
|
|
||||||
func GetObjectByIRI(iri string) (string, bool, time.Time, error) {
|
|
||||||
row, err := _datastore.GetQueries().GetObjectFromOutboxByIRI(context.Background(), iri)
|
|
||||||
return string(row.Value), row.LiveNotification.Bool, row.CreatedAt.Time, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLocalPostCount will return the number of posts existing locally.
|
|
||||||
func GetLocalPostCount() (int64, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
return _datastore.GetQueries().GetLocalPostCount(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveInboundFediverseActivity will save an event to the ap_inbound_activities table.
|
|
||||||
func SaveInboundFediverseActivity(objectIRI string, actorIRI string, eventType string, timestamp time.Time) error {
|
|
||||||
if err := _datastore.GetQueries().AddToAcceptedActivities(context.Background(), db.AddToAcceptedActivitiesParams{
|
|
||||||
Iri: objectIRI,
|
|
||||||
Actor: actorIRI,
|
|
||||||
Type: eventType,
|
|
||||||
Timestamp: timestamp,
|
|
||||||
}); err != nil {
|
|
||||||
return errors.Wrap(err, "error saving event "+objectIRI)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetInboundActivities will return a collection of saved, federated activities
|
|
||||||
// limited and offset by the values provided to support pagination.
|
|
||||||
func GetInboundActivities(limit int, offset int) ([]models.FederatedActivity, int, error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
rows, err := _datastore.GetQueries().GetInboundActivitiesWithOffset(ctx, db.GetInboundActivitiesWithOffsetParams{
|
|
||||||
Limit: int32(limit),
|
|
||||||
Offset: int32(offset),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
activities := make([]models.FederatedActivity, 0)
|
|
||||||
|
|
||||||
total, err := _datastore.GetQueries().GetInboundActivityCount(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, errors.Wrap(err, "unable to fetch total activity count")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, row := range rows {
|
|
||||||
singleActivity := models.FederatedActivity{
|
|
||||||
IRI: row.Iri,
|
|
||||||
ActorIRI: row.Actor,
|
|
||||||
Type: row.Type,
|
|
||||||
Timestamp: row.Timestamp,
|
|
||||||
}
|
|
||||||
activities = append(activities, singleActivity)
|
|
||||||
}
|
|
||||||
|
|
||||||
return activities, int(total), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasPreviouslyHandledInboundActivity will return if we have previously handled
|
|
||||||
// an inbound federated activity.
|
|
||||||
func HasPreviouslyHandledInboundActivity(iri string, actorIRI string, eventType string) (bool, error) {
|
|
||||||
exists, err := _datastore.GetQueries().DoesInboundActivityExist(context.Background(), db.DoesInboundActivityExistParams{
|
|
||||||
Iri: iri,
|
|
||||||
Actor: actorIRI,
|
|
||||||
Type: eventType,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return exists > 0, nil
|
|
||||||
}
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/go-fed/activity/streams/vocab"
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
"github.com/owncast/owncast/activitypub/apmodels"
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
"github.com/owncast/owncast/activitypub/crypto"
|
"github.com/owncast/owncast/activitypub/crypto"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -50,7 +51,9 @@ func ResolveIRI(c context.Context, iri string, callbacks ...interface{}) error {
|
||||||
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, iri, nil)
|
req, _ := http.NewRequest(http.MethodGet, iri, nil)
|
||||||
|
|
||||||
actor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
|
actor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
|
||||||
if err := crypto.SignRequest(req, nil, actor); err != nil {
|
if err := crypto.SignRequest(req, nil, actor); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/owncast/owncast/services/config"
|
"github.com/owncast/owncast/services/config"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -18,6 +19,8 @@ var (
|
||||||
chatMessagesSentCounter prometheus.Gauge
|
chatMessagesSentCounter prometheus.Gauge
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var configRepository = configrepository.Get()
|
||||||
|
|
||||||
// Start begins the chat server.
|
// Start begins the chat server.
|
||||||
func Start(getStatusFunc func() models.Status) error {
|
func Start(getStatusFunc func() models.Status) error {
|
||||||
setupPersistence()
|
setupPersistence()
|
||||||
|
@ -35,7 +38,7 @@ func Start(getStatusFunc func() models.Status) error {
|
||||||
Help: "The number of chat messages incremented over time.",
|
Help: "The number of chat messages incremented over time.",
|
||||||
ConstLabels: map[string]string{
|
ConstLabels: map[string]string{
|
||||||
"version": c.VersionNumber,
|
"version": c.VersionNumber,
|
||||||
"host": data.GetServerURL(),
|
"host": configRepository.GetServerURL(),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
|
||||||
proposedUsername := receivedEvent.NewName
|
proposedUsername := receivedEvent.NewName
|
||||||
|
|
||||||
// Check if name is on the blocklist
|
// Check if name is on the blocklist
|
||||||
blocklist := data.GetForbiddenUsernameList()
|
blocklist := configRepository.GetForbiddenUsernameList()
|
||||||
userRepository := storage.GetUserRepository()
|
userRepository := storage.GetUserRepository()
|
||||||
|
|
||||||
// Names have a max length
|
// Names have a max length
|
||||||
|
@ -96,7 +96,7 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
|
||||||
// Send chat user name changed webhook
|
// Send chat user name changed webhook
|
||||||
receivedEvent.User = savedUser
|
receivedEvent.User = savedUser
|
||||||
receivedEvent.ClientID = eventData.client.Id
|
receivedEvent.ClientID = eventData.client.Id
|
||||||
webhookManager := webhooks.GetWebhooks()
|
webhookManager := webhooks.Get()
|
||||||
webhookManager.SendChatEventUsernameChanged(receivedEvent)
|
webhookManager.SendChatEventUsernameChanged(receivedEvent)
|
||||||
|
|
||||||
// Resend the client's user so their username is in sync.
|
// Resend the client's user so their username is in sync.
|
||||||
|
@ -150,7 +150,9 @@ func (s *Server) userMessageSent(eventData chatClientEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event.User = user.GetUserByToken(eventData.client.accessToken)
|
userRepository := storage.GetUserRepository()
|
||||||
|
|
||||||
|
event.User = userRepository.GetUserByToken(eventData.client.accessToken)
|
||||||
|
|
||||||
// Guard against nil users
|
// Guard against nil users
|
||||||
if event.User == nil {
|
if event.User == nil {
|
||||||
|
@ -164,7 +166,7 @@ func (s *Server) userMessageSent(eventData chatClientEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send chat message sent webhook
|
// Send chat message sent webhook
|
||||||
webhookManager := webhooks.GetWebhooks()
|
webhookManager := webhooks.Get()
|
||||||
webhookManager.SendChatEvent(&event)
|
webhookManager.SendChatEvent(&event)
|
||||||
chatMessagesSentCounter.Inc()
|
chatMessagesSentCounter.Inc()
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package events
|
package events
|
||||||
|
|
||||||
import "github.com/owncast/owncast/core/data"
|
import (
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
|
)
|
||||||
|
|
||||||
// FediverseEngagementEvent is a message displayed in chat on representing an action on the Fediverse.
|
// FediverseEngagementEvent is a message displayed in chat on representing an action on the Fediverse.
|
||||||
type FediverseEngagementEvent struct {
|
type FediverseEngagementEvent struct {
|
||||||
|
@ -11,6 +13,8 @@ type FediverseEngagementEvent struct {
|
||||||
UserAccountName string `json:"title"`
|
UserAccountName string `json:"title"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var configRepository = configrepository.Get()
|
||||||
|
|
||||||
// GetBroadcastPayload will return the object to send to all chat users.
|
// GetBroadcastPayload will return the object to send to all chat users.
|
||||||
func (e *FediverseEngagementEvent) GetBroadcastPayload() EventPayload {
|
func (e *FediverseEngagementEvent) GetBroadcastPayload() EventPayload {
|
||||||
return EventPayload{
|
return EventPayload{
|
||||||
|
@ -22,7 +26,7 @@ func (e *FediverseEngagementEvent) GetBroadcastPayload() EventPayload {
|
||||||
"title": e.UserAccountName,
|
"title": e.UserAccountName,
|
||||||
"link": e.Link,
|
"link": e.Link,
|
||||||
"user": EventPayload{
|
"user": EventPayload{
|
||||||
"displayName": data.GetServerName(),
|
"displayName": configRepository.GetServerName(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package events
|
package events
|
||||||
|
|
||||||
import "github.com/owncast/owncast/core/data"
|
|
||||||
|
|
||||||
// SystemMessageEvent is a message displayed in chat on behalf of the server.
|
// SystemMessageEvent is a message displayed in chat on behalf of the server.
|
||||||
type SystemMessageEvent struct {
|
type SystemMessageEvent struct {
|
||||||
Event
|
Event
|
||||||
|
@ -16,7 +14,7 @@ func (e *SystemMessageEvent) GetBroadcastPayload() EventPayload {
|
||||||
"body": e.Body,
|
"body": e.Body,
|
||||||
"type": SystemMessageSent,
|
"type": SystemMessageSent,
|
||||||
"user": EventPayload{
|
"user": EventPayload{
|
||||||
"displayName": data.GetServerName(),
|
"displayName": configRepository.GetServerName(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ func SetMessagesVisibility(messageIDs []string, visibility bool) error {
|
||||||
return errors.New("error broadcasting message visibility payload " + err.Error())
|
return errors.New("error broadcasting message visibility payload " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
webhookManager := webhooks.GetWebhooks()
|
webhookManager := webhooks.Get()
|
||||||
webhookManager.SendChatEventSetMessageVisibility(event)
|
webhookManager.SendChatEventSetMessageVisibility(event)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -8,10 +8,11 @@ import (
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/storage/data"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _datastore *data.Datastore
|
var _datastore *data.Store
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxBacklogHours = 2 // Keep backlog max hours worth of messages
|
maxBacklogHours = 2 // Keep backlog max hours worth of messages
|
||||||
|
|
|
@ -94,7 +94,11 @@ func (s *Server) Addclient(conn *websocket.Conn, user *models.User, accessToken
|
||||||
ConnectedAt: time.Now(),
|
ConnectedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldSendJoinedMessages := data.GetChatJoinPartMessagesEnabled()
|
// Do not send user re-joined broadcast message if they've been active within 10 minutes.
|
||||||
|
shouldSendJoinedMessages := configRepository.GetChatJoinPartMessagesEnabled()
|
||||||
|
if previouslyLastSeen, ok := _lastSeenCache[user.ID]; ok && time.Since(previouslyLastSeen) < time.Minute*10 {
|
||||||
|
shouldSendJoinedMessages = false
|
||||||
|
}
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
{
|
{
|
||||||
|
@ -127,7 +131,7 @@ func (s *Server) Addclient(conn *websocket.Conn, user *models.User, accessToken
|
||||||
s.sendWelcomeMessageToClient(client)
|
s.sendWelcomeMessageToClient(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asynchronously, optionally, fetch GeoIP data.
|
// Asynchronously, optionally, fetch GeoIP configRepository.
|
||||||
go func(client *Client) {
|
go func(client *Client) {
|
||||||
client.Geo = s.geoipClient.GetGeoFromIP(ipAddress)
|
client.Geo = s.geoipClient.GetGeoFromIP(ipAddress)
|
||||||
}(client)
|
}(client)
|
||||||
|
@ -146,7 +150,7 @@ func (s *Server) sendUserJoinedMessage(c *Client) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send chat user joined webhook
|
// Send chat user joined webhook
|
||||||
webhookManager := webhooks.GetWebhooks()
|
webhookManager := webhooks.Get()
|
||||||
webhookManager.SendChatEventUserJoined(userJoinedEvent)
|
webhookManager.SendChatEventUserJoined(userJoinedEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,14 +195,14 @@ func (s *Server) sendUserPartedMessage(c *Client) {
|
||||||
|
|
||||||
// HandleClientConnection is fired when a single client connects to the websocket.
|
// HandleClientConnection is fired when a single client connects to the websocket.
|
||||||
func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request) {
|
||||||
if data.GetChatDisabled() {
|
if configRepository.GetChatDisabled() {
|
||||||
_, _ = w.Write([]byte(events.ChatDisabled))
|
_, _ = w.Write([]byte(events.ChatDisabled))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ipAddress := utils.GetIPAddressFromRequest(r)
|
ipAddress := utils.GetIPAddressFromRequest(r)
|
||||||
// Check if this client's IP address is banned. If so send a rejection.
|
// Check if this client's IP address is banned. If so send a rejection.
|
||||||
if blocked, err := data.IsIPAddressBanned(ipAddress); blocked {
|
if blocked, err := configRepository.IsIPAddressBanned(ipAddress); blocked {
|
||||||
log.Debugln("Client ip address has been blocked. Rejecting.")
|
log.Debugln("Client ip address has been blocked. Rejecting.")
|
||||||
|
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
@ -374,7 +378,7 @@ func (s *Server) eventReceived(event chatClientEvent) {
|
||||||
|
|
||||||
// If established chat user only mode is enabled and the user is not old
|
// If established chat user only mode is enabled and the user is not old
|
||||||
// enough then reject this event and send them an informative message.
|
// enough then reject this event and send them an informative message.
|
||||||
if u != nil && data.GetChatEstbalishedUsersOnlyMode() && time.Since(event.client.User.CreatedAt) < config.GetDefaults().ChatEstablishedUserModeTimeDuration && !u.IsModerator() {
|
if u != nil && configRepository.GetChatEstbalishedUsersOnlyMode() && time.Since(event.client.User.CreatedAt) < config.GetDefaults().ChatEstablishedUserModeTimeDuration && !u.IsModerator() {
|
||||||
s.sendActionToClient(c, "You have not been an established chat participant long enough to take part in chat. Please enjoy the stream and try again later.")
|
s.sendActionToClient(c, "You have not been an established chat participant long enough to take part in chat. Please enjoy the stream and try again later.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -404,7 +408,7 @@ func (s *Server) sendWelcomeMessageToClient(c *Client) {
|
||||||
// Add an artificial delay so people notice this message come in.
|
// Add an artificial delay so people notice this message come in.
|
||||||
time.Sleep(7 * time.Second)
|
time.Sleep(7 * time.Second)
|
||||||
|
|
||||||
welcomeMessage := utils.RenderSimpleMarkdown(data.GetServerWelcomeMessage())
|
welcomeMessage := utils.RenderSimpleMarkdown(configRepository.GetServerWelcomeMessage())
|
||||||
|
|
||||||
if welcomeMessage != "" {
|
if welcomeMessage != "" {
|
||||||
s.sendSystemMessageToClient(c, welcomeMessage)
|
s.sendSystemMessageToClient(c, welcomeMessage)
|
||||||
|
@ -412,7 +416,7 @@ func (s *Server) sendWelcomeMessageToClient(c *Client) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) sendAllWelcomeMessage() {
|
func (s *Server) sendAllWelcomeMessage() {
|
||||||
welcomeMessage := utils.RenderSimpleMarkdown(data.GetServerWelcomeMessage())
|
welcomeMessage := utils.RenderSimpleMarkdown(configRepository.GetServerWelcomeMessage())
|
||||||
|
|
||||||
if welcomeMessage != "" {
|
if welcomeMessage != "" {
|
||||||
clientMessage := events.SystemMessageEvent{
|
clientMessage := events.SystemMessageEvent{
|
||||||
|
|
16
core/core.go
16
core/core.go
|
@ -14,6 +14,8 @@ import (
|
||||||
"github.com/owncast/owncast/services/notifications"
|
"github.com/owncast/owncast/services/notifications"
|
||||||
"github.com/owncast/owncast/services/webhooks"
|
"github.com/owncast/owncast/services/webhooks"
|
||||||
"github.com/owncast/owncast/services/yp"
|
"github.com/owncast/owncast/services/yp"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
|
"github.com/owncast/owncast/storage/data"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
"github.com/owncast/owncast/video/rtmp"
|
"github.com/owncast/owncast/video/rtmp"
|
||||||
"github.com/owncast/owncast/video/transcoder"
|
"github.com/owncast/owncast/video/transcoder"
|
||||||
|
@ -29,13 +31,15 @@ var (
|
||||||
fileWriter = transcoder.FileWriterReceiverService{}
|
fileWriter = transcoder.FileWriterReceiverService{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var configRepository = configrepository.Get()
|
||||||
|
|
||||||
// Start starts up the core processing.
|
// Start starts up the core processing.
|
||||||
func Start() error {
|
func Start() error {
|
||||||
resetDirectories()
|
resetDirectories()
|
||||||
|
|
||||||
data.PopulateDefaults()
|
configRepository.PopulateDefaults()
|
||||||
|
|
||||||
if err := data.VerifySettings(); err != nil {
|
if err := configRepository.VerifySettings(); err != nil {
|
||||||
log.Error(err)
|
log.Error(err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -73,14 +77,14 @@ func Start() error {
|
||||||
// start the rtmp server
|
// start the rtmp server
|
||||||
go rtmp.Start(setStreamAsConnected, setBroadcaster)
|
go rtmp.Start(setStreamAsConnected, setBroadcaster)
|
||||||
|
|
||||||
rtmpPort := data.GetRTMPPortNumber()
|
rtmpPort := configRepository.GetRTMPPortNumber()
|
||||||
if rtmpPort != 1935 {
|
if rtmpPort != 1935 {
|
||||||
log.Infof("RTMP is accepting inbound streams on port %d.", rtmpPort)
|
log.Infof("RTMP is accepting inbound streams on port %d.", rtmpPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
webhooks.InitTemporarySingleton(GetStatus)
|
webhooks.InitTemporarySingleton(GetStatus)
|
||||||
|
|
||||||
notifications.Setup(data.GetStore())
|
notifications.Setup(data.GetDatastore())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -111,7 +115,7 @@ func transitionToOfflineVideoStreamContent() {
|
||||||
go _transcoder.Start(false)
|
go _transcoder.Start(false)
|
||||||
|
|
||||||
// Copy the logo to be the thumbnail
|
// Copy the logo to be the thumbnail
|
||||||
logo := data.GetLogoPath()
|
logo := configRepository.GetLogoPath()
|
||||||
c := config.GetConfig()
|
c := config.GetConfig()
|
||||||
dst := filepath.Join(c.TempDir, "thumbnail.jpg")
|
dst := filepath.Join(c.TempDir, "thumbnail.jpg")
|
||||||
if err = utils.Copy(filepath.Join("data", logo), dst); err != nil {
|
if err = utils.Copy(filepath.Join("data", logo), dst); err != nil {
|
||||||
|
@ -130,7 +134,7 @@ func resetDirectories() {
|
||||||
utils.CleanupDirectory(c.HLSStoragePath)
|
utils.CleanupDirectory(c.HLSStoragePath)
|
||||||
|
|
||||||
// Remove the previous thumbnail
|
// Remove the previous thumbnail
|
||||||
logo := data.GetLogoPath()
|
logo := configRepository.GetLogoPath()
|
||||||
if utils.DoesFileExists(logo) {
|
if utils.DoesFileExists(logo) {
|
||||||
err := utils.Copy(path.Join("data", logo), filepath.Join(config.DataDirectory, "thumbnail.jpg"))
|
err := utils.Copy(path.Join("data", logo), filepath.Join(config.DataDirectory, "thumbnail.jpg"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -47,7 +47,7 @@ func IsStreamConnected() bool {
|
||||||
// Kind of a hack. It takes a handful of seconds between a RTMP connection and when HLS data is available.
|
// Kind of a hack. It takes a handful of seconds between a RTMP connection and when HLS data is available.
|
||||||
// So account for that with an artificial buffer of four segments.
|
// So account for that with an artificial buffer of four segments.
|
||||||
timeSinceLastConnected := time.Since(_stats.LastConnectTime.Time).Seconds()
|
timeSinceLastConnected := time.Since(_stats.LastConnectTime.Time).Seconds()
|
||||||
waitTime := math.Max(float64(data.GetStreamLatencyLevel().SecondsPerSegment)*3.0, 7)
|
waitTime := math.Max(float64(configRepository.GetStreamLatencyLevel().SecondsPerSegment)*3.0, 7)
|
||||||
if timeSinceLastConnected < waitTime {
|
if timeSinceLastConnected < waitTime {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ func SetViewerActive(viewer *models.Viewer) {
|
||||||
l.Lock()
|
l.Lock()
|
||||||
defer l.Unlock()
|
defer l.Unlock()
|
||||||
|
|
||||||
// Asynchronously, optionally, fetch GeoIP data.
|
// Asynchronously, optionally, fetch GeoIP configRepository.
|
||||||
go func(viewer *models.Viewer) {
|
go func(viewer *models.Viewer) {
|
||||||
viewer.Geo = _geoIPClient.GetGeoFromIP(viewer.IPAddress)
|
viewer.Geo = _geoIPClient.GetGeoFromIP(viewer.IPAddress)
|
||||||
}(viewer)
|
}(viewer)
|
||||||
|
@ -110,27 +110,27 @@ func pruneViewerCount() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveStats() {
|
func saveStats() {
|
||||||
if err := data.SetPeakOverallViewerCount(_stats.OverallMaxViewerCount); err != nil {
|
if err := configRepository.SetPeakOverallViewerCount(_stats.OverallMaxViewerCount); err != nil {
|
||||||
log.Errorln("error saving viewer count", err)
|
log.Errorln("error saving viewer count", err)
|
||||||
}
|
}
|
||||||
if err := data.SetPeakSessionViewerCount(_stats.SessionMaxViewerCount); err != nil {
|
if err := configRepository.SetPeakSessionViewerCount(_stats.SessionMaxViewerCount); err != nil {
|
||||||
log.Errorln("error saving viewer count", err)
|
log.Errorln("error saving viewer count", err)
|
||||||
}
|
}
|
||||||
if _stats.LastDisconnectTime != nil && _stats.LastDisconnectTime.Valid {
|
if _stats.LastDisconnectTime != nil && _stats.LastDisconnectTime.Valid {
|
||||||
if err := data.SetLastDisconnectTime(_stats.LastDisconnectTime.Time); err != nil {
|
if err := configRepository.SetLastDisconnectTime(_stats.LastDisconnectTime.Time); err != nil {
|
||||||
log.Errorln("error saving disconnect time", err)
|
log.Errorln("error saving disconnect time", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSavedStats() models.Stats {
|
func getSavedStats() models.Stats {
|
||||||
savedLastDisconnectTime, _ := data.GetLastDisconnectTime()
|
savedLastDisconnectTime, _ := configRepository.GetLastDisconnectTime()
|
||||||
|
|
||||||
result := models.Stats{
|
result := models.Stats{
|
||||||
ChatClients: make(map[string]models.Client),
|
ChatClients: make(map[string]models.Client),
|
||||||
Viewers: make(map[string]*models.Viewer),
|
Viewers: make(map[string]*models.Viewer),
|
||||||
SessionMaxViewerCount: data.GetPeakSessionViewerCount(),
|
SessionMaxViewerCount: configRepository.GetPeakSessionViewerCount(),
|
||||||
OverallMaxViewerCount: data.GetPeakOverallViewerCount(),
|
OverallMaxViewerCount: configRepository.GetPeakOverallViewerCount(),
|
||||||
LastDisconnectTime: savedLastDisconnectTime,
|
LastDisconnectTime: savedLastDisconnectTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ func GetStatus() models.Status {
|
||||||
LastDisconnectTime: _stats.LastDisconnectTime,
|
LastDisconnectTime: _stats.LastDisconnectTime,
|
||||||
LastConnectTime: _stats.LastConnectTime,
|
LastConnectTime: _stats.LastConnectTime,
|
||||||
VersionNumber: c.VersionNumber,
|
VersionNumber: c.VersionNumber,
|
||||||
StreamTitle: data.GetStreamTitle(),
|
StreamTitle: configRepository.GetStreamTitle(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupStorage() error {
|
func setupStorage() error {
|
||||||
config := configrepository.GetConfigRepository()
|
config := configrepository.Get()
|
||||||
s3Config := config.GetS3Config()
|
s3Config := config.GetS3Config()
|
||||||
|
|
||||||
if s3Config.Enabled {
|
if s3Config.Enabled {
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/owncast/owncast/services/config"
|
"github.com/owncast/owncast/services/config"
|
||||||
"github.com/owncast/owncast/services/notifications"
|
"github.com/owncast/owncast/services/notifications"
|
||||||
"github.com/owncast/owncast/services/webhooks"
|
"github.com/owncast/owncast/services/webhooks"
|
||||||
|
"github.com/owncast/owncast/storage/data"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
"github.com/owncast/owncast/video/rtmp"
|
"github.com/owncast/owncast/video/rtmp"
|
||||||
"github.com/owncast/owncast/video/transcoder"
|
"github.com/owncast/owncast/video/transcoder"
|
||||||
|
@ -39,8 +40,8 @@ func setStreamAsConnected(rtmpOut *io.PipeReader) {
|
||||||
_stats.SessionMaxViewerCount = 0
|
_stats.SessionMaxViewerCount = 0
|
||||||
|
|
||||||
_currentBroadcast = &models.CurrentBroadcast{
|
_currentBroadcast = &models.CurrentBroadcast{
|
||||||
LatencyLevel: data.GetStreamLatencyLevel(),
|
LatencyLevel: configRepository.GetStreamLatencyLevel(),
|
||||||
OutputSettings: data.GetStreamOutputVariants(),
|
OutputSettings: configRepository.GetStreamOutputVariants(),
|
||||||
}
|
}
|
||||||
|
|
||||||
StopOfflineCleanupTimer()
|
StopOfflineCleanupTimer()
|
||||||
|
@ -68,9 +69,9 @@ func setStreamAsConnected(rtmpOut *io.PipeReader) {
|
||||||
_transcoder.Start(true)
|
_transcoder.Start(true)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
webhookManager := webhooks.GetWebhooks()
|
webhookManager := webhooks.Get()
|
||||||
go webhookManager.SendStreamStatusEvent(models.StreamStarted)
|
go webhookManager.SendStreamStatusEvent(models.StreamStarted)
|
||||||
transcoder.StartThumbnailGenerator(segmentPath, data.FindHighestVideoQualityIndex(_currentBroadcast.OutputSettings))
|
transcoder.StartThumbnailGenerator(segmentPath, configRepository.FindHighestVideoQualityIndex(_currentBroadcast.OutputSettings))
|
||||||
|
|
||||||
_ = chat.SendSystemAction("Stay tuned, the stream is **starting**!", true)
|
_ = chat.SendSystemAction("Stay tuned, the stream is **starting**!", true)
|
||||||
chat.SendAllWelcomeMessage()
|
chat.SendAllWelcomeMessage()
|
||||||
|
@ -126,7 +127,7 @@ func SetStreamAsDisconnected() {
|
||||||
stopOnlineCleanupTimer()
|
stopOnlineCleanupTimer()
|
||||||
saveStats()
|
saveStats()
|
||||||
|
|
||||||
webhookManager := webhooks.GetWebhooks()
|
webhookManager := webhooks.Get()
|
||||||
go webhookManager.SendStreamStatusEvent(models.StreamStopped)
|
go webhookManager.SendStreamStatusEvent(models.StreamStopped)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,7 +179,7 @@ func startLiveStreamNotificationsTimer() context.CancelFunc {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send Fediverse message.
|
// Send Fediverse message.
|
||||||
if data.GetFederationEnabled() {
|
if configRepository.GetFederationEnabled() {
|
||||||
log.Traceln("Sending Federated Go Live message.")
|
log.Traceln("Sending Federated Go Live message.")
|
||||||
if err := activitypub.SendLive(); err != nil {
|
if err := activitypub.SendLive(); err != nil {
|
||||||
log.Errorln(err)
|
log.Errorln(err)
|
||||||
|
|
15
main.go
15
main.go
|
@ -6,6 +6,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/owncast/owncast/logging"
|
"github.com/owncast/owncast/logging"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
"github.com/owncast/owncast/webserver"
|
"github.com/owncast/owncast/webserver"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
@ -31,6 +32,8 @@ var (
|
||||||
config *configservice.Config
|
config *configservice.Config
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var configRepository = configrepository.Get()
|
||||||
|
|
||||||
// nolint:cyclop
|
// nolint:cyclop
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
@ -116,7 +119,7 @@ func main() {
|
||||||
|
|
||||||
func handleCommandLineFlags() {
|
func handleCommandLineFlags() {
|
||||||
if *newAdminPassword != "" {
|
if *newAdminPassword != "" {
|
||||||
if err := data.SetAdminPassword(*newAdminPassword); err != nil {
|
if err := configRepository.SetAdminPassword(*newAdminPassword); err != nil {
|
||||||
log.Errorln("Error setting your admin password.", err)
|
log.Errorln("Error setting your admin password.", err)
|
||||||
log.Exit(1)
|
log.Exit(1)
|
||||||
} else {
|
} else {
|
||||||
|
@ -138,25 +141,25 @@ func handleCommandLineFlags() {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Saving new web server port number to", portNumber)
|
log.Println("Saving new web server port number to", portNumber)
|
||||||
if err := data.SetHTTPPortNumber(float64(portNumber)); err != nil {
|
if err := configRepository.SetHTTPPortNumber(float64(portNumber)); err != nil {
|
||||||
log.Errorln(err)
|
log.Errorln(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.WebServerPort = data.GetHTTPPortNumber()
|
config.WebServerPort = configRepository.GetHTTPPortNumber()
|
||||||
|
|
||||||
// Set the web server ip
|
// Set the web server ip
|
||||||
if *webServerIPOverride != "" {
|
if *webServerIPOverride != "" {
|
||||||
log.Println("Saving new web server listen IP address to", *webServerIPOverride)
|
log.Println("Saving new web server listen IP address to", *webServerIPOverride)
|
||||||
if err := data.SetHTTPListenAddress(*webServerIPOverride); err != nil {
|
if err := configRepository.SetHTTPListenAddress(*webServerIPOverride); err != nil {
|
||||||
log.Errorln(err)
|
log.Errorln(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
config.WebServerIP = data.GetHTTPListenAddress()
|
config.WebServerIP = configRepository.GetHTTPListenAddress()
|
||||||
|
|
||||||
// Set the rtmp server port
|
// Set the rtmp server port
|
||||||
if *rtmpPortOverride > 0 {
|
if *rtmpPortOverride > 0 {
|
||||||
log.Println("Saving new RTMP server port number to", *rtmpPortOverride)
|
log.Println("Saving new RTMP server port number to", *rtmpPortOverride)
|
||||||
if err := data.SetRTMPPortNumber(float64(*rtmpPortOverride)); err != nil {
|
if err := configRepository.SetRTMPPortNumber(float64(*rtmpPortOverride)); err != nil {
|
||||||
log.Errorln(err)
|
log.Errorln(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package auth
|
package models
|
||||||
|
|
||||||
// Type represents a form of authentication.
|
// Type represents a form of authentication.
|
||||||
type Type string
|
type AuthType string
|
||||||
|
|
||||||
// The different auth types we support.
|
// The different auth types we support.
|
||||||
const (
|
const (
|
||||||
// IndieAuth https://indieauth.spec.indieweb.org/.
|
// IndieAuth https://indieauth.spec.indieweb.org/.
|
||||||
IndieAuth Type = "indieauth"
|
IndieAuth AuthType = "indieauth"
|
||||||
Fediverse Type = "fediverse"
|
Fediverse AuthType = "fediverse"
|
||||||
)
|
)
|
|
@ -1,4 +1,4 @@
|
||||||
package metrics
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
@ -12,7 +12,7 @@ type TimestampedValue struct {
|
||||||
Value float64 `json:"value"`
|
Value float64 `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeTimestampedValuesFromDatapoints(dp []*tstorage.DataPoint) []TimestampedValue {
|
func MakeTimestampedValuesFromDatapoints(dp []*tstorage.DataPoint) []TimestampedValue {
|
||||||
tv := []TimestampedValue{}
|
tv := []TimestampedValue{}
|
||||||
for _, d := range dp {
|
for _, d := range dp {
|
||||||
tv = append(tv, TimestampedValue{Time: time.Unix(d.Timestamp, 0), Value: d.Value})
|
tv = append(tv, TimestampedValue{Time: time.Unix(d.Timestamp, 0), Value: d.Value})
|
|
@ -10,8 +10,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
"github.com/owncast/owncast/core/data"
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
|
=======
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
|
>>>>>>> 659a19bf2 (WIP refactored all storage into repos. Tests pass.)
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -41,6 +45,7 @@ func (c *IndieAuthClient) StartAuthFlow(authHost, userID, accessToken, displayNa
|
||||||
return nil, errors.New("Please try again later. Too many pending requests.")
|
return nil, errors.New("Please try again later. Too many pending requests.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
// Reject any requests to our internal network or loopback
|
// Reject any requests to our internal network or loopback
|
||||||
if utils.IsHostnameInternal(authHost) {
|
if utils.IsHostnameInternal(authHost) {
|
||||||
return nil, errors.New("unable to use provided host")
|
return nil, errors.New("unable to use provided host")
|
||||||
|
@ -58,6 +63,10 @@ func (c *IndieAuthClient) StartAuthFlow(authHost, userID, accessToken, displayNa
|
||||||
}
|
}
|
||||||
|
|
||||||
serverURL := data.GetServerURL()
|
serverURL := data.GetServerURL()
|
||||||
|
=======
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
serverURL := configRepository.GetServerURL()
|
||||||
|
>>>>>>> 659a19bf2 (WIP refactored all storage into repos. Tests pass.)
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
return nil, errors.New("Owncast server URL must be set when using auth")
|
return nil, errors.New("Owncast server URL must be set when using auth")
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,22 @@ package indieauth
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/storage/data"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var indieAuthServer = GetIndieAuthServer()
|
func TestMain(m *testing.M) {
|
||||||
|
_, err := data.NewStore(":memory:")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Run()
|
||||||
|
}
|
||||||
|
|
||||||
func TestLimitGlobalPendingRequests(t *testing.T) {
|
func TestLimitGlobalPendingRequests(t *testing.T) {
|
||||||
|
indieAuthServer := GetIndieAuthServer()
|
||||||
|
|
||||||
// Simulate 10 pending requests
|
// Simulate 10 pending requests
|
||||||
for i := 0; i < maxPendingRequests-1; i++ {
|
for i := 0; i < maxPendingRequests-1; i++ {
|
||||||
cid, _ := utils.GenerateRandomString(10)
|
cid, _ := utils.GenerateRandomString(10)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/teris-io/shortid"
|
"github.com/teris-io/shortid"
|
||||||
)
|
)
|
||||||
|
@ -85,12 +86,14 @@ func (s *IndieAuthServer) CompleteServerAuth(code, redirectURI, clientID string,
|
||||||
return nil, errors.New("code verifier is incorrect")
|
return nil, errors.New("code verifier is incorrect")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
configRepository := configrepository.Get()
|
||||||
|
|
||||||
response := ServerProfileResponse{
|
response := ServerProfileResponse{
|
||||||
Me: data.GetServerURL(),
|
Me: configRepository.GetServerURL(),
|
||||||
Profile: ServerProfile{
|
Profile: ServerProfile{
|
||||||
Name: data.GetServerName(),
|
Name: configRepository.GetServerName(),
|
||||||
URL: data.GetServerURL(),
|
URL: configRepository.GetServerURL(),
|
||||||
Photo: fmt.Sprintf("%s/%s", data.GetServerURL(), data.GetLogoPath()),
|
Photo: fmt.Sprintf("%s/%s", configRepository.GetServerURL(), configRepository.GetLogoPath()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/models"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/db"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _datastore *data.Datastore
|
|
||||||
|
|
||||||
// Setup will initialize auth persistence.
|
|
||||||
func Setup(db *data.Datastore) {
|
|
||||||
_datastore = db
|
|
||||||
|
|
||||||
createTableSQL := `CREATE TABLE IF NOT EXISTS auth (
|
|
||||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
||||||
"user_id" TEXT NOT NULL,
|
|
||||||
"token" TEXT NOT NULL,
|
|
||||||
"type" TEXT NOT NULL,
|
|
||||||
"timestamp" DATE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
||||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
|
||||||
);`
|
|
||||||
_datastore.MustExec(createTableSQL)
|
|
||||||
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_auth_token ON auth (token);`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddAuth will add an external authentication token and type for a user.
|
|
||||||
func AddAuth(userID, authToken string, authType Type) error {
|
|
||||||
return _datastore.GetQueries().AddAuthForUser(context.Background(), db.AddAuthForUserParams{
|
|
||||||
UserID: userID,
|
|
||||||
Token: authToken,
|
|
||||||
Type: string(authType),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUserByAuth will return an existing user given auth details if a user
|
|
||||||
// has previously authenticated with that method.
|
|
||||||
func GetUserByAuth(authToken string, authType Type) *models.User {
|
|
||||||
u, err := _datastore.GetQueries().GetUserByAuth(context.Background(), db.GetUserByAuthParams{
|
|
||||||
Token: authToken,
|
|
||||||
Type: string(authType),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var scopes []string
|
|
||||||
if u.Scopes.Valid {
|
|
||||||
scopes = strings.Split(u.Scopes.String, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &models.User{
|
|
||||||
ID: u.ID,
|
|
||||||
DisplayName: u.DisplayName,
|
|
||||||
DisplayColor: int(u.DisplayColor),
|
|
||||||
CreatedAt: u.CreatedAt.Time,
|
|
||||||
DisabledAt: &u.DisabledAt.Time,
|
|
||||||
PreviousNames: strings.Split(u.PreviousNames.String, ","),
|
|
||||||
NameChangedAt: &u.NamechangedAt.Time,
|
|
||||||
AuthenticatedAt: &u.AuthenticatedAt.Time,
|
|
||||||
Scopes: scopes,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,6 +3,7 @@ package metrics
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -22,18 +23,18 @@ var errorResetDuration = time.Minute * 5
|
||||||
|
|
||||||
const alertingError = "The %s utilization of %f%% could cause problems with video generation and delivery. Visit the documentation at http://owncast.online/docs/troubleshooting/ if you are experiencing issues."
|
const alertingError = "The %s utilization of %f%% could cause problems with video generation and delivery. Visit the documentation at http://owncast.online/docs/troubleshooting/ if you are experiencing issues."
|
||||||
|
|
||||||
func handleAlerting() {
|
func (m *Metrics) handleAlerting() {
|
||||||
handleCPUAlerting()
|
m.handleCPUAlerting()
|
||||||
handleRAMAlerting()
|
m.handleRAMAlerting()
|
||||||
handleDiskAlerting()
|
m.handleDiskAlerting()
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCPUAlerting() {
|
func (m *Metrics) handleCPUAlerting() {
|
||||||
if len(metrics.CPUUtilizations) < 2 {
|
if len(m.metrics.CPUUtilizations) < 2 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
avg := recentAverage(metrics.CPUUtilizations)
|
avg := m.recentAverage(m.metrics.CPUUtilizations)
|
||||||
if avg > maxCPUAlertingThresholdPCT && !inCPUAlertingState {
|
if avg > maxCPUAlertingThresholdPCT && !inCPUAlertingState {
|
||||||
log.Warnf(alertingError, "CPU", avg)
|
log.Warnf(alertingError, "CPU", avg)
|
||||||
inCPUAlertingState = true
|
inCPUAlertingState = true
|
||||||
|
@ -46,12 +47,12 @@ func handleCPUAlerting() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleRAMAlerting() {
|
func (m *Metrics) handleRAMAlerting() {
|
||||||
if len(metrics.RAMUtilizations) < 2 {
|
if len(m.metrics.RAMUtilizations) < 2 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
avg := recentAverage(metrics.RAMUtilizations)
|
avg := m.recentAverage(m.metrics.RAMUtilizations)
|
||||||
if avg > maxRAMAlertingThresholdPCT && !inRAMAlertingState {
|
if avg > maxRAMAlertingThresholdPCT && !inRAMAlertingState {
|
||||||
log.Warnf(alertingError, "memory", avg)
|
log.Warnf(alertingError, "memory", avg)
|
||||||
inRAMAlertingState = true
|
inRAMAlertingState = true
|
||||||
|
@ -64,12 +65,12 @@ func handleRAMAlerting() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleDiskAlerting() {
|
func (m *Metrics) handleDiskAlerting() {
|
||||||
if len(metrics.DiskUtilizations) < 2 {
|
if len(m.metrics.DiskUtilizations) < 2 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
avg := recentAverage(metrics.DiskUtilizations)
|
avg := m.recentAverage(m.metrics.DiskUtilizations)
|
||||||
|
|
||||||
if avg > maxDiskAlertingThresholdPCT && !inDiskAlertingState {
|
if avg > maxDiskAlertingThresholdPCT && !inDiskAlertingState {
|
||||||
log.Warnf(alertingError, "disk", avg)
|
log.Warnf(alertingError, "disk", avg)
|
||||||
|
@ -83,6 +84,6 @@ func handleDiskAlerting() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func recentAverage(values []TimestampedValue) float64 {
|
func (m *Metrics) recentAverage(values []models.TimestampedValue) float64 {
|
||||||
return (values[len(values)-1].Value + values[len(values)-2].Value) / 2
|
return (values[len(values)-1].Value + values[len(values)-2].Value) / 2
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package metrics
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/shirou/gopsutil/v3/cpu"
|
"github.com/shirou/gopsutil/v3/cpu"
|
||||||
"github.com/shirou/gopsutil/v3/disk"
|
"github.com/shirou/gopsutil/v3/disk"
|
||||||
"github.com/shirou/gopsutil/v3/mem"
|
"github.com/shirou/gopsutil/v3/mem"
|
||||||
|
@ -13,9 +14,9 @@ import (
|
||||||
// Max number of metrics we want to keep.
|
// Max number of metrics we want to keep.
|
||||||
const maxCollectionValues = 300
|
const maxCollectionValues = 300
|
||||||
|
|
||||||
func collectCPUUtilization() {
|
func (m *Metrics) collectCPUUtilization() {
|
||||||
if len(metrics.CPUUtilizations) > maxCollectionValues {
|
if len(m.metrics.CPUUtilizations) > maxCollectionValues {
|
||||||
metrics.CPUUtilizations = metrics.CPUUtilizations[1:]
|
m.metrics.CPUUtilizations = m.metrics.CPUUtilizations[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
v, err := cpu.Percent(0, false)
|
v, err := cpu.Percent(0, false)
|
||||||
|
@ -31,29 +32,29 @@ func collectCPUUtilization() {
|
||||||
value = v[0]
|
value = v[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
metricValue := TimestampedValue{time.Now(), value}
|
metricValue := models.TimestampedValue{time.Now(), value}
|
||||||
metrics.CPUUtilizations = append(metrics.CPUUtilizations, metricValue)
|
m.metrics.CPUUtilizations = append(m.metrics.CPUUtilizations, metricValue)
|
||||||
cpuUsage.Set(metricValue.Value)
|
m.cpuUsage.Set(metricValue.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectRAMUtilization() {
|
func (m *Metrics) collectRAMUtilization() {
|
||||||
if len(metrics.RAMUtilizations) > maxCollectionValues {
|
if len(m.metrics.RAMUtilizations) > maxCollectionValues {
|
||||||
metrics.RAMUtilizations = metrics.RAMUtilizations[1:]
|
m.metrics.RAMUtilizations = m.metrics.RAMUtilizations[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
memoryUsage, _ := mem.VirtualMemory()
|
memoryUsage, _ := mem.VirtualMemory()
|
||||||
metricValue := TimestampedValue{time.Now(), memoryUsage.UsedPercent}
|
metricValue := models.TimestampedValue{time.Now(), memoryUsage.UsedPercent}
|
||||||
metrics.RAMUtilizations = append(metrics.RAMUtilizations, metricValue)
|
m.metrics.RAMUtilizations = append(m.metrics.RAMUtilizations, metricValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectDiskUtilization() {
|
func (m *Metrics) collectDiskUtilization() {
|
||||||
path := "./"
|
path := "./"
|
||||||
diskUse, _ := disk.Usage(path)
|
diskUse, _ := disk.Usage(path)
|
||||||
|
|
||||||
if len(metrics.DiskUtilizations) > maxCollectionValues {
|
if len(m.metrics.DiskUtilizations) > maxCollectionValues {
|
||||||
metrics.DiskUtilizations = metrics.DiskUtilizations[1:]
|
m.metrics.DiskUtilizations = m.metrics.DiskUtilizations[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
metricValue := TimestampedValue{time.Now(), diskUse.UsedPercent}
|
metricValue := models.TimestampedValue{time.Now(), diskUse.UsedPercent}
|
||||||
metrics.DiskUtilizations = append(metrics.DiskUtilizations, metricValue)
|
m.metrics.DiskUtilizations = append(m.metrics.DiskUtilizations, metricValue)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/owncast/owncast/core"
|
"github.com/owncast/owncast/core"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -15,60 +16,62 @@ const (
|
||||||
minClientCountForDetails = 3
|
minClientCountForDetails = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var configRepository = configrepository.Get()
|
||||||
|
|
||||||
// GetStreamHealthOverview will return the stream health overview.
|
// GetStreamHealthOverview will return the stream health overview.
|
||||||
func GetStreamHealthOverview() *models.StreamHealthOverview {
|
func (m *Metrics) GetStreamHealthOverview() *models.StreamHealthOverview {
|
||||||
return metrics.streamHealthOverview
|
return m.metrics.streamHealthOverview
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateStreamHealthOverview() {
|
func (m *Metrics) generateStreamHealthOverview() {
|
||||||
// Determine what percentage of total players are represented in our overview.
|
// Determine what percentage of total players are represented in our overview.
|
||||||
totalPlayerCount := len(core.GetActiveViewers())
|
totalPlayerCount := len(core.GetActiveViewers())
|
||||||
if totalPlayerCount == 0 {
|
if totalPlayerCount == 0 {
|
||||||
metrics.streamHealthOverview = nil
|
m.metrics.streamHealthOverview = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pct := getClientErrorHeathyPercentage()
|
pct := m.getClientErrorHeathyPercentage()
|
||||||
if pct < 1 {
|
if pct < 1 {
|
||||||
metrics.streamHealthOverview = nil
|
m.metrics.streamHealthOverview = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
overview := &models.StreamHealthOverview{
|
overview := &models.StreamHealthOverview{
|
||||||
Healthy: pct > healthyPercentageMinValue,
|
Healthy: pct > healthyPercentageMinValue,
|
||||||
HealthyPercentage: pct,
|
HealthyPercentage: pct,
|
||||||
Message: getStreamHealthOverviewMessage(),
|
Message: m.getStreamHealthOverviewMessage(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if totalPlayerCount > 0 && len(windowedBandwidths) > 0 {
|
if totalPlayerCount > 0 && len(m.windowedBandwidths) > 0 {
|
||||||
representation := utils.IntPercentage(len(windowedBandwidths), totalPlayerCount)
|
representation := utils.IntPercentage(len(m.windowedBandwidths), totalPlayerCount)
|
||||||
overview.Representation = representation
|
overview.Representation = representation
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.streamHealthOverview = overview
|
m.metrics.streamHealthOverview = overview
|
||||||
}
|
}
|
||||||
|
|
||||||
func getStreamHealthOverviewMessage() string {
|
func (m *Metrics) getStreamHealthOverviewMessage() string {
|
||||||
if message := wastefulBitrateOverviewMessage(); message != "" {
|
if message := m.wastefulBitrateOverviewMessage(); message != "" {
|
||||||
return message
|
return message
|
||||||
} else if message := cpuUsageHealthOverviewMessage(); message != "" {
|
} else if message := m.cpuUsageHealthOverviewMessage(); message != "" {
|
||||||
return message
|
return message
|
||||||
} else if message := networkSpeedHealthOverviewMessage(); message != "" {
|
} else if message := m.networkSpeedHealthOverviewMessage(); message != "" {
|
||||||
return message
|
return message
|
||||||
} else if message := errorCountHealthOverviewMessage(); message != "" {
|
} else if message := m.errorCountHealthOverviewMessage(); message != "" {
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func networkSpeedHealthOverviewMessage() string {
|
func (m *Metrics) networkSpeedHealthOverviewMessage() string {
|
||||||
type singleVariant struct {
|
type singleVariant struct {
|
||||||
isVideoPassthrough bool
|
isVideoPassthrough bool
|
||||||
bitrate int
|
bitrate int
|
||||||
}
|
}
|
||||||
|
|
||||||
outputVariants := data.GetStreamOutputVariants()
|
outputVariants := configRepository.GetStreamOutputVariants()
|
||||||
|
|
||||||
streamSortVariants := make([]singleVariant, len(outputVariants))
|
streamSortVariants := make([]singleVariant, len(outputVariants))
|
||||||
for i, variant := range outputVariants {
|
for i, variant := range outputVariants {
|
||||||
|
@ -92,7 +95,7 @@ func networkSpeedHealthOverviewMessage() string {
|
||||||
})
|
})
|
||||||
|
|
||||||
lowestSupportedBitrate := float64(streamSortVariants[len(streamSortVariants)-1].bitrate)
|
lowestSupportedBitrate := float64(streamSortVariants[len(streamSortVariants)-1].bitrate)
|
||||||
totalNumberOfClients := len(windowedBandwidths)
|
totalNumberOfClients := len(m.windowedBandwidths)
|
||||||
|
|
||||||
if totalNumberOfClients == 0 {
|
if totalNumberOfClients == 0 {
|
||||||
return ""
|
return ""
|
||||||
|
@ -101,7 +104,7 @@ func networkSpeedHealthOverviewMessage() string {
|
||||||
// Determine healthy status based on bandwidth speeds of clients.
|
// Determine healthy status based on bandwidth speeds of clients.
|
||||||
unhealthyClientCount := 0
|
unhealthyClientCount := 0
|
||||||
|
|
||||||
for _, speed := range windowedBandwidths {
|
for _, speed := range m.windowedBandwidths {
|
||||||
if int(speed) < int(lowestSupportedBitrate*1.1) {
|
if int(speed) < int(lowestSupportedBitrate*1.1) {
|
||||||
unhealthyClientCount++
|
unhealthyClientCount++
|
||||||
}
|
}
|
||||||
|
@ -117,13 +120,13 @@ func networkSpeedHealthOverviewMessage() string {
|
||||||
// wastefulBitrateOverviewMessage attempts to determine if a streamer is sending to
|
// wastefulBitrateOverviewMessage attempts to determine if a streamer is sending to
|
||||||
// Owncast at a bitrate higher than they're streaming to their viewers leading
|
// Owncast at a bitrate higher than they're streaming to their viewers leading
|
||||||
// to wasted CPU by having to compress it.
|
// to wasted CPU by having to compress it.
|
||||||
func wastefulBitrateOverviewMessage() string {
|
func (m *Metrics) wastefulBitrateOverviewMessage() string {
|
||||||
if len(metrics.CPUUtilizations) < 2 {
|
if len(m.metrics.CPUUtilizations) < 2 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only return an alert if the CPU usage is around the max cpu threshold.
|
// Only return an alert if the CPU usage is around the max cpu threshold.
|
||||||
recentCPUUses := metrics.CPUUtilizations[len(metrics.CPUUtilizations)-2:]
|
recentCPUUses := m.metrics.CPUUtilizations[len(m.metrics.CPUUtilizations)-2:]
|
||||||
values := make([]float64, len(recentCPUUses))
|
values := make([]float64, len(recentCPUUses))
|
||||||
for i, val := range recentCPUUses {
|
for i, val := range recentCPUUses {
|
||||||
values[i] = val.Value
|
values[i] = val.Value
|
||||||
|
@ -154,7 +157,7 @@ func wastefulBitrateOverviewMessage() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
outputVariants := data.GetStreamOutputVariants()
|
outputVariants := configRepository.GetStreamOutputVariants()
|
||||||
|
|
||||||
type singleVariant struct {
|
type singleVariant struct {
|
||||||
isVideoPassthrough bool
|
isVideoPassthrough bool
|
||||||
|
@ -190,12 +193,12 @@ func wastefulBitrateOverviewMessage() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func cpuUsageHealthOverviewMessage() string {
|
func (m *Metrics) cpuUsageHealthOverviewMessage() string {
|
||||||
if len(metrics.CPUUtilizations) < 2 {
|
if len(m.metrics.CPUUtilizations) < 2 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
recentCPUUses := metrics.CPUUtilizations[len(metrics.CPUUtilizations)-2:]
|
recentCPUUses := m.metrics.CPUUtilizations[len(m.metrics.CPUUtilizations)-2:]
|
||||||
values := make([]float64, len(recentCPUUses))
|
values := make([]float64, len(recentCPUUses))
|
||||||
for i, val := range recentCPUUses {
|
for i, val := range recentCPUUses {
|
||||||
values[i] = val.Value
|
values[i] = val.Value
|
||||||
|
@ -208,13 +211,13 @@ func cpuUsageHealthOverviewMessage() string {
|
||||||
return fmt.Sprintf("The CPU usage on your server is over %d%%. This may cause video to be provided slower than necessary, causing buffering for your viewers. Consider increasing the resources available or reducing the number of output variants you made available.", maxCPUUsage)
|
return fmt.Sprintf("The CPU usage on your server is over %d%%. This may cause video to be provided slower than necessary, causing buffering for your viewers. Consider increasing the resources available or reducing the number of output variants you made available.", maxCPUUsage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func errorCountHealthOverviewMessage() string {
|
func (m *Metrics) errorCountHealthOverviewMessage() string {
|
||||||
totalNumberOfClients := len(windowedBandwidths)
|
totalNumberOfClients := len(m.windowedBandwidths)
|
||||||
if totalNumberOfClients == 0 {
|
if totalNumberOfClients == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
clientsWithErrors := getClientsWithErrorsCount()
|
clientsWithErrors := m.getClientsWithErrorsCount()
|
||||||
|
|
||||||
if clientsWithErrors == 0 {
|
if clientsWithErrors == 0 {
|
||||||
return ""
|
return ""
|
||||||
|
@ -228,7 +231,7 @@ func errorCountHealthOverviewMessage() string {
|
||||||
healthyPercentage := utils.IntPercentage(clientsWithErrors, totalNumberOfClients)
|
healthyPercentage := utils.IntPercentage(clientsWithErrors, totalNumberOfClients)
|
||||||
|
|
||||||
isUsingPassthrough := false
|
isUsingPassthrough := false
|
||||||
outputVariants := data.GetStreamOutputVariants()
|
outputVariants := configRepository.GetStreamOutputVariants()
|
||||||
for _, variant := range outputVariants {
|
for _, variant := range outputVariants {
|
||||||
if variant.IsVideoPassthrough {
|
if variant.IsVideoPassthrough {
|
||||||
isUsingPassthrough = true
|
isUsingPassthrough = true
|
||||||
|
@ -250,9 +253,9 @@ func errorCountHealthOverviewMessage() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func getClientsWithErrorsCount() int {
|
func (m *Metrics) getClientsWithErrorsCount() int {
|
||||||
clientsWithErrors := 0
|
clientsWithErrors := 0
|
||||||
for _, errors := range windowedErrorCounts {
|
for _, errors := range m.windowedErrorCounts {
|
||||||
if errors > 0 {
|
if errors > 0 {
|
||||||
clientsWithErrors++
|
clientsWithErrors++
|
||||||
}
|
}
|
||||||
|
@ -260,13 +263,13 @@ func getClientsWithErrorsCount() int {
|
||||||
return clientsWithErrors
|
return clientsWithErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
func getClientErrorHeathyPercentage() int {
|
func (m *Metrics) getClientErrorHeathyPercentage() int {
|
||||||
totalNumberOfClients := len(windowedErrorCounts)
|
totalNumberOfClients := len(m.windowedErrorCounts)
|
||||||
if totalNumberOfClients == 0 {
|
if totalNumberOfClients == 0 {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
clientsWithErrors := getClientsWithErrorsCount()
|
clientsWithErrors := m.getClientsWithErrorsCount()
|
||||||
|
|
||||||
if clientsWithErrors == 0 {
|
if clientsWithErrors == 0 {
|
||||||
return 100
|
return 100
|
||||||
|
|
|
@ -6,8 +6,28 @@ import (
|
||||||
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/owncast/owncast/services/config"
|
"github.com/owncast/owncast/services/config"
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Metrics struct {
|
||||||
|
metrics *CollectedMetrics
|
||||||
|
getStatus func() models.Status
|
||||||
|
windowedErrorCounts map[string]float64
|
||||||
|
windowedQualityVariantChanges map[string]float64
|
||||||
|
windowedBandwidths map[string]float64
|
||||||
|
windowedLatencies map[string]float64
|
||||||
|
windowedDownloadDurations map[string]float64
|
||||||
|
|
||||||
|
// Prometheus
|
||||||
|
labels map[string]string
|
||||||
|
activeViewerCount prometheus.Gauge
|
||||||
|
activeChatClientCount prometheus.Gauge
|
||||||
|
cpuUsage prometheus.Gauge
|
||||||
|
chatUserCount prometheus.Gauge
|
||||||
|
currentChatMessageCount prometheus.Gauge
|
||||||
|
playbackErrorCount prometheus.Gauge
|
||||||
|
}
|
||||||
|
|
||||||
// How often we poll for updates.
|
// How often we poll for updates.
|
||||||
const (
|
const (
|
||||||
hardwareMetricsPollingInterval = 2 * time.Minute
|
hardwareMetricsPollingInterval = 2 * time.Minute
|
||||||
|
@ -25,81 +45,98 @@ const (
|
||||||
type CollectedMetrics struct {
|
type CollectedMetrics struct {
|
||||||
streamHealthOverview *models.StreamHealthOverview
|
streamHealthOverview *models.StreamHealthOverview
|
||||||
|
|
||||||
medianSegmentDownloadSeconds []TimestampedValue `json:"-"`
|
medianSegmentDownloadSeconds []models.TimestampedValue `json:"-"`
|
||||||
maximumSegmentDownloadSeconds []TimestampedValue `json:"-"`
|
maximumSegmentDownloadSeconds []models.TimestampedValue `json:"-"`
|
||||||
DiskUtilizations []TimestampedValue `json:"disk"`
|
DiskUtilizations []models.TimestampedValue `json:"disk"`
|
||||||
|
|
||||||
errorCount []TimestampedValue `json:"-"`
|
errorCount []models.TimestampedValue `json:"-"`
|
||||||
lowestBitrate []TimestampedValue `json:"-"`
|
lowestBitrate []models.TimestampedValue `json:"-"`
|
||||||
medianBitrate []TimestampedValue `json:"-"`
|
medianBitrate []models.TimestampedValue `json:"-"`
|
||||||
RAMUtilizations []TimestampedValue `json:"memory"`
|
RAMUtilizations []models.TimestampedValue `json:"memory"`
|
||||||
|
|
||||||
CPUUtilizations []TimestampedValue `json:"cpu"`
|
CPUUtilizations []models.TimestampedValue `json:"cpu"`
|
||||||
highestBitrate []TimestampedValue `json:"-"`
|
highestBitrate []models.TimestampedValue `json:"-"`
|
||||||
|
|
||||||
minimumSegmentDownloadSeconds []TimestampedValue `json:"-"`
|
minimumSegmentDownloadSeconds []models.TimestampedValue `json:"-"`
|
||||||
|
|
||||||
minimumLatency []TimestampedValue `json:"-"`
|
minimumLatency []models.TimestampedValue `json:"-"`
|
||||||
maximumLatency []TimestampedValue `json:"-"`
|
maximumLatency []models.TimestampedValue `json:"-"`
|
||||||
medianLatency []TimestampedValue `json:"-"`
|
medianLatency []models.TimestampedValue `json:"-"`
|
||||||
|
|
||||||
qualityVariantChanges []TimestampedValue `json:"-"`
|
qualityVariantChanges []models.TimestampedValue `json:"-"`
|
||||||
|
|
||||||
m sync.Mutex `json:"-"`
|
m sync.Mutex `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metrics is the shared Metrics instance.
|
// New will return a new Metrics instance.
|
||||||
var metrics *CollectedMetrics
|
func New() *Metrics {
|
||||||
|
return &Metrics{
|
||||||
|
windowedErrorCounts: map[string]float64{},
|
||||||
|
windowedQualityVariantChanges: map[string]float64{},
|
||||||
|
windowedBandwidths: map[string]float64{},
|
||||||
|
windowedLatencies: map[string]float64{},
|
||||||
|
windowedDownloadDurations: map[string]float64{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var _getStatus func() models.Status
|
// Metrics is the shared Metrics instance.
|
||||||
|
|
||||||
// Start will begin the metrics collection and alerting.
|
// Start will begin the metrics collection and alerting.
|
||||||
func Start(getStatus func() models.Status) {
|
func (m *Metrics) Start(getStatus func() models.Status) {
|
||||||
_getStatus = getStatus
|
m.getStatus = getStatus
|
||||||
host := data.GetServerURL()
|
host := configRepository.GetServerURL()
|
||||||
if host == "" {
|
if host == "" {
|
||||||
host = "unknown"
|
host = "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
c := config.GetConfig()
|
c := config.GetConfig()
|
||||||
|
|
||||||
labels = map[string]string{
|
m.labels = map[string]string{
|
||||||
"version": c.VersionNumber,
|
"version": c.VersionNumber,
|
||||||
"host": host,
|
"host": host,
|
||||||
}
|
}
|
||||||
|
|
||||||
setupPrometheusCollectors()
|
m.setupPrometheusCollectors()
|
||||||
|
|
||||||
metrics = new(CollectedMetrics)
|
m.metrics = new(CollectedMetrics)
|
||||||
go startViewerCollectionMetrics()
|
go m.startViewerCollectionMetrics()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for range time.Tick(hardwareMetricsPollingInterval) {
|
for range time.Tick(hardwareMetricsPollingInterval) {
|
||||||
handlePolling()
|
m.handlePolling()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for range time.Tick(playbackMetricsPollingInterval) {
|
for range time.Tick(playbackMetricsPollingInterval) {
|
||||||
handlePlaybackPolling()
|
m.handlePlaybackPolling()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePolling() {
|
func (m *Metrics) handlePolling() {
|
||||||
metrics.m.Lock()
|
m.metrics.m.Lock()
|
||||||
defer metrics.m.Unlock()
|
defer m.metrics.m.Unlock()
|
||||||
|
|
||||||
// Collect hardware stats
|
// Collect hardware stats
|
||||||
collectCPUUtilization()
|
m.collectCPUUtilization()
|
||||||
collectRAMUtilization()
|
m.collectRAMUtilization()
|
||||||
collectDiskUtilization()
|
m.collectDiskUtilization()
|
||||||
|
|
||||||
// Alerting
|
// Alerting
|
||||||
handleAlerting()
|
m.handleAlerting()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMetrics will return the collected metrics.
|
// GetMetrics will return the collected metrics.
|
||||||
func GetMetrics() *CollectedMetrics {
|
func (m *Metrics) GetMetrics() *CollectedMetrics {
|
||||||
return metrics
|
return m.metrics
|
||||||
|
}
|
||||||
|
|
||||||
|
var temporaryGlobalInstance *Metrics
|
||||||
|
|
||||||
|
func Get() *Metrics {
|
||||||
|
if temporaryGlobalInstance == nil {
|
||||||
|
temporaryGlobalInstance = new(Metrics)
|
||||||
|
}
|
||||||
|
return temporaryGlobalInstance
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,318 +5,310 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core"
|
"github.com/owncast/owncast/core"
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Playback error counts reported since the last time we collected metrics.
|
func (m *Metrics) handlePlaybackPolling() {
|
||||||
var (
|
m.metrics.m.Lock()
|
||||||
windowedErrorCounts = map[string]float64{}
|
defer m.metrics.m.Unlock()
|
||||||
windowedQualityVariantChanges = map[string]float64{}
|
|
||||||
windowedBandwidths = map[string]float64{}
|
|
||||||
windowedLatencies = map[string]float64{}
|
|
||||||
windowedDownloadDurations = map[string]float64{}
|
|
||||||
)
|
|
||||||
|
|
||||||
func handlePlaybackPolling() {
|
|
||||||
metrics.m.Lock()
|
|
||||||
defer metrics.m.Unlock()
|
|
||||||
|
|
||||||
// Make sure this is fired first before all the values get cleared below.
|
// Make sure this is fired first before all the values get cleared below.
|
||||||
if _getStatus().Online {
|
if m.getStatus().Online {
|
||||||
generateStreamHealthOverview()
|
m.generateStreamHealthOverview()
|
||||||
}
|
}
|
||||||
|
|
||||||
collectPlaybackErrorCount()
|
m.collectPlaybackErrorCount()
|
||||||
collectLatencyValues()
|
m.collectLatencyValues()
|
||||||
collectSegmentDownloadDuration()
|
m.collectSegmentDownloadDuration()
|
||||||
collectLowestBandwidth()
|
m.collectLowestBandwidth()
|
||||||
collectQualityVariantChanges()
|
m.collectQualityVariantChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterPlaybackErrorCount will add to the windowed playback error count.
|
// RegisterPlaybackErrorCount will add to the windowed playback error count.
|
||||||
func RegisterPlaybackErrorCount(clientID string, count float64) {
|
func (m *Metrics) RegisterPlaybackErrorCount(clientID string, count float64) {
|
||||||
metrics.m.Lock()
|
m.metrics.m.Lock()
|
||||||
defer metrics.m.Unlock()
|
defer m.metrics.m.Unlock()
|
||||||
windowedErrorCounts[clientID] = count
|
m.windowedErrorCounts[clientID] = count
|
||||||
// windowedErrorCounts = append(windowedErrorCounts, count)
|
// windowedErrorCounts = append(windowedErrorCounts, count)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterQualityVariantChangesCount will add to the windowed quality variant
|
// RegisterQualityVariantChangesCount will add to the windowed quality variant
|
||||||
// change count.
|
// change count.
|
||||||
func RegisterQualityVariantChangesCount(clientID string, count float64) {
|
func (m *Metrics) RegisterQualityVariantChangesCount(clientID string, count float64) {
|
||||||
metrics.m.Lock()
|
m.metrics.m.Lock()
|
||||||
defer metrics.m.Unlock()
|
defer m.metrics.m.Unlock()
|
||||||
windowedQualityVariantChanges[clientID] = count
|
m.windowedQualityVariantChanges[clientID] = count
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterPlayerBandwidth will add to the windowed playback bandwidth.
|
// RegisterPlayerBandwidth will add to the windowed playback bandwidth.
|
||||||
func RegisterPlayerBandwidth(clientID string, kbps float64) {
|
func (m *Metrics) RegisterPlayerBandwidth(clientID string, kbps float64) {
|
||||||
metrics.m.Lock()
|
m.metrics.m.Lock()
|
||||||
defer metrics.m.Unlock()
|
defer m.metrics.m.Unlock()
|
||||||
windowedBandwidths[clientID] = kbps
|
m.windowedBandwidths[clientID] = kbps
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterPlayerLatency will add to the windowed player latency values.
|
// RegisterPlayerLatency will add to the windowed player latency values.
|
||||||
func RegisterPlayerLatency(clientID string, seconds float64) {
|
func (m *Metrics) RegisterPlayerLatency(clientID string, seconds float64) {
|
||||||
metrics.m.Lock()
|
m.metrics.m.Lock()
|
||||||
defer metrics.m.Unlock()
|
defer m.metrics.m.Unlock()
|
||||||
windowedLatencies[clientID] = seconds
|
m.windowedLatencies[clientID] = seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterPlayerSegmentDownloadDuration will add to the windowed player segment
|
// RegisterPlayerSegmentDownloadDuration will add to the windowed player segment
|
||||||
// download duration values.
|
// download duration values.
|
||||||
func RegisterPlayerSegmentDownloadDuration(clientID string, seconds float64) {
|
func (m *Metrics) RegisterPlayerSegmentDownloadDuration(clientID string, seconds float64) {
|
||||||
metrics.m.Lock()
|
m.metrics.m.Lock()
|
||||||
defer metrics.m.Unlock()
|
defer m.metrics.m.Unlock()
|
||||||
windowedDownloadDurations[clientID] = seconds
|
m.windowedDownloadDurations[clientID] = seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectPlaybackErrorCount will take all of the error counts each individual
|
// collectPlaybackErrorCount will take all of the error counts each individual
|
||||||
// player reported and average them into a single metric. This is done so
|
// player reported and average them into a single metric. This is done so
|
||||||
// one person with bad connectivity doesn't make it look like everything is
|
// one person with bad connectivity doesn't make it look like everything is
|
||||||
// horrible for everyone.
|
// horrible for everyone.
|
||||||
func collectPlaybackErrorCount() {
|
func (m *Metrics) collectPlaybackErrorCount() {
|
||||||
valueSlice := utils.Float64MapToSlice(windowedErrorCounts)
|
valueSlice := utils.Float64MapToSlice(m.windowedErrorCounts)
|
||||||
count := utils.Sum(valueSlice)
|
count := utils.Sum(valueSlice)
|
||||||
windowedErrorCounts = map[string]float64{}
|
m.windowedErrorCounts = map[string]float64{}
|
||||||
|
|
||||||
metrics.errorCount = append(metrics.errorCount, TimestampedValue{
|
m.metrics.errorCount = append(m.metrics.errorCount, models.TimestampedValue{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Value: count,
|
Value: count,
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(metrics.errorCount) > maxCollectionValues {
|
if len(m.metrics.errorCount) > maxCollectionValues {
|
||||||
metrics.errorCount = metrics.errorCount[1:]
|
m.metrics.errorCount = m.metrics.errorCount[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to Prometheus collector.
|
// Save to Prometheus collector.
|
||||||
playbackErrorCount.Set(count)
|
m.playbackErrorCount.Set(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectSegmentDownloadDuration() {
|
func (m *Metrics) collectSegmentDownloadDuration() {
|
||||||
median := 0.0
|
median := 0.0
|
||||||
max := 0.0
|
max := 0.0
|
||||||
min := 0.0
|
min := 0.0
|
||||||
|
|
||||||
valueSlice := utils.Float64MapToSlice(windowedDownloadDurations)
|
valueSlice := utils.Float64MapToSlice(m.windowedDownloadDurations)
|
||||||
|
|
||||||
if len(valueSlice) > 0 {
|
if len(valueSlice) > 0 {
|
||||||
median = utils.Median(valueSlice)
|
median = utils.Median(valueSlice)
|
||||||
min, max = utils.MinMax(valueSlice)
|
min, max = utils.MinMax(valueSlice)
|
||||||
windowedDownloadDurations = map[string]float64{}
|
m.windowedDownloadDurations = map[string]float64{}
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.medianSegmentDownloadSeconds = append(metrics.medianSegmentDownloadSeconds, TimestampedValue{
|
m.metrics.medianSegmentDownloadSeconds = append(m.metrics.medianSegmentDownloadSeconds, models.TimestampedValue{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Value: median,
|
Value: median,
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(metrics.medianSegmentDownloadSeconds) > maxCollectionValues {
|
if len(m.metrics.medianSegmentDownloadSeconds) > maxCollectionValues {
|
||||||
metrics.medianSegmentDownloadSeconds = metrics.medianSegmentDownloadSeconds[1:]
|
m.metrics.medianSegmentDownloadSeconds = m.metrics.medianSegmentDownloadSeconds[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.minimumSegmentDownloadSeconds = append(metrics.minimumSegmentDownloadSeconds, TimestampedValue{
|
m.metrics.minimumSegmentDownloadSeconds = append(m.metrics.minimumSegmentDownloadSeconds, models.TimestampedValue{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Value: min,
|
Value: min,
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(metrics.minimumSegmentDownloadSeconds) > maxCollectionValues {
|
if len(m.metrics.minimumSegmentDownloadSeconds) > maxCollectionValues {
|
||||||
metrics.minimumSegmentDownloadSeconds = metrics.minimumSegmentDownloadSeconds[1:]
|
m.metrics.minimumSegmentDownloadSeconds = m.metrics.minimumSegmentDownloadSeconds[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.maximumSegmentDownloadSeconds = append(metrics.maximumSegmentDownloadSeconds, TimestampedValue{
|
m.metrics.maximumSegmentDownloadSeconds = append(m.metrics.maximumSegmentDownloadSeconds, models.TimestampedValue{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Value: max,
|
Value: max,
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(metrics.maximumSegmentDownloadSeconds) > maxCollectionValues {
|
if len(m.metrics.maximumSegmentDownloadSeconds) > maxCollectionValues {
|
||||||
metrics.maximumSegmentDownloadSeconds = metrics.maximumSegmentDownloadSeconds[1:]
|
m.metrics.maximumSegmentDownloadSeconds = m.metrics.maximumSegmentDownloadSeconds[1:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMedianDownloadDurationsOverTime will return a window of durations errors over time.
|
// GetMedianDownloadDurationsOverTime will return a window of durations errors over time.
|
||||||
func GetMedianDownloadDurationsOverTime() []TimestampedValue {
|
func (m *Metrics) GetMedianDownloadDurationsOverTime() []models.TimestampedValue {
|
||||||
return metrics.medianSegmentDownloadSeconds
|
return m.metrics.medianSegmentDownloadSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMaximumDownloadDurationsOverTime will return a maximum durations errors over time.
|
// GetMaximumDownloadDurationsOverTime will return a maximum durations errors over time.
|
||||||
func GetMaximumDownloadDurationsOverTime() []TimestampedValue {
|
func (m *Metrics) GetMaximumDownloadDurationsOverTime() []models.TimestampedValue {
|
||||||
return metrics.maximumSegmentDownloadSeconds
|
return m.metrics.maximumSegmentDownloadSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMinimumDownloadDurationsOverTime will return a maximum durations errors over time.
|
// GetMinimumDownloadDurationsOverTime will return a maximum durations errors over time.
|
||||||
func GetMinimumDownloadDurationsOverTime() []TimestampedValue {
|
func (m *Metrics) GetMinimumDownloadDurationsOverTime() []models.TimestampedValue {
|
||||||
return metrics.minimumSegmentDownloadSeconds
|
return m.metrics.minimumSegmentDownloadSeconds
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPlaybackErrorCountOverTime will return a window of playback errors over time.
|
// GetPlaybackErrorCountOverTime will return a window of playback errors over time.
|
||||||
func GetPlaybackErrorCountOverTime() []TimestampedValue {
|
func (m *Metrics) GetPlaybackErrorCountOverTime() []models.TimestampedValue {
|
||||||
return metrics.errorCount
|
return m.metrics.errorCount
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectLatencyValues() {
|
func (m *Metrics) collectLatencyValues() {
|
||||||
median := 0.0
|
median := 0.0
|
||||||
min := 0.0
|
min := 0.0
|
||||||
max := 0.0
|
max := 0.0
|
||||||
|
|
||||||
valueSlice := utils.Float64MapToSlice(windowedLatencies)
|
valueSlice := utils.Float64MapToSlice(m.windowedLatencies)
|
||||||
windowedLatencies = map[string]float64{}
|
m.windowedLatencies = map[string]float64{}
|
||||||
|
|
||||||
if len(valueSlice) > 0 {
|
if len(valueSlice) > 0 {
|
||||||
median = utils.Median(valueSlice)
|
median = utils.Median(valueSlice)
|
||||||
min, max = utils.MinMax(valueSlice)
|
min, max = utils.MinMax(valueSlice)
|
||||||
windowedLatencies = map[string]float64{}
|
m.windowedLatencies = map[string]float64{}
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.medianLatency = append(metrics.medianLatency, TimestampedValue{
|
m.metrics.medianLatency = append(m.metrics.medianLatency, models.TimestampedValue{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Value: median,
|
Value: median,
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(metrics.medianLatency) > maxCollectionValues {
|
if len(m.metrics.medianLatency) > maxCollectionValues {
|
||||||
metrics.medianLatency = metrics.medianLatency[1:]
|
m.metrics.medianLatency = m.metrics.medianLatency[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.minimumLatency = append(metrics.minimumLatency, TimestampedValue{
|
m.metrics.minimumLatency = append(m.metrics.minimumLatency, models.TimestampedValue{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Value: min,
|
Value: min,
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(metrics.minimumLatency) > maxCollectionValues {
|
if len(m.metrics.minimumLatency) > maxCollectionValues {
|
||||||
metrics.minimumLatency = metrics.minimumLatency[1:]
|
m.metrics.minimumLatency = m.metrics.minimumLatency[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.maximumLatency = append(metrics.maximumLatency, TimestampedValue{
|
m.metrics.maximumLatency = append(m.metrics.maximumLatency, models.TimestampedValue{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Value: max,
|
Value: max,
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(metrics.maximumLatency) > maxCollectionValues {
|
if len(m.metrics.maximumLatency) > maxCollectionValues {
|
||||||
metrics.maximumLatency = metrics.maximumLatency[1:]
|
m.metrics.maximumLatency = m.metrics.maximumLatency[1:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMedianLatencyOverTime will return the median latency values over time.
|
// GetMedianLatencyOverTime will return the median latency values over time.
|
||||||
func GetMedianLatencyOverTime() []TimestampedValue {
|
func (m *Metrics) GetMedianLatencyOverTime() []models.TimestampedValue {
|
||||||
if len(metrics.medianLatency) == 0 {
|
if len(m.metrics.medianLatency) == 0 {
|
||||||
return []TimestampedValue{}
|
return []models.TimestampedValue{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return metrics.medianLatency
|
return m.metrics.medianLatency
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMinimumLatencyOverTime will return the min latency values over time.
|
// GetMinimumLatencyOverTime will return the min latency values over time.
|
||||||
func GetMinimumLatencyOverTime() []TimestampedValue {
|
func (m *Metrics) GetMinimumLatencyOverTime() []models.TimestampedValue {
|
||||||
if len(metrics.minimumLatency) == 0 {
|
if len(m.metrics.minimumLatency) == 0 {
|
||||||
return []TimestampedValue{}
|
return []models.TimestampedValue{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return metrics.minimumLatency
|
return m.metrics.minimumLatency
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMaximumLatencyOverTime will return the max latency values over time.
|
// GetMaximumLatencyOverTime will return the max latency values over time.
|
||||||
func GetMaximumLatencyOverTime() []TimestampedValue {
|
func (m *Metrics) GetMaximumLatencyOverTime() []models.TimestampedValue {
|
||||||
if len(metrics.maximumLatency) == 0 {
|
if len(m.metrics.maximumLatency) == 0 {
|
||||||
return []TimestampedValue{}
|
return []models.TimestampedValue{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return metrics.maximumLatency
|
return m.metrics.maximumLatency
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectLowestBandwidth will collect the bandwidth currently collected
|
// collectLowestBandwidth will collect the bandwidth currently collected
|
||||||
// so we can report to the streamer the worst possible streaming condition
|
// so we can report to the streamer the worst possible streaming condition
|
||||||
// being experienced.
|
// being experienced.
|
||||||
func collectLowestBandwidth() {
|
func (m *Metrics) collectLowestBandwidth() {
|
||||||
min := 0.0
|
min := 0.0
|
||||||
median := 0.0
|
median := 0.0
|
||||||
max := 0.0
|
max := 0.0
|
||||||
|
|
||||||
valueSlice := utils.Float64MapToSlice(windowedBandwidths)
|
valueSlice := utils.Float64MapToSlice(m.windowedBandwidths)
|
||||||
|
|
||||||
if len(windowedBandwidths) > 0 {
|
if len(m.windowedBandwidths) > 0 {
|
||||||
min, max = utils.MinMax(valueSlice)
|
min, max = utils.MinMax(valueSlice)
|
||||||
min = math.Round(min)
|
min = math.Round(min)
|
||||||
max = math.Round(max)
|
max = math.Round(max)
|
||||||
median = utils.Median(valueSlice)
|
median = utils.Median(valueSlice)
|
||||||
windowedBandwidths = map[string]float64{}
|
m.windowedBandwidths = map[string]float64{}
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.lowestBitrate = append(metrics.lowestBitrate, TimestampedValue{
|
m.metrics.lowestBitrate = append(m.metrics.lowestBitrate, models.TimestampedValue{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Value: math.Round(min),
|
Value: math.Round(min),
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(metrics.lowestBitrate) > maxCollectionValues {
|
if len(m.metrics.lowestBitrate) > maxCollectionValues {
|
||||||
metrics.lowestBitrate = metrics.lowestBitrate[1:]
|
m.metrics.lowestBitrate = m.metrics.lowestBitrate[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.medianBitrate = append(metrics.medianBitrate, TimestampedValue{
|
m.metrics.medianBitrate = append(m.metrics.medianBitrate, models.TimestampedValue{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Value: math.Round(median),
|
Value: math.Round(median),
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(metrics.medianBitrate) > maxCollectionValues {
|
if len(m.metrics.medianBitrate) > maxCollectionValues {
|
||||||
metrics.medianBitrate = metrics.medianBitrate[1:]
|
m.metrics.medianBitrate = m.metrics.medianBitrate[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.highestBitrate = append(metrics.highestBitrate, TimestampedValue{
|
m.metrics.highestBitrate = append(m.metrics.highestBitrate, models.TimestampedValue{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Value: math.Round(max),
|
Value: math.Round(max),
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(metrics.highestBitrate) > maxCollectionValues {
|
if len(m.metrics.highestBitrate) > maxCollectionValues {
|
||||||
metrics.highestBitrate = metrics.highestBitrate[1:]
|
m.metrics.highestBitrate = m.metrics.highestBitrate[1:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSlowestDownloadRateOverTime will return the collected lowest bandwidth values
|
// GetSlowestDownloadRateOverTime will return the collected lowest bandwidth values
|
||||||
// over time.
|
// over time.
|
||||||
func GetSlowestDownloadRateOverTime() []TimestampedValue {
|
func (m *Metrics) GetSlowestDownloadRateOverTime() []models.TimestampedValue {
|
||||||
if len(metrics.lowestBitrate) == 0 {
|
if len(m.metrics.lowestBitrate) == 0 {
|
||||||
return []TimestampedValue{}
|
return []models.TimestampedValue{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return metrics.lowestBitrate
|
return m.metrics.lowestBitrate
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMedianDownloadRateOverTime will return the collected median bandwidth values.
|
// GetMedianDownloadRateOverTime will return the collected median bandwidth values.
|
||||||
func GetMedianDownloadRateOverTime() []TimestampedValue {
|
func (m *Metrics) GetMedianDownloadRateOverTime() []models.TimestampedValue {
|
||||||
if len(metrics.medianBitrate) == 0 {
|
if len(m.metrics.medianBitrate) == 0 {
|
||||||
return []TimestampedValue{}
|
return []models.TimestampedValue{}
|
||||||
}
|
}
|
||||||
return metrics.medianBitrate
|
return m.metrics.medianBitrate
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMaximumDownloadRateOverTime will return the collected maximum bandwidth values.
|
// GetMaximumDownloadRateOverTime will return the collected maximum bandwidth values.
|
||||||
func GetMaximumDownloadRateOverTime() []TimestampedValue {
|
func (m *Metrics) GetMaximumDownloadRateOverTime() []models.TimestampedValue {
|
||||||
if len(metrics.maximumLatency) == 0 {
|
if len(m.metrics.maximumLatency) == 0 {
|
||||||
return []TimestampedValue{}
|
return []models.TimestampedValue{}
|
||||||
}
|
}
|
||||||
return metrics.maximumLatency
|
return m.metrics.maximumLatency
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMinimumDownloadRateOverTime will return the collected minimum bandwidth values.
|
// GetMinimumDownloadRateOverTime will return the collected minimum bandwidth values.
|
||||||
func GetMinimumDownloadRateOverTime() []TimestampedValue {
|
func (m *Metrics) GetMinimumDownloadRateOverTime() []models.TimestampedValue {
|
||||||
if len(metrics.minimumLatency) == 0 {
|
if len(m.metrics.minimumLatency) == 0 {
|
||||||
return []TimestampedValue{}
|
return []models.TimestampedValue{}
|
||||||
}
|
}
|
||||||
return metrics.minimumLatency
|
return m.metrics.minimumLatency
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMaxDownloadRateOverTime will return the collected highest bandwidth values.
|
// GetMaxDownloadRateOverTime will return the collected highest bandwidth values.
|
||||||
func GetMaxDownloadRateOverTime() []TimestampedValue {
|
func (m *Metrics) GetMaxDownloadRateOverTime() []models.TimestampedValue {
|
||||||
if len(metrics.highestBitrate) == 0 {
|
if len(m.metrics.highestBitrate) == 0 {
|
||||||
return []TimestampedValue{}
|
return []models.TimestampedValue{}
|
||||||
}
|
}
|
||||||
return metrics.highestBitrate
|
return m.metrics.highestBitrate
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectQualityVariantChanges() {
|
func (m *Metrics) collectQualityVariantChanges() {
|
||||||
valueSlice := utils.Float64MapToSlice(windowedQualityVariantChanges)
|
valueSlice := utils.Float64MapToSlice(m.windowedQualityVariantChanges)
|
||||||
count := utils.Sum(valueSlice)
|
count := utils.Sum(valueSlice)
|
||||||
windowedQualityVariantChanges = map[string]float64{}
|
m.windowedQualityVariantChanges = map[string]float64{}
|
||||||
|
|
||||||
metrics.qualityVariantChanges = append(metrics.qualityVariantChanges, TimestampedValue{
|
m.metrics.qualityVariantChanges = append(m.metrics.qualityVariantChanges, models.TimestampedValue{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
Value: count,
|
Value: count,
|
||||||
})
|
})
|
||||||
|
@ -324,14 +316,14 @@ func collectQualityVariantChanges() {
|
||||||
|
|
||||||
// GetQualityVariantChangesOverTime will return the collected quality variant
|
// GetQualityVariantChangesOverTime will return the collected quality variant
|
||||||
// changes.
|
// changes.
|
||||||
func GetQualityVariantChangesOverTime() []TimestampedValue {
|
func (m *Metrics) GetQualityVariantChangesOverTime() []models.TimestampedValue {
|
||||||
return metrics.qualityVariantChanges
|
return m.metrics.qualityVariantChanges
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPlaybackMetricsRepresentation returns what percentage of all known players
|
// GetPlaybackMetricsRepresentation returns what percentage of all known players
|
||||||
// the metrics represent.
|
// the metrics represent.
|
||||||
func GetPlaybackMetricsRepresentation() int {
|
func (m *Metrics) GetPlaybackMetricsRepresentation() int {
|
||||||
totalPlayerCount := len(core.GetActiveViewers())
|
totalPlayerCount := len(core.GetActiveViewers())
|
||||||
representation := utils.IntPercentage(len(windowedBandwidths), totalPlayerCount)
|
representation := utils.IntPercentage(len(m.windowedBandwidths), totalPlayerCount)
|
||||||
return representation
|
return representation
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,51 +5,41 @@ import (
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
func (m *Metrics) setupPrometheusCollectors() {
|
||||||
labels map[string]string
|
|
||||||
activeViewerCount prometheus.Gauge
|
|
||||||
activeChatClientCount prometheus.Gauge
|
|
||||||
cpuUsage prometheus.Gauge
|
|
||||||
chatUserCount prometheus.Gauge
|
|
||||||
currentChatMessageCount prometheus.Gauge
|
|
||||||
playbackErrorCount prometheus.Gauge
|
|
||||||
)
|
|
||||||
|
|
||||||
func setupPrometheusCollectors() {
|
|
||||||
// Setup the Prometheus collectors.
|
// Setup the Prometheus collectors.
|
||||||
activeViewerCount = promauto.NewGauge(prometheus.GaugeOpts{
|
m.activeViewerCount = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
Name: "owncast_instance_active_viewer_count",
|
Name: "owncast_instance_active_viewer_count",
|
||||||
Help: "The number of viewers.",
|
Help: "The number of viewers.",
|
||||||
ConstLabels: labels,
|
ConstLabels: m.labels,
|
||||||
})
|
})
|
||||||
|
|
||||||
activeChatClientCount = promauto.NewGauge(prometheus.GaugeOpts{
|
m.activeChatClientCount = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
Name: "owncast_instance_active_chat_client_count",
|
Name: "owncast_instance_active_chat_client_count",
|
||||||
Help: "The number of connected chat clients.",
|
Help: "The number of connected chat clients.",
|
||||||
ConstLabels: labels,
|
ConstLabels: m.labels,
|
||||||
})
|
})
|
||||||
|
|
||||||
chatUserCount = promauto.NewGauge(prometheus.GaugeOpts{
|
m.chatUserCount = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
Name: "owncast_instance_total_chat_users",
|
Name: "owncast_instance_total_chat_users",
|
||||||
Help: "The total number of chat users on this Owncast instance.",
|
Help: "The total number of chat users on this Owncast instance.",
|
||||||
ConstLabels: labels,
|
ConstLabels: m.labels,
|
||||||
})
|
})
|
||||||
|
|
||||||
currentChatMessageCount = promauto.NewGauge(prometheus.GaugeOpts{
|
m.currentChatMessageCount = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
Name: "owncast_instance_current_chat_message_count",
|
Name: "owncast_instance_current_chat_message_count",
|
||||||
Help: "The number of chat messages currently saved before cleanup.",
|
Help: "The number of chat messages currently saved before cleanup.",
|
||||||
ConstLabels: labels,
|
ConstLabels: m.labels,
|
||||||
})
|
})
|
||||||
|
|
||||||
playbackErrorCount = promauto.NewGauge(prometheus.GaugeOpts{
|
m.playbackErrorCount = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
Name: "owncast_instance_playback_error_count",
|
Name: "owncast_instance_playback_error_count",
|
||||||
Help: "Errors collected from players within this window",
|
Help: "Errors collected from players within this window",
|
||||||
ConstLabels: labels,
|
ConstLabels: m.labels,
|
||||||
})
|
})
|
||||||
|
|
||||||
cpuUsage = promauto.NewGauge(prometheus.GaugeOpts{
|
m.cpuUsage = promauto.NewGauge(prometheus.GaugeOpts{
|
||||||
Name: "owncast_instance_cpu_usage",
|
Name: "owncast_instance_cpu_usage",
|
||||||
Help: "CPU usage as seen internally to Owncast.",
|
Help: "CPU usage as seen internally to Owncast.",
|
||||||
ConstLabels: labels,
|
ConstLabels: m.labels,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,27 +6,30 @@ import (
|
||||||
"github.com/nakabonne/tstorage"
|
"github.com/nakabonne/tstorage"
|
||||||
"github.com/owncast/owncast/core"
|
"github.com/owncast/owncast/core"
|
||||||
"github.com/owncast/owncast/core/chat"
|
"github.com/owncast/owncast/core/chat"
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/storage/chatrepository"
|
||||||
|
"github.com/owncast/owncast/storage/userrepository"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var storage tstorage.Storage
|
var storage tstorage.Storage
|
||||||
|
|
||||||
func startViewerCollectionMetrics() {
|
func (m *Metrics) startViewerCollectionMetrics() {
|
||||||
storage, _ = tstorage.NewStorage(
|
storage, _ = tstorage.NewStorage(
|
||||||
tstorage.WithTimestampPrecision(tstorage.Seconds),
|
tstorage.WithTimestampPrecision(tstorage.Seconds),
|
||||||
tstorage.WithDataPath("./data/metrics"),
|
tstorage.WithDataPath("./data/metrics"),
|
||||||
)
|
)
|
||||||
defer storage.Close()
|
defer storage.Close()
|
||||||
|
|
||||||
collectViewerCount()
|
m.collectViewerCount()
|
||||||
|
|
||||||
for range time.Tick(viewerMetricsPollingInterval) {
|
for range time.Tick(viewerMetricsPollingInterval) {
|
||||||
collectViewerCount()
|
m.collectViewerCount()
|
||||||
collectChatClientCount()
|
m.collectChatClientCount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectViewerCount() {
|
func (m *Metrics) collectViewerCount() {
|
||||||
// Don't collect metrics for viewers if there's no stream active.
|
// Don't collect metrics for viewers if there's no stream active.
|
||||||
if !core.GetStatus().Online {
|
if !core.GetStatus().Online {
|
||||||
return
|
return
|
||||||
|
@ -35,7 +38,7 @@ func collectViewerCount() {
|
||||||
count := core.GetStatus().ViewerCount
|
count := core.GetStatus().ViewerCount
|
||||||
|
|
||||||
// Save active viewer count to our Prometheus collector.
|
// Save active viewer count to our Prometheus collector.
|
||||||
activeViewerCount.Set(float64(count))
|
m.activeViewerCount.Set(float64(count))
|
||||||
|
|
||||||
// Insert active viewer count into our on-disk time series storage.
|
// Insert active viewer count into our on-disk time series storage.
|
||||||
if err := storage.InsertRows([]tstorage.Row{
|
if err := storage.InsertRows([]tstorage.Row{
|
||||||
|
@ -48,19 +51,21 @@ func collectViewerCount() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectChatClientCount() {
|
func (m *Metrics) collectChatClientCount() {
|
||||||
count := len(chat.GetClients())
|
count := len(chat.GetClients())
|
||||||
activeChatClientCount.Set(float64(count))
|
m.activeChatClientCount.Set(float64(count))
|
||||||
|
chatRepository := chatrepository.GetChatRepository()
|
||||||
|
usersRepository := userrepository.Get()
|
||||||
|
|
||||||
// Total message count
|
// Total message count
|
||||||
cmc := data.GetMessagesCount()
|
cmc := chatRepository.GetMessagesCount()
|
||||||
// Insert message count into Prometheus collector.
|
// Insert message count into Prometheus collector.
|
||||||
currentChatMessageCount.Set(float64(cmc))
|
m.currentChatMessageCount.Set(float64(cmc))
|
||||||
|
|
||||||
// Total user count
|
// Total user count
|
||||||
uc := data.GetUsersCount()
|
uc := usersRepository.GetUsersCount()
|
||||||
// Insert user count into Prometheus collector.
|
// Insert user count into Prometheus collector.
|
||||||
chatUserCount.Set(float64(uc))
|
m.chatUserCount.Set(float64(uc))
|
||||||
|
|
||||||
// Insert active chat user count into our on-disk time series storage.
|
// Insert active chat user count into our on-disk time series storage.
|
||||||
if err := storage.InsertRows([]tstorage.Row{
|
if err := storage.InsertRows([]tstorage.Row{
|
||||||
|
@ -74,23 +79,23 @@ func collectChatClientCount() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetViewersOverTime will return a window of viewer counts over time.
|
// GetViewersOverTime will return a window of viewer counts over time.
|
||||||
func GetViewersOverTime(start, end time.Time) []TimestampedValue {
|
func (m *Metrics) GetViewersOverTime(start, end time.Time) []models.TimestampedValue {
|
||||||
p, err := storage.Select(activeViewerCountKey, nil, start.Unix(), end.Unix())
|
p, err := storage.Select(activeViewerCountKey, nil, start.Unix(), end.Unix())
|
||||||
if err != nil && err != tstorage.ErrNoDataPoints {
|
if err != nil && err != tstorage.ErrNoDataPoints {
|
||||||
log.Errorln(err)
|
log.Errorln(err)
|
||||||
}
|
}
|
||||||
datapoints := makeTimestampedValuesFromDatapoints(p)
|
datapoints := models.MakeTimestampedValuesFromDatapoints(p)
|
||||||
|
|
||||||
return datapoints
|
return datapoints
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetChatClientCountOverTime will return a window of connected chat clients over time.
|
// GetChatClientCountOverTime will return a window of connected chat clients over time.
|
||||||
func GetChatClientCountOverTime(start, end time.Time) []TimestampedValue {
|
func (m *Metrics) GetChatClientCountOverTime(start, end time.Time) []models.TimestampedValue {
|
||||||
p, err := storage.Select(activeChatClientCountKey, nil, start.Unix(), end.Unix())
|
p, err := storage.Select(activeChatClientCountKey, nil, start.Unix(), end.Unix())
|
||||||
if err != nil && err != tstorage.ErrNoDataPoints {
|
if err != nil && err != tstorage.ErrNoDataPoints {
|
||||||
log.Errorln(err)
|
log.Errorln(err)
|
||||||
}
|
}
|
||||||
datapoints := makeTimestampedValuesFromDatapoints(p)
|
datapoints := models.MakeTimestampedValuesFromDatapoints(p)
|
||||||
|
|
||||||
return datapoints
|
return datapoints
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,18 +4,19 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"github.com/SherClockHolmes/webpush-go"
|
"github.com/SherClockHolmes/webpush-go"
|
||||||
|
"github.com/owncast/owncast/storage/data"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Browser is an instance of the Browser service.
|
// Browser is an instance of the Browser service.
|
||||||
type Browser struct {
|
type Browser struct {
|
||||||
datastore *data.Datastore
|
datastore *data.Store
|
||||||
privateKey string
|
privateKey string
|
||||||
publicKey string
|
publicKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New will create a new instance of the Browser service.
|
// New will create a new instance of the Browser service.
|
||||||
func New(datastore *data.Datastore, publicKey, privateKey string) (*Browser, error) {
|
func New(datastore *data.Store, publicKey, privateKey string) (*Browser, error) {
|
||||||
return &Browser{
|
return &Browser{
|
||||||
datastore: datastore,
|
datastore: datastore,
|
||||||
privateKey: privateKey,
|
privateKey: privateKey,
|
||||||
|
|
|
@ -7,26 +7,33 @@ import (
|
||||||
"github.com/owncast/owncast/services/config"
|
"github.com/owncast/owncast/services/config"
|
||||||
"github.com/owncast/owncast/services/notifications/browser"
|
"github.com/owncast/owncast/services/notifications/browser"
|
||||||
"github.com/owncast/owncast/services/notifications/discord"
|
"github.com/owncast/owncast/services/notifications/discord"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
|
"github.com/owncast/owncast/storage/data"
|
||||||
|
"github.com/owncast/owncast/storage/notificationsrepository"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Notifier is an instance of the live stream notifier.
|
// Notifier is an instance of the live stream notifier.
|
||||||
type Notifier struct {
|
type Notifier struct {
|
||||||
datastore *data.Datastore
|
datastore *data.Store
|
||||||
browser *browser.Browser
|
browser *browser.Browser
|
||||||
discord *discord.Discord
|
discord *discord.Discord
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
configRepository = configrepository.Get()
|
||||||
|
notificationsRepository = notificationsrepository.Get()
|
||||||
|
)
|
||||||
|
|
||||||
// Setup will perform any pre-use setup for the notifier.
|
// Setup will perform any pre-use setup for the notifier.
|
||||||
func Setup(datastore *data.Datastore) {
|
func Setup(datastore *data.Store) {
|
||||||
createNotificationsTable(datastore.DB)
|
|
||||||
initializeBrowserPushIfNeeded()
|
initializeBrowserPushIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
func initializeBrowserPushIfNeeded() {
|
func initializeBrowserPushIfNeeded() {
|
||||||
pubKey, _ := data.GetBrowserPushPublicKey()
|
pubKey, _ := configRepository.GetBrowserPushPublicKey()
|
||||||
privKey, _ := data.GetBrowserPushPrivateKey()
|
privKey, _ := configRepository.GetBrowserPushPrivateKey()
|
||||||
|
|
||||||
// We need browser push keys so people can register for pushes.
|
// We need browser push keys so people can register for pushes.
|
||||||
if pubKey == "" || privKey == "" {
|
if pubKey == "" || privKey == "" {
|
||||||
|
@ -35,24 +42,24 @@ func initializeBrowserPushIfNeeded() {
|
||||||
log.Errorln("unable to initialize browser push notification keys", err)
|
log.Errorln("unable to initialize browser push notification keys", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetBrowserPushPrivateKey(browserPrivateKey); err != nil {
|
if err := configRepository.SetBrowserPushPrivateKey(browserPrivateKey); err != nil {
|
||||||
log.Errorln("unable to set browser push private key", err)
|
log.Errorln("unable to set browser push private key", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetBrowserPushPublicKey(browserPublicKey); err != nil {
|
if err := configRepository.SetBrowserPushPublicKey(browserPublicKey); err != nil {
|
||||||
log.Errorln("unable to set browser push public key", err)
|
log.Errorln("unable to set browser push public key", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable browser push notifications by default.
|
// Enable browser push notifications by default.
|
||||||
if !data.GetHasPerformedInitialNotificationsConfig() {
|
if !configRepository.GetHasPerformedInitialNotificationsConfig() {
|
||||||
_ = data.SetBrowserPushConfig(models.BrowserNotificationConfiguration{Enabled: true, GoLiveMessage: config.GetDefaults().FederationGoLiveMessage})
|
_ = configRepository.SetBrowserPushConfig(models.BrowserNotificationConfiguration{Enabled: true, GoLiveMessage: config.GetDefaults().FederationGoLiveMessage})
|
||||||
_ = data.SetHasPerformedInitialNotificationsConfig(true)
|
_ = configRepository.SetHasPerformedInitialNotificationsConfig(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new instance of the Notifier.
|
// New creates a new instance of the Notifier.
|
||||||
func New(datastore *data.Datastore) (*Notifier, error) {
|
func New(datastore *data.Store) (*Notifier, error) {
|
||||||
notifier := Notifier{
|
notifier := Notifier{
|
||||||
datastore: datastore,
|
datastore: datastore,
|
||||||
}
|
}
|
||||||
|
@ -68,13 +75,13 @@ func New(datastore *data.Datastore) (*Notifier, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notifier) setupBrowserPush() error {
|
func (n *Notifier) setupBrowserPush() error {
|
||||||
if data.GetBrowserPushConfig().Enabled {
|
if configRepository.GetBrowserPushConfig().Enabled {
|
||||||
publicKey, err := data.GetBrowserPushPublicKey()
|
publicKey, err := configRepository.GetBrowserPushPublicKey()
|
||||||
if err != nil || publicKey == "" {
|
if err != nil || publicKey == "" {
|
||||||
return errors.Wrap(err, "browser notifier disabled, failed to get browser push public key")
|
return errors.Wrap(err, "browser notifier disabled, failed to get browser push public key")
|
||||||
}
|
}
|
||||||
|
|
||||||
privateKey, err := data.GetBrowserPushPrivateKey()
|
privateKey, err := configRepository.GetBrowserPushPrivateKey()
|
||||||
if err != nil || privateKey == "" {
|
if err != nil || privateKey == "" {
|
||||||
return errors.Wrap(err, "browser notifier disabled, failed to get browser push private key")
|
return errors.Wrap(err, "browser notifier disabled, failed to get browser push private key")
|
||||||
}
|
}
|
||||||
|
@ -89,15 +96,15 @@ func (n *Notifier) setupBrowserPush() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notifier) notifyBrowserPush() {
|
func (n *Notifier) notifyBrowserPush() {
|
||||||
destinations, err := GetNotificationDestinationsForChannel(BrowserPushNotification)
|
destinations, err := notificationsRepository.GetNotificationDestinationsForChannel(BrowserPushNotification)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorln("error getting browser push notification destinations", err)
|
log.Errorln("error getting browser push notification destinations", err)
|
||||||
}
|
}
|
||||||
for _, destination := range destinations {
|
for _, destination := range destinations {
|
||||||
unsubscribed, err := n.browser.Send(destination, data.GetServerName(), data.GetBrowserPushConfig().GoLiveMessage)
|
unsubscribed, err := n.browser.Send(destination, configRepository.GetServerName(), configRepository.GetBrowserPushConfig().GoLiveMessage)
|
||||||
if unsubscribed {
|
if unsubscribed {
|
||||||
// If the error is "unsubscribed", then remove the destination from the database.
|
// If the error is "unsubscribed", then remove the destination from the database.
|
||||||
if err := RemoveNotificationForChannel(BrowserPushNotification, destination); err != nil {
|
if err := notificationsRepository.RemoveNotificationForChannel(BrowserPushNotification, destination); err != nil {
|
||||||
log.Errorln(err)
|
log.Errorln(err)
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
|
@ -107,14 +114,14 @@ func (n *Notifier) notifyBrowserPush() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notifier) setupDiscord() error {
|
func (n *Notifier) setupDiscord() error {
|
||||||
discordConfig := data.GetDiscordConfig()
|
discordConfig := configRepository.GetDiscordConfig()
|
||||||
if discordConfig.Enabled && discordConfig.Webhook != "" {
|
if discordConfig.Enabled && discordConfig.Webhook != "" {
|
||||||
var image string
|
var image string
|
||||||
if serverURL := data.GetServerURL(); serverURL != "" {
|
if serverURL := configRepository.GetServerURL(); serverURL != "" {
|
||||||
image = serverURL + "/logo"
|
image = serverURL + "/logo"
|
||||||
}
|
}
|
||||||
discordNotifier, err := discord.New(
|
discordNotifier, err := discord.New(
|
||||||
data.GetServerName(),
|
configRepository.GetServerName(),
|
||||||
image,
|
image,
|
||||||
discordConfig.Webhook,
|
discordConfig.Webhook,
|
||||||
)
|
)
|
||||||
|
@ -127,12 +134,12 @@ func (n *Notifier) setupDiscord() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Notifier) notifyDiscord() {
|
func (n *Notifier) notifyDiscord() {
|
||||||
goLiveMessage := data.GetDiscordConfig().GoLiveMessage
|
goLiveMessage := configRepository.GetDiscordConfig().GoLiveMessage
|
||||||
streamTitle := data.GetStreamTitle()
|
streamTitle := configRepository.GetStreamTitle()
|
||||||
if streamTitle != "" {
|
if streamTitle != "" {
|
||||||
goLiveMessage += "\n" + streamTitle
|
goLiveMessage += "\n" + streamTitle
|
||||||
}
|
}
|
||||||
message := fmt.Sprintf("%s\n\n%s", goLiveMessage, data.GetServerURL())
|
message := fmt.Sprintf("%s\n\n%s", goLiveMessage, configRepository.GetServerURL())
|
||||||
|
|
||||||
if err := n.discord.Send(message); err != nil {
|
if err := n.discord.Send(message); err != nil {
|
||||||
log.Errorln("error sending discord message", err)
|
log.Errorln("error sending discord message", err)
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
package notifications
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/owncast/owncast/db"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AddNotification saves a new user notification destination.
|
|
||||||
func AddNotification(channel, destination string) error {
|
|
||||||
return data.GetDatastore().GetQueries().AddNotification(context.Background(), db.AddNotificationParams{
|
|
||||||
Channel: channel,
|
|
||||||
Destination: destination,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveNotificationForChannel removes a notification destination.
|
|
||||||
func RemoveNotificationForChannel(channel, destination string) error {
|
|
||||||
log.Debugln("Removing notification for channel", channel)
|
|
||||||
return data.GetDatastore().GetQueries().RemoveNotificationDestinationForChannel(context.Background(), db.RemoveNotificationDestinationForChannelParams{
|
|
||||||
Channel: channel,
|
|
||||||
Destination: destination,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNotificationDestinationsForChannel will return a collection of
|
|
||||||
// destinations to notify for a given channel.
|
|
||||||
func GetNotificationDestinationsForChannel(channel string) ([]string, error) {
|
|
||||||
result, err := data.GetDatastore().GetQueries().GetNotificationDestinationsForChannel(context.Background(), channel)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "unable to query notification destinations for channel "+channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
|
@ -13,8 +13,8 @@ type LiveWebhookManager struct {
|
||||||
getStatus func() models.Status
|
getStatus func() models.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWebhookManager creates a new webhook manager.
|
// New creates a new webhook manager.
|
||||||
func NewWebhookManager(getStatusFunc func() models.Status) *LiveWebhookManager {
|
func New(getStatusFunc func() models.Status) *LiveWebhookManager {
|
||||||
m := &LiveWebhookManager{
|
m := &LiveWebhookManager{
|
||||||
getStatus: getStatusFunc,
|
getStatus: getStatusFunc,
|
||||||
}
|
}
|
||||||
|
@ -25,13 +25,13 @@ func NewWebhookManager(getStatusFunc func() models.Status) *LiveWebhookManager {
|
||||||
// InitTemporarySingleton initializes the the temporary global instance of the webhook manager
|
// InitTemporarySingleton initializes the the temporary global instance of the webhook manager
|
||||||
// to be deleted once dependency injection is implemented.
|
// to be deleted once dependency injection is implemented.
|
||||||
func InitTemporarySingleton(getStatusFunc func() models.Status) {
|
func InitTemporarySingleton(getStatusFunc func() models.Status) {
|
||||||
temporaryGlobalInstance = NewWebhookManager(getStatusFunc)
|
temporaryGlobalInstance = New(getStatusFunc)
|
||||||
}
|
}
|
||||||
|
|
||||||
var temporaryGlobalInstance *LiveWebhookManager
|
var temporaryGlobalInstance *LiveWebhookManager
|
||||||
|
|
||||||
// GetWebhooks returns the temporary global instance of the webhook manager.
|
// Get returns the temporary global instance of the webhook manager.
|
||||||
// Remove this after dependency injection is implemented.
|
// Remove this after dependency injection is implemented.
|
||||||
func GetWebhooks() *LiveWebhookManager {
|
func Get() *LiveWebhookManager {
|
||||||
return temporaryGlobalInstance
|
return temporaryGlobalInstance
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
"github.com/teris-io/shortid"
|
"github.com/teris-io/shortid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var configRepository = configrepository.Get()
|
||||||
|
|
||||||
// SendStreamStatusEvent will send all webhook destinations the current stream status.
|
// SendStreamStatusEvent will send all webhook destinations the current stream status.
|
||||||
func (w *LiveWebhookManager) SendStreamStatusEvent(eventType models.EventType) {
|
func (w *LiveWebhookManager) SendStreamStatusEvent(eventType models.EventType) {
|
||||||
w.sendStreamStatusEvent(eventType, shortid.MustGenerate(), time.Now())
|
w.sendStreamStatusEvent(eventType, shortid.MustGenerate(), time.Now())
|
||||||
|
@ -17,9 +20,9 @@ func (w *LiveWebhookManager) sendStreamStatusEvent(eventType models.EventType, i
|
||||||
Type: eventType,
|
Type: eventType,
|
||||||
EventData: map[string]interface{}{
|
EventData: map[string]interface{}{
|
||||||
"id": id,
|
"id": id,
|
||||||
"name": data.GetServerName(),
|
"name": configRepository.GetServerName(),
|
||||||
"summary": data.GetServerSummary(),
|
"summary": configRepository.GetServerSummary(),
|
||||||
"streamTitle": data.GetStreamTitle(),
|
"streamTitle": configRepository.GetStreamTitle(),
|
||||||
"status": w.getStatus(),
|
"status": w.getStatus(),
|
||||||
"timestamp": timestamp,
|
"timestamp": timestamp,
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,9 +9,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestSendStreamStatusEvent(t *testing.T) {
|
func TestSendStreamStatusEvent(t *testing.T) {
|
||||||
data.SetServerName("my server")
|
configRepository.SetServerName("my server")
|
||||||
data.SetServerSummary("my server where I stream")
|
configRepository.SetServerSummary("my server where I stream")
|
||||||
data.SetStreamTitle("my stream")
|
configRepository.SetStreamTitle("my stream")
|
||||||
|
|
||||||
checkPayload(t, models.StreamStarted, func() {
|
checkPayload(t, models.StreamStarted, func() {
|
||||||
manager.sendStreamStatusEvent(events.StreamStarted, "id", time.Unix(72, 6).UTC())
|
manager.sendStreamStatusEvent(events.StreamStarted, "id", time.Unix(72, 6).UTC())
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebhookEvent represents an event sent as a webhook.
|
// WebhookEvent represents an event sent as a webhook.
|
||||||
|
@ -24,13 +25,15 @@ type WebhookChatMessage struct {
|
||||||
Visible bool `json:"visible"`
|
Visible bool `json:"visible"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var webhookRepository = storage.GetWebhookRepository()
|
||||||
|
|
||||||
// SendEventToWebhooks will send a single webhook event to all webhook destinations.
|
// SendEventToWebhooks will send a single webhook event to all webhook destinations.
|
||||||
func (w *LiveWebhookManager) SendEventToWebhooks(payload WebhookEvent) {
|
func (w *LiveWebhookManager) SendEventToWebhooks(payload WebhookEvent) {
|
||||||
w.sendEventToWebhooks(payload, nil)
|
w.sendEventToWebhooks(payload, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LiveWebhookManager) sendEventToWebhooks(payload WebhookEvent, wg *sync.WaitGroup) {
|
func (w *LiveWebhookManager) sendEventToWebhooks(payload WebhookEvent, wg *sync.WaitGroup) {
|
||||||
webhooks := data.GetWebhooksForEvent(payload.Type)
|
webhooks := webhookRepository.GetWebhooksForEvent(payload.Type)
|
||||||
|
|
||||||
for _, webhook := range webhooks {
|
for _, webhook := range webhooks {
|
||||||
// Use wg to track the number of notifications to be sent.
|
// Use wg to track the number of notifications to be sent.
|
||||||
|
|
|
@ -44,7 +44,7 @@ func TestMain(m *testing.M) {
|
||||||
}
|
}
|
||||||
|
|
||||||
InitTemporarySingleton(fakeGetStatus)
|
InitTemporarySingleton(fakeGetStatus)
|
||||||
manager = GetWebhooks()
|
manager = Get()
|
||||||
defer close(manager.queue)
|
defer close(manager.queue)
|
||||||
|
|
||||||
m.Run()
|
m.Run()
|
||||||
|
@ -64,12 +64,12 @@ func TestPublicSend(t *testing.T) {
|
||||||
}))
|
}))
|
||||||
defer svr.Close()
|
defer svr.Close()
|
||||||
|
|
||||||
hook, err := data.InsertWebhook(svr.URL, []models.EventType{models.MessageSent})
|
hook, err := webhookRepository.InsertWebhook(svr.URL, []models.EventType{models.MessageSent})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := data.DeleteWebhook(hook); err != nil {
|
if err := webhookRepository.DeleteWebhook(hook); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -110,12 +110,12 @@ func TestRouting(t *testing.T) {
|
||||||
defer svr.Close()
|
defer svr.Close()
|
||||||
|
|
||||||
for _, eventType := range eventTypes {
|
for _, eventType := range eventTypes {
|
||||||
hook, err := data.InsertWebhook(svr.URL+"/"+eventType, []models.EventType{eventType})
|
hook, err := webhookRepository.InsertWebhook(svr.URL+"/"+eventType, []models.EventType{eventType})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := data.DeleteWebhook(hook); err != nil {
|
if err := webhookRepository.DeleteWebhook(hook); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -151,12 +151,12 @@ func TestMultiple(t *testing.T) {
|
||||||
defer svr.Close()
|
defer svr.Close()
|
||||||
|
|
||||||
for i := 0; i < times; i++ {
|
for i := 0; i < times; i++ {
|
||||||
hook, err := data.InsertWebhook(fmt.Sprintf("%v/%v", svr.URL, i), []models.EventType{models.MessageSent})
|
hook, err := webhookRepository.InsertWebhook(fmt.Sprintf("%v/%v", svr.URL, i), []models.EventType{models.MessageSent})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := data.DeleteWebhook(hook); err != nil {
|
if err := webhookRepository.DeleteWebhook(hook); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -189,13 +189,13 @@ func TestTimestamps(t *testing.T) {
|
||||||
defer svr.Close()
|
defer svr.Close()
|
||||||
|
|
||||||
for i, eventType := range eventTypes {
|
for i, eventType := range eventTypes {
|
||||||
hook, err := data.InsertWebhook(svr.URL+"/"+eventType, []models.EventType{eventType})
|
hook, err := webhookRepository.InsertWebhook(svr.URL+"/"+eventType, []models.EventType{eventType})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
handlerIds[i] = hook
|
handlerIds[i] = hook
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := data.DeleteWebhook(hook); err != nil {
|
if err := webhookRepository.DeleteWebhook(hook); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -211,7 +211,7 @@ func TestTimestamps(t *testing.T) {
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
hooks, err := data.GetWebhooks()
|
hooks, err := webhookRepository.GetWebhooks()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -287,12 +287,12 @@ func TestParallel(t *testing.T) {
|
||||||
}))
|
}))
|
||||||
defer svr.Close()
|
defer svr.Close()
|
||||||
|
|
||||||
hook, err := data.InsertWebhook(svr.URL, []models.EventType{models.MessageSent})
|
hook, err := webhookRepository.InsertWebhook(svr.URL, []models.EventType{models.MessageSent})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := data.DeleteWebhook(hook); err != nil {
|
if err := webhookRepository.DeleteWebhook(hook); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -323,12 +323,12 @@ func checkPayload(t *testing.T, eventType models.EventType, send func(), expecte
|
||||||
defer svr.Close()
|
defer svr.Close()
|
||||||
|
|
||||||
// Subscribe to the webhook.
|
// Subscribe to the webhook.
|
||||||
hook, err := data.InsertWebhook(svr.URL, []models.EventType{eventType})
|
hook, err := webhookRepository.InsertWebhook(svr.URL, []models.EventType{eventType})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := data.DeleteWebhook(hook); err != nil {
|
if err := webhookRepository.DeleteWebhook(hook); err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -75,7 +75,7 @@ func (w *LiveWebhookManager) sendWebhook(job Job) error {
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if err := data.SetWebhookAsUsed(job.webhook); err != nil {
|
if err := webhookRepository.SetWebhookAsUsed(job.webhook); err != nil {
|
||||||
log.Warnln(err)
|
log.Warnln(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/owncast/owncast/services/config"
|
"github.com/owncast/owncast/services/config"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -21,6 +22,8 @@ var (
|
||||||
_inErrorState = false
|
_inErrorState = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var configRepository = configrepository.Get()
|
||||||
|
|
||||||
// YP is a service for handling listing in the Owncast directory.
|
// YP is a service for handling listing in the Owncast directory.
|
||||||
type YP struct {
|
type YP struct {
|
||||||
timer *time.Ticker
|
timer *time.Ticker
|
||||||
|
@ -60,7 +63,7 @@ func (yp *YP) Stop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (yp *YP) ping() {
|
func (yp *YP) ping() {
|
||||||
if !data.GetDirectoryEnabled() {
|
if !configRepository.GetDirectoryEnabled() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +73,7 @@ func (yp *YP) ping() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
myInstanceURL := data.GetServerURL()
|
myInstanceURL := configRepository.GetServerURL()
|
||||||
if myInstanceURL == "" {
|
if myInstanceURL == "" {
|
||||||
log.Warnln("Server URL not set in the configuration. Directory access is disabled until this is set.")
|
log.Warnln("Server URL not set in the configuration. Directory access is disabled until this is set.")
|
||||||
return
|
return
|
||||||
|
@ -84,9 +87,9 @@ func (yp *YP) ping() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
key := data.GetDirectoryRegistrationKey()
|
key := configRepository.GetDirectoryRegistrationKey()
|
||||||
|
|
||||||
log.Traceln("Pinging YP as: ", data.GetServerName(), "with key", key)
|
log.Traceln("Pinging YP as: ", configRepository.GetServerName(), "with key", key)
|
||||||
|
|
||||||
request := ypPingRequest{
|
request := ypPingRequest{
|
||||||
Key: key,
|
Key: key,
|
||||||
|
@ -128,7 +131,7 @@ func (yp *YP) ping() {
|
||||||
_inErrorState = false
|
_inErrorState = false
|
||||||
|
|
||||||
if pingResponse.Key != key {
|
if pingResponse.Key != key {
|
||||||
if err := data.SetDirectoryRegistrationKey(key); err != nil {
|
if err := configRepository.SetDirectoryRegistrationKey(key); err != nil {
|
||||||
log.Errorln("unable to save directory key:", err)
|
log.Errorln("unable to save directory key:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
version: 1
|
version: 1
|
||||||
packages:
|
packages:
|
||||||
- path: db
|
- path: storage/sqlstorage
|
||||||
name: db
|
name: sqlstorage
|
||||||
schema: 'db/schema.sql'
|
schema: 'storage/sqlstorage/schema.sql'
|
||||||
queries: 'db/query.sql'
|
queries: 'storage/sqlstorage/query.sql'
|
||||||
|
|
5
storage/emoji/emoji.go → static/emoji.go
vendored
5
storage/emoji/emoji.go → static/emoji.go
vendored
|
@ -1,4 +1,4 @@
|
||||||
package emoji
|
package static
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -11,7 +11,6 @@ import (
|
||||||
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/owncast/owncast/services/config"
|
"github.com/owncast/owncast/services/config"
|
||||||
"github.com/owncast/owncast/static"
|
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -112,7 +111,7 @@ func SetupEmojiDirectory() (err error) {
|
||||||
return fmt.Errorf("unable to create custom emoji directory: %w", err)
|
return fmt.Errorf("unable to create custom emoji directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
staticFS := static.GetEmoji()
|
staticFS := GetEmoji()
|
||||||
files := []emojiDirectory{}
|
files := []emojiDirectory{}
|
||||||
|
|
||||||
walkFunction := func(path string, d os.DirEntry, err error) error {
|
walkFunction := func(path string, d os.DirEntry, err error) error {
|
27
storage/chatrepository/chatrepository.go
Normal file
27
storage/chatrepository/chatrepository.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package chatrepository
|
||||||
|
|
||||||
|
import "github.com/owncast/owncast/storage/data"
|
||||||
|
|
||||||
|
type ChatRepository struct {
|
||||||
|
datastore *data.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(datastore *data.Store) *ChatRepository {
|
||||||
|
r := &ChatRepository{
|
||||||
|
datastore: datastore,
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This is temporary during the transition period.
|
||||||
|
var temporaryGlobalInstance *ChatRepository
|
||||||
|
|
||||||
|
// GetUserRepository will return the user repository.
|
||||||
|
func GetChatRepository() *ChatRepository {
|
||||||
|
if temporaryGlobalInstance == nil {
|
||||||
|
i := New(data.GetDatastore())
|
||||||
|
temporaryGlobalInstance = i
|
||||||
|
}
|
||||||
|
return temporaryGlobalInstance
|
||||||
|
}
|
|
@ -1,17 +1,17 @@
|
||||||
package storage
|
package chatrepository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
"github.com/owncast/owncast/db"
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/storage/sqlstorage"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetMessagesCount will return the number of messages in the database.
|
// GetMessagesCount will return the number of messages in the database.
|
||||||
func GetMessagesCount() int64 {
|
func (c *ChatRepository) GetMessagesCount() int64 {
|
||||||
query := `SELECT COUNT(*) FROM messages`
|
query := `SELECT COUNT(*) FROM messages`
|
||||||
rows, err := _db.Query(query)
|
rows, err := c.datastore.DB.Query(query)
|
||||||
if err != nil || rows.Err() != nil {
|
if err != nil || rows.Err() != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
@ -26,22 +26,22 @@ func GetMessagesCount() int64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// BanIPAddress will persist a new IP address ban to the datastore.
|
// BanIPAddress will persist a new IP address ban to the datastore.
|
||||||
func BanIPAddress(address, note string) error {
|
func (c *ChatRepository) BanIPAddress(address, note string) error {
|
||||||
return _datastore.GetQueries().BanIPAddress(context.Background(), db.BanIPAddressParams{
|
return c.datastore.GetQueries().BanIPAddress(context.Background(), sqlstorage.BanIPAddressParams{
|
||||||
IpAddress: address,
|
IpAddress: address,
|
||||||
Notes: sql.NullString{String: note, Valid: true},
|
Notes: sql.NullString{String: note, Valid: true},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsIPAddressBanned will return if an IP address has been previously blocked.
|
// IsIPAddressBanned will return if an IP address has been previously blocked.
|
||||||
func IsIPAddressBanned(address string) (bool, error) {
|
func (c *ChatRepository) IsIPAddressBanned(address string) (bool, error) {
|
||||||
blocked, error := _datastore.GetQueries().IsIPAddressBlocked(context.Background(), address)
|
blocked, error := c.datastore.GetQueries().IsIPAddressBlocked(context.Background(), address)
|
||||||
return blocked > 0, error
|
return blocked > 0, error
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIPAddressBans will return all the banned IP addresses.
|
// GetIPAddressBans will return all the banned IP addresses.
|
||||||
func GetIPAddressBans() ([]models.IPAddress, error) {
|
func (c *ChatRepository) GetIPAddressBans() ([]models.IPAddress, error) {
|
||||||
result, err := _datastore.GetQueries().GetIPAddressBans(context.Background())
|
result, err := c.datastore.GetQueries().GetIPAddressBans(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,6 @@ func GetIPAddressBans() ([]models.IPAddress, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveIPAddressBan will remove a previously banned IP address.
|
// RemoveIPAddressBan will remove a previously banned IP address.
|
||||||
func RemoveIPAddressBan(address string) error {
|
func (c *ChatRepository) RemoveIPAddressBan(address string) error {
|
||||||
return _datastore.GetQueries().RemoveIPAddressBan(context.Background(), address)
|
return c.datastore.GetQueries().RemoveIPAddressBan(context.Background(), address)
|
||||||
}
|
}
|
7
storage/configrepository/README.md
Normal file
7
storage/configrepository/README.md
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# Config Repository
|
||||||
|
|
||||||
|
The configuration repository represents all the getters, setters and storage logic for user-defined configuration values. This includes things like the server name, enabled/disabled flags, etc. See `keys.go` to see the full list of keys that are used for accessing these values in the database.
|
||||||
|
|
||||||
|
## Migrations
|
||||||
|
|
||||||
|
Add migrations to `migrations.go` and you can use the datastore and config repository to make your required changes between datastore versions.
|
|
@ -5,7 +5,7 @@ import (
|
||||||
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/owncast/owncast/services/config"
|
"github.com/owncast/owncast/services/config"
|
||||||
"github.com/owncast/owncast/storage/datastore"
|
"github.com/owncast/owncast/storage/data"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -122,15 +122,15 @@ type ConfigRepository interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SqlConfigRepository struct {
|
type SqlConfigRepository struct {
|
||||||
datastore *datastore.Datastore
|
datastore *data.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfigRepository will create a new config repository.
|
// New will create a new config repository.
|
||||||
func NewConfigRepository(datastore *datastore.Datastore) *SqlConfigRepository {
|
func New(datastore *data.Store) *SqlConfigRepository {
|
||||||
r := SqlConfigRepository{
|
r := SqlConfigRepository{
|
||||||
datastore: datastore,
|
datastore: datastore,
|
||||||
}
|
}
|
||||||
|
temporaryGlobalInstance = &r
|
||||||
return &r
|
return &r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,9 +138,9 @@ func NewConfigRepository(datastore *datastore.Datastore) *SqlConfigRepository {
|
||||||
var temporaryGlobalInstance *SqlConfigRepository
|
var temporaryGlobalInstance *SqlConfigRepository
|
||||||
|
|
||||||
// GetUserRepository will return the temporary repository singleton.
|
// GetUserRepository will return the temporary repository singleton.
|
||||||
func GetConfigRepository() *SqlConfigRepository {
|
func Get() *SqlConfigRepository {
|
||||||
if temporaryGlobalInstance == nil {
|
if temporaryGlobalInstance == nil {
|
||||||
i := NewConfigRepository(datastore.GetDatastore())
|
i := New(data.GetDatastore())
|
||||||
temporaryGlobalInstance = i
|
temporaryGlobalInstance = i
|
||||||
}
|
}
|
||||||
return temporaryGlobalInstance
|
return temporaryGlobalInstance
|
||||||
|
@ -148,10 +148,6 @@ func GetConfigRepository() *SqlConfigRepository {
|
||||||
|
|
||||||
// Setup will create the datastore table and perform initial initialization.
|
// Setup will create the datastore table and perform initial initialization.
|
||||||
func (cr *SqlConfigRepository) Setup() {
|
func (cr *SqlConfigRepository) Setup() {
|
||||||
// ds.cache = make(map[string][]byte)
|
|
||||||
// ds.DB = GetDatabase()
|
|
||||||
// ds.DbLock = &sync.Mutex{}
|
|
||||||
|
|
||||||
if !cr.HasPopulatedDefaults() {
|
if !cr.HasPopulatedDefaults() {
|
||||||
cr.PopulateDefaults()
|
cr.PopulateDefaults()
|
||||||
}
|
}
|
||||||
|
@ -169,6 +165,4 @@ func (cr *SqlConfigRepository) Setup() {
|
||||||
if hasSetInitDate, _ := cr.GetServerInitTime(); hasSetInitDate == nil || !hasSetInitDate.Valid {
|
if hasSetInitDate, _ := cr.GetServerInitTime(); hasSetInitDate == nil || !hasSetInitDate.Valid {
|
||||||
_ = cr.SetServerInitTime(time.Now())
|
_ = cr.SetServerInitTime(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
// migrateDatastoreValues(_datastore)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,25 @@
|
||||||
package configrepository
|
package configrepository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/owncast/owncast/storage/datastore"
|
"github.com/owncast/owncast/storage/data"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_datastore *datastore.Datastore
|
_datastore *data.Store
|
||||||
_configRepository *SqlConfigRepository
|
_configRepository *SqlConfigRepository
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
dbFile, err := os.CreateTemp(os.TempDir(), ":memory:")
|
ds, err := data.NewStore(":memory")
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ds, err := datastore.NewDatastore(dbFile.Name())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
_datastore = ds
|
_datastore = ds
|
||||||
|
|
||||||
_configRepository = NewConfigRepository(_datastore)
|
_configRepository = New(_datastore)
|
||||||
|
|
||||||
m.Run()
|
m.Run()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
package migrations
|
package configrepository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/owncast/owncast/storage/configrepository"
|
"github.com/owncast/owncast/storage/data"
|
||||||
"github.com/owncast/owncast/storage/datastore"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -15,7 +14,7 @@ const (
|
||||||
datastoreValueVersionKey = "DATA_STORE_VERSION"
|
datastoreValueVersionKey = "DATA_STORE_VERSION"
|
||||||
)
|
)
|
||||||
|
|
||||||
func migrateDatastoreValues(ds *datastore.Datastore, cr *configrepository.ConfigRepository) {
|
func migrateDatastoreValues(ds *data.Store, cr *SqlConfigRepository) {
|
||||||
currentVersion, _ := ds.GetNumber(datastoreValueVersionKey)
|
currentVersion, _ := ds.GetNumber(datastoreValueVersionKey)
|
||||||
if currentVersion == 0 {
|
if currentVersion == 0 {
|
||||||
currentVersion = datastoreValuesVersion
|
currentVersion = datastoreValuesVersion
|
||||||
|
@ -25,11 +24,11 @@ func migrateDatastoreValues(ds *datastore.Datastore, cr *configrepository.Config
|
||||||
log.Infof("Migration datastore values from %d to %d\n", int(v), int(v+1))
|
log.Infof("Migration datastore values from %d to %d\n", int(v), int(v+1))
|
||||||
switch v {
|
switch v {
|
||||||
case 0:
|
case 0:
|
||||||
migrateToDatastoreValues1(ds)
|
migrateToDatastoreValues1(ds, cr)
|
||||||
case 1:
|
case 1:
|
||||||
migrateToDatastoreValues2(ds)
|
migrateToDatastoreValues2(ds, cr)
|
||||||
case 2:
|
case 2:
|
||||||
migrateToDatastoreValues3ServingEndpoint3(ds)
|
migrateToDatastoreValues3ServingEndpoint3(ds, cr)
|
||||||
default:
|
default:
|
||||||
log.Fatalln("missing datastore values migration step")
|
log.Fatalln("missing datastore values migration step")
|
||||||
}
|
}
|
||||||
|
@ -39,7 +38,7 @@ func migrateDatastoreValues(ds *datastore.Datastore, cr *configrepository.Config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateToDatastoreValues1(ds *datastore.Datastore, cr *configrepository.ConfigRepository) {
|
func migrateToDatastoreValues1(ds *data.Store, cr *SqlConfigRepository) {
|
||||||
// Migrate the forbidden usernames to be a slice instead of a string.
|
// Migrate the forbidden usernames to be a slice instead of a string.
|
||||||
|
|
||||||
forbiddenUsernamesString, _ := ds.GetString(blockedUsernamesKey)
|
forbiddenUsernamesString, _ := ds.GetString(blockedUsernamesKey)
|
||||||
|
@ -60,20 +59,20 @@ func migrateToDatastoreValues1(ds *datastore.Datastore, cr *configrepository.Con
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateToDatastoreValues2(ds *datastore.Datastore) {
|
func migrateToDatastoreValues2(ds *data.Store, cr *SqlConfigRepository) {
|
||||||
oldAdminPassword, _ := datastore.GetString("stream_key")
|
oldAdminPassword, _ := ds.GetString("stream_key")
|
||||||
_ = SetAdminPassword(oldAdminPassword)
|
_ = cr.SetAdminPassword(oldAdminPassword)
|
||||||
_ = SetStreamKeys([]models.StreamKey{
|
_ = cr.SetStreamKeys([]models.StreamKey{
|
||||||
{Key: oldAdminPassword, Comment: "Default stream key"},
|
{Key: oldAdminPassword, Comment: "Default stream key"},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateToDatastoreValues3ServingEndpoint3(_ *Datastore) {
|
func migrateToDatastoreValues3ServingEndpoint3(_ *data.Store, cr *SqlConfigRepository) {
|
||||||
s3Config := GetS3Config()
|
s3Config := cr.GetS3Config()
|
||||||
|
|
||||||
if !s3Config.Enabled {
|
if !s3Config.Enabled {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = SetVideoServingEndpoint(s3Config.ServingEndpoint)
|
_ = cr.SetVideoServingEndpoint(s3Config.ServingEndpoint)
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package datastore
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -10,7 +10,7 @@ import (
|
||||||
var _cacheLock = sync.Mutex{}
|
var _cacheLock = sync.Mutex{}
|
||||||
|
|
||||||
// GetCachedValue will return a value for key from the cache.
|
// GetCachedValue will return a value for key from the cache.
|
||||||
func (ds *Datastore) GetCachedValue(key string) ([]byte, error) {
|
func (ds *Store) GetCachedValue(key string) ([]byte, error) {
|
||||||
_cacheLock.Lock()
|
_cacheLock.Lock()
|
||||||
defer _cacheLock.Unlock()
|
defer _cacheLock.Unlock()
|
||||||
|
|
||||||
|
@ -23,14 +23,14 @@ func (ds *Datastore) GetCachedValue(key string) ([]byte, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCachedValue will set a value for key in the cache.
|
// SetCachedValue will set a value for key in the cache.
|
||||||
func (ds *Datastore) SetCachedValue(key string, b []byte) {
|
func (ds *Store) SetCachedValue(key string, b []byte) {
|
||||||
_cacheLock.Lock()
|
_cacheLock.Lock()
|
||||||
defer _cacheLock.Unlock()
|
defer _cacheLock.Unlock()
|
||||||
|
|
||||||
ds.cache[key] = b
|
ds.cache[key] = b
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *Datastore) warmCache() {
|
func (ds *Store) warmCache() {
|
||||||
log.Traceln("Warming config value cache")
|
log.Traceln("Warming config value cache")
|
||||||
|
|
||||||
res, err := ds.DB.Query("SELECT key, value FROM datastore")
|
res, err := ds.DB.Query("SELECT key, value FROM datastore")
|
22
storage/data/cache_test.go
Normal file
22
storage/data/cache_test.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCachedString(t *testing.T) {
|
||||||
|
const testKey = "test string key"
|
||||||
|
const testValue = "test string value"
|
||||||
|
|
||||||
|
_datastore.SetCachedValue(testKey, []byte(testValue))
|
||||||
|
|
||||||
|
// Get the config entry from the database
|
||||||
|
stringTestResult, err := _datastore.GetCachedValue(testKey)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(stringTestResult) != testValue {
|
||||||
|
t.Error("expected", testValue, "but test returned", stringTestResult)
|
||||||
|
}
|
||||||
|
}
|
126
storage/data/datastore.go
Normal file
126
storage/data/datastore.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/gob"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
// sqlite requires a blank import.
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/services/config"
|
||||||
|
"github.com/owncast/owncast/storage/sqlstorage"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
schemaVersion = 7
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store is the global key/value store for configuration values.
|
||||||
|
type Store struct {
|
||||||
|
DB *sql.DB
|
||||||
|
cache map[string][]byte
|
||||||
|
DbLock *sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStore creates a new datastore.
|
||||||
|
func NewStore(file string) (*Store, error) {
|
||||||
|
s := &Store{
|
||||||
|
cache: make(map[string][]byte),
|
||||||
|
DbLock: &sync.Mutex{},
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := sqlstorage.InitializeDatabase(file, schemaVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.DB = db
|
||||||
|
s.warmCache()
|
||||||
|
temporaryGlobalDatastoreInstance = s
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var temporaryGlobalDatastoreInstance *Store
|
||||||
|
|
||||||
|
// GetDatastore returns the shared instance of the owncast datastore.
|
||||||
|
func GetDatastore() *Store {
|
||||||
|
if temporaryGlobalDatastoreInstance == nil {
|
||||||
|
c := config.GetConfig()
|
||||||
|
i, err := NewStore(c.DatabaseFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
temporaryGlobalDatastoreInstance = i
|
||||||
|
}
|
||||||
|
return temporaryGlobalDatastoreInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQueries will return the shared instance of the SQL query generator.
|
||||||
|
func (ds *Store) GetQueries() *sqlstorage.Queries {
|
||||||
|
return sqlstorage.New(ds.DB)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get will query the database for the key and return the entry.
|
||||||
|
func (ds *Store) Get(key string) (models.ConfigEntry, error) {
|
||||||
|
cachedValue, err := ds.GetCachedValue(key)
|
||||||
|
if err == nil {
|
||||||
|
return models.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 models.ConfigEntry{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := models.ConfigEntry{
|
||||||
|
Key: resultKey,
|
||||||
|
Value: resultValue,
|
||||||
|
}
|
||||||
|
ds.SetCachedValue(resultKey, resultValue)
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save will save the models.ConfigEntry to the database.
|
||||||
|
func (ds *Store) Save(e models.ConfigEntry) error {
|
||||||
|
ds.DbLock.Lock()
|
||||||
|
defer ds.DbLock.Unlock()
|
||||||
|
|
||||||
|
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
|
||||||
|
stmt, err = tx.Prepare("INSERT INTO datastore (key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = stmt.Exec(e.Key, dataGob.Bytes())
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package datastore
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -8,7 +8,7 @@ import (
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _datastore *Datastore
|
var _datastore *Store
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
dbFile, err := os.CreateTemp(os.TempDir(), "owncast-test-db.db")
|
dbFile, err := os.CreateTemp(os.TempDir(), "owncast-test-db.db")
|
||||||
|
@ -16,7 +16,7 @@ func TestMain(m *testing.M) {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ds, err := NewDatastore(dbFile.Name())
|
ds, err := NewStore(dbFile.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
package datastore
|
package data
|
||||||
|
|
||||||
import "github.com/owncast/owncast/models"
|
import "github.com/owncast/owncast/models"
|
||||||
|
|
||||||
// GetStringSlice will return the string slice value for a key.
|
// GetStringSlice will return the string slice value for a key.
|
||||||
func (ds *Datastore) GetStringSlice(key string) ([]string, error) {
|
func (ds *Store) GetStringSlice(key string) ([]string, error) {
|
||||||
configEntry, err := ds.Get(key)
|
configEntry, err := ds.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, err
|
return []string{}, err
|
||||||
|
@ -12,13 +12,13 @@ func (ds *Datastore) GetStringSlice(key string) ([]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetStringSlice will set the string slice value for a key.
|
// SetStringSlice will set the string slice value for a key.
|
||||||
func (ds *Datastore) SetStringSlice(key string, value []string) error {
|
func (ds *Store) SetStringSlice(key string, value []string) error {
|
||||||
configEntry := models.ConfigEntry{key, value}
|
configEntry := models.ConfigEntry{key, value}
|
||||||
return ds.Save(configEntry)
|
return ds.Save(configEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetString will return the string value for a key.
|
// GetString will return the string value for a key.
|
||||||
func (ds *Datastore) GetString(key string) (string, error) {
|
func (ds *Store) GetString(key string) (string, error) {
|
||||||
configEntry, err := ds.Get(key)
|
configEntry, err := ds.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -27,13 +27,13 @@ func (ds *Datastore) GetString(key string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetString will set the string value for a key.
|
// SetString will set the string value for a key.
|
||||||
func (ds *Datastore) SetString(key string, value string) error {
|
func (ds *Store) SetString(key string, value string) error {
|
||||||
configEntry := models.ConfigEntry{key, value}
|
configEntry := models.ConfigEntry{key, value}
|
||||||
return ds.Save(configEntry)
|
return ds.Save(configEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNumber will return the numeric value for a key.
|
// GetNumber will return the numeric value for a key.
|
||||||
func (ds *Datastore) GetNumber(key string) (float64, error) {
|
func (ds *Store) GetNumber(key string) (float64, error) {
|
||||||
configEntry, err := ds.Get(key)
|
configEntry, err := ds.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
@ -42,13 +42,13 @@ func (ds *Datastore) GetNumber(key string) (float64, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNumber will set the numeric value for a key.
|
// SetNumber will set the numeric value for a key.
|
||||||
func (ds *Datastore) SetNumber(key string, value float64) error {
|
func (ds *Store) SetNumber(key string, value float64) error {
|
||||||
configEntry := models.ConfigEntry{Key: key, Value: value}
|
configEntry := models.ConfigEntry{Key: key, Value: value}
|
||||||
return ds.Save(configEntry)
|
return ds.Save(configEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBool will return the boolean value for a key.
|
// GetBool will return the boolean value for a key.
|
||||||
func (ds *Datastore) GetBool(key string) (bool, error) {
|
func (ds *Store) GetBool(key string) (bool, error) {
|
||||||
configEntry, err := ds.Get(key)
|
configEntry, err := ds.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
@ -57,13 +57,13 @@ func (ds *Datastore) GetBool(key string) (bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetBool will set the boolean value for a key.
|
// SetBool will set the boolean value for a key.
|
||||||
func (ds *Datastore) SetBool(key string, value bool) error {
|
func (ds *Store) SetBool(key string, value bool) error {
|
||||||
configEntry := models.ConfigEntry{Key: key, Value: value}
|
configEntry := models.ConfigEntry{Key: key, Value: value}
|
||||||
return ds.Save(configEntry)
|
return ds.Save(configEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStringMap will return the string map value for a key.
|
// GetStringMap will return the string map value for a key.
|
||||||
func (ds *Datastore) GetStringMap(key string) (map[string]string, error) {
|
func (ds *Store) GetStringMap(key string) (map[string]string, error) {
|
||||||
configEntry, err := ds.Get(key)
|
configEntry, err := ds.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return map[string]string{}, err
|
return map[string]string{}, err
|
||||||
|
@ -72,7 +72,7 @@ func (ds *Datastore) GetStringMap(key string) (map[string]string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetStringMap will set the string map value for a key.
|
// SetStringMap will set the string map value for a key.
|
||||||
func (ds *Datastore) SetStringMap(key string, value map[string]string) error {
|
func (ds *Store) SetStringMap(key string, value map[string]string) error {
|
||||||
configEntry := models.ConfigEntry{Key: key, Value: value}
|
configEntry := models.ConfigEntry{Key: key, Value: value}
|
||||||
return ds.Save(configEntry)
|
return ds.Save(configEntry)
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package datastore
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
|
@ -1,229 +0,0 @@
|
||||||
package datastore
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"database/sql"
|
|
||||||
"encoding/gob"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
// sqlite requires a blank import.
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
"github.com/owncast/owncast/db"
|
|
||||||
"github.com/owncast/owncast/models"
|
|
||||||
"github.com/owncast/owncast/services/config"
|
|
||||||
"github.com/owncast/owncast/utils"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
schemaVersion = 7
|
|
||||||
)
|
|
||||||
|
|
||||||
// Datastore is the global key/value store for configuration values.
|
|
||||||
type Datastore struct {
|
|
||||||
DB *sql.DB
|
|
||||||
cache map[string][]byte
|
|
||||||
DbLock *sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
var temporaryGlobalDatastoreInstance *Datastore
|
|
||||||
|
|
||||||
// NewDatastore creates a new datastore.
|
|
||||||
func NewDatastore(file string) (*Datastore, error) {
|
|
||||||
r := &Datastore{
|
|
||||||
cache: make(map[string][]byte),
|
|
||||||
DbLock: &sync.Mutex{},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.InitializeDatabase(file); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return r, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDatastore returns the shared instance of the owncast datastore.
|
|
||||||
func GetDatastore() *Datastore {
|
|
||||||
if temporaryGlobalDatastoreInstance == nil {
|
|
||||||
c := config.GetConfig()
|
|
||||||
i, err := NewDatastore(c.DatabaseFilePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
temporaryGlobalDatastoreInstance = i
|
|
||||||
}
|
|
||||||
return temporaryGlobalDatastoreInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetQueries will return the shared instance of the SQL query generator.
|
|
||||||
func (ds *Datastore) GetQueries() *db.Queries {
|
|
||||||
return db.New(ds.DB)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get will query the database for the key and return the entry.
|
|
||||||
func (ds *Datastore) Get(key string) (models.ConfigEntry, error) {
|
|
||||||
cachedValue, err := ds.GetCachedValue(key)
|
|
||||||
if err == nil {
|
|
||||||
return models.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 models.ConfigEntry{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result := models.ConfigEntry{
|
|
||||||
Key: resultKey,
|
|
||||||
Value: resultValue,
|
|
||||||
}
|
|
||||||
ds.SetCachedValue(resultKey, resultValue)
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save will save the models.ConfigEntry to the database.
|
|
||||||
func (ds *Datastore) Save(e models.ConfigEntry) error {
|
|
||||||
ds.DbLock.Lock()
|
|
||||||
defer ds.DbLock.Unlock()
|
|
||||||
|
|
||||||
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
|
|
||||||
stmt, err = tx.Prepare("INSERT INTO datastore (key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = stmt.Exec(e.Key, dataGob.Bytes())
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustExec will execute a SQL statement on a provided database instance.
|
|
||||||
func (ds *Datastore) MustExec(s string) {
|
|
||||||
stmt, err := ds.DB.Prepare(s)
|
|
||||||
if err != nil {
|
|
||||||
log.Panic(err)
|
|
||||||
}
|
|
||||||
defer stmt.Close()
|
|
||||||
_, err = stmt.Exec()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnln(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitializeDatabase will open the datastore and make it available.
|
|
||||||
func (ds *Datastore) InitializeDatabase(file string) error {
|
|
||||||
// Allow support for in-memory databases for tests.
|
|
||||||
|
|
||||||
var db *sql.DB
|
|
||||||
|
|
||||||
if file == ":memory:" {
|
|
||||||
inMemoryDb, err := sql.Open("sqlite3", file)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
db = inMemoryDb
|
|
||||||
} else {
|
|
||||||
// Create empty DB file if it doesn't exist.
|
|
||||||
if !utils.DoesFileExists(file) {
|
|
||||||
log.Traceln("Creating new database at", file)
|
|
||||||
|
|
||||||
_, err := os.Create(file) //nolint:gosec
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onDiskDb, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?_cache_size=10000&cache=shared&_journal_mode=WAL", file))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
db = onDiskDb
|
|
||||||
db.SetMaxOpenConns(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
ds.DB = db
|
|
||||||
|
|
||||||
// Some SQLite optimizations
|
|
||||||
_, _ = db.Exec("pragma journal_mode = WAL")
|
|
||||||
_, _ = db.Exec("pragma synchronous = normal")
|
|
||||||
_, _ = db.Exec("pragma temp_store = memory")
|
|
||||||
_, _ = db.Exec("pragma wal_checkpoint(full)")
|
|
||||||
|
|
||||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS config (
|
|
||||||
"key" string NOT NULL PRIMARY KEY,
|
|
||||||
"value" TEXT
|
|
||||||
);`); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ds.createTables()
|
|
||||||
|
|
||||||
var version int
|
|
||||||
err := db.QueryRow("SELECT value FROM config WHERE key='version'").
|
|
||||||
Scan(&version)
|
|
||||||
if err != nil {
|
|
||||||
if err != sql.ErrNoRows {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// fresh database: initialize it with the current schema version
|
|
||||||
_, err := db.Exec("INSERT INTO config(key, value) VALUES(?, ?)", "version", schemaVersion)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
version = schemaVersion
|
|
||||||
}
|
|
||||||
|
|
||||||
// is database from a newer Owncast version?
|
|
||||||
if version > schemaVersion {
|
|
||||||
return fmt.Errorf("incompatible database version %d (versions up to %d are supported)",
|
|
||||||
version, schemaVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// is database schema outdated?
|
|
||||||
if version < schemaVersion {
|
|
||||||
if err := migrateDatabaseSchema(db, version, schemaVersion); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dbBackupTicker := time.NewTicker(1 * time.Hour)
|
|
||||||
go func() {
|
|
||||||
c := config.GetConfig()
|
|
||||||
backupFile := filepath.Join(c.BackupDirectory, "owncastdb.bak")
|
|
||||||
for range dbBackupTicker.C {
|
|
||||||
utils.Backup(db, backupFile)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
423
storage/federationrepository/federationrepository.go
Normal file
423
storage/federationrepository/federationrepository.go
Normal file
|
@ -0,0 +1,423 @@
|
||||||
|
package federationrepository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-fed/activity/streams"
|
||||||
|
"github.com/go-fed/activity/streams/vocab"
|
||||||
|
"github.com/owncast/owncast/activitypub/apmodels"
|
||||||
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/storage/data"
|
||||||
|
"github.com/owncast/owncast/storage/sqlstorage"
|
||||||
|
"github.com/owncast/owncast/utils"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FederationRepository struct {
|
||||||
|
datastore *data.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(datastore *data.Store) *FederationRepository {
|
||||||
|
r := &FederationRepository{
|
||||||
|
datastore: datastore,
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: This is temporary during the transition period.
|
||||||
|
var temporaryGlobalInstance *FederationRepository
|
||||||
|
|
||||||
|
// GetUserRepository will return the user repository.
|
||||||
|
func Get() *FederationRepository {
|
||||||
|
if temporaryGlobalInstance == nil {
|
||||||
|
i := New(data.GetDatastore())
|
||||||
|
temporaryGlobalInstance = i
|
||||||
|
}
|
||||||
|
return temporaryGlobalInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFollowerCount will return the number of followers we're keeping track of.
|
||||||
|
func (f *FederationRepository) GetFollowerCount() (int64, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
return f.datastore.GetQueries().GetFollowerCount(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFederationFollowers will return a slice of the followers we keep track of locally.
|
||||||
|
func (f *FederationRepository) GetFederationFollowers(limit int, offset int) ([]models.Follower, int, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
total, err := f.datastore.GetQueries().GetFollowerCount(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, errors.Wrap(err, "unable to fetch total number of followers")
|
||||||
|
}
|
||||||
|
|
||||||
|
followersResult, err := f.datastore.GetQueries().GetFederationFollowersWithOffset(ctx, sqlstorage.GetFederationFollowersWithOffsetParams{
|
||||||
|
Limit: int32(limit),
|
||||||
|
Offset: int32(offset),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
followers := make([]models.Follower, 0)
|
||||||
|
|
||||||
|
for _, row := range followersResult {
|
||||||
|
singleFollower := models.Follower{
|
||||||
|
Name: row.Name.String,
|
||||||
|
Username: row.Username,
|
||||||
|
Image: row.Image.String,
|
||||||
|
ActorIRI: row.Iri,
|
||||||
|
Inbox: row.Inbox,
|
||||||
|
Timestamp: utils.NullTime(row.CreatedAt),
|
||||||
|
}
|
||||||
|
|
||||||
|
followers = append(followers, singleFollower)
|
||||||
|
}
|
||||||
|
|
||||||
|
return followers, int(total), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPendingFollowRequests will return pending follow requests.
|
||||||
|
func (f *FederationRepository) GetPendingFollowRequests() ([]models.Follower, error) {
|
||||||
|
pendingFollowersResult, err := f.datastore.GetQueries().GetFederationFollowerApprovalRequests(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
followers := make([]models.Follower, 0)
|
||||||
|
|
||||||
|
for _, row := range pendingFollowersResult {
|
||||||
|
singleFollower := models.Follower{
|
||||||
|
Name: row.Name.String,
|
||||||
|
Username: row.Username,
|
||||||
|
Image: row.Image.String,
|
||||||
|
ActorIRI: row.Iri,
|
||||||
|
Inbox: row.Inbox,
|
||||||
|
Timestamp: utils.NullTime{Time: row.CreatedAt.Time, Valid: true},
|
||||||
|
}
|
||||||
|
followers = append(followers, singleFollower)
|
||||||
|
}
|
||||||
|
|
||||||
|
return followers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBlockedAndRejectedFollowers will return blocked and rejected followers.
|
||||||
|
func (f *FederationRepository) GetBlockedAndRejectedFollowers() ([]models.Follower, error) {
|
||||||
|
pendingFollowersResult, err := f.datastore.GetQueries().GetRejectedAndBlockedFollowers(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
followers := make([]models.Follower, 0)
|
||||||
|
|
||||||
|
for _, row := range pendingFollowersResult {
|
||||||
|
singleFollower := models.Follower{
|
||||||
|
Name: row.Name.String,
|
||||||
|
Username: row.Username,
|
||||||
|
Image: row.Image.String,
|
||||||
|
ActorIRI: row.Iri,
|
||||||
|
DisabledAt: utils.NullTime{Time: row.DisabledAt.Time, Valid: true},
|
||||||
|
Timestamp: utils.NullTime{Time: row.CreatedAt.Time, Valid: true},
|
||||||
|
}
|
||||||
|
followers = append(followers, singleFollower)
|
||||||
|
}
|
||||||
|
|
||||||
|
return followers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFollow will save a follow to the datastore.
|
||||||
|
func (f *FederationRepository) AddFollow(follow apmodels.ActivityPubActor, approved bool) error {
|
||||||
|
log.Traceln("Saving", follow.ActorIri, "as a follower.")
|
||||||
|
var image string
|
||||||
|
if follow.Image != nil {
|
||||||
|
image = follow.Image.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
followRequestObject, err := apmodels.Serialize(follow.RequestObject)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "error serializing follow request object")
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.createFollow(follow.ActorIri.String(), follow.Inbox.String(), follow.FollowRequestIri.String(), follow.Name, follow.Username, image, followRequestObject, approved)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveFollow will remove a follow from the datastore.
|
||||||
|
func (f *FederationRepository) RemoveFollow(unfollow apmodels.ActivityPubActor) error {
|
||||||
|
log.Traceln("Removing", unfollow.ActorIri, "as a follower.")
|
||||||
|
return f.removeFollow(unfollow.ActorIri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFollower will return a single follower/request given an IRI.
|
||||||
|
func (f *FederationRepository) GetFollower(iri string) (*apmodels.ActivityPubActor, error) {
|
||||||
|
result, err := f.datastore.GetQueries().GetFollowerByIRI(context.Background(), iri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
followIRI, err := url.Parse(result.Request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error parsing follow request IRI")
|
||||||
|
}
|
||||||
|
|
||||||
|
iriURL, err := url.Parse(result.Iri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error parsing actor IRI")
|
||||||
|
}
|
||||||
|
|
||||||
|
inbox, err := url.Parse(result.Inbox)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error parsing acting inbox")
|
||||||
|
}
|
||||||
|
|
||||||
|
image, _ := url.Parse(result.Image.String)
|
||||||
|
|
||||||
|
var disabledAt *time.Time
|
||||||
|
if result.DisabledAt.Valid {
|
||||||
|
disabledAt = &result.DisabledAt.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
follower := apmodels.ActivityPubActor{
|
||||||
|
ActorIri: iriURL,
|
||||||
|
Inbox: inbox,
|
||||||
|
Name: result.Name.String,
|
||||||
|
Username: result.Username,
|
||||||
|
Image: image,
|
||||||
|
FollowRequestIri: followIRI,
|
||||||
|
DisabledAt: disabledAt,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &follower, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApprovePreviousFollowRequest will approve a follow request.
|
||||||
|
func (f *FederationRepository) ApprovePreviousFollowRequest(iri string) error {
|
||||||
|
return f.datastore.GetQueries().ApproveFederationFollower(context.Background(), sqlstorage.ApproveFederationFollowerParams{
|
||||||
|
Iri: iri,
|
||||||
|
ApprovedAt: sql.NullTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockOrRejectFollower will block an existing follower or reject a follow request.
|
||||||
|
func (f *FederationRepository) BlockOrRejectFollower(iri string) error {
|
||||||
|
return f.datastore.GetQueries().RejectFederationFollower(context.Background(), sqlstorage.RejectFederationFollowerParams{
|
||||||
|
Iri: iri,
|
||||||
|
DisabledAt: sql.NullTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
Valid: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FederationRepository) createFollow(actor, inbox, request, name, username, image string, requestObject []byte, approved bool) error {
|
||||||
|
tx, err := f.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugln(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
var approvedAt sql.NullTime
|
||||||
|
if approved {
|
||||||
|
approvedAt = sql.NullTime{
|
||||||
|
Time: time.Now(),
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = f.datastore.GetQueries().WithTx(tx).AddFollower(context.Background(), sqlstorage.AddFollowerParams{
|
||||||
|
Iri: actor,
|
||||||
|
Inbox: inbox,
|
||||||
|
Name: sql.NullString{String: name, Valid: true},
|
||||||
|
Username: username,
|
||||||
|
Image: sql.NullString{String: image, Valid: true},
|
||||||
|
ApprovedAt: approvedAt,
|
||||||
|
Request: request,
|
||||||
|
RequestObject: requestObject,
|
||||||
|
}); err != nil {
|
||||||
|
log.Errorln("error creating new federation follow: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFollower will update the details of a stored follower given an IRI.
|
||||||
|
func (f *FederationRepository) UpdateFollower(actorIRI string, inbox string, name string, username string, image string) error {
|
||||||
|
f.datastore.DbLock.Lock()
|
||||||
|
defer f.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
|
tx, err := f.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugln(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = f.datastore.GetQueries().WithTx(tx).UpdateFollowerByIRI(context.Background(), sqlstorage.UpdateFollowerByIRIParams{
|
||||||
|
Inbox: inbox,
|
||||||
|
Name: sql.NullString{String: name, Valid: true},
|
||||||
|
Username: username,
|
||||||
|
Image: sql.NullString{String: image, Valid: true},
|
||||||
|
Iri: actorIRI,
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("error updating follower %s %s", actorIRI, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FederationRepository) removeFollow(actor *url.URL) error {
|
||||||
|
f.datastore.DbLock.Lock()
|
||||||
|
defer f.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
|
tx, err := f.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := f.datastore.GetQueries().WithTx(tx).RemoveFollowerByIRI(context.Background(), actor.String()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOutboxPostCount will return the number of posts in the outbox.
|
||||||
|
func (f *FederationRepository) GetOutboxPostCount() (int64, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
return f.datastore.GetQueries().GetLocalPostCount(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOutbox will return an instance of the outbox populated by stored items.
|
||||||
|
func (f *FederationRepository) GetOutbox(limit int, offset int) (vocab.ActivityStreamsOrderedCollection, error) {
|
||||||
|
collection := streams.NewActivityStreamsOrderedCollection()
|
||||||
|
orderedItems := streams.NewActivityStreamsOrderedItemsProperty()
|
||||||
|
rows, err := f.datastore.GetQueries().GetOutboxWithOffset(
|
||||||
|
context.Background(),
|
||||||
|
sqlstorage.GetOutboxWithOffsetParams{Limit: int32(limit), Offset: int32(offset)},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return collection, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, value := range rows {
|
||||||
|
createCallback := func(c context.Context, activity vocab.ActivityStreamsCreate) error {
|
||||||
|
orderedItems.AppendActivityStreamsCreate(activity)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := resolvers.Resolve(context.Background(), value, createCallback); err != nil {
|
||||||
|
return collection, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddToOutbox will store a single payload to the persistence layer.
|
||||||
|
func (f *FederationRepository) AddToOutbox(iri string, itemData []byte, typeString string, isLiveNotification bool) error {
|
||||||
|
tx, err := f.datastore.DB.Begin()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugln(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err = f.datastore.GetQueries().WithTx(tx).AddToOutbox(context.Background(), sqlstorage.AddToOutboxParams{
|
||||||
|
Iri: iri,
|
||||||
|
Value: itemData,
|
||||||
|
Type: typeString,
|
||||||
|
LiveNotification: sql.NullBool{Bool: isLiveNotification, Valid: true},
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("error creating new item in federation outbox %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetObjectByIRI will return a string representation of a single object by the IRI.
|
||||||
|
func (f *FederationRepository) GetObjectByIRI(iri string) (string, bool, time.Time, error) {
|
||||||
|
row, err := f.datastore.GetQueries().GetObjectFromOutboxByIRI(context.Background(), iri)
|
||||||
|
return string(row.Value), row.LiveNotification.Bool, row.CreatedAt.Time, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocalPostCount will return the number of posts existing locally.
|
||||||
|
func (f *FederationRepository) GetLocalPostCount() (int64, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
return f.datastore.GetQueries().GetLocalPostCount(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveInboundFediverseActivity will save an event to the ap_inbound_activities table.
|
||||||
|
func (f *FederationRepository) SaveInboundFediverseActivity(objectIRI string, actorIRI string, eventType string, timestamp time.Time) error {
|
||||||
|
if err := f.datastore.GetQueries().AddToAcceptedActivities(context.Background(), sqlstorage.AddToAcceptedActivitiesParams{
|
||||||
|
Iri: objectIRI,
|
||||||
|
Actor: actorIRI,
|
||||||
|
Type: eventType,
|
||||||
|
Timestamp: timestamp,
|
||||||
|
}); err != nil {
|
||||||
|
return errors.Wrap(err, "error saving event "+objectIRI)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInboundActivities will return a collection of saved, federated activities
|
||||||
|
// limited and offset by the values provided to support pagination.
|
||||||
|
func (f *FederationRepository) GetInboundActivities(limit int, offset int) ([]models.FederatedActivity, int, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
rows, err := f.datastore.GetQueries().GetInboundActivitiesWithOffset(ctx, sqlstorage.GetInboundActivitiesWithOffsetParams{
|
||||||
|
Limit: int32(limit),
|
||||||
|
Offset: int32(offset),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
activities := make([]models.FederatedActivity, 0)
|
||||||
|
|
||||||
|
total, err := f.datastore.GetQueries().GetInboundActivityCount(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, errors.Wrap(err, "unable to fetch total activity count")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
singleActivity := models.FederatedActivity{
|
||||||
|
IRI: row.Iri,
|
||||||
|
ActorIRI: row.Actor,
|
||||||
|
Type: row.Type,
|
||||||
|
Timestamp: row.Timestamp,
|
||||||
|
}
|
||||||
|
activities = append(activities, singleActivity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return activities, int(total), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPreviouslyHandledInboundActivity will return if we have previously handled
|
||||||
|
// an inbound federated activity.
|
||||||
|
func (f *FederationRepository) HasPreviouslyHandledInboundActivity(iri string, actorIRI string, eventType string) (bool, error) {
|
||||||
|
exists, err := f.datastore.GetQueries().DoesInboundActivityExist(context.Background(), sqlstorage.DoesInboundActivityExistParams{
|
||||||
|
Iri: iri,
|
||||||
|
Actor: actorIRI,
|
||||||
|
Type: eventType,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return exists > 0, nil
|
||||||
|
}
|
|
@ -1,35 +1,37 @@
|
||||||
package persistence
|
package federationrepository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/storage/data"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
setup()
|
ds, err := data.NewStore(":memory:")
|
||||||
code := m.Run()
|
if err != nil {
|
||||||
os.Exit(code)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var followers = []models.Follower{}
|
federationRepository := New(ds)
|
||||||
|
|
||||||
func setup() {
|
|
||||||
data.SetupPersistence(":memory:")
|
|
||||||
_datastore = data.GetDatastore()
|
|
||||||
|
|
||||||
number := 100
|
number := 100
|
||||||
for i := 0; i < number; i++ {
|
for i := 0; i < number; i++ {
|
||||||
u := createFakeFollower()
|
u := createFakeFollower()
|
||||||
createFollow(u.ActorIRI, u.Inbox, "https://fake.fediverse.server/some/request", u.Name, u.Username, u.Image, nil, true)
|
federationRepository.createFollow(u.ActorIRI, u.Inbox, "https://fake.fediverse.server/some/request", u.Name, u.Username, u.Image, nil, true)
|
||||||
followers = append(followers, u)
|
followers = append(followers, u)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var followers = []models.Follower{}
|
||||||
|
|
||||||
func TestQueryFollowers(t *testing.T) {
|
func TestQueryFollowers(t *testing.T) {
|
||||||
f, total, err := GetFederationFollowers(10, 0)
|
federationRepository := Get()
|
||||||
|
|
||||||
|
f, total, err := federationRepository.GetFederationFollowers(10, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error querying followers: %s", err)
|
t.Errorf("Error querying followers: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -44,7 +46,9 @@ func TestQueryFollowers(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQueryFollowersWithOffset(t *testing.T) {
|
func TestQueryFollowersWithOffset(t *testing.T) {
|
||||||
f, total, err := GetFederationFollowers(10, 10)
|
federationRepository := Get()
|
||||||
|
|
||||||
|
f, total, err := federationRepository.GetFederationFollowers(10, 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error querying followers: %s", err)
|
t.Errorf("Error querying followers: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -59,7 +63,9 @@ func TestQueryFollowersWithOffset(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQueryFollowersWithOffsetAndLimit(t *testing.T) {
|
func TestQueryFollowersWithOffsetAndLimit(t *testing.T) {
|
||||||
f, total, err := GetFederationFollowers(10, 90)
|
federationRepository := Get()
|
||||||
|
|
||||||
|
f, total, err := federationRepository.GetFederationFollowers(10, 90)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error querying followers: %s", err)
|
t.Errorf("Error querying followers: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -74,7 +80,9 @@ func TestQueryFollowersWithOffsetAndLimit(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestQueryFollowersWithPagination(t *testing.T) {
|
func TestQueryFollowersWithPagination(t *testing.T) {
|
||||||
f, _, err := GetFederationFollowers(15, 10)
|
federationRepository := Get()
|
||||||
|
|
||||||
|
f, _, err := federationRepository.GetFederationFollowers(15, 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Error querying followers: %s", err)
|
t.Errorf("Error querying followers: %s", err)
|
||||||
}
|
}
|
57
storage/notificationsrepository/notificationsrepository.go
Normal file
57
storage/notificationsrepository/notificationsrepository.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package notificationsrepository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/storage/data"
|
||||||
|
"github.com/owncast/owncast/storage/sqlstorage"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationsRepository interface{}
|
||||||
|
|
||||||
|
type SqlNotificationsRepository struct {
|
||||||
|
datastore *data.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(datastore *data.Store) *SqlNotificationsRepository {
|
||||||
|
return &SqlNotificationsRepository{datastore}
|
||||||
|
}
|
||||||
|
|
||||||
|
var temporaryGlobalInstance *SqlNotificationsRepository
|
||||||
|
|
||||||
|
func Get() *SqlNotificationsRepository {
|
||||||
|
if temporaryGlobalInstance == nil {
|
||||||
|
temporaryGlobalInstance = &SqlNotificationsRepository{}
|
||||||
|
}
|
||||||
|
return temporaryGlobalInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddNotification saves a new user notification destination.
|
||||||
|
func (r *SqlNotificationsRepository) AddNotification(channel, destination string) error {
|
||||||
|
return data.GetDatastore().GetQueries().AddNotification(context.Background(), sqlstorage.AddNotificationParams{
|
||||||
|
Channel: channel,
|
||||||
|
Destination: destination,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveNotificationForChannel removes a notification destination.
|
||||||
|
func (r *SqlNotificationsRepository) RemoveNotificationForChannel(channel, destination string) error {
|
||||||
|
log.Debugln("Removing notification for channel", channel)
|
||||||
|
return data.GetDatastore().GetQueries().RemoveNotificationDestinationForChannel(context.Background(), sqlstorage.RemoveNotificationDestinationForChannelParams{
|
||||||
|
Channel: channel,
|
||||||
|
Destination: destination,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNotificationDestinationsForChannel will return a collection of
|
||||||
|
// destinations to notify for a given channel.
|
||||||
|
func (r *SqlNotificationsRepository) GetNotificationDestinationsForChannel(channel string) ([]string, error) {
|
||||||
|
result, err := data.GetDatastore().GetQueries().GetNotificationDestinationsForChannel(context.Background(), channel)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "unable to query notification destinations for channel "+channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
|
@ -1,6 +1,14 @@
|
||||||
# SQL Queries
|
# SQL Storage
|
||||||
|
|
||||||
sqlc generates **type-safe code** from SQL. Here's how it works:
|
This package contains the base SQL schema, migrations and queries. It should not need to be imported by any package other than the datstore.
|
||||||
|
|
||||||
|
## SQL Migrations
|
||||||
|
|
||||||
|
Add migrations to `migrations.go` and use raw SQL make your required changes between schema versions.
|
||||||
|
|
||||||
|
## SQL Queries
|
||||||
|
|
||||||
|
_sqlc_ generates **type-safe code** from SQL. Here's how it works:
|
||||||
|
|
||||||
1. You define the schema in `schema.sql`.
|
1. You define the schema in `schema.sql`.
|
||||||
1. You write your queries in `query.sql` using regular SQL.
|
1. You write your queries in `query.sql` using regular SQL.
|
|
@ -1,8 +1,8 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.15.0
|
// sqlc v1.18.0
|
||||||
|
|
||||||
package db
|
package sqlstorage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
102
storage/sqlstorage/initialize.go
Normal file
102
storage/sqlstorage/initialize.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
package sqlstorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/services/config"
|
||||||
|
"github.com/owncast/owncast/utils"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitializeDatabase will open the datastore and make it available.
|
||||||
|
func InitializeDatabase(file string, schemaVersion int) (*sql.DB, error) {
|
||||||
|
// Allow support for in-memory databases for tests.
|
||||||
|
|
||||||
|
var db *sql.DB
|
||||||
|
|
||||||
|
if file == ":memory:" {
|
||||||
|
inMemoryDb, err := sql.Open("sqlite3", file)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
db = inMemoryDb
|
||||||
|
} else {
|
||||||
|
// Create empty DB file if it doesn't exist.
|
||||||
|
if !utils.DoesFileExists(file) {
|
||||||
|
log.Traceln("Creating new database at", file)
|
||||||
|
|
||||||
|
_, err := os.Create(file) //nolint:gosec
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDiskDb, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?_cache_size=10000&cache=shared&_journal_mode=WAL", file))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
db = onDiskDb
|
||||||
|
db.SetMaxOpenConns(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some SQLite optimizations
|
||||||
|
_, _ = db.Exec("pragma journal_mode = WAL")
|
||||||
|
_, _ = db.Exec("pragma synchronous = normal")
|
||||||
|
_, _ = db.Exec("pragma temp_store = memory")
|
||||||
|
_, _ = db.Exec("pragma wal_checkpoint(full)")
|
||||||
|
|
||||||
|
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS config (
|
||||||
|
"key" string NOT NULL PRIMARY KEY,
|
||||||
|
"value" TEXT
|
||||||
|
);`); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateAllTables(db)
|
||||||
|
|
||||||
|
var version int
|
||||||
|
err := db.QueryRow("SELECT value FROM config WHERE key='version'").
|
||||||
|
Scan(&version)
|
||||||
|
if err != nil {
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// fresh database: initialize it with the current schema version
|
||||||
|
_, err := db.Exec("INSERT INTO config(key, value) VALUES(?, ?)", "version", schemaVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
version = schemaVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
// is database from a newer Owncast version?
|
||||||
|
if version > schemaVersion {
|
||||||
|
return nil, fmt.Errorf("incompatible database version %d (versions up to %d are supported)",
|
||||||
|
version, schemaVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// is database schema outdated?
|
||||||
|
if version < schemaVersion {
|
||||||
|
migrations := NewSqlMigrations(db)
|
||||||
|
|
||||||
|
if err := migrations.MigrateDatabaseSchema(db, version, schemaVersion); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dbBackupTicker := time.NewTicker(1 * time.Hour)
|
||||||
|
go func() {
|
||||||
|
c := config.GetConfig()
|
||||||
|
backupFile := filepath.Join(c.BackupDirectory, "owncastdb.bak")
|
||||||
|
for range dbBackupTicker.C {
|
||||||
|
utils.Backup(db, backupFile)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package datastore
|
package sqlstorage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
@ -12,7 +12,15 @@ import (
|
||||||
"github.com/teris-io/shortid"
|
"github.com/teris-io/shortid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func migrateDatabaseSchema(db *sql.DB, from, to int) error {
|
type SqlMigrations struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSqlMigrations(db *sql.DB) *SqlMigrations {
|
||||||
|
return &SqlMigrations{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SqlMigrations) MigrateDatabaseSchema(db *sql.DB, from, to int) error {
|
||||||
c := config.GetConfig()
|
c := config.GetConfig()
|
||||||
|
|
||||||
log.Printf("Migrating database from version %d to %d", from, to)
|
log.Printf("Migrating database from version %d to %d", from, to)
|
||||||
|
@ -22,19 +30,19 @@ func migrateDatabaseSchema(db *sql.DB, from, to int) error {
|
||||||
log.Tracef("Migration step from %d to %d\n", v, v+1)
|
log.Tracef("Migration step from %d to %d\n", v, v+1)
|
||||||
switch v {
|
switch v {
|
||||||
case 0:
|
case 0:
|
||||||
migrateToSchema1(db)
|
m.migrateToSchema1(db)
|
||||||
case 1:
|
case 1:
|
||||||
migrateToSchema2(db)
|
m.migrateToSchema2(db)
|
||||||
case 2:
|
case 2:
|
||||||
migrateToSchema3(db)
|
m.migrateToSchema3(db)
|
||||||
case 3:
|
case 3:
|
||||||
migrateToSchema4(db)
|
m.migrateToSchema4(db)
|
||||||
case 4:
|
case 4:
|
||||||
migrateToSchema5(db)
|
m.migrateToSchema5(db)
|
||||||
case 5:
|
case 5:
|
||||||
migrateToSchema6(db)
|
m.migrateToSchema6(db)
|
||||||
case 6:
|
case 6:
|
||||||
migrateToSchema7(db)
|
m.migrateToSchema7(db)
|
||||||
default:
|
default:
|
||||||
log.Fatalln("missing database migration step")
|
log.Fatalln("missing database migration step")
|
||||||
}
|
}
|
||||||
|
@ -48,7 +56,7 @@ func migrateDatabaseSchema(db *sql.DB, from, to int) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateToSchema7(db *sql.DB) {
|
func (m *SqlMigrations) migrateToSchema7(db *sql.DB) {
|
||||||
log.Println("Migrating users. This may take time if you have lots of users...")
|
log.Println("Migrating users. This may take time if you have lots of users...")
|
||||||
|
|
||||||
var ids []string
|
var ids []string
|
||||||
|
@ -92,7 +100,7 @@ func migrateToSchema7(db *sql.DB) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateToSchema6(db *sql.DB) {
|
func (m *SqlMigrations) migrateToSchema6(db *sql.DB) {
|
||||||
// Fix chat messages table schema. Since chat is ephemeral we can drop
|
// Fix chat messages table schema. Since chat is ephemeral we can drop
|
||||||
// the table and recreate it.
|
// the table and recreate it.
|
||||||
// Drop the old messages table
|
// Drop the old messages table
|
||||||
|
@ -103,9 +111,9 @@ func migrateToSchema6(db *sql.DB) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:cyclop
|
// nolint:cyclop
|
||||||
func migrateToSchema5(db *sql.DB) {
|
func (m *SqlMigrations) migrateToSchema5(db *sql.DB) {
|
||||||
// Create the access tokens table.
|
// Create the access tokens table.
|
||||||
createAccessTokenTable(db)
|
CreateAccessTokenTable(db)
|
||||||
|
|
||||||
// 1. Authenticated bool added to the users table.
|
// 1. Authenticated bool added to the users table.
|
||||||
// 2. Access tokens are now stored in their own table.
|
// 2. Access tokens are now stored in their own table.
|
||||||
|
@ -212,7 +220,7 @@ func migrateToSchema5(db *sql.DB) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateToSchema4(db *sql.DB) {
|
func (m *SqlMigrations) migrateToSchema4(db *sql.DB) {
|
||||||
// We now save the follow request object.
|
// We now save the follow request object.
|
||||||
stmt, err := db.Prepare("ALTER TABLE ap_followers ADD COLUMN request_object BLOB")
|
stmt, err := db.Prepare("ALTER TABLE ap_followers ADD COLUMN request_object BLOB")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -227,7 +235,7 @@ func migrateToSchema4(db *sql.DB) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateToSchema3(db *sql.DB) {
|
func (m *SqlMigrations) migrateToSchema3(db *sql.DB) {
|
||||||
// Since it's just a backlog of chat messages let's wipe the old messages
|
// Since it's just a backlog of chat messages let's wipe the old messages
|
||||||
// and recreate the table.
|
// and recreate the table.
|
||||||
|
|
||||||
|
@ -246,7 +254,7 @@ func migrateToSchema3(db *sql.DB) {
|
||||||
CreateMessagesTable(db)
|
CreateMessagesTable(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateToSchema2(db *sql.DB) {
|
func (m *SqlMigrations) migrateToSchema2(db *sql.DB) {
|
||||||
// Since it's just a backlog of chat messages let's wipe the old messages
|
// Since it's just a backlog of chat messages let's wipe the old messages
|
||||||
// and recreate the table.
|
// and recreate the table.
|
||||||
|
|
||||||
|
@ -265,7 +273,7 @@ func migrateToSchema2(db *sql.DB) {
|
||||||
CreateMessagesTable(db)
|
CreateMessagesTable(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateToSchema1(db *sql.DB) {
|
func (m *SqlMigrations) migrateToSchema1(db *sql.DB) {
|
||||||
// Since it's just a backlog of chat messages let's wipe the old messages
|
// Since it's just a backlog of chat messages let's wipe the old messages
|
||||||
// and recreate the table.
|
// and recreate the table.
|
||||||
|
|
||||||
|
@ -340,13 +348,13 @@ func migrateToSchema1(db *sql.DB) {
|
||||||
// Recreate them as users
|
// Recreate them as users
|
||||||
for _, token := range oldAccessTokens {
|
for _, token := range oldAccessTokens {
|
||||||
color := utils.GenerateRandomDisplayColor(config.MaxUserColor)
|
color := utils.GenerateRandomDisplayColor(config.MaxUserColor)
|
||||||
if err := insertAPIToken(db, token.accessToken, token.displayName, color, token.scopes); err != nil {
|
if err := m.insertAPIToken(db, token.accessToken, token.displayName, color, token.scopes); err != nil {
|
||||||
log.Errorln("Error migrating access token", err)
|
log.Errorln("Error migrating access token", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertAPIToken(db *sql.DB, token string, name string, color int, scopes string) error {
|
func (m *SqlMigrations) insertAPIToken(db *sql.DB, token string, name string, color int, scopes string) error {
|
||||||
log.Debugln("Adding new access token:", name)
|
log.Debugln("Adding new access token:", name)
|
||||||
|
|
||||||
id := shortid.MustGenerate()
|
id := shortid.MustGenerate()
|
|
@ -1,8 +1,8 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.15.0
|
// sqlc v1.18.0
|
||||||
|
|
||||||
package db
|
package sqlstorage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
|
@ -1,9 +1,9 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.19.1
|
// sqlc v1.18.0
|
||||||
// source: query.sql
|
// source: query.sql
|
||||||
|
|
||||||
package db
|
package sqlstorage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
|
@ -1,4 +1,4 @@
|
||||||
package datastore
|
package sqlstorage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
@ -7,16 +7,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// This is a central point for creating all the tables required for the application.
|
// This is a central point for creating all the tables required for the application.
|
||||||
func (ds *Datastore) createTables() {
|
func CreateAllTables(db *sql.DB) {
|
||||||
createDatastoreTable(ds.DB)
|
createDatastoreTable(db)
|
||||||
createUsersTable(ds.DB)
|
createUsersTable(db)
|
||||||
createAccessTokenTable(ds.DB)
|
CreateAccessTokenTable(db)
|
||||||
createWebhooksTable(ds.DB)
|
createWebhooksTable(db)
|
||||||
createFederationFollowersTable(ds.DB)
|
createFederationFollowersTable(db)
|
||||||
createFederationOutboxTable(ds.DB)
|
createFederationOutboxTable(db)
|
||||||
createNotificationsTable(ds.DB)
|
createNotificationsTable(db)
|
||||||
CreateBanIPTable(ds.DB)
|
CreateBanIPTable(db)
|
||||||
CreateMessagesTable(ds.DB)
|
CreateMessagesTable(db)
|
||||||
|
createAuthTable(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDatastoreTable(db *sql.DB) {
|
func createDatastoreTable(db *sql.DB) {
|
||||||
|
@ -74,7 +75,7 @@ func createUsersTable(db *sql.DB) {
|
||||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_disabled_at ON users (disabled_at);`, db)
|
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_disabled_at ON users (disabled_at);`, db)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createAccessTokenTable(db *sql.DB) {
|
func CreateAccessTokenTable(db *sql.DB) {
|
||||||
createTableSQL := `CREATE TABLE IF NOT EXISTS user_access_tokens (
|
createTableSQL := `CREATE TABLE IF NOT EXISTS user_access_tokens (
|
||||||
"token" TEXT NOT NULL PRIMARY KEY,
|
"token" TEXT NOT NULL PRIMARY KEY,
|
||||||
"user_id" TEXT NOT NULL,
|
"user_id" TEXT NOT NULL,
|
||||||
|
@ -200,3 +201,16 @@ func createNotificationsTable(db *sql.DB) {
|
||||||
MustExec(createTableSQL, db)
|
MustExec(createTableSQL, db)
|
||||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_channel ON notifications (channel);`, db)
|
MustExec(`CREATE INDEX IF NOT EXISTS idx_channel ON notifications (channel);`, db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createAuthTable(db *sql.DB) {
|
||||||
|
createTableSQL := `CREATE TABLE IF NOT EXISTS auth (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"user_id" TEXT NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"timestamp" DATE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||||
|
);`
|
||||||
|
MustExec(createTableSQL, db)
|
||||||
|
MustExec(`CREATE INDEX IF NOT EXISTS idx_auth_token ON auth (token);`, db)
|
||||||
|
}
|
20
storage/sqlstorage/utils.go
Normal file
20
storage/sqlstorage/utils.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package sqlstorage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MustExec will execute a SQL statement on a provided database instance.
|
||||||
|
func MustExec(s string, db *sql.DB) {
|
||||||
|
stmt, err := db.Prepare(s)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
_, err = stmt.Exec()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnln(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,10 @@
|
||||||
package storage
|
package userrepository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
|
"github.com/owncast/owncast/storage/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -10,16 +13,18 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
testScopes = []string{"test-scope"}
|
testScopes = []string{"test-scope"}
|
||||||
userRepository UserRepository
|
userRepository *SqlUserRepository
|
||||||
|
configRepository configrepository.ConfigRepository
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
if err := data.SetupPersistence(":memory:"); err != nil {
|
ds, err := data.NewStore(":memory:")
|
||||||
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
userRepository = NewUserRepository(data.GetDatastore())
|
userRepository = New(ds)
|
||||||
|
|
||||||
m.Run()
|
m.Run()
|
||||||
}
|
}
|
47
storage/userrepository/userauth.go
Normal file
47
storage/userrepository/userauth.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
package userrepository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/storage/sqlstorage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddAuth will add an external authentication token and type for a user.
|
||||||
|
func (r *SqlUserRepository) AddAuth(userID, authToken string, authType models.AuthType) error {
|
||||||
|
return r.datastore.GetQueries().AddAuthForUser(context.Background(), sqlstorage.AddAuthForUserParams{
|
||||||
|
UserID: userID,
|
||||||
|
Token: authToken,
|
||||||
|
Type: string(authType),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByAuth will return an existing user given auth details if a user
|
||||||
|
// has previously authenticated with that method.
|
||||||
|
func (r *SqlUserRepository) GetUserByAuth(authToken string, authType models.AuthType) *models.User {
|
||||||
|
u, err := r.datastore.GetQueries().GetUserByAuth(context.Background(), sqlstorage.GetUserByAuthParams{
|
||||||
|
Token: authToken,
|
||||||
|
Type: string(authType),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var scopes []string
|
||||||
|
if u.Scopes.Valid {
|
||||||
|
scopes = strings.Split(u.Scopes.String, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &models.User{
|
||||||
|
ID: u.ID,
|
||||||
|
DisplayName: u.DisplayName,
|
||||||
|
DisplayColor: int(u.DisplayColor),
|
||||||
|
CreatedAt: u.CreatedAt.Time,
|
||||||
|
DisabledAt: &u.DisabledAt.Time,
|
||||||
|
PreviousNames: strings.Split(u.PreviousNames.String, ","),
|
||||||
|
NameChangedAt: &u.NamechangedAt.Time,
|
||||||
|
AuthenticatedAt: &u.AuthenticatedAt.Time,
|
||||||
|
Scopes: scopes,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package storage
|
package userrepository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -8,10 +8,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/owncast/owncast/db"
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/owncast/owncast/services/config"
|
"github.com/owncast/owncast/services/config"
|
||||||
"github.com/owncast/owncast/storage/datastore"
|
"github.com/owncast/owncast/storage/data"
|
||||||
|
"github.com/owncast/owncast/storage/sqlstorage"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/teris-io/shortid"
|
"github.com/teris-io/shortid"
|
||||||
|
@ -37,19 +37,23 @@ type UserRepository interface {
|
||||||
SetModerator(userID string, isModerator bool) error
|
SetModerator(userID string, isModerator bool) error
|
||||||
SetUserAsAuthenticated(userID string) error
|
SetUserAsAuthenticated(userID string) error
|
||||||
HasValidScopes(scopes []string) bool
|
HasValidScopes(scopes []string) bool
|
||||||
|
GetUserByAuth(authToken string, authType models.AuthType) *models.User
|
||||||
|
AddAuth(userID, authToken string, authType models.AuthType) error
|
||||||
|
SetExternalAPIUserAccessTokenAsUsed(token string) error
|
||||||
|
GetUsersCount() int
|
||||||
}
|
}
|
||||||
|
|
||||||
type SqlUserRepository struct {
|
type SqlUserRepository struct {
|
||||||
datastore *datastore.Datastore
|
datastore *data.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: This is temporary during the transition period.
|
// NOTE: This is temporary during the transition period.
|
||||||
var temporaryGlobalInstance UserRepository
|
var temporaryGlobalInstance *SqlUserRepository
|
||||||
|
|
||||||
// GetUserRepository will return the user repository.
|
// Get will return the user repository.
|
||||||
func GetUserRepository() UserRepository {
|
func Get() *SqlUserRepository {
|
||||||
if temporaryGlobalInstance == nil {
|
if temporaryGlobalInstance == nil {
|
||||||
i := NewUserRepository(datastore.GetDatastore())
|
i := New(data.GetDatastore())
|
||||||
temporaryGlobalInstance = i
|
temporaryGlobalInstance = i
|
||||||
}
|
}
|
||||||
return temporaryGlobalInstance
|
return temporaryGlobalInstance
|
||||||
|
@ -69,7 +73,7 @@ const (
|
||||||
// User represents a single chat user.
|
// User represents a single chat user.
|
||||||
|
|
||||||
// SetupUsers will perform the initial initialization of the user package.
|
// SetupUsers will perform the initial initialization of the user package.
|
||||||
func NewUserRepository(datastore *datastore.Datastore) UserRepository {
|
func New(datastore *data.Store) *SqlUserRepository {
|
||||||
r := &SqlUserRepository{
|
r := &SqlUserRepository{
|
||||||
datastore: datastore,
|
datastore: datastore,
|
||||||
}
|
}
|
||||||
|
@ -134,7 +138,7 @@ func (r *SqlUserRepository) ChangeUsername(userID string, username string) error
|
||||||
r.datastore.DbLock.Lock()
|
r.datastore.DbLock.Lock()
|
||||||
defer r.datastore.DbLock.Unlock()
|
defer r.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
if err := r.datastore.GetQueries().ChangeDisplayName(context.Background(), db.ChangeDisplayNameParams{
|
if err := r.datastore.GetQueries().ChangeDisplayName(context.Background(), sqlstorage.ChangeDisplayNameParams{
|
||||||
DisplayName: username,
|
DisplayName: username,
|
||||||
ID: userID,
|
ID: userID,
|
||||||
PreviousNames: sql.NullString{String: fmt.Sprintf(",%s", username), Valid: true},
|
PreviousNames: sql.NullString{String: fmt.Sprintf(",%s", username), Valid: true},
|
||||||
|
@ -151,7 +155,7 @@ func (r *SqlUserRepository) ChangeUserColor(userID string, color int) error {
|
||||||
r.datastore.DbLock.Lock()
|
r.datastore.DbLock.Lock()
|
||||||
defer r.datastore.DbLock.Unlock()
|
defer r.datastore.DbLock.Unlock()
|
||||||
|
|
||||||
if err := r.datastore.GetQueries().ChangeDisplayColor(context.Background(), db.ChangeDisplayColorParams{
|
if err := r.datastore.GetQueries().ChangeDisplayColor(context.Background(), sqlstorage.ChangeDisplayColorParams{
|
||||||
DisplayColor: int32(color),
|
DisplayColor: int32(color),
|
||||||
ID: userID,
|
ID: userID,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
@ -162,7 +166,7 @@ func (r *SqlUserRepository) ChangeUserColor(userID string, color int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SqlUserRepository) addAccessTokenForUser(accessToken, userID string) error {
|
func (r *SqlUserRepository) addAccessTokenForUser(accessToken, userID string) error {
|
||||||
return r.datastore.GetQueries().AddAccessTokenForUser(context.Background(), db.AddAccessTokenForUserParams{
|
return r.datastore.GetQueries().AddAccessTokenForUser(context.Background(), sqlstorage.AddAccessTokenForUserParams{
|
||||||
Token: accessToken,
|
Token: accessToken,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
})
|
})
|
||||||
|
@ -266,7 +270,7 @@ func (r *SqlUserRepository) GetUserByToken(token string) *models.User {
|
||||||
// SetAccessTokenToOwner will reassign an access token to be owned by a
|
// SetAccessTokenToOwner will reassign an access token to be owned by a
|
||||||
// different user. Used for logging in with external auth.
|
// different user. Used for logging in with external auth.
|
||||||
func (r *SqlUserRepository) SetAccessTokenToOwner(token, userID string) error {
|
func (r *SqlUserRepository) SetAccessTokenToOwner(token, userID string) error {
|
||||||
return r.datastore.GetQueries().SetAccessTokenToOwner(context.Background(), db.SetAccessTokenToOwnerParams{
|
return r.datastore.GetQueries().SetAccessTokenToOwner(context.Background(), sqlstorage.SetAccessTokenToOwnerParams{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Token: token,
|
Token: token,
|
||||||
})
|
})
|
|
@ -1,22 +1,41 @@
|
||||||
package webhooks
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
|
"github.com/owncast/owncast/storage/data"
|
||||||
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type WebhookRepository struct {
|
||||||
|
datastore *data.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebhookRepository(datastore *data.Store) *WebhookRepository {
|
||||||
|
return &WebhookRepository{datastore: datastore}
|
||||||
|
}
|
||||||
|
|
||||||
|
var temporaryGlobalWebhooksInstance *WebhookRepository
|
||||||
|
|
||||||
|
// GetWebhookRepository returns the shared instance of the owncast datastore.
|
||||||
|
func GetWebhookRepository() *WebhookRepository {
|
||||||
|
if temporaryGlobalWebhooksInstance == nil {
|
||||||
|
temporaryGlobalWebhooksInstance = NewWebhookRepository(data.GetDatastore())
|
||||||
|
}
|
||||||
|
return temporaryGlobalWebhooksInstance
|
||||||
|
}
|
||||||
|
|
||||||
// InsertWebhook will add a new webhook to the database.
|
// InsertWebhook will add a new webhook to the database.
|
||||||
func InsertWebhook(url string, events []models.EventType) (int, error) {
|
func (w *WebhookRepository) InsertWebhook(url string, events []models.EventType) (int, error) {
|
||||||
log.Traceln("Adding new webhook")
|
log.Traceln("Adding new webhook")
|
||||||
|
|
||||||
eventsString := strings.Join(events, ",")
|
eventsString := strings.Join(events, ",")
|
||||||
|
|
||||||
tx, err := _db.Begin()
|
tx, err := w.datastore.DB.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
@ -44,10 +63,10 @@ func InsertWebhook(url string, events []models.EventType) (int, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteWebhook will delete a webhook from the database.
|
// DeleteWebhook will delete a webhook from the database.
|
||||||
func DeleteWebhook(id int) error {
|
func (w *WebhookRepository) DeleteWebhook(id int) error {
|
||||||
log.Traceln("Deleting webhook")
|
log.Traceln("Deleting webhook")
|
||||||
|
|
||||||
tx, err := _db.Begin()
|
tx, err := w.datastore.DB.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -75,7 +94,7 @@ func DeleteWebhook(id int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWebhooksForEvent will return all of the webhooks that want to be notified about an event type.
|
// GetWebhooksForEvent will return all of the webhooks that want to be notified about an event type.
|
||||||
func GetWebhooksForEvent(event models.EventType) []models.Webhook {
|
func (w *WebhookRepository) GetWebhooksForEvent(event models.EventType) []models.Webhook {
|
||||||
webhooks := make([]models.Webhook, 0)
|
webhooks := make([]models.Webhook, 0)
|
||||||
|
|
||||||
query := `SELECT * FROM (
|
query := `SELECT * FROM (
|
||||||
|
@ -92,7 +111,7 @@ func GetWebhooksForEvent(event models.EventType) []models.Webhook {
|
||||||
WHERE event <> ''
|
WHERE event <> ''
|
||||||
) AS webhook WHERE event IS "` + event + `"`
|
) AS webhook WHERE event IS "` + event + `"`
|
||||||
|
|
||||||
rows, err := _db.Query(query)
|
rows, err := w.datastore.DB.Query(query)
|
||||||
if err != nil || rows.Err() != nil {
|
if err != nil || rows.Err() != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -119,12 +138,12 @@ func GetWebhooksForEvent(event models.EventType) []models.Webhook {
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWebhooks will return all the webhooks.
|
// GetWebhooks will return all the webhooks.
|
||||||
func GetWebhooks() ([]models.Webhook, error) { //nolint
|
func (w *WebhookRepository) GetWebhooks() ([]models.Webhook, error) { //nolint
|
||||||
webhooks := make([]models.Webhook, 0)
|
webhooks := make([]models.Webhook, 0)
|
||||||
|
|
||||||
query := "SELECT * FROM webhooks"
|
query := "SELECT * FROM webhooks"
|
||||||
|
|
||||||
rows, err := _db.Query(query)
|
rows, err := w.datastore.DB.Query(query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return webhooks, err
|
return webhooks, err
|
||||||
}
|
}
|
||||||
|
@ -172,8 +191,8 @@ func GetWebhooks() ([]models.Webhook, error) { //nolint
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWebhookAsUsed will update the last used time for a webhook.
|
// SetWebhookAsUsed will update the last used time for a webhook.
|
||||||
func SetWebhookAsUsed(webhook models.Webhook) error {
|
func (w *WebhookRepository) SetWebhookAsUsed(webhook models.Webhook) error {
|
||||||
tx, err := _db.Begin()
|
tx, err := w.datastore.DB.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/nareix/joy5/format/rtmp"
|
"github.com/nareix/joy5/format/rtmp"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/owncast/owncast/services/config"
|
"github.com/owncast/owncast/services/config"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _hasInboundRTMPConnection = false
|
var _hasInboundRTMPConnection = false
|
||||||
|
@ -27,12 +28,14 @@ var (
|
||||||
_setBroadcaster func(models.Broadcaster)
|
_setBroadcaster func(models.Broadcaster)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var configRepository = configrepository.Get()
|
||||||
|
|
||||||
// Start starts the rtmp service, listening on specified RTMP port.
|
// Start starts the rtmp service, listening on specified RTMP port.
|
||||||
func Start(setStreamAsConnected func(*io.PipeReader), setBroadcaster func(models.Broadcaster)) {
|
func Start(setStreamAsConnected func(*io.PipeReader), setBroadcaster func(models.Broadcaster)) {
|
||||||
_setStreamAsConnected = setStreamAsConnected
|
_setStreamAsConnected = setStreamAsConnected
|
||||||
_setBroadcaster = setBroadcaster
|
_setBroadcaster = setBroadcaster
|
||||||
|
|
||||||
port := data.GetRTMPPortNumber()
|
port := configRepository.GetRTMPPortNumber()
|
||||||
s := rtmp.NewServer()
|
s := rtmp.NewServer()
|
||||||
var lis net.Listener
|
var lis net.Listener
|
||||||
var err error
|
var err error
|
||||||
|
@ -78,7 +81,7 @@ func HandleConn(c *rtmp.Conn, nc net.Conn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
accessGranted := false
|
accessGranted := false
|
||||||
validStreamingKeys := data.GetStreamKeys()
|
validStreamingKeys := configRepository.GetStreamKeys()
|
||||||
|
|
||||||
configservice := config.GetConfig()
|
configservice := config.GetConfig()
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
"github.com/owncast/owncast/services/config"
|
"github.com/owncast/owncast/services/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -64,7 +63,7 @@ func (s *LocalStorage) Save(filePath string, retryCount int) (string, error) {
|
||||||
|
|
||||||
func (s *LocalStorage) Cleanup() error {
|
func (s *LocalStorage) Cleanup() error {
|
||||||
// Determine how many files we should keep on disk
|
// Determine how many files we should keep on disk
|
||||||
maxNumber := data.GetStreamLatencyLevel().SegmentCount
|
maxNumber := configRepository.GetStreamLatencyLevel().SegmentCount
|
||||||
buffer := 10
|
buffer := 10
|
||||||
c := config.GetConfig()
|
c := config.GetConfig()
|
||||||
baseDirectory := c.HLSStoragePath
|
baseDirectory := c.HLSStoragePath
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -52,6 +53,8 @@ type S3Storage struct {
|
||||||
s3ForcePathStyle bool
|
s3ForcePathStyle bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var configRepository = configrepository.Get()
|
||||||
|
|
||||||
// NewS3Storage returns a new S3Storage instance.
|
// NewS3Storage returns a new S3Storage instance.
|
||||||
func NewS3Storage() *S3Storage {
|
func NewS3Storage() *S3Storage {
|
||||||
return &S3Storage{
|
return &S3Storage{
|
||||||
|
@ -64,8 +67,8 @@ func NewS3Storage() *S3Storage {
|
||||||
func (s *S3Storage) Setup() error {
|
func (s *S3Storage) Setup() error {
|
||||||
log.Trace("Setting up S3 for external storage of video...")
|
log.Trace("Setting up S3 for external storage of video...")
|
||||||
|
|
||||||
s3Config := data.GetS3Config()
|
s3Config := configRepository.GetS3Config()
|
||||||
customVideoServingEndpoint := data.GetVideoServingEndpoint()
|
customVideoServingEndpoint := configRepository.GetVideoServingEndpoint()
|
||||||
|
|
||||||
if customVideoServingEndpoint != "" {
|
if customVideoServingEndpoint != "" {
|
||||||
s.host = customVideoServingEndpoint
|
s.host = customVideoServingEndpoint
|
||||||
|
@ -106,7 +109,7 @@ func (s *S3Storage) SegmentWritten(localFilePath string) {
|
||||||
|
|
||||||
// Warn the user about long-running save operations
|
// Warn the user about long-running save operations
|
||||||
if averagePerformance != 0 {
|
if averagePerformance != 0 {
|
||||||
if averagePerformance > float64(data.GetStreamLatencyLevel().SecondsPerSegment)*0.9 {
|
if averagePerformance > float64(configRepository.GetStreamLatencyLevel().SecondsPerSegment)*0.9 {
|
||||||
log.Warnln("Possible slow uploads: average upload S3 save duration", averagePerformance, "s. troubleshoot this issue by visiting https://owncast.online/docs/troubleshooting/")
|
log.Warnln("Possible slow uploads: average upload S3 save duration", averagePerformance, "s. troubleshoot this issue by visiting https://owncast.online/docs/troubleshooting/")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,7 +220,7 @@ func (s *S3Storage) Save(filePath string, retryCount int) (string, error) {
|
||||||
|
|
||||||
func (s *S3Storage) Cleanup() error {
|
func (s *S3Storage) Cleanup() error {
|
||||||
// Determine how many files we should keep on S3 storage
|
// Determine how many files we should keep on S3 storage
|
||||||
maxNumber := data.GetStreamLatencyLevel().SegmentCount
|
maxNumber := configRepository.GetStreamLatencyLevel().SegmentCount
|
||||||
buffer := 20
|
buffer := 20
|
||||||
|
|
||||||
keys, err := s.getDeletableVideoSegmentsWithOffset(maxNumber + buffer)
|
keys, err := s.getDeletableVideoSegmentsWithOffset(maxNumber + buffer)
|
||||||
|
|
|
@ -10,12 +10,15 @@ import (
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
"github.com/owncast/owncast/services/config"
|
"github.com/owncast/owncast/services/config"
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _timer *time.Ticker
|
var (
|
||||||
|
_timer *time.Ticker
|
||||||
|
configRepository = configrepository.Get()
|
||||||
|
)
|
||||||
|
|
||||||
// StopThumbnailGenerator will stop the periodic generating of a thumbnail from video.
|
// StopThumbnailGenerator will stop the periodic generating of a thumbnail from video.
|
||||||
func StopThumbnailGenerator() {
|
func StopThumbnailGenerator() {
|
||||||
|
@ -92,7 +95,7 @@ func fireThumbnailGenerator(segmentPath string, variantIndex int) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
mostRecentFile := path.Join(framePath, names[0])
|
mostRecentFile := path.Join(framePath, names[0])
|
||||||
ffmpegPath := utils.ValidatedFfmpegPath(data.GetFfMpegPath())
|
ffmpegPath := utils.ValidatedFfmpegPath(configRepository.GetFfMpegPath())
|
||||||
outputFileTemp := path.Join(c.TempDir, "tempthumbnail.jpg")
|
outputFileTemp := path.Join(c.TempDir, "tempthumbnail.jpg")
|
||||||
|
|
||||||
thumbnailCmdFlags := []string{
|
thumbnailCmdFlags := []string{
|
||||||
|
@ -123,7 +126,7 @@ func fireThumbnailGenerator(segmentPath string, variantIndex int) error {
|
||||||
|
|
||||||
func makeAnimatedGifPreview(sourceFile string, outputFile string) {
|
func makeAnimatedGifPreview(sourceFile string, outputFile string) {
|
||||||
c := config.GetConfig()
|
c := config.GetConfig()
|
||||||
ffmpegPath := utils.ValidatedFfmpegPath(data.GetFfMpegPath())
|
ffmpegPath := utils.ValidatedFfmpegPath(configRepository.GetFfMpegPath())
|
||||||
outputFileTemp := path.Join(c.TempDir, "temppreview.gif")
|
outputFileTemp := path.Join(c.TempDir, "temppreview.gif")
|
||||||
|
|
||||||
// Filter is pulled from https://engineering.giphy.com/how-to-make-gifs-with-ffmpeg/
|
// Filter is pulled from https://engineering.giphy.com/how-to-make-gifs-with-ffmpeg/
|
||||||
|
|
|
@ -274,16 +274,16 @@ func getVariantFromConfigQuality(quality models.StreamOutputVariant, index int)
|
||||||
|
|
||||||
// NewTranscoder will return a new Transcoder, populated by the config.
|
// NewTranscoder will return a new Transcoder, populated by the config.
|
||||||
func NewTranscoder() *Transcoder {
|
func NewTranscoder() *Transcoder {
|
||||||
ffmpegPath := utils.ValidatedFfmpegPath(data.GetFfMpegPath())
|
ffmpegPath := utils.ValidatedFfmpegPath(configRepository.GetFfMpegPath())
|
||||||
c := config.GetConfig()
|
c := config.GetConfig()
|
||||||
|
|
||||||
transcoder := new(Transcoder)
|
transcoder := new(Transcoder)
|
||||||
transcoder.ffmpegPath = ffmpegPath
|
transcoder.ffmpegPath = ffmpegPath
|
||||||
transcoder.internalListenerPort = c.InternalHLSListenerPort
|
transcoder.internalListenerPort = c.InternalHLSListenerPort
|
||||||
|
|
||||||
transcoder.currentStreamOutputSettings = data.GetStreamOutputVariants()
|
transcoder.currentStreamOutputSettings = configRepository.GetStreamOutputVariants()
|
||||||
transcoder.currentLatencyLevel = data.GetStreamLatencyLevel()
|
transcoder.currentLatencyLevel = configRepository.GetStreamLatencyLevel()
|
||||||
transcoder.codec = getCodec(data.GetVideoCodec())
|
transcoder.codec = getCodec(configRepository.GetVideoCodec())
|
||||||
transcoder.segmentOutputPath = c.HLSStoragePath
|
transcoder.segmentOutputPath = c.HLSStoragePath
|
||||||
transcoder.playlistOutputPath = c.HLSStoragePath
|
transcoder.playlistOutputPath = c.HLSStoragePath
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/data"
|
|
||||||
"github.com/owncast/owncast/services/config"
|
"github.com/owncast/owncast/services/config"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -102,8 +101,8 @@ func createVariantDirectories() {
|
||||||
// Create private hls data dirs
|
// Create private hls data dirs
|
||||||
utils.CleanupDirectory(c.HLSStoragePath)
|
utils.CleanupDirectory(c.HLSStoragePath)
|
||||||
|
|
||||||
if len(data.GetStreamOutputVariants()) != 0 {
|
if len(configRepository.GetStreamOutputVariants()) != 0 {
|
||||||
for index := range data.GetStreamOutputVariants() {
|
for index := range configRepository.GetStreamOutputVariants() {
|
||||||
if err := os.MkdirAll(path.Join(c.HLSStoragePath, strconv.Itoa(index)), 0o750); err != nil {
|
if err := os.MkdirAll(path.Join(c.HLSStoragePath, strconv.Itoa(index)), 0o750); err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,13 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/owncast/owncast/storage/configrepository"
|
||||||
"github.com/owncast/owncast/webserver/requests"
|
"github.com/owncast/owncast/webserver/requests"
|
||||||
"github.com/owncast/owncast/webserver/responses"
|
"github.com/owncast/owncast/webserver/responses"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var configRepository = configrepository.Get()
|
||||||
|
|
||||||
// SetCustomColorVariableValues sets the custom color variables.
|
// SetCustomColorVariableValues sets the custom color variables.
|
||||||
func (h *Handlers) SetCustomColorVariableValues(w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) SetCustomColorVariableValues(w http.ResponseWriter, r *http.Request) {
|
||||||
if !requests.RequirePOST(w, r) {
|
if !requests.RequirePOST(w, r) {
|
||||||
|
@ -26,7 +29,7 @@ func (h *Handlers) SetCustomColorVariableValues(w http.ResponseWriter, r *http.R
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetCustomColorVariableValues(values.Value); err != nil {
|
if err := configRepository.SetCustomColorVariableValues(values.Value); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,10 @@ import (
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/chat"
|
"github.com/owncast/owncast/core/chat"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
|
"github.com/owncast/owncast/core/data"
|
||||||
"github.com/owncast/owncast/models"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/owncast/owncast/storage"
|
"github.com/owncast/owncast/storage/chatrepository"
|
||||||
|
"github.com/owncast/owncast/storage/userrepository"
|
||||||
"github.com/owncast/owncast/utils"
|
"github.com/owncast/owncast/utils"
|
||||||
"github.com/owncast/owncast/webserver/requests"
|
"github.com/owncast/owncast/webserver/requests"
|
||||||
"github.com/owncast/owncast/webserver/responses"
|
"github.com/owncast/owncast/webserver/responses"
|
||||||
|
@ -20,8 +22,13 @@ import (
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
chatRepository = chatrepository.GetChatRepository()
|
||||||
|
userRepository = userrepository.Get()
|
||||||
|
)
|
||||||
|
|
||||||
// ExternalUpdateMessageVisibility updates an array of message IDs to have the same visiblity.
|
// ExternalUpdateMessageVisibility updates an array of message IDs to have the same visiblity.
|
||||||
func (h *Handlers) ExternalUpdateMessageVisibility(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) ExternalUpdateMessageVisibility(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
||||||
h.UpdateMessageVisibility(w, r)
|
h.UpdateMessageVisibility(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +73,7 @@ func (h *Handlers) BanIPAddress(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.BanIPAddress(configValue.Value.(string), "manually added"); err != nil {
|
if err := chatRepository.BanIPAddress(configValue.Value.(string), "manually added"); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, "error saving IP address ban")
|
responses.WriteSimpleResponse(w, false, "error saving IP address ban")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -86,7 +93,7 @@ func (h *Handlers) UnBanIPAddress(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.RemoveIPAddressBan(configValue.Value.(string)); err != nil {
|
if err := chatRepository.RemoveIPAddressBan(configValue.Value.(string)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, "error removing IP address ban")
|
responses.WriteSimpleResponse(w, false, "error removing IP address ban")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -96,7 +103,7 @@ func (h *Handlers) UnBanIPAddress(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// GetIPAddressBans will return all the banned IP addresses.
|
// GetIPAddressBans will return all the banned IP addresses.
|
||||||
func (h *Handlers) GetIPAddressBans(w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) GetIPAddressBans(w http.ResponseWriter, r *http.Request) {
|
||||||
bans, err := data.GetIPAddressBans()
|
bans, err := chatRepository.GetIPAddressBans()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
|
@ -132,7 +139,7 @@ func (h *Handlers) UpdateUserEnabled(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable/enable the user
|
// Disable/enable the user
|
||||||
if err := user.SetEnabled(request.UserID, request.Enabled); err != nil {
|
if err := userRepository.SetEnabled(request.UserID, request.Enabled); err != nil {
|
||||||
log.Errorln("error changing user enabled status", err)
|
log.Errorln("error changing user enabled status", err)
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
|
@ -163,7 +170,7 @@ func (h *Handlers) UpdateUserEnabled(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
chat.DisconnectClients(clients)
|
chat.DisconnectClients(clients)
|
||||||
disconnectedUser := user.GetUserByID(request.UserID)
|
disconnectedUser := userRepository.GetUserByID(request.UserID)
|
||||||
_ = chat.SendSystemAction(fmt.Sprintf("**%s** has been removed from chat.", disconnectedUser.DisplayName), true)
|
_ = chat.SendSystemAction(fmt.Sprintf("**%s** has been removed from chat.", disconnectedUser.DisplayName), true)
|
||||||
|
|
||||||
localIP4Address := "127.0.0.1"
|
localIP4Address := "127.0.0.1"
|
||||||
|
@ -188,7 +195,7 @@ func (h *Handlers) UpdateUserEnabled(w http.ResponseWriter, r *http.Request) {
|
||||||
func (h *Handlers) GetDisabledUsers(w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) GetDisabledUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
users := user.GetDisabledUsers()
|
users := userRepository.GetDisabledUsers()
|
||||||
responses.WriteResponse(w, users)
|
responses.WriteResponse(w, users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -213,7 +220,7 @@ func (h *Handlers) UpdateUserModerator(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the user object with new moderation access.
|
// Update the user object with new moderation access.
|
||||||
if err := user.SetModerator(req.UserID, req.IsModerator); err != nil {
|
if err := userRepository.SetModerator(req.UserID, req.IsModerator); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -230,7 +237,7 @@ func (h *Handlers) UpdateUserModerator(w http.ResponseWriter, r *http.Request) {
|
||||||
func (h *Handlers) GetModerators(w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) GetModerators(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
users := user.GetModeratorUsers()
|
users := userRepository.GetModeratorUsers()
|
||||||
responses.WriteResponse(w, users)
|
responses.WriteResponse(w, users)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -243,7 +250,7 @@ func (h *Handlers) GetAdminChatMessages(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendSystemMessage will send an official "SYSTEM" message to chat on behalf of your server.
|
// SendSystemMessage will send an official "SYSTEM" message to chat on behalf of your server.
|
||||||
func (h *Handlers) SendSystemMessage(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) SendSystemMessage(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
var message events.SystemMessageEvent
|
var message events.SystemMessageEvent
|
||||||
|
@ -260,7 +267,7 @@ func (h *Handlers) SendSystemMessage(integration user.ExternalAPIUser, w http.Re
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendSystemMessageToConnectedClient will handle incoming requests to send a single message to a single connected client by ID.
|
// SendSystemMessageToConnectedClient will handle incoming requests to send a single message to a single connected client by ID.
|
||||||
func (h *Handlers) SendSystemMessageToConnectedClient(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) SendSystemMessageToConnectedClient(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
clientIDText, err := utils.ReadRestURLParameter(r, "clientId")
|
clientIDText, err := utils.ReadRestURLParameter(r, "clientId")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -285,13 +292,13 @@ func (h *Handlers) SendSystemMessageToConnectedClient(integration user.ExternalA
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendUserMessage will send a message to chat on behalf of a user. *Depreciated*.
|
// SendUserMessage will send a message to chat on behalf of a user. *Depreciated*.
|
||||||
func (h *Handlers) SendUserMessage(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) SendUserMessage(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
responses.BadRequestHandler(w, errors.New("no longer supported. see /api/integrations/chat/send"))
|
responses.BadRequestHandler(w, errors.New("no longer supported. see /api/integrations/chat/send"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendIntegrationChatMessage will send a chat message on behalf of an external chat integration.
|
// SendIntegrationChatMessage will send a chat message on behalf of an external chat integration.
|
||||||
func (h *Handlers) SendIntegrationChatMessage(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) SendIntegrationChatMessage(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
name := integration.DisplayName
|
name := integration.DisplayName
|
||||||
|
@ -315,7 +322,7 @@ func (h *Handlers) SendIntegrationChatMessage(integration user.ExternalAPIUser,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
event.User = &user.User{
|
event.User = &models.User{
|
||||||
ID: integration.ID,
|
ID: integration.ID,
|
||||||
DisplayName: name,
|
DisplayName: name,
|
||||||
DisplayColor: integration.DisplayColor,
|
DisplayColor: integration.DisplayColor,
|
||||||
|
@ -334,7 +341,7 @@ func (h *Handlers) SendIntegrationChatMessage(integration user.ExternalAPIUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendChatAction will send a generic chat action.
|
// SendChatAction will send a generic chat action.
|
||||||
func (h *Handlers) SendChatAction(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) SendChatAction(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
var message events.SystemActionEvent
|
var message events.SystemActionEvent
|
||||||
|
@ -367,7 +374,7 @@ func (h *Handlers) SetEnableEstablishedChatUserMode(w http.ResponseWriter, r *ht
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetChatEstablishedUsersOnlyMode(configValue.Value.(bool)); err != nil {
|
if err := configRepository.SetChatEstablishedUsersOnlyMode(configValue.Value.(bool)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ func (h *Handlers) SetTags(w http.ResponseWriter, r *http.Request) {
|
||||||
tagStrings = append(tagStrings, strings.TrimLeft(tag.Value.(string), "#"))
|
tagStrings = append(tagStrings, strings.TrimLeft(tag.Value.(string), "#"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetServerMetadataTags(tagStrings); err != nil {
|
if err := configRepository.SetServerMetadataTags(tagStrings); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -63,20 +63,20 @@ func (h *Handlers) SetStreamTitle(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
value := configValue.Value.(string)
|
value := configValue.Value.(string)
|
||||||
|
|
||||||
if err := data.SetStreamTitle(value); err != nil {
|
if err := configRepository.SetStreamTitle(value); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if value != "" {
|
if value != "" {
|
||||||
sendSystemChatAction(fmt.Sprintf("Stream title changed to **%s**", value), true)
|
sendSystemChatAction(fmt.Sprintf("Stream title changed to **%s**", value), true)
|
||||||
webhookManager := webhooks.GetWebhooks()
|
webhookManager := webhooks.Get()
|
||||||
go webhookManager.SendStreamStatusEvent(models.StreamTitleUpdated)
|
go webhookManager.SendStreamStatusEvent(models.StreamTitleUpdated)
|
||||||
}
|
}
|
||||||
responses.WriteSimpleResponse(w, true, "changed")
|
responses.WriteSimpleResponse(w, true, "changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExternalSetStreamTitle will change the stream title on behalf of an external integration API request.
|
// ExternalSetStreamTitle will change the stream title on behalf of an external integration API request.
|
||||||
func (h *Handlers) ExternalSetStreamTitle(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) ExternalSetStreamTitle(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
||||||
h.SetStreamTitle(w, r)
|
h.SetStreamTitle(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,7 +97,7 @@ func (h *Handlers) SetServerName(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetServerName(configValue.Value.(string)); err != nil {
|
if err := configRepository.SetServerName(configValue.Value.(string)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -122,7 +122,7 @@ func (h *Handlers) SetServerSummary(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetServerSummary(configValue.Value.(string)); err != nil {
|
if err := configRepository.SetServerSummary(configValue.Value.(string)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -147,7 +147,7 @@ func (h *Handlers) SetCustomOfflineMessage(w http.ResponseWriter, r *http.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetCustomOfflineMessage(strings.TrimSpace(configValue.Value.(string))); err != nil {
|
if err := configRepository.SetCustomOfflineMessage(strings.TrimSpace(configValue.Value.(string))); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -166,7 +166,7 @@ func (h *Handlers) SetServerWelcomeMessage(w http.ResponseWriter, r *http.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetServerWelcomeMessage(strings.TrimSpace(configValue.Value.(string))); err != nil {
|
if err := configRepository.SetServerWelcomeMessage(strings.TrimSpace(configValue.Value.(string))); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -185,7 +185,7 @@ func (h *Handlers) SetExtraPageContent(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetExtraPageBodyContent(configValue.Value.(string)); err != nil {
|
if err := configRepository.SetExtraPageBodyContent(configValue.Value.(string)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -204,7 +204,7 @@ func (h *Handlers) SetAdminPassword(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetAdminPassword(configValue.Value.(string)); err != nil {
|
if err := configRepository.SetAdminPassword(configValue.Value.(string)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -240,12 +240,12 @@ func (h *Handlers) SetLogo(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetLogoPath("logo" + extension); err != nil {
|
if err := configRepository.SetLogoPath("logo" + extension); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetLogoUniquenessString(shortid.MustGenerate()); err != nil {
|
if err := configRepository.SetLogoUniquenessString(shortid.MustGenerate()); err != nil {
|
||||||
log.Error("Error saving logo uniqueness string: ", err)
|
log.Error("Error saving logo uniqueness string: ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,7 +269,7 @@ func (h *Handlers) SetNSFW(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetNSFW(configValue.Value.(bool)); err != nil {
|
if err := configRepository.SetNSFW(configValue.Value.(bool)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -294,7 +294,7 @@ func (h *Handlers) SetFfmpegPath(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetFfmpegPath(configValue.Value.(string)); err != nil {
|
if err := configRepository.SetFfmpegPath(configValue.Value.(string)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -318,7 +318,7 @@ func (h *Handlers) SetWebServerPort(w http.ResponseWriter, r *http.Request) {
|
||||||
responses.WriteSimpleResponse(w, false, "Port number must be between 1 and 65535")
|
responses.WriteSimpleResponse(w, false, "Port number must be between 1 and 65535")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := data.SetHTTPPortNumber(port); err != nil {
|
if err := configRepository.SetHTTPPortNumber(port); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -343,7 +343,7 @@ func (h *Handlers) SetWebServerIP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
if input, ok := configValue.Value.(string); ok {
|
if input, ok := configValue.Value.(string); ok {
|
||||||
if ip := net.ParseIP(input); ip != nil {
|
if ip := net.ParseIP(input); ip != nil {
|
||||||
if err := data.SetHTTPListenAddress(ip.String()); err != nil {
|
if err := configRepository.SetHTTPListenAddress(ip.String()); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -369,7 +369,7 @@ func (h *Handlers) SetRTMPServerPort(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetRTMPPortNumber(configValue.Value.(float64)); err != nil {
|
if err := configRepository.SetRTMPPortNumber(configValue.Value.(float64)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -403,7 +403,7 @@ func (h *Handlers) SetServerURL(w http.ResponseWriter, r *http.Request) {
|
||||||
// Trim any trailing slash
|
// Trim any trailing slash
|
||||||
serverURL := strings.TrimRight(rawValue, "/")
|
serverURL := strings.TrimRight(rawValue, "/")
|
||||||
|
|
||||||
if err := data.SetServerURL(serverURL); err != nil {
|
if err := configRepository.SetServerURL(serverURL); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -422,7 +422,7 @@ func (h *Handlers) SetSocketHostOverride(w http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetWebsocketOverrideHost(configValue.Value.(string)); err != nil {
|
if err := configRepository.SetWebsocketOverrideHost(configValue.Value.(string)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -441,7 +441,7 @@ func (h *Handlers) SetDirectoryEnabled(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetDirectoryEnabled(configValue.Value.(bool)); err != nil {
|
if err := configRepository.SetDirectoryEnabled(configValue.Value.(bool)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -459,7 +459,7 @@ func (h *Handlers) SetStreamLatencyLevel(w http.ResponseWriter, r *http.Request)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetStreamLatencyLevel(configValue.Value.(float64)); err != nil {
|
if err := configRepository.SetStreamLatencyLevel(configValue.Value.(float64)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, "error setting stream latency "+err.Error())
|
responses.WriteSimpleResponse(w, false, "error setting stream latency "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -506,7 +506,7 @@ func (h *Handlers) SetS3Configuration(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetS3Config(newS3Config.Value); err != nil {
|
if err := configRepository.SetS3Config(newS3Config.Value); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -530,7 +530,7 @@ func (h *Handlers) SetStreamOutputVariants(w http.ResponseWriter, r *http.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetStreamOutputVariants(videoVariants.Value); err != nil {
|
if err := configRepository.SetStreamOutputVariants(videoVariants.Value); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, "unable to update video config with provided values "+err.Error())
|
responses.WriteSimpleResponse(w, false, "unable to update video config with provided values "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -555,7 +555,7 @@ func (h *Handlers) SetSocialHandles(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetSocialHandles(socialHandles.Value); err != nil {
|
if err := configRepository.SetSocialHandles(socialHandles.Value); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, "unable to update social handles with provided values")
|
responses.WriteSimpleResponse(w, false, "unable to update social handles with provided values")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -581,7 +581,7 @@ func (h *Handlers) SetChatDisabled(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetChatDisabled(configValue.Value.(bool)); err != nil {
|
if err := configRepository.SetChatDisabled(configValue.Value.(bool)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -601,7 +601,7 @@ func (h *Handlers) SetVideoCodec(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetVideoCodec(configValue.Value.(string)); err != nil {
|
if err := configRepository.SetVideoCodec(configValue.Value.(string)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, "unable to update codec")
|
responses.WriteSimpleResponse(w, false, "unable to update codec")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -622,7 +622,7 @@ func (h *Handlers) SetExternalActions(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetExternalActions(actions.Value); err != nil {
|
if err := configRepository.SetExternalActions(actions.Value); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, "unable to update external actions with provided values")
|
responses.WriteSimpleResponse(w, false, "unable to update external actions with provided values")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -638,7 +638,7 @@ func (h *Handlers) SetCustomStyles(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetCustomStyles(customStyles.Value.(string)); err != nil {
|
if err := configRepository.SetCustomStyles(customStyles.Value.(string)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -654,7 +654,7 @@ func (h *Handlers) SetCustomJavascript(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetCustomJavascript(customJavascript.Value.(string)); err != nil {
|
if err := configRepository.SetCustomJavascript(customJavascript.Value.(string)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -675,7 +675,7 @@ func (h *Handlers) SetForbiddenUsernameList(w http.ResponseWriter, r *http.Reque
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetForbiddenUsernameList(request.Value); err != nil {
|
if err := configRepository.SetForbiddenUsernameList(request.Value); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -697,7 +697,7 @@ func (h *Handlers) SetSuggestedUsernameList(w http.ResponseWriter, r *http.Reque
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetSuggestedUsernamesList(request.Value); err != nil {
|
if err := configRepository.SetSuggestedUsernamesList(request.Value); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -717,7 +717,7 @@ func (h *Handlers) SetChatJoinMessagesEnabled(w http.ResponseWriter, r *http.Req
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetChatJoinMessagesEnabled(configValue.Value.(bool)); err != nil {
|
if err := configRepository.SetChatJoinMessagesEnabled(configValue.Value.(bool)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -737,7 +737,7 @@ func (h *Handlers) SetHideViewerCount(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetHideViewerCount(configValue.Value.(bool)); err != nil {
|
if err := configRepository.SetHideViewerCount(configValue.Value.(bool)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -757,7 +757,7 @@ func (h *Handlers) SetDisableSearchIndexing(w http.ResponseWriter, r *http.Reque
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetDisableSearchIndexing(configValue.Value.(bool)); err != nil {
|
if err := configRepository.SetDisableSearchIndexing(configValue.Value.(bool)); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -779,7 +779,7 @@ func (h *Handlers) SetVideoServingEndpoint(w http.ResponseWriter, r *http.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetVideoServingEndpoint(value); err != nil {
|
if err := configRepository.SetVideoServingEndpoint(value); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -816,7 +816,7 @@ func (h *Handlers) SetStreamKeys(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := data.SetStreamKeys(streamKeys.Value); err != nil {
|
if err := configRepository.SetStreamKeys(streamKeys.Value); err != nil {
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/owncast/owncast/core/chat"
|
"github.com/owncast/owncast/core/chat"
|
||||||
"github.com/owncast/owncast/core/user"
|
"github.com/owncast/owncast/models"
|
||||||
"github.com/owncast/owncast/webserver/responses"
|
"github.com/owncast/owncast/webserver/responses"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -20,6 +20,6 @@ func (h *Handlers) GetConnectedChatClients(w http.ResponseWriter, r *http.Reques
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExternalGetConnectedChatClients returns currently connected clients.
|
// ExternalGetConnectedChatClients returns currently connected clients.
|
||||||
func (h *Handlers) ExternalGetConnectedChatClients(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) ExternalGetConnectedChatClients(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
||||||
h.GetConnectedChatClients(w, r)
|
h.GetConnectedChatClients(w, r)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
// ResetYPRegistration will clear the YP protocol registration key.
|
// ResetYPRegistration will clear the YP protocol registration key.
|
||||||
func (h *Handlers) ResetYPRegistration(w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) ResetYPRegistration(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Traceln("Resetting YP registration key")
|
log.Traceln("Resetting YP registration key")
|
||||||
if err := data.SetDirectoryRegistrationKey(""); err != nil {
|
if err := configRepository.SetDirectoryRegistrationKey(""); err != nil {
|
||||||
log.Errorln(err)
|
log.Errorln(err)
|
||||||
responses.WriteSimpleResponse(w, false, err.Error())
|
responses.WriteSimpleResponse(w, false, err.Error())
|
||||||
return
|
return
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue