Compare commits

...

18 Commits

  1. 2
      .gitignore
  2. 56
      activitypub/activitypub.go
  3. 42
      activitypub/controllers/object.go
  4. 62
      activitypub/inbox/chat.go
  5. 25
      activitypub/inbox/update.go
  6. 119
      activitypub/persistence/followers.go
  7. 351
      activitypub/persistence/persistence.go
  8. 35
      activitypub/router.go
  9. 67
      auth/persistence.go
  10. 48
      cmd/application.go
  11. 20
      cmd/backuprestore.go
  12. 14
      cmd/concurrentConnections.go
  13. 2
      cmd/concurrentConnections_freebsd.go
  14. 2
      cmd/concurrentConnections_windows.go
  15. 71
      cmd/config.go
  16. 7
      cmd/console.go
  17. 4
      cmd/data.go
  18. 23
      cmd/flags.go
  19. 4
      cmd/services.go
  20. 162
      cmd/setup.go
  21. 0
      cmd/utils_windows.go
  22. 70
      config/config.go
  23. 1
      config/configUtils.go
  24. 8
      config/updaterConfig_enabled.go
  25. 66
      config/verifyInstall.go
  26. 13
      controllers/admin.go
  27. 35
      controllers/admin/appearance.go
  28. 374
      controllers/admin/chat.go
  29. 871
      controllers/admin/config.go
  30. 25
      controllers/admin/connectedClients.go
  31. 21
      controllers/admin/disconnect.go
  32. 92
      controllers/admin/emoji.go
  33. 102
      controllers/admin/externalAPIUsers.go
  34. 179
      controllers/admin/federation.go
  35. 82
      controllers/admin/followers.go
  36. 60
      controllers/admin/notifications.go
  37. 170
      controllers/admin/serverConfig.go
  38. 87
      controllers/admin/video.go
  39. 84
      controllers/admin/webhooks.go
  40. 20
      controllers/admin/yp.go
  41. 108
      controllers/auth/fediverse/fediverse.go
  42. 105
      controllers/auth/indieauth/client.go
  43. 7
      controllers/constants.go
  44. 13
      controllers/customJavascript.go
  45. 22
      controllers/followers.go
  46. 184
      core/chat/chat.go
  47. 17
      core/chat/events/userJoinedEvent.go
  48. 22
      core/chat/utils.go
  49. 97
      core/core.go
  50. 13
      core/data/activitypub.go
  51. 29
      core/data/cache.go
  52. 988
      core/data/config.go
  53. 60
      core/data/configEntry.go
  54. 23
      core/data/crypto.go
  55. 75
      core/data/datastoreMigrations.go
  56. 54
      core/data/defaults.go
  57. 172
      core/data/emoji.go
  58. 108
      core/data/messages.go
  59. 179
      core/data/persistence.go
  60. 67
      core/data/users.go
  61. 13
      core/offlineState.go
  62. 26
      core/stats.go
  63. 45
      core/status.go
  64. 7
      core/storage.go
  65. 200
      core/streamState.go
  66. 311
      core/user/externalAPIUser.go
  67. 473
      core/user/user.go
  68. 28
      core/webhooks/stream.go
  69. 44
      core/webhooks/webhooks.go
  70. 12
      logging/logging.go
  71. 16
      logging/paths.go
  72. 181
      main.go
  73. 59
      metrics/hardware.go
  74. 101
      metrics/metrics.go
  75. 337
      metrics/playback.go
  76. 2
      models/actionEvent.go
  77. 8
      models/auth.go
  78. 12
      models/chatAccessScopes.go
  79. 4
      models/chatEventTypes.go
  80. 2
      models/client.go
  81. 67
      models/configEntry.go
  82. 6
      models/connectedClientInfo.go
  83. 29
      models/eventType.go
  84. 19
      models/externalAPIUser.go
  85. 6
      models/fediverseEngagementEvent.go
  86. 5
      models/messageEvents.go
  87. 2
      models/nameChangeEvent.go
  88. 2
      models/setMessageVisibilityEvent.go
  89. 2
      models/stats.go
  90. 7
      models/systemMessageEvent.go
  91. 4
      models/timestampedValue.go
  92. 36
      models/user.go
  93. 2
      models/userDisabledEvent.go
  94. 20
      models/userJoinedEvent.go
  95. 2
      models/userMessageEvent.go
  96. 2
      models/viewer.go
  97. 52
      notifications/persistence.go
  98. 444
      router/router.go
  99. 82
      services/apfederation/activitypub.go
  100. 10
      services/apfederation/apmodels/activity.go
  101. Some files were not shown because too many files have changed in this diff Show More

2
.gitignore vendored

@ -46,3 +46,5 @@ web/style-definitions/build/ @@ -46,3 +46,5 @@ web/style-definitions/build/
web/public/sw.js
web/public/workbox-*.js
!storage/data

56
activitypub/activitypub.go

@ -1,56 +0,0 @@ @@ -1,56 +0,0 @@
package activitypub
import (
"github.com/owncast/owncast/activitypub/crypto"
"github.com/owncast/owncast/activitypub/inbox"
"github.com/owncast/owncast/activitypub/outbox"
"github.com/owncast/owncast/activitypub/persistence"
"github.com/owncast/owncast/activitypub/workerpool"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
log "github.com/sirupsen/logrus"
)
// Start will initialize and start the federation support.
func Start(datastore *data.Datastore) {
persistence.Setup(datastore)
workerpool.InitOutboundWorkerPool()
inbox.InitInboxWorkerPool()
StartRouter()
// Generate the keys for signing federated activity if needed.
if data.GetPrivateKey() == "" {
privateKey, publicKey, err := crypto.GenerateKeys()
_ = data.SetPrivateKey(string(privateKey))
_ = data.SetPublicKey(string(publicKey))
if err != nil {
log.Errorln("Unable to get private key", err)
}
}
}
// SendLive will send a "Go Live" message to followers.
func SendLive() error {
return outbox.SendLive()
}
// SendPublicFederatedMessage will send an arbitrary provided message to followers.
func SendPublicFederatedMessage(message string) error {
return outbox.SendPublicMessage(message)
}
// SendDirectFederatedMessage will send a direct message to a single account.
func SendDirectFederatedMessage(message, account string) error {
return outbox.SendDirectMessageToAccount(message, account)
}
// GetFollowerCount will return the local tracked follower count.
func GetFollowerCount() (int64, error) {
return persistence.GetFollowerCount()
}
// GetPendingFollowRequests will return the pending follow requests.
func GetPendingFollowRequests() ([]models.Follower, error) {
return persistence.GetPendingFollowRequests()
}

42
activitypub/controllers/object.go

@ -1,42 +0,0 @@ @@ -1,42 +0,0 @@
package controllers
import (
"net/http"
"strings"
"github.com/owncast/owncast/activitypub/apmodels"
"github.com/owncast/owncast/activitypub/crypto"
"github.com/owncast/owncast/activitypub/persistence"
"github.com/owncast/owncast/activitypub/requests"
"github.com/owncast/owncast/core/data"
log "github.com/sirupsen/logrus"
)
// ObjectHandler handles requests for a single federated ActivityPub object.
func ObjectHandler(w http.ResponseWriter, r *http.Request) {
if !data.GetFederationEnabled() {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// If private federation mode is enabled do not allow access to objects.
if data.GetFederationIsPrivate() {
w.WriteHeader(http.StatusNotFound)
return
}
iri := strings.Join([]string{strings.TrimSuffix(data.GetServerURL(), "/"), r.URL.Path}, "")
object, _, _, err := persistence.GetObjectByIRI(iri)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
accountName := data.GetDefaultFederationUsername()
actorIRI := apmodels.MakeLocalIRIForAccount(accountName)
publicKey := crypto.GetPublicKey(actorIRI)
if err := requests.WriteResponse([]byte(object), w, publicKey); err != nil {
log.Errorln(err)
}
}

62
activitypub/inbox/chat.go

@ -1,62 +0,0 @@ @@ -1,62 +0,0 @@
package inbox
import (
"fmt"
"github.com/go-fed/activity/streams/vocab"
"github.com/owncast/owncast/activitypub/resolvers"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/data"
)
func handleEngagementActivity(eventType events.EventType, isLiveNotification bool, actorReference vocab.ActivityStreamsActorProperty, action string) error {
// Do nothing if displaying engagement actions has been turned off.
if !data.GetFederationShowEngagement() {
return nil
}
// Do nothing if chat is disabled
if data.GetChatDisabled() {
return nil
}
// Get actor of the action
actor, _ := resolvers.GetResolvedActorFromActorProperty(actorReference)
// Send chat message
actorName := actor.Name
if actorName == "" {
actorName = actor.Username
}
actorIRI := actorReference.Begin().GetIRI().String()
userPrefix := fmt.Sprintf("%s ", actorName)
var suffix string
if isLiveNotification && action == events.FediverseEngagementLike {
suffix = "liked that this stream went live."
} else if action == events.FediverseEngagementLike {
suffix = fmt.Sprintf("liked a post from %s.", data.GetServerName())
} else if isLiveNotification && action == events.FediverseEngagementRepost {
suffix = "shared this stream with their followers."
} else if action == events.FediverseEngagementRepost {
suffix = fmt.Sprintf("shared a post from %s.", data.GetServerName())
} else if action == events.FediverseEngagementFollow {
suffix = "followed this stream."
} else {
return fmt.Errorf("could not handle event for sending to chat: %s", action)
}
body := fmt.Sprintf("%s %s", userPrefix, suffix)
var image *string
if actor.Image != nil {
s := actor.Image.String()
image = &s
}
if err := chat.SendFediverseAction(eventType, actor.FullUsername, image, body, actorIRI); err != nil {
return err
}
return nil
}

25
activitypub/inbox/update.go

@ -1,25 +0,0 @@ @@ -1,25 +0,0 @@
package inbox
import (
"context"
"github.com/go-fed/activity/streams/vocab"
"github.com/owncast/owncast/activitypub/persistence"
"github.com/owncast/owncast/activitypub/resolvers"
log "github.com/sirupsen/logrus"
)
func handleUpdateRequest(c context.Context, activity vocab.ActivityStreamsUpdate) error {
// We only care about update events to followers.
if !activity.GetActivityStreamsObject().At(0).IsActivityStreamsPerson() {
return nil
}
actor, err := resolvers.GetResolvedActorFromActorProperty(activity.GetActivityStreamsActor())
if err != nil {
log.Errorln(err)
return err
}
return persistence.UpdateFollower(actor.ActorIri.String(), actor.Inbox.String(), actor.Name, actor.FullUsername, actor.Image.String())
}

119
activitypub/persistence/followers.go

@ -1,119 +0,0 @@ @@ -1,119 +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"
log "github.com/sirupsen/logrus"
)
func createFederationFollowersTable() {
log.Traceln("Creating federation followers table...")
createTableSQL := `CREATE TABLE IF NOT EXISTS ap_followers (
"iri" TEXT NOT NULL,
"inbox" TEXT NOT NULL,
"name" TEXT,
"username" TEXT NOT NULL,
"image" TEXT,
"request" TEXT NOT NULL,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"approved_at" TIMESTAMP,
"disabled_at" TIMESTAMP,
"request_object" BLOB,
PRIMARY KEY (iri));`
_datastore.MustExec(createTableSQL)
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_iri ON ap_followers (iri);`)
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_approved_at ON ap_followers (approved_at);`)
}
// 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
}

351
activitypub/persistence/persistence.go

@ -1,351 +0,0 @@ @@ -1,351 +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/core/data"
"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()
}
// createFederatedActivitiesTable will create the accepted
// activities table if needed.
func createFederatedActivitiesTable() {
createTableSQL := `CREATE TABLE IF NOT EXISTS ap_accepted_activities (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"iri" TEXT NOT NULL,
"actor" TEXT NOT NULL,
"type" TEXT NOT NULL,
"timestamp" TIMESTAMP NOT NULL
);`
_datastore.MustExec(createTableSQL)
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_iri_actor_index ON ap_accepted_activities (iri,actor);`)
}
func createFederationOutboxTable() {
log.Traceln("Creating federation outbox table...")
createTableSQL := `CREATE TABLE IF NOT EXISTS ap_outbox (
"iri" TEXT NOT NULL,
"value" BLOB,
"type" TEXT NOT NULL,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"live_notification" BOOLEAN DEFAULT FALSE,
PRIMARY KEY (iri));`
_datastore.MustExec(createTableSQL)
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_iri ON ap_outbox (iri);`)
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_type ON ap_outbox (type);`)
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_live_notification ON ap_outbox (live_notification);`)
}
// 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
}

35
activitypub/router.go

@ -1,35 +0,0 @@ @@ -1,35 +0,0 @@
package activitypub
import (
"net/http"
"github.com/owncast/owncast/activitypub/controllers"
"github.com/owncast/owncast/router/middleware"
)
// StartRouter will start the federation specific http router.
func StartRouter() {
// WebFinger
http.HandleFunc("/.well-known/webfinger", controllers.WebfingerHandler)
// Host Metadata
http.HandleFunc("/.well-known/host-meta", controllers.HostMetaController)
// Nodeinfo v1
http.HandleFunc("/.well-known/nodeinfo", controllers.NodeInfoController)
// x-nodeinfo v2
http.HandleFunc("/.well-known/x-nodeinfo2", controllers.XNodeInfo2Controller)
// Nodeinfo v2
http.HandleFunc("/nodeinfo/2.0", controllers.NodeInfoV2Controller)
// Instance details
http.HandleFunc("/api/v1/instance", controllers.InstanceV1Controller)
// Single ActivityPub Actor
http.HandleFunc("/federation/user/", middleware.RequireActivityPubOrRedirect(controllers.ActorHandler))
// Single AP object
http.HandleFunc("/federation/", middleware.RequireActivityPubOrRedirect(controllers.ObjectHandler))
}

67
auth/persistence.go

@ -1,67 +0,0 @@ @@ -1,67 +0,0 @@
package auth
import (
"context"
"strings"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
"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) *user.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 &user.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,
}
}

48
cmd/application.go

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
package cmd
import (
"github.com/owncast/owncast/services/config"
"github.com/owncast/owncast/services/metrics"
"github.com/owncast/owncast/storage/configrepository"
log "github.com/sirupsen/logrus"
)
type Application struct {
configservice *config.Config
metricsservice *metrics.Metrics
configRepository *configrepository.SqlConfigRepository
maximumConcurrentConnectionLimit int64
}
/*
The order of this setup matters.
- Parse flags
- Set the session runtime values
- Use the session values to configure data persistence
*/
func (app *Application) Start() {
app.configservice = config.Get()
app.parseFlags()
app.configureLogging(*enableDebugOptions, *enableVerboseLogging, app.configservice.LogDirectory)
app.showStartupMessage()
app.setSessionConfig()
app.createDirectories()
app.maximumConcurrentConnectionLimit = getMaximumConcurrentConnectionLimit()
setSystemConcurrentConnectionLimit(app.maximumConcurrentConnectionLimit)
// If we're restoring a backup, do that and exit.
if *restoreDatabaseFile != "" {
app.handleRestoreBackup(restoreDatabaseFile)
log.Exit(0)
}
if *backupDirectory != "" {
app.configservice.BackupDirectory = *backupDirectory
}
app.startServices()
}

20
cmd/backuprestore.go

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
package cmd
import (
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
)
func (app *Application) handleRestoreBackup(restoreDatabaseFile *string) {
// Allows a user to restore a specific database backup
databaseFile := app.configservice.DatabaseFilePath
if *dbFile != "" {
databaseFile = *dbFile
}
if err := utils.Restore(*restoreDatabaseFile, databaseFile); err != nil {
log.Fatalln(err)
}
log.Println("Database has been restored. Restart Owncast.")
}

14
core/chat/concurrentConnections.go → cmd/concurrentConnections.go

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
//go:build !freebsd && !windows
// +build !freebsd,!windows
package chat
package cmd
import (
"syscall"
@ -24,3 +24,15 @@ func setSystemConcurrentConnectionLimit(limit int64) { @@ -24,3 +24,15 @@ func setSystemConcurrentConnectionLimit(limit int64) {
log.Traceln("Max process connection count changed from system limit of", originalLimit, "to", limit)
}
func getMaximumConcurrentConnectionLimit() int64 {
var rLimit syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
log.Fatalln(err)
}
// Return the limit to 70% of max so the machine doesn't die even if it's maxed out for some reason.
proposedLimit := int64(float32(rLimit.Max) * 0.7)
return proposedLimit
}

2
core/chat/concurrentConnections_freebsd.go → cmd/concurrentConnections_freebsd.go

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
//go:build freebsd
// +build freebsd
package chat
package cmd
import (
"syscall"

2
core/chat/concurrentConnections_windows.go → cmd/concurrentConnections_windows.go

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
//go:build windows
// +build windows
package chat
package cmd
func setSystemConcurrentConnectionLimit(limit int64) {}

71
cmd/config.go

@ -0,0 +1,71 @@ @@ -0,0 +1,71 @@
package cmd
import (
"strconv"
"github.com/owncast/owncast/storage/configrepository"
log "github.com/sirupsen/logrus"
)
func (app *Application) setSessionConfig() {
// Stream key
if *newStreamKey != "" {
log.Println("Temporary stream key is set for this session.")
app.configservice.TemporaryStreamKey = *newStreamKey
}
app.configservice.EnableDebugFeatures = *enableDebugOptions
if *dbFile != "" {
app.configservice.DatabaseFilePath = *dbFile
}
if *logDirectory != "" {
app.configservice.LogDirectory = *logDirectory
}
}
func (app *Application) saveUpdatedConfig() {
configRepository := configrepository.Get()
if *newAdminPassword != "" {
if err := configRepository.SetAdminPassword(*newAdminPassword); err != nil {
log.Errorln("Error setting your admin password.", err)
log.Exit(1)
} else {
log.Infoln("Admin password changed")
}
}
// Set the web server port
if *webServerPortOverride != "" {
portNumber, err := strconv.Atoi(*webServerPortOverride)
if err != nil {
log.Warnln(err)
return
}
log.Println("Saving new web server port number to", portNumber)
if err := configRepository.SetHTTPPortNumber(float64(portNumber)); err != nil {
log.Errorln(err)
}
}
app.configservice.WebServerPort = configRepository.GetHTTPPortNumber()
// Set the web server ip
if *webServerIPOverride != "" {
log.Println("Saving new web server listen IP address to", *webServerIPOverride)
if err := configRepository.SetHTTPListenAddress(*webServerIPOverride); err != nil {
log.Errorln(err)
}
}
app.configservice.WebServerIP = configRepository.GetHTTPListenAddress()
// Set the rtmp server port
if *rtmpPortOverride > 0 {
log.Println("Saving new RTMP server port number to", *rtmpPortOverride)
if err := configRepository.SetRTMPPortNumber(float64(*rtmpPortOverride)); err != nil {
log.Errorln(err)
}
}
}

7
cmd/console.go

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
package cmd
import log "github.com/sirupsen/logrus"
func (app *Application) showStartupMessage() {
log.Infoln(app.configservice.GetReleaseString())
}

4
cmd/data.go

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
package cmd
func initializeData() {
}

23
cmd/flags.go

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
package cmd
import (
"flag"
)
var (
dbFile = flag.String("database", "", "Path to the database file.")
logDirectory = flag.String("logdir", "", "Directory where logs will be written to")
backupDirectory = flag.String("backupdir", "", "Directory where backups will be written to")
enableDebugOptions = flag.Bool("enableDebugFeatures", false, "Enable additional debugging options.")
enableVerboseLogging = flag.Bool("enableVerboseLogging", false, "Enable additional logging.")
restoreDatabaseFile = flag.String("restoreDatabase", "", "Restore an Owncast database backup")
newAdminPassword = flag.String("adminpassword", "", "Set your admin password")
newStreamKey = flag.String("streamkey", "", "Set a temporary stream key for this session")
webServerPortOverride = flag.String("webserverport", "", "Force the web server to listen on a specific port")
webServerIPOverride = flag.String("webserverip", "", "Force web server to listen on this IP address")
rtmpPortOverride = flag.Int("rtmpport", 0, "Set listen port for the RTMP server")
)
func (app *Application) parseFlags() {
flag.Parse()
}

4
cmd/services.go

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
package cmd
func (app *Application) startServices() {
}

162
cmd/setup.go

@ -0,0 +1,162 @@ @@ -0,0 +1,162 @@
package cmd
import (
"fmt"
"io"
"io/fs"
"os"
"path"
"path/filepath"
"github.com/owncast/owncast/logging"
"github.com/owncast/owncast/services/config"
"github.com/owncast/owncast/static"
"github.com/owncast/owncast/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
func (app *Application) createDirectories() {
// Create the data directory if needed
if !utils.DoesFileExists("data") {
if err := os.Mkdir("./data", 0o700); err != nil {
log.Fatalln("Cannot create data directory", err)
}
}
// Recreate the temp dir
if utils.DoesFileExists(app.configservice.TempDir) {
err := os.RemoveAll(app.configservice.TempDir)
if err != nil {
log.Fatalln("Unable to remove temp dir! Check permissions.", app.configservice.TempDir, err)
}
}
if err := os.Mkdir(app.configservice.TempDir, 0o700); err != nil {
log.Fatalln("Unable to create temp dir!", err)
}
}
func (app *Application) configureLogging(enableDebugFeatures bool, enableVerboseLogging bool, logDirectory string) {
logging.Setup(enableDebugFeatures, enableVerboseLogging, logDirectory)
log.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
})
}
// setupEmojiDirectory sets up the custom emoji directory by copying all built-in
// emojis if the directory does not yet exist.
func (app *Application) setupEmojiDirectory() (err error) {
type emojiDirectory struct {
path string
isDir bool
}
// Migrate old (pre 0.1.0) emoji to new location if they exist.
app.migrateCustomEmojiLocations()
if utils.DoesFileExists(app.configservice.CustomEmojiPath) {
return nil
}
if err = os.MkdirAll(app.configservice.CustomEmojiPath, 0o750); err != nil {
return fmt.Errorf("unable to create custom emoji directory: %w", err)
}
staticFS := static.GetEmoji()
files := []emojiDirectory{}
walkFunction := func(path string, d os.DirEntry, err error) error {
if path == "." {
return nil
}
if d.Name() == "LICENSE.md" {
return nil
}
files = append(files, emojiDirectory{path: path, isDir: d.IsDir()})
return nil
}
if err := fs.WalkDir(staticFS, ".", walkFunction); err != nil {
log.Errorln("unable to fetch emojis: " + err.Error())
return errors.Wrap(err, "unable to fetch embedded emoji files")
}
if err != nil {
return fmt.Errorf("unable to read built-in emoji files: %w", err)
}
// Now copy all built-in emojis to the custom emoji directory
for _, path := range files {
emojiPath := filepath.Join(app.configservice.CustomEmojiPath, path.path)
if path.isDir {
if err := os.Mkdir(emojiPath, 0o700); err != nil {
return errors.Wrap(err, "unable to create emoji directory, check permissions?: "+path.path)
}
continue
}
memFile, staticOpenErr := staticFS.Open(path.path)
if staticOpenErr != nil {
return errors.Wrap(staticOpenErr, "unable to open emoji file from embedded filesystem")
}
// nolint:gosec
diskFile, err := os.Create(emojiPath)
if err != nil {
return fmt.Errorf("unable to create custom emoji file on disk: %w", err)
}
if err != nil {
_ = diskFile.Close()
return fmt.Errorf("unable to open built-in emoji file: %w", err)
}
if _, err = io.Copy(diskFile, memFile); err != nil {
_ = diskFile.Close()
_ = os.Remove(emojiPath)
return fmt.Errorf("unable to copy built-in emoji file to disk: %w", err)
}
if err = diskFile.Close(); err != nil {
_ = os.Remove(emojiPath)
return fmt.Errorf("unable to close custom emoji file on disk: %w", err)
}
}
return nil
}
// MigrateCustomEmojiLocations migrates custom emoji from the old location to the new location.
func (app *Application) migrateCustomEmojiLocations() {
oldLocation := path.Join("webroot", "img", "emoji")
newLocation := path.Join("data", "emoji")
if !utils.DoesFileExists(oldLocation) {
return
}
log.Println("Moving custom emoji directory from", oldLocation, "to", newLocation)
if err := utils.Move(oldLocation, newLocation); err != nil {
log.Errorln("error moving custom emoji directory", err)
}
}
func (app *Application) resetDirectories() {
log.Trace("Resetting file directories to a clean slate.")
// Wipe hls data directory
utils.CleanupDirectory(app.configservice.HLSStoragePath)
// Remove the previous thumbnail
logo := app.configRepository.GetLogoPath()
if utils.DoesFileExists(logo) {
err := utils.Copy(path.Join("data", logo), filepath.Join(config.DataDirectory, "thumbnail.jpg"))
if err != nil {
log.Warnln(err)
}
}
}

0
core/chat/utils_windows.go → cmd/utils_windows.go

70
config/config.go

@ -1,70 +0,0 @@ @@ -1,70 +0,0 @@
package config
import (
"fmt"
"time"
)
// These are runtime-set values used for configuration.
// DatabaseFilePath is the path to the file ot be used as the global database for this run of the application.
var DatabaseFilePath = "data/owncast.db"
// LogDirectory is the path to various log files.
var LogDirectory = "./data/logs"
// TempDir is where we store temporary files.
var TempDir = "./data/tmp"
// EnableDebugFeatures will print additional data to help in debugging.
var EnableDebugFeatures = false
// VersionNumber is the current version string.
var VersionNumber = StaticVersionNumber
// WebServerPort is the port for Owncast's webserver that is used for this execution of the service.
var WebServerPort = 8080
// WebServerIP is the IP address to bind the web server to. All interfaces by default.
var WebServerIP = "0.0.0.0"
// InternalHLSListenerPort is the port for HLS writes that is used for this execution of the service.
var InternalHLSListenerPort = "8927"
// GitCommit is an optional commit this build was made from.
var GitCommit = ""
// BuildPlatform is the optional platform this release was built for.
var BuildPlatform = "dev"
// EnableAutoUpdate will explicitly enable in-place auto-updates via the admin.
var EnableAutoUpdate = false
// A temporary stream key that can be set via the command line.
var TemporaryStreamKey = ""
// GetCommit will return an identifier used for identifying the point in time this build took place.
func GetCommit() string {
if GitCommit == "" {
GitCommit = time.Now().Format("20060102")
}
return GitCommit
}
// DefaultForbiddenUsernames are a list of usernames forbidden from being used in chat.
var DefaultForbiddenUsernames = []string{
"owncast", "operator", "admin", "system",
}
// MaxSocketPayloadSize is the maximum payload we will allow to to be received via the chat socket.
const MaxSocketPayloadSize = 2048
// GetReleaseString gets the version string.
func GetReleaseString() string {
versionNumber := VersionNumber
buildPlatform := BuildPlatform
gitCommit := GetCommit()
return fmt.Sprintf("Owncast v%s-%s (%s)", versionNumber, buildPlatform, gitCommit)
}

1
config/configUtils.go

@ -1 +0,0 @@ @@ -1 +0,0 @@
package config

8
config/updaterConfig_enabled.go

@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
//go:build enable_updates
// +build enable_updates
package config
func init() {
EnableAutoUpdate = true
}

66
config/verifyInstall.go

@ -1,66 +0,0 @@ @@ -1,66 +0,0 @@
package config
import (
"errors"
"fmt"
"os"
"os/exec"
"strings"
log "github.com/sirupsen/logrus"
"golang.org/x/mod/semver"
)
// VerifyFFMpegPath verifies that the path exists, is a file, and is executable.
func VerifyFFMpegPath(path string) error {
stat, err := os.Stat(path)
if os.IsNotExist(err) {
return errors.New("ffmpeg path does not exist")
}
if err != nil {
return fmt.Errorf("error while verifying the ffmpeg path: %s", err.Error())
}
if stat.IsDir() {
return errors.New("ffmpeg path can not be a folder")
}
mode := stat.Mode()
//source: https://stackoverflow.com/a/60128480
if mode&0111 == 0 {
return errors.New("ffmpeg path is not executable")
}
cmd := exec.Command(path)
out, _ := cmd.CombinedOutput()
response := string(out)
if response == "" {
return fmt.Errorf("unable to determine the version of your ffmpeg installation at %s you may experience issues with video", path)
}
responseComponents := strings.Split(response, " ")
if len(responseComponents) < 3 {
log.Debugf("unable to determine the version of your ffmpeg installation at %s you may experience issues with video", path)
return nil
}
fullVersionString := responseComponents[2]
versionString := "v" + strings.Split(fullVersionString, "-")[0]
// Some builds of ffmpeg have weird build numbers that we can't parse
if !semver.IsValid(versionString) {
log.Debugf("unable to determine if ffmpeg version %s is recent enough. if you experience issues with video you may want to look into updating", fullVersionString)
return nil
}
if semver.Compare(versionString, FfmpegSuggestedVersion) == -1 {
return fmt.Errorf("your %s version of ffmpeg at %s may be older than the suggested version of %s you may experience issues with video", versionString, path, FfmpegSuggestedVersion)
}
return nil
}

13
controllers/admin.go

@ -1,13 +0,0 @@ @@ -1,13 +0,0 @@
package controllers
import (
"net/http"
"github.com/owncast/owncast/core/rtmp"
)
// DisconnectInboundConnection will force-disconnect an inbound stream.
func DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) {
rtmp.Disconnect()
w.WriteHeader(http.StatusOK)
}

35
controllers/admin/appearance.go

@ -1,35 +0,0 @@ @@ -1,35 +0,0 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/data"
)
// SetCustomColorVariableValues sets the custom color variables.
func SetCustomColorVariableValues(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type request struct {
Value map[string]string `json:"value"`
}
decoder := json.NewDecoder(r.Body)
var values request
if err := decoder.Decode(&values); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update appearance variable values")
return
}
if err := data.SetCustomColorVariableValues(values.Value); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "custom appearance variables updated")
}

374
controllers/admin/chat.go

@ -1,374 +0,0 @@ @@ -1,374 +0,0 @@
package admin
// this is endpoint logic
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
)
// ExternalUpdateMessageVisibility updates an array of message IDs to have the same visiblity.
func ExternalUpdateMessageVisibility(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
UpdateMessageVisibility(w, r)
}
// UpdateMessageVisibility updates an array of message IDs to have the same visiblity.
func UpdateMessageVisibility(w http.ResponseWriter, r *http.Request) {
type messageVisibilityUpdateRequest struct {
IDArray []string `json:"idArray"`
Visible bool `json:"visible"`
}
if r.Method != controllers.POST {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var request messageVisibilityUpdateRequest
if err := decoder.Decode(&request); err != nil {
log.Errorln(err)
controllers.WriteSimpleResponse(w, false, "")
return
}
if err := chat.SetMessagesVisibility(request.IDArray, request.Visible); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "changed")
}
// BanIPAddress will manually ban an IP address.
func BanIPAddress(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to ban IP address")
return
}
if err := data.BanIPAddress(configValue.Value.(string), "manually added"); err != nil {
controllers.WriteSimpleResponse(w, false, "error saving IP address ban")
return
}
controllers.WriteSimpleResponse(w, true, "IP address banned")
}
// UnBanIPAddress will remove an IP address ban.
func UnBanIPAddress(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to unban IP address")
return
}
if err := data.RemoveIPAddressBan(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, "error removing IP address ban")
return
}
controllers.WriteSimpleResponse(w, true, "IP address unbanned")
}
// GetIPAddressBans will return all the banned IP addresses.
func GetIPAddressBans(w http.ResponseWriter, r *http.Request) {
bans, err := data.GetIPAddressBans()
if err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteResponse(w, bans)
}
// UpdateUserEnabled enable or disable a single user by ID.
func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) {
type blockUserRequest struct {
UserID string `json:"userId"`
Enabled bool `json:"enabled"`
}
if r.Method != controllers.POST {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var request blockUserRequest
if err := decoder.Decode(&request); err != nil {
log.Errorln(err)
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
if request.UserID == "" {
controllers.WriteSimpleResponse(w, false, "must provide userId")
return
}
// Disable/enable the user
if err := user.SetEnabled(request.UserID, request.Enabled); err != nil {
log.Errorln("error changing user enabled status", err)
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
// Hide/show the user's chat messages if disabling.
// Leave hidden messages hidden to be safe.
if !request.Enabled {
if err := chat.SetMessageVisibilityForUserID(request.UserID, request.Enabled); err != nil {
log.Errorln("error changing user messages visibility", err)
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
}
// Forcefully disconnect the user from the chat
if !request.Enabled {
clients, err := chat.GetClientsForUser(request.UserID)
if len(clients) == 0 {
// Nothing to do
return
}
if err != nil {
log.Errorln("error fetching clients for user: ", err)
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
chat.DisconnectClients(clients)
disconnectedUser := user.GetUserByID(request.UserID)
_ = chat.SendSystemAction(fmt.Sprintf("**%s** has been removed from chat.", disconnectedUser.DisplayName), true)
localIP4Address := "127.0.0.1"
localIP6Address := "::1"
// Ban this user's IP address.
for _, client := range clients {
ipAddress := client.IPAddress
if ipAddress != localIP4Address && ipAddress != localIP6Address {
reason := fmt.Sprintf("Banning of %s", disconnectedUser.DisplayName)
if err := data.BanIPAddress(ipAddress, reason); err != nil {
log.Errorln("error banning IP address: ", err)
}
}
}
}
controllers.WriteSimpleResponse(w, true, fmt.Sprintf("%s enabled: %t", request.UserID, request.Enabled))
}
// GetDisabledUsers will return all the disabled users.
func GetDisabledUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
users := user.GetDisabledUsers()
controllers.WriteResponse(w, users)
}
// UpdateUserModerator will set the moderator status for a user ID.
func UpdateUserModerator(w http.ResponseWriter, r *http.Request) {
type request struct {
UserID string `json:"userId"`
IsModerator bool `json:"isModerator"`
}
if r.Method != controllers.POST {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var req request
if err := decoder.Decode(&req); err != nil {
controllers.WriteSimpleResponse(w, false, "")
return
}
// Update the user object with new moderation access.
if err := user.SetModerator(req.UserID, req.IsModerator); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
// Update the clients for this user to know about the moderator access change.
if err := chat.SendConnectedClientInfoToUser(req.UserID); err != nil {
log.Debugln(err)
}
controllers.WriteSimpleResponse(w, true, fmt.Sprintf("%s is moderator: %t", req.UserID, req.IsModerator))
}
// GetModerators will return a list of moderator users.
func GetModerators(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
users := user.GetModeratorUsers()
controllers.WriteResponse(w, users)
}
// GetChatMessages returns all of the chat messages, unfiltered.
func GetChatMessages(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
messages := chat.GetChatModerationHistory()
controllers.WriteResponse(w, messages)
}
// SendSystemMessage will send an official "SYSTEM" message to chat on behalf of your server.
func SendSystemMessage(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var message events.SystemMessageEvent
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
controllers.InternalErrorHandler(w, err)
return
}
if err := chat.SendSystemMessage(message.Body, false); err != nil {
controllers.BadRequestHandler(w, err)
}
controllers.WriteSimpleResponse(w, true, "sent")
}
// SendSystemMessageToConnectedClient will handle incoming requests to send a single message to a single connected client by ID.
func SendSystemMessageToConnectedClient(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
clientIDText, err := utils.ReadRestURLParameter(r, "clientId")
if err != nil {
controllers.BadRequestHandler(w, err)
return
}
clientIDNumeric, err := strconv.ParseUint(clientIDText, 10, 32)
if err != nil {
controllers.BadRequestHandler(w, err)
return
}
var message events.SystemMessageEvent
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
controllers.InternalErrorHandler(w, err)
return
}
chat.SendSystemMessageToClient(uint(clientIDNumeric), message.Body)
controllers.WriteSimpleResponse(w, true, "sent")
}
// SendUserMessage will send a message to chat on behalf of a user. *Depreciated*.
func SendUserMessage(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
controllers.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.
func SendIntegrationChatMessage(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
name := integration.DisplayName
if name == "" {
controllers.BadRequestHandler(w, errors.New("unknown integration for provided access token"))
return
}
var event events.UserMessageEvent
if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
controllers.InternalErrorHandler(w, err)
return
}
event.SetDefaults()
event.RenderBody()
event.Type = "CHAT"
if event.Empty() {
controllers.BadRequestHandler(w, errors.New("invalid message"))
return
}
event.User = &user.User{
ID: integration.ID,
DisplayName: name,
DisplayColor: integration.DisplayColor,
CreatedAt: integration.CreatedAt,
IsBot: true,
}
if err := chat.Broadcast(&event); err != nil {
controllers.BadRequestHandler(w, err)
return
}
chat.SaveUserMessage(event)
controllers.WriteSimpleResponse(w, true, "sent")
}
// SendChatAction will send a generic chat action.
func SendChatAction(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var message events.SystemActionEvent
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
controllers.InternalErrorHandler(w, err)
return
}
message.SetDefaults()
message.RenderBody()
if err := chat.SendSystemAction(message.Body, false); err != nil {
controllers.BadRequestHandler(w, err)
return
}
controllers.WriteSimpleResponse(w, true, "sent")
}
// SetEnableEstablishedChatUserMode sets the requirement for a chat user
// to be "established" for some time before taking part in chat.
func SetEnableEstablishedChatUserMode(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to update chat established user only mode")
return
}
if err := data.SetChatEstablishedUsersOnlyMode(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "chat established users only mode updated")
}

871
controllers/admin/config.go

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

25
controllers/admin/connectedClients.go

@ -1,25 +0,0 @@ @@ -1,25 +0,0 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/user"
)
// GetConnectedChatClients returns currently connected clients.
func GetConnectedChatClients(w http.ResponseWriter, r *http.Request) {
clients := chat.GetClients()
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(clients); err != nil {
controllers.InternalErrorHandler(w, err)
}
}
// ExternalGetConnectedChatClients returns currently connected clients.
func ExternalGetConnectedChatClients(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
GetConnectedChatClients(w, r)
}

21
controllers/admin/disconnect.go

@ -1,21 +0,0 @@ @@ -1,21 +0,0 @@
package admin
import (
"net/http"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/rtmp"
)
// DisconnectInboundConnection will force-disconnect an inbound stream.
func DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) {
if !core.GetStatus().Online {
controllers.WriteSimpleResponse(w, false, "no inbound stream connected")
return
}
rtmp.Disconnect()
controllers.WriteSimpleResponse(w, true, "inbound stream disconnected")
}

92
controllers/admin/emoji.go

@ -1,92 +0,0 @@ @@ -1,92 +0,0 @@
package admin
import (
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/utils"
)
// UploadCustomEmoji allows POSTing a new custom emoji to the server.
func UploadCustomEmoji(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type postEmoji struct {
Name string `json:"name"`
Data string `json:"data"`
}
emoji := new(postEmoji)
if err := json.NewDecoder(r.Body).Decode(emoji); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
bytes, _, err := utils.DecodeBase64Image(emoji.Data)
if err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
// Prevent path traversal attacks
emojiFileName := filepath.Base(emoji.Name)
targetPath := filepath.Join(config.CustomEmojiPath, emojiFileName)
err = os.MkdirAll(config.CustomEmojiPath, 0o700)
if err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
if utils.DoesFileExists(targetPath) {
controllers.WriteSimpleResponse(w, false, fmt.Sprintf("An emoji with the name %q already exists", emojiFileName))
return
}
if err = os.WriteFile(targetPath, bytes, 0o600); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, fmt.Sprintf("Emoji %q has been uploaded", emojiFileName))
}
// DeleteCustomEmoji deletes a custom emoji.
func DeleteCustomEmoji(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type deleteEmoji struct {
Name string `json:"name"`
}
emoji := new(deleteEmoji)
if err := json.NewDecoder(r.Body).Decode(emoji); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
// var emojiFileName = filepath.Base(emoji.Name)
targetPath := filepath.Join(config.CustomEmojiPath, emoji.Name)
if err := os.Remove(targetPath); err != nil {
if os.IsNotExist(err) {
controllers.WriteSimpleResponse(w, false, fmt.Sprintf("Emoji %q doesn't exist", emoji.Name))
} else {
controllers.WriteSimpleResponse(w, false, err.Error())
}
return
}
controllers.WriteSimpleResponse(w, true, fmt.Sprintf("Emoji %q has been deleted", emoji.Name))
}

102
controllers/admin/externalAPIUsers.go

@ -1,102 +0,0 @@ @@ -1,102 +0,0 @@
package admin
import (
"encoding/json"
"errors"
"net/http"
"time"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/utils"
)
type deleteExternalAPIUserRequest struct {
Token string `json:"token"`
}
type createExternalAPIUserRequest struct {
Name string `json:"name"`
Scopes []string `json:"scopes"`
}
// CreateExternalAPIUser will generate a 3rd party access token.
func CreateExternalAPIUser(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var request createExternalAPIUserRequest
if err := decoder.Decode(&request); err != nil {
controllers.BadRequestHandler(w, err)
return
}
// Verify all the scopes provided are valid
if !user.HasValidScopes(request.Scopes) {
controllers.BadRequestHandler(w, errors.New("one or more invalid scopes provided"))
return
}
token, err := utils.GenerateAccessToken()
if err != nil {
controllers.InternalErrorHandler(w, err)
return
}
color := utils.GenerateRandomDisplayColor(config.MaxUserColor)
if err := user.InsertExternalAPIUser(token, request.Name, color, request.Scopes); err != nil {
controllers.InternalErrorHandler(w, err)
return
}
w.Header().Set("Content-Type", "application/json")
controllers.WriteResponse(w, user.ExternalAPIUser{
AccessToken: token,
DisplayName: request.Name,
DisplayColor: color,
Scopes: request.Scopes,
CreatedAt: time.Now(),
LastUsedAt: nil,
})
}
// GetExternalAPIUsers will return all 3rd party access tokens.
func GetExternalAPIUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
tokens, err := user.GetExternalAPIUser()
if err != nil {
controllers.InternalErrorHandler(w, err)
return
}
controllers.WriteResponse(w, tokens)
}
// DeleteExternalAPIUser will return a single 3rd party access token.
func DeleteExternalAPIUser(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != controllers.POST {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var request deleteExternalAPIUserRequest
if err := decoder.Decode(&request); err != nil {
controllers.BadRequestHandler(w, err)
return
}
if request.Token == "" {
controllers.BadRequestHandler(w, errors.New("must provide a token"))
return
}
if err := user.DeleteExternalAPIUser(request.Token); err != nil {
controllers.InternalErrorHandler(w, err)
return
}
controllers.WriteSimpleResponse(w, true, "deleted token")
}

179
controllers/admin/federation.go

@ -1,179 +0,0 @@ @@ -1,179 +0,0 @@
package admin
import (
"net/http"
"github.com/owncast/owncast/activitypub"
"github.com/owncast/owncast/activitypub/outbox"
"github.com/owncast/owncast/activitypub/persistence"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/data"
)
// SendFederatedMessage will send a manual message to the fediverse.
func SendFederatedMessage(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
message, ok := configValue.Value.(string)
if !ok {
controllers.WriteSimpleResponse(w, false, "unable to send message")
return
}
if err := activitypub.SendPublicFederatedMessage(message); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "sent")
}
// SetFederationEnabled will set if Federation features are enabled.
func SetFederationEnabled(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetFederationEnabled(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "federation features saved")
}
// SetFederationActivityPrivate will set if Federation features are private to followers.
func SetFederationActivityPrivate(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetFederationIsPrivate(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
// Update Fediverse followers about this change.
if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "federation private saved")
}
// SetFederationShowEngagement will set if Fedivese engagement shows in chat.
func SetFederationShowEngagement(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetFederationShowEngagement(configValue.Value.(bool)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "federation show engagement saved")
}
// SetFederationUsername will set the local actor username used for federation activities.
func SetFederationUsername(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetFederationUsername(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "username saved")
}
// SetFederationGoLiveMessage will set the federated message sent when the streamer goes live.
func SetFederationGoLiveMessage(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValue, success := getValueFromRequest(w, r)
if !success {
return
}
if err := data.SetFederationGoLiveMessage(configValue.Value.(string)); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "message saved")
}
// SetFederationBlockDomains saves a list of domains to block on the Fediverse.
func SetFederationBlockDomains(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
configValues, success := getValuesFromRequest(w, r)
if !success {
controllers.WriteSimpleResponse(w, false, "unable to handle provided domains")
return
}
domainStrings := make([]string, 0)
for _, domain := range configValues {
domainStrings = append(domainStrings, domain.Value.(string))
}
if err := data.SetBlockedFederatedDomains(domainStrings); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "saved")
}
// GetFederatedActions will return the saved list of accepted inbound
// federated activities.
func GetFederatedActions(page int, pageSize int, w http.ResponseWriter, r *http.Request) {
offset := pageSize * page
activities, total, err := persistence.GetInboundActivities(pageSize, offset)
if err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
response := controllers.PaginatedResponse{
Total: total,
Results: activities,
}
controllers.WriteResponse(w, response)
}

82
controllers/admin/followers.go

@ -1,82 +0,0 @@ @@ -1,82 +0,0 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/activitypub/persistence"
"github.com/owncast/owncast/activitypub/requests"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/data"
)
// ApproveFollower will approve a federated follow request.
func ApproveFollower(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type approveFollowerRequest struct {
ActorIRI string `json:"actorIRI"`
Approved bool `json:"approved"`
}
decoder := json.NewDecoder(r.Body)
var approval approveFollowerRequest
if err := decoder.Decode(&approval); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to handle follower state with provided values")
return
}
if approval.Approved {
// Approve a follower
if err := persistence.ApprovePreviousFollowRequest(approval.ActorIRI); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
localAccountName := data.GetDefaultFederationUsername()
followRequest, err := persistence.GetFollower(approval.ActorIRI)
if err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
// Send the approval to the follow requestor.
if err := requests.SendFollowAccept(followRequest.Inbox, followRequest.RequestObject, localAccountName); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
} else {
// Remove/block a follower
if err := persistence.BlockOrRejectFollower(approval.ActorIRI); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
}
controllers.WriteSimpleResponse(w, true, "follower updated")
}
// GetPendingFollowRequests will return a list of pending follow requests.
func GetPendingFollowRequests(w http.ResponseWriter, r *http.Request) {
requests, err := persistence.GetPendingFollowRequests()
if err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteResponse(w, requests)
}
// GetBlockedAndRejectedFollowers will return blocked and rejected followers.
func GetBlockedAndRejectedFollowers(w http.ResponseWriter, r *http.Request) {
rejections, err := persistence.GetBlockedAndRejectedFollowers()
if err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteResponse(w, rejections)
}

60
controllers/admin/notifications.go

@ -1,60 +0,0 @@ @@ -1,60 +0,0 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
)
// SetDiscordNotificationConfiguration will set the discord notification configuration.
func SetDiscordNotificationConfiguration(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type request struct {
Value models.DiscordConfiguration `json:"value"`
}
decoder := json.NewDecoder(r.Body)
var config request
if err := decoder.Decode(&config); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update discord config with provided values")
return
}
if err := data.SetDiscordConfig(config.Value); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update discord config with provided values")
return
}
controllers.WriteSimpleResponse(w, true, "updated discord config with provided values")
}
// SetBrowserNotificationConfiguration will set the browser notification configuration.
func SetBrowserNotificationConfiguration(w http.ResponseWriter, r *http.Request) {
if !requirePOST(w, r) {
return
}
type request struct {
Value models.BrowserNotificationConfiguration `json:"value"`
}
decoder := json.NewDecoder(r.Body)
var config request
if err := decoder.Decode(&config); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update browser push config with provided values")
return
}
if err := data.SetBrowserPushConfig(config.Value); err != nil {
controllers.WriteSimpleResponse(w, false, "unable to update browser push config with provided values")
return
}
controllers.WriteSimpleResponse(w, true, "updated browser push config with provided values")
}

170
controllers/admin/serverConfig.go

@ -1,170 +0,0 @@ @@ -1,170 +0,0 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/transcoder"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/router/middleware"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
)
// GetServerConfig gets the config details of the server.
func GetServerConfig(w http.ResponseWriter, r *http.Request) {
ffmpeg := utils.ValidatedFfmpegPath(data.GetFfMpegPath())
usernameBlocklist := data.GetForbiddenUsernameList()
usernameSuggestions := data.GetSuggestedUsernamesList()
videoQualityVariants := make([]models.StreamOutputVariant, 0)
for _, variant := range data.GetStreamOutputVariants() {
videoQualityVariants = append(videoQualityVariants, models.StreamOutputVariant{
Name: variant.GetName(),
IsAudioPassthrough: variant.GetIsAudioPassthrough(),
IsVideoPassthrough: variant.IsVideoPassthrough,
Framerate: variant.GetFramerate(),
VideoBitrate: variant.VideoBitrate,
AudioBitrate: variant.AudioBitrate,
CPUUsageLevel: variant.CPUUsageLevel,
ScaledWidth: variant.ScaledWidth,
ScaledHeight: variant.ScaledHeight,
})
}
response := serverConfigAdminResponse{
InstanceDetails: webConfigResponse{
Name: data.GetServerName(),
Summary: data.GetServerSummary(),
Tags: data.GetServerMetadataTags(),
ExtraPageContent: data.GetExtraPageBodyContent(),
StreamTitle: data.GetStreamTitle(),
WelcomeMessage: data.GetServerWelcomeMessage(),
OfflineMessage: data.GetCustomOfflineMessage(),
Logo: data.GetLogoPath(),
SocialHandles: data.GetSocialHandles(),
NSFW: data.GetNSFW(),
CustomStyles: data.GetCustomStyles(),
CustomJavascript: data.GetCustomJavascript(),
AppearanceVariables: data.GetCustomColorVariableValues(),
},
FFmpegPath: ffmpeg,
AdminPassword: data.GetAdminPassword(),
StreamKeys: data.GetStreamKeys(),
StreamKeyOverridden: config.TemporaryStreamKey != "",
WebServerPort: config.WebServerPort,
WebServerIP: config.WebServerIP,
RTMPServerPort: data.GetRTMPPortNumber(),
ChatDisabled: data.GetChatDisabled(),
ChatJoinMessagesEnabled: data.GetChatJoinMessagesEnabled(),
SocketHostOverride: data.GetWebsocketOverrideHost(),
VideoServingEndpoint: data.GetVideoServingEndpoint(),
ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(),
HideViewerCount: data.GetHideViewerCount(),
DisableSearchIndexing: data.GetDisableSearchIndexing(),
VideoSettings: videoSettings{
VideoQualityVariants: videoQualityVariants,
LatencyLevel: data.GetStreamLatencyLevel().Level,
},
YP: yp{
Enabled: data.GetDirectoryEnabled(),
InstanceURL: data.GetServerURL(),
},
S3: data.GetS3Config(),
ExternalActions: data.GetExternalActions(),
SupportedCodecs: transcoder.GetCodecs(ffmpeg),
VideoCodec: data.GetVideoCodec(),
ForbiddenUsernames: usernameBlocklist,
SuggestedUsernames: usernameSuggestions,
Federation: federationConfigResponse{
Enabled: data.GetFederationEnabled(),
IsPrivate: data.GetFederationIsPrivate(),
Username: data.GetFederationUsername(),
GoLiveMessage: data.GetFederationGoLiveMessage(),
ShowEngagement: data.GetFederationShowEngagement(),
BlockedDomains: data.GetBlockedFederatedDomains(),
},
Notifications: notificationsConfigResponse{
Discord: data.GetDiscordConfig(),
Browser: data.GetBrowserPushConfig(),
},
}
w.Header().Set("Content-Type", "application/json")
middleware.DisableCache(w)
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Errorln(err)
}
}
type serverConfigAdminResponse struct {
InstanceDetails webConfigResponse `json:"instanceDetails"`
Notifications notificationsConfigResponse `json:"notifications"`
YP yp `json:"yp"`
FFmpegPath string `json:"ffmpegPath"`
AdminPassword string `json:"adminPassword"`
SocketHostOverride string `json:"socketHostOverride,omitempty"`
WebServerIP string `json:"webServerIP"`
VideoCodec string `json:"videoCodec"`
VideoServingEndpoint string `json:"videoServingEndpoint"`
S3 models.S3 `json:"s3"`
Federation federationConfigResponse `json:"federation"`
SupportedCodecs []string `json:"supportedCodecs"`
ExternalActions []models.ExternalAction `json:"externalActions"`
ForbiddenUsernames []string `json:"forbiddenUsernames"`
SuggestedUsernames []string `json:"suggestedUsernames"`
StreamKeys []models.StreamKey `json:"streamKeys"`
VideoSettings videoSettings `json:"videoSettings"`
RTMPServerPort int `json:"rtmpServerPort"`
WebServerPort int `json:"webServerPort"`
ChatDisabled bool `json:"chatDisabled"`
ChatJoinMessagesEnabled bool `json:"chatJoinMessagesEnabled"`
ChatEstablishedUserMode bool `json:"chatEstablishedUserMode"`
DisableSearchIndexing bool `json:"disableSearchIndexing"`
StreamKeyOverridden bool `json:"streamKeyOverridden"`
HideViewerCount bool `json:"hideViewerCount"`
}
type videoSettings struct {
VideoQualityVariants []models.StreamOutputVariant `json:"videoQualityVariants"`
LatencyLevel int `json:"latencyLevel"`
}
type webConfigResponse struct {
AppearanceVariables map[string]string `json:"appearanceVariables"`
Version string `json:"version"`
WelcomeMessage string `json:"welcomeMessage"`
OfflineMessage string `json:"offlineMessage"`
Logo string `json:"logo"`
Name string `json:"name"`
ExtraPageContent string `json:"extraPageContent"`
StreamTitle string `json:"streamTitle"` // What's going on with the current stream
CustomStyles string `json:"customStyles"`
CustomJavascript string `json:"customJavascript"`
Summary string `json:"summary"`
Tags []string `json:"tags"`
SocialHandles []models.SocialHandle `json:"socialHandles"`
NSFW bool `json:"nsfw"`
}
type yp struct {
InstanceURL string `json:"instanceUrl"` // The public URL the directory should link to
YPServiceURL string `json:"-"` // The base URL to the YP API to register with (optional)
Enabled bool `json:"enabled"`
}
type federationConfigResponse struct {
Username string `json:"username"`
GoLiveMessage string `json:"goLiveMessage"`
BlockedDomains []string `json:"blockedDomains"`
Enabled bool `json:"enabled"`
IsPrivate bool `json:"isPrivate"`
ShowEngagement bool `json:"showEngagement"`
}
type notificationsConfigResponse struct {
Browser models.BrowserNotificationConfiguration `json:"browser"`
Discord models.DiscordConfiguration `json:"discord"`
}

87
controllers/admin/video.go

@ -1,87 +0,0 @@ @@ -1,87 +0,0 @@
package admin
import (
"encoding/json"
"net/http"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/metrics"
log "github.com/sirupsen/logrus"
)
// GetVideoPlaybackMetrics returns video playback metrics.
func GetVideoPlaybackMetrics(w http.ResponseWriter, r *http.Request) {
type response struct {
Errors []metrics.TimestampedValue `json:"errors"`
QualityVariantChanges []metrics.TimestampedValue `json:"qualityVariantChanges"`
HighestLatency []metrics.TimestampedValue `json:"highestLatency"`
MedianLatency []metrics.TimestampedValue `json:"medianLatency"`
LowestLatency []metrics.TimestampedValue `json:"lowestLatency"`
MedianDownloadDuration []metrics.TimestampedValue `json:"medianSegmentDownloadDuration"`
MaximumDownloadDuration []metrics.TimestampedValue `json:"maximumSegmentDownloadDuration"`
MinimumDownloadDuration []metrics.TimestampedValue `json:"minimumSegmentDownloadDuration"`
SlowestDownloadRate []metrics.TimestampedValue `json:"minPlayerBitrate"`
MedianDownloadRate []metrics.TimestampedValue `json:"medianPlayerBitrate"`
HighestDownloadRater []metrics.TimestampedValue `json:"maxPlayerBitrate"`
AvailableBitrates []int `json:"availableBitrates"`
SegmentLength int `json:"segmentLength"`
Representation int `json:"representation"`
}
availableBitrates := []int{}
var segmentLength int
if core.GetCurrentBroadcast() != nil {
segmentLength = core.GetCurrentBroadcast().LatencyLevel.SecondsPerSegment
for _, variants := range core.GetCurrentBroadcast().OutputSettings {
availableBitrates = append(availableBitrates, variants.VideoBitrate)
}
} else {
segmentLength = data.GetStreamLatencyLevel().SecondsPerSegment
for _, variants := range data.GetStreamOutputVariants() {
availableBitrates = append(availableBitrates, variants.VideoBitrate)
}
}
errors := metrics.GetPlaybackErrorCountOverTime()
medianLatency := metrics.GetMedianLatencyOverTime()
minimumLatency := metrics.GetMinimumLatencyOverTime()
maximumLatency := metrics.GetMaximumLatencyOverTime()
medianDurations := metrics.GetMedianDownloadDurationsOverTime()
maximumDurations := metrics.GetMaximumDownloadDurationsOverTime()
minimumDurations := metrics.GetMinimumDownloadDurationsOverTime()
minPlayerBitrate := metrics.GetSlowestDownloadRateOverTime()
medianPlayerBitrate := metrics.GetMedianDownloadRateOverTime()
maxPlayerBitrate := metrics.GetMaxDownloadRateOverTime()
qualityVariantChanges := metrics.GetQualityVariantChangesOverTime()
representation := metrics.GetPlaybackMetricsRepresentation()
resp := response{
AvailableBitrates: availableBitrates,
Errors: errors,
MedianLatency: medianLatency,
HighestLatency: maximumLatency,
LowestLatency: minimumLatency,
SegmentLength: segmentLength,
MedianDownloadDuration: medianDurations,
MaximumDownloadDuration: maximumDurations,
MinimumDownloadDuration: minimumDurations,
SlowestDownloadRate: minPlayerBitrate,
MedianDownloadRate: medianPlayerBitrate,
HighestDownloadRater: maxPlayerBitrate,
QualityVariantChanges: qualityVariantChanges,
Representation: representation,
}
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(resp)
if err != nil {
log.Errorln(err)
}
}

84
controllers/admin/webhooks.go

@ -1,84 +0,0 @@ @@ -1,84 +0,0 @@
package admin
import (
"encoding/json"
"errors"
"net/http"
"time"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
)
type deleteWebhookRequest struct {
ID int `json:"id"`
}
type createWebhookRequest struct {
URL string `json:"url"`
Events []models.EventType `json:"events"`
}
// CreateWebhook will add a single webhook.
func CreateWebhook(w http.ResponseWriter, r *http.Request) {
decoder := json.NewDecoder(r.Body)
var request createWebhookRequest
if err := decoder.Decode(&request); err != nil {
controllers.BadRequestHandler(w, err)
return
}
// Verify all the scopes provided are valid
if !models.HasValidEvents(request.Events) {
controllers.BadRequestHandler(w, errors.New("one or more invalid event provided"))
return
}
newWebhookID, err := data.InsertWebhook(request.URL, request.Events)
if err != nil {
controllers.InternalErrorHandler(w, err)
return
}
controllers.WriteResponse(w, models.Webhook{
ID: newWebhookID,
URL: request.URL,
Events: request.Events,
Timestamp: time.Now(),
LastUsed: nil,
})
}
// GetWebhooks will return all webhooks.
func GetWebhooks(w http.ResponseWriter, r *http.Request) {
webhooks, err := data.GetWebhooks()
if err != nil {
controllers.InternalErrorHandler(w, err)
return
}
controllers.WriteResponse(w, webhooks)
}
// DeleteWebhook will delete a single webhook.
func DeleteWebhook(w http.ResponseWriter, r *http.Request) {
if r.Method != controllers.POST {
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
decoder := json.NewDecoder(r.Body)
var request deleteWebhookRequest
if err := decoder.Decode(&request); err != nil {
controllers.BadRequestHandler(w, err)
return
}
if err := data.DeleteWebhook(request.ID); err != nil {
controllers.InternalErrorHandler(w, err)
return
}
controllers.WriteSimpleResponse(w, true, "deleted webhook")
}

20
controllers/admin/yp.go

@ -1,20 +0,0 @@ @@ -1,20 +0,0 @@
package admin
import (
"net/http"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/data"
log "github.com/sirupsen/logrus"
)
// ResetYPRegistration will clear the YP protocol registration key.
func ResetYPRegistration(w http.ResponseWriter, r *http.Request) {
log.Traceln("Resetting YP registration key")
if err := data.SetDirectoryRegistrationKey(""); err != nil {
log.Errorln(err)
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "reset")
}

108
controllers/auth/fediverse/fediverse.go

@ -1,108 +0,0 @@ @@ -1,108 +0,0 @@
package fediverse
import (
"encoding/json"
"fmt"
"net/http"
"github.com/owncast/owncast/activitypub"
"github.com/owncast/owncast/auth"
fediverseauth "github.com/owncast/owncast/auth/fediverse"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
log "github.com/sirupsen/logrus"
)
// RegisterFediverseOTPRequest registers a new OTP request for the given access token.
func RegisterFediverseOTPRequest(u user.User, w http.ResponseWriter, r *http.Request) {
type request struct {
FediverseAccount string `json:"account"`
}
var req request
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&req); err != nil {
controllers.WriteSimpleResponse(w, false, "Could not decode request: "+err.Error())
return
}
accessToken := r.URL.Query().Get("accessToken")
reg, success, err := fediverseauth.RegisterFediverseOTP(accessToken, u.ID, u.DisplayName, req.FediverseAccount)
if err != nil {
controllers.WriteSimpleResponse(w, false, "Could not register auth request: "+err.Error())
return
}
if !success {
controllers.WriteSimpleResponse(w, false, "Could not register auth request. One may already be pending. Try again later.")
return
}
msg := fmt.Sprintf("<p>This is an automated message from %s. If you did not request this message please ignore or block. Your requested one-time code is:</p><p>%s</p>", data.GetServerName(), reg.Code)
if err := activitypub.SendDirectFederatedMessage(msg, reg.Account); err != nil {
controllers.WriteSimpleResponse(w, false, "Could not send code to fediverse: "+err.Error())
return
}
controllers.WriteSimpleResponse(w, true, "")
}
// VerifyFediverseOTPRequest verifies the given OTP code for the given access token.
func VerifyFediverseOTPRequest(w http.ResponseWriter, r *http.Request) {
type request struct {
Code string `json:"code"`
}
var req request
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&req); err != nil {
controllers.WriteSimpleResponse(w, false, "Could not decode request: "+err.Error())
return
}
accessToken := r.URL.Query().Get("accessToken")
valid, authRegistration := fediverseauth.ValidateFediverseOTP(accessToken, req.Code)
if !valid {
w.WriteHeader(http.StatusForbidden)
return
}
// Check if a user with this auth already exists, if so, log them in.
if u := auth.GetUserByAuth(authRegistration.Account, auth.Fediverse); u != nil {
// Handle existing auth.
log.Debugln("user with provided fedvierse identity already exists, logging them in")
// Update the current user's access token to point to the existing user id.
userID := u.ID
if err := user.SetAccessTokenToOwner(accessToken, userID); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
if authRegistration.UserDisplayName != u.DisplayName {
loginMessage := fmt.Sprintf("**%s** is now authenticated as **%s**", authRegistration.UserDisplayName, u.DisplayName)
if err := chat.SendSystemAction(loginMessage, true); err != nil {
log.Errorln(err)
}
}
controllers.WriteSimpleResponse(w, true, "")
return
}
// Otherwise, save this as new auth.
log.Debug("fediverse account does not already exist, saving it as a new one for the current user")
if err := auth.AddAuth(authRegistration.UserID, authRegistration.Account, auth.Fediverse); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
// Update the current user's authenticated flag so we can show it in
// the chat UI.
if err := user.SetUserAsAuthenticated(authRegistration.UserID); err != nil {
log.Errorln(err)
}
controllers.WriteSimpleResponse(w, true, "")
}

105
controllers/auth/indieauth/client.go

@ -1,105 +0,0 @@ @@ -1,105 +0,0 @@
package indieauth
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/owncast/owncast/auth"
ia "github.com/owncast/owncast/auth/indieauth"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/user"
log "github.com/sirupsen/logrus"
)
// StartAuthFlow will begin the IndieAuth flow for the current user.
func StartAuthFlow(u user.User, w http.ResponseWriter, r *http.Request) {
type request struct {
AuthHost string `json:"authHost"`
}
type response struct {
Redirect string `json:"redirect"`
}
var authRequest request
p, err := io.ReadAll(r.Body)
if err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
if err := json.Unmarshal(p, &authRequest); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
accessToken := r.URL.Query().Get("accessToken")
redirectURL, err := ia.StartAuthFlow(authRequest.AuthHost, u.ID, accessToken, u.DisplayName)
if err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
redirectResponse := response{
Redirect: redirectURL.String(),
}
controllers.WriteResponse(w, redirectResponse)
}
// HandleRedirect will handle the redirect from an IndieAuth server to
// continue the auth flow.
func HandleRedirect(w http.ResponseWriter, r *http.Request) {
state := r.URL.Query().Get("state")
code := r.URL.Query().Get("code")
request, response, err := ia.HandleCallbackCode(code, state)
if err != nil {
log.Debugln(err)
msg := `Unable to complete authentication. <a href="/">Go back.</a><hr/>`
_ = controllers.WriteString(w, msg, http.StatusBadRequest)
return
}
// Check if a user with this auth already exists, if so, log them in.
if u := auth.GetUserByAuth(response.Me, auth.IndieAuth); u != nil {
// Handle existing auth.
log.Debugln("user with provided indieauth already exists, logging them in")
// Update the current user's access token to point to the existing user id.
accessToken := request.CurrentAccessToken
userID := u.ID
if err := user.SetAccessTokenToOwner(accessToken, userID); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
if request.DisplayName != u.DisplayName {
loginMessage := fmt.Sprintf("**%s** is now authenticated as **%s**", request.DisplayName, u.DisplayName)
if err := chat.SendSystemAction(loginMessage, true); err != nil {
log.Errorln(err)
}
}
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
return
}
// Otherwise, save this as new auth.
log.Debug("indieauth token does not already exist, saving it as a new one for the current user")
if err := auth.AddAuth(request.UserID, response.Me, auth.IndieAuth); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error())
return
}
// Update the current user's authenticated flag so we can show it in
// the chat UI.
if err := user.SetUserAsAuthenticated(request.UserID); err != nil {
log.Errorln(err)
}
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}

7
controllers/constants.go

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
package controllers
// POST is the HTTP POST method.
const POST = "POST"
// GET is the HTTP GET method.
const GET = "GET"

13
controllers/customJavascript.go

@ -1,13 +0,0 @@ @@ -1,13 +0,0 @@
package controllers
import (
"net/http"
"github.com/owncast/owncast/core/data"
)
// ServeCustomJavascript will serve optional custom Javascript.
func ServeCustomJavascript(w http.ResponseWriter, r *http.Request) {
js := data.GetCustomJavascript()
_, _ = w.Write([]byte(js))
}

22
controllers/followers.go

@ -1,22 +0,0 @@ @@ -1,22 +0,0 @@
package controllers
import (
"net/http"
"github.com/owncast/owncast/activitypub/persistence"
)
// GetFollowers will handle an API request to fetch the list of followers (non-activitypub response).
func GetFollowers(offset int, limit int, w http.ResponseWriter, r *http.Request) {
followers, total, err := persistence.GetFederationFollowers(limit, offset)
if err != nil {
WriteSimpleResponse(w, false, "unable to fetch followers")
return
}
response := PaginatedResponse{
Total: total,
Results: followers,
}
WriteResponse(w, response)
}

184
core/chat/chat.go

@ -1,184 +0,0 @@ @@ -1,184 +0,0 @@
package chat
import (
"errors"
"net/http"
"sort"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
log "github.com/sirupsen/logrus"
)
var (
getStatus func() models.Status
chatMessagesSentCounter prometheus.Gauge
)
// Start begins the chat server.
func Start(getStatusFunc func() models.Status) error {
setupPersistence()
getStatus = getStatusFunc
_server = NewChat()
go _server.Run()
log.Traceln("Chat server started with max connection count of", _server.maxSocketConnectionLimit)
chatMessagesSentCounter = promauto.NewGauge(prometheus.GaugeOpts{
Name: "total_chat_message_count",
Help: "The number of chat messages incremented over time.",
ConstLabels: map[string]string{
"version": config.VersionNumber,
"host": data.GetServerURL(),
},
})
return nil
}
// GetClientsForUser will return chat connections that are owned by a specific user.
func GetClientsForUser(userID string) ([]*Client, error) {
_server.mu.Lock()
defer _server.mu.Unlock()
clients := map[string][]*Client{}
for _, client := range _server.clients {
clients[client.User.ID] = append(clients[client.User.ID], client)
}
if _, exists := clients[userID]; !exists {
return nil, errors.New("no connections for user found")
}
return clients[userID], nil
}
// FindClientByID will return a single connected client by ID.
func FindClientByID(clientID uint) (*Client, bool) {
client, found := _server.clients[clientID]
return client, found
}
// GetClients will return all the current chat clients connected.
func GetClients() []*Client {
clients := []*Client{}
if _server == nil {
return clients
}
// Convert the keyed map to a slice.
for _, client := range _server.clients {
clients = append(clients, client)
}
sort.Slice(clients, func(i, j int) bool {
return clients[i].ConnectedAt.Before(clients[j].ConnectedAt)
})
return clients
}
// SendSystemMessage will send a message string as a system message to all clients.
func SendSystemMessage(text string, ephemeral bool) error {
message := events.SystemMessageEvent{
MessageEvent: events.MessageEvent{
Body: text,
},
}
message.SetDefaults()
message.RenderBody()
if err := Broadcast(&message); err != nil {
log.Errorln("error sending system message", err)
}
if !ephemeral {
saveEvent(message.ID, nil, message.Body, message.GetMessageType(), nil, message.Timestamp, nil, nil, nil, nil)
}
return nil
}
// SendFediverseAction will send a message indicating some Fediverse engagement took place.
func SendFediverseAction(eventType string, userAccountName string, image *string, body string, link string) error {
message := events.FediverseEngagementEvent{
Event: events.Event{
Type: eventType,
},
MessageEvent: events.MessageEvent{
Body: body,
},
UserAccountName: userAccountName,
Image: image,
Link: link,
}
message.SetDefaults()
message.RenderBody()
if err := Broadcast(&message); err != nil {
log.Errorln("error sending system message", err)
return err
}
saveFederatedAction(message)
return nil
}
// SendSystemAction will send a system action string as an action event to all clients.
func SendSystemAction(text string, ephemeral bool) error {
message := events.ActionEvent{
MessageEvent: events.MessageEvent{
Body: text,
},
}
message.SetDefaults()
message.RenderBody()
if err := Broadcast(&message); err != nil {
log.Errorln("error sending system chat action")
}
if !ephemeral {
saveEvent(message.ID, nil, message.Body, message.GetMessageType(), nil, message.Timestamp, nil, nil, nil, nil)
}
return nil
}
// SendAllWelcomeMessage will send the chat message to all connected clients.
func SendAllWelcomeMessage() {
_server.sendAllWelcomeMessage()
}
// SendSystemMessageToClient will send a single message to a single connected chat client.
func SendSystemMessageToClient(clientID uint, text string) {
if client, foundClient := FindClientByID(clientID); foundClient {
_server.sendSystemMessageToClient(client, text)
}
}
// Broadcast will send all connected clients the outbound object provided.
func Broadcast(event events.OutboundEvent) error {
return _server.Broadcast(event.GetBroadcastPayload())
}
// HandleClientConnection handles a single inbound websocket connection.
func HandleClientConnection(w http.ResponseWriter, r *http.Request) {
_server.HandleClientConnection(w, r)
}
// DisconnectClients will forcefully disconnect all clients belonging to a user by ID.
func DisconnectClients(clients []*Client) {
_server.DisconnectClients(clients)
}

17
core/chat/events/userJoinedEvent.go

@ -1,17 +0,0 @@ @@ -1,17 +0,0 @@
package events
// UserJoinedEvent is the event fired when a user joins chat.
type UserJoinedEvent struct {
Event
UserEvent
}
// GetBroadcastPayload will return the object to send to all chat users.
func (e *UserJoinedEvent) GetBroadcastPayload() EventPayload {
return EventPayload{
"type": UserJoined,
"id": e.ID,
"timestamp": e.Timestamp,
"user": e.User,
}
}

22
core/chat/utils.go

@ -1,22 +0,0 @@ @@ -1,22 +0,0 @@
//go:build !windows
// +build !windows
package chat
import (
"syscall"
log "github.com/sirupsen/logrus"
)
func getMaximumConcurrentConnectionLimit() int64 {
var rLimit syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
log.Fatalln(err)
}
// Return the limit to 70% of max so the machine doesn't die even if it's maxed out for some reason.
proposedLimit := int64(float32(rLimit.Max) * 0.7)
return proposedLimit
}

97
core/core.go

@ -7,18 +7,18 @@ import ( @@ -7,18 +7,18 @@ import (
log "github.com/sirupsen/logrus"
"github.com/owncast/owncast/auth"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/rtmp"
"github.com/owncast/owncast/core/transcoder"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/core/webhooks"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/notifications"
"github.com/owncast/owncast/services/config"
"github.com/owncast/owncast/services/notifications"
"github.com/owncast/owncast/services/status"
"github.com/owncast/owncast/services/webhooks"
"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/yp"
"github.com/owncast/owncast/video/rtmp"
"github.com/owncast/owncast/video/transcoder"
)
var (
@ -34,10 +34,11 @@ var ( @@ -34,10 +34,11 @@ var (
// Start starts up the core processing.
func Start() error {
resetDirectories()
configRepository := configrepository.Get()
data.PopulateDefaults()
configRepository.PopulateDefaults()
if err := data.VerifySettings(); err != nil {
if err := configRepository.VerifySettings(); err != nil {
log.Error(err)
return err
}
@ -56,8 +57,8 @@ func Start() error { @@ -56,8 +57,8 @@ func Start() error {
log.Errorln("storage error", err)
}
user.SetupUsers()
auth.Setup(data.GetDatastore())
// user.SetupUsers()
// auth.Setup(data.GetDatastore())
fileWriter.SetupFileWriterReceiverService(&handler)
@ -66,75 +67,29 @@ func Start() error { @@ -66,75 +67,29 @@ func Start() error {
return err
}
_yp = yp.NewYP(GetStatus)
s := status.Get()
gsf := func() *models.Status {
s := status.Get()
return &s.Status
}
_yp = yp.NewYP(gsf)
if err := chat.Start(GetStatus); err != nil {
if err := chat.Start(gsf); err != nil {
log.Errorln(err)
}
// start the rtmp server
go rtmp.Start(setStreamAsConnected, setBroadcaster)
go rtmp.Start(setStreamAsConnected, s.SetBroadcaster)
rtmpPort := data.GetRTMPPortNumber()
rtmpPort := configRepository.GetRTMPPortNumber()
if rtmpPort != 1935 {
log.Infof("RTMP is accepting inbound streams on port %d.", rtmpPort)
}
webhooks.SetupWebhooks(GetStatus)
notifications.Setup(data.GetStore())
return nil
}
webhooks.InitTemporarySingleton(gsf)
func createInitialOfflineState() error {
transitionToOfflineVideoStreamContent()
notifications.Setup(data.GetDatastore())
return nil
}
// transitionToOfflineVideoStreamContent will overwrite the current stream with the
// offline video stream state only. No live stream HLS segments will continue to be
// referenced.
func transitionToOfflineVideoStreamContent() {
log.Traceln("Firing transcoder with offline stream state")
_transcoder := transcoder.NewTranscoder()
_transcoder.SetIdentifier("offline")
_transcoder.SetLatencyLevel(models.GetLatencyLevel(4))
_transcoder.SetIsEvent(true)
offlineFilePath, err := saveOfflineClipToDisk("offline.ts")
if err != nil {
log.Fatalln("unable to save offline clip:", err)
}
_transcoder.SetInput(offlineFilePath)
go _transcoder.Start(false)
// Copy the logo to be the thumbnail
logo := data.GetLogoPath()
dst := filepath.Join(config.TempDir, "thumbnail.jpg")
if err = utils.Copy(filepath.Join("data", logo), dst); err != nil {
log.Warnln(err)
}
// Delete the preview Gif
_ = os.Remove(path.Join(config.DataDirectory, "preview.gif"))
}
func resetDirectories() {
log.Trace("Resetting file directories to a clean slate.")
// Wipe hls data directory
utils.CleanupDirectory(config.HLSStoragePath)
// Remove the previous thumbnail
logo := data.GetLogoPath()
if utils.DoesFileExists(logo) {
err := utils.Copy(path.Join("data", logo), filepath.Join(config.DataDirectory, "thumbnail.jpg"))
if err != nil {
log.Warnln(err)
}
}
}

13
core/data/activitypub.go

@ -1,13 +0,0 @@ @@ -1,13 +0,0 @@
package data
// GetFederatedInboxMap is a mapping between account names and their outbox.
func GetFederatedInboxMap() map[string]string {
return map[string]string{
GetDefaultFederationUsername(): GetDefaultFederationUsername(),
}
}
// GetDefaultFederationUsername will return the username used for sending federation activities.
func GetDefaultFederationUsername() string {
return GetFederationUsername()
}

29
core/data/cache.go

@ -1,29 +0,0 @@ @@ -1,29 +0,0 @@
package data
import (
"errors"
"sync"
)
var _cacheLock = sync.Mutex{}
// GetCachedValue will return a value for key from the cache.
func (ds *Datastore) GetCachedValue(key string) ([]byte, error) {
_cacheLock.Lock()
defer _cacheLock.Unlock()
// Check for a cached value
if val, ok := ds.cache[key]; ok {
return val, nil
}
return nil, errors.New(key + " not found in cache")
}
// SetCachedValue will set a value for key in the cache.
func (ds *Datastore) SetCachedValue(key string, b []byte) {
_cacheLock.Lock()
defer _cacheLock.Unlock()
ds.cache[key] = b
}

988
core/data/config.go

@ -1,988 +0,0 @@ @@ -1,988 +0,0 @@
package data
import (
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/static"
"github.com/owncast/owncast/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
const (
extraContentKey = "extra_page_content"
streamTitleKey = "stream_title"
adminPasswordKey = "admin_password_key"
logoPathKey = "logo_path"
logoUniquenessKey = "logo_uniqueness"
serverSummaryKey = "server_summary"
serverWelcomeMessageKey = "server_welcome_message"
serverNameKey = "server_name"
serverURLKey = "server_url"
httpPortNumberKey = "http_port_number"
httpListenAddressKey = "http_listen_address"
websocketHostOverrideKey = "websocket_host_override"
rtmpPortNumberKey = "rtmp_port_number"
serverMetadataTagsKey = "server_metadata_tags"
directoryEnabledKey = "directory_enabled"
directoryRegistrationKeyKey = "directory_registration_key"
socialHandlesKey = "social_handles"
peakViewersSessionKey = "peak_viewers_session"
peakViewersOverallKey = "peak_viewers_overall"
lastDisconnectTimeKey = "last_disconnect_time"
ffmpegPathKey = "ffmpeg_path"
nsfwKey = "nsfw"
s3StorageConfigKey = "s3_storage_config"
videoLatencyLevel = "video_latency_level"
videoStreamOutputVariantsKey = "video_stream_output_variants"
chatDisabledKey = "chat_disabled"
externalActionsKey = "external_actions"
customStylesKey = "custom_styles"
customJavascriptKey = "custom_javascript"
videoCodecKey = "video_codec"
blockedUsernamesKey = "blocked_usernames"
publicKeyKey = "public_key"
privateKeyKey = "private_key"
serverInitDateKey = "server_init_date"
federationEnabledKey = "federation_enabled"
federationUsernameKey = "federation_username"
federationPrivateKey = "federation_private"
federationGoLiveMessageKey = "federation_go_live_message"
federationShowEngagementKey = "federation_show_engagement"
federationBlockedDomainsKey = "federation_blocked_domains"
suggestedUsernamesKey = "suggested_usernames"
chatJoinMessagesEnabledKey = "chat_join_messages_enabled"
chatEstablishedUsersOnlyModeKey = "chat_established_users_only_mode"
notificationsEnabledKey = "notifications_enabled"
discordConfigurationKey = "discord_configuration"
browserPushConfigurationKey = "browser_push_configuration"
browserPushPublicKeyKey = "browser_push_public_key"
browserPushPrivateKeyKey = "browser_push_private_key"
hasConfiguredInitialNotificationsKey = "has_configured_initial_notifications"
hideViewerCountKey = "hide_viewer_count"
customOfflineMessageKey = "custom_offline_message"
customColorVariableValuesKey = "custom_color_variable_values"
streamKeysKey = "stream_keys"
disableSearchIndexingKey = "disable_search_indexing"
videoServingEndpointKey = "video_serving_endpoint"
)
// GetExtraPageBodyContent will return the user-supplied body content.
func GetExtraPageBodyContent() string {
content, err := _datastore.GetString(extraContentKey)
if err != nil {
log.Traceln(extraContentKey, err)
return config.GetDefaults().PageBodyContent
}
return content
}
// SetExtraPageBodyContent will set the user-supplied body content.
func SetExtraPageBodyContent(content string) error {
return _datastore.SetString(extraContentKey, content)
}
// GetStreamTitle will return the name of the current stream.
func GetStreamTitle() string {
title, err := _datastore.GetString(streamTitleKey)
if err != nil {
return ""
}
return title
}
// SetStreamTitle will set the name of the current stream.
func SetStreamTitle(title string) error {
return _datastore.SetString(streamTitleKey, title)
}
// GetAdminPassword will return the admin password.
func GetAdminPassword() string {
key, _ := _datastore.GetString(adminPasswordKey)
return key
}
// SetAdminPassword will set the admin password.
func SetAdminPassword(key string) error {
return _datastore.SetString(adminPasswordKey, key)
}
// GetLogoPath will return the path for the logo, relative to webroot.
func GetLogoPath() string {
logo, err := _datastore.GetString(logoPathKey)
if err != nil {
log.Traceln(logoPathKey, err)
return config.GetDefaults().Logo
}
if logo == "" {
return config.GetDefaults().Logo
}
return logo
}
// SetLogoPath will set the path for the logo, relative to webroot.
func SetLogoPath(logo string) error {
return _datastore.SetString(logoPathKey, logo)
}
// SetLogoUniquenessString will set the logo cache busting string.
func SetLogoUniquenessString(uniqueness string) error {
return _datastore.SetString(logoUniquenessKey, uniqueness)
}
// GetLogoUniquenessString will return the logo cache busting string.
func GetLogoUniquenessString() string {
uniqueness, err := _datastore.GetString(logoUniquenessKey)
if err != nil {
log.Traceln(logoUniquenessKey, err)
return ""
}
return uniqueness
}
// GetServerSummary will return the server summary text.
func GetServerSummary() string {
summary, err := _datastore.GetString(serverSummaryKey)
if err != nil {
log.Traceln(serverSummaryKey, err)
return ""
}
return summary
}
// SetServerSummary will set the server summary text.
func SetServerSummary(summary string) error {
return _datastore.SetString(serverSummaryKey, summary)
}
// GetServerWelcomeMessage will return the server welcome message text.
func GetServerWelcomeMessage() string {
welcomeMessage, err := _datastore.GetString(serverWelcomeMessageKey)
if err != nil {
log.Traceln(serverWelcomeMessageKey, err)
return config.GetDefaults().ServerWelcomeMessage
}
return welcomeMessage
}
// SetServerWelcomeMessage will set the server welcome message text.
func SetServerWelcomeMessage(welcomeMessage string) error {
return _datastore.SetString(serverWelcomeMessageKey, welcomeMessage)
}
// GetServerName will return the server name text.
func GetServerName() string {
name, err := _datastore.GetString(serverNameKey)
if err != nil {
log.Traceln(serverNameKey, err)
return config.GetDefaults().Name
}
return name
}
// SetServerName will set the server name text.
func SetServerName(name string) error {
return _datastore.SetString(serverNameKey, name)
}
// GetServerURL will return the server URL.
func GetServerURL() string {
url, err := _datastore.GetString(serverURLKey)
if err != nil {
return ""
}
return url
}
// SetServerURL will set the server URL.
func SetServerURL(url string) error {
return _datastore.SetString(serverURLKey, url)
}
// GetHTTPPortNumber will return the server HTTP port.
func GetHTTPPortNumber() int {
port, err := _datastore.GetNumber(httpPortNumberKey)
if err != nil {
log.Traceln(httpPortNumberKey, err)
return config.GetDefaults().WebServerPort
}
if port == 0 {
return config.GetDefaults().WebServerPort
}
return int(port)
}
// SetWebsocketOverrideHost will set the host override for websockets.
func SetWebsocketOverrideHost(host string) error {
return _datastore.SetString(websocketHostOverrideKey, host)
}
// GetWebsocketOverrideHost will return the host override for websockets.
func GetWebsocketOverrideHost() string {
host, _ := _datastore.GetString(websocketHostOverrideKey)
return host
}
// SetHTTPPortNumber will set the server HTTP port.
func SetHTTPPortNumber(port float64) error {
return _datastore.SetNumber(httpPortNumberKey, port)
}
// GetHTTPListenAddress will return the HTTP listen address.
func GetHTTPListenAddress() string {
address, err := _datastore.GetString(httpListenAddressKey)
if err != nil {
log.Traceln(httpListenAddressKey, err)
return config.GetDefaults().WebServerIP
}
return address
}
// SetHTTPListenAddress will set the server HTTP listen address.
func SetHTTPListenAddress(address string) error {
return _datastore.SetString(httpListenAddressKey, address)
}
// GetRTMPPortNumber will return the server RTMP port.
func GetRTMPPortNumber() int {
port, err := _datastore.GetNumber(rtmpPortNumberKey)
if err != nil {
log.Traceln(rtmpPortNumberKey, err)
return config.GetDefaults().RTMPServerPort
}
if port == 0 {
return config.GetDefaults().RTMPServerPort
}
return int(port)
}
// SetRTMPPortNumber will set the server RTMP port.
func SetRTMPPortNumber(port float64) error {
return _datastore.SetNumber(rtmpPortNumberKey, port)
}
// GetServerMetadataTags will return the metadata tags.
func GetServerMetadataTags() []string {
tagsString, err := _datastore.GetString(serverMetadataTagsKey)
if tagsString == "" {
return []string{}
}
if err != nil {
log.Traceln(serverMetadataTagsKey, err)
return []string{}
}
return strings.Split(tagsString, ",")
}
// SetServerMetadataTags will return the metadata tags.
func SetServerMetadataTags(tags []string) error {
tagString := strings.Join(tags, ",")
return _datastore.SetString(serverMetadataTagsKey, tagString)
}
// GetDirectoryEnabled will return if this server should register to YP.
func GetDirectoryEnabled() bool {
enabled, err := _datastore.GetBool(directoryEnabledKey)
if err != nil {
return config.GetDefaults().YPEnabled
}
return enabled
}
// SetDirectoryEnabled will set if this server should register to YP.
func SetDirectoryEnabled(enabled bool) error {
return _datastore.SetBool(directoryEnabledKey, enabled)
}
// SetDirectoryRegistrationKey will set the YP protocol registration key.
func SetDirectoryRegistrationKey(key string) error {
return _datastore.SetString(directoryRegistrationKeyKey, key)
}
// GetDirectoryRegistrationKey will return the YP protocol registration key.
func GetDirectoryRegistrationKey() string {
key, _ := _datastore.GetString(directoryRegistrationKeyKey)
return key
}
// GetSocialHandles will return the external social links.
func GetSocialHandles() []models.SocialHandle {
var socialHandles []models.SocialHandle
configEntry, err := _datastore.Get(socialHandlesKey)
if err != nil {
log.Traceln(socialHandlesKey, err)
return socialHandles
}
if err := configEntry.getObject(&socialHandles); err != nil {
log.Traceln(err)
return socialHandles
}
return socialHandles
}
// SetSocialHandles will set the external social links.
func SetSocialHandles(socialHandles []models.SocialHandle) error {
configEntry := ConfigEntry{Key: socialHandlesKey, Value: socialHandles}
return _datastore.Save(configEntry)
}
// GetPeakSessionViewerCount will return the max number of viewers for this stream.
func GetPeakSessionViewerCount() int {
count, err := _datastore.GetNumber(peakViewersSessionKey)
if err != nil {
return 0
}
return int(count)
}
// SetPeakSessionViewerCount will set the max number of viewers for this stream.
func SetPeakSessionViewerCount(count int) error {
return _datastore.SetNumber(peakViewersSessionKey, float64(count))
}
// GetPeakOverallViewerCount will return the overall max number of viewers.
func GetPeakOverallViewerCount() int {
count, err := _datastore.GetNumber(peakViewersOverallKey)
if err != nil {
return 0
}
return int(count)
}
// SetPeakOverallViewerCount will set the overall max number of viewers.
func SetPeakOverallViewerCount(count int) error {
return _datastore.SetNumber(peakViewersOverallKey, float64(count))
}
// GetLastDisconnectTime will return the time the last stream ended.
func GetLastDisconnectTime() (*utils.NullTime, error) {
var disconnectTime utils.NullTime
configEntry, err := _datastore.Get(lastDisconnectTimeKey)
if err != nil {
return nil, err
}
if err := configEntry.getObject(&disconnectTime); err != nil {
return nil, err
}
if !disconnectTime.Valid || disconnectTime.Time.IsZero() {
return nil, err
}
return &disconnectTime, nil
}
// SetLastDisconnectTime will set the time the last stream ended.
func SetLastDisconnectTime(disconnectTime time.Time) error {
savedDisconnectTime := utils.NullTime{Time: disconnectTime, Valid: true}
configEntry := ConfigEntry{Key: lastDisconnectTimeKey, Value: savedDisconnectTime}
return _datastore.Save(configEntry)
}
// SetNSFW will set if this stream has NSFW content.
func SetNSFW(isNSFW bool) error {
return _datastore.SetBool(nsfwKey, isNSFW)
}
// GetNSFW will return if this stream has NSFW content.
func GetNSFW() bool {
nsfw, err := _datastore.GetBool(nsfwKey)
if err != nil {
return false
}
return nsfw
}
// SetFfmpegPath will set the custom ffmpeg path.
func SetFfmpegPath(path string) error {
return _datastore.SetString(ffmpegPathKey, path)
}
// GetFfMpegPath will return the ffmpeg path.
func GetFfMpegPath() string {
path, err := _datastore.GetString(ffmpegPathKey)
if err != nil {
return ""
}
return path
}
// GetS3Config will return the external storage configuration.
func GetS3Config() models.S3 {
configEntry, err := _datastore.Get(s3StorageConfigKey)
if err != nil {
return models.S3{Enabled: false}
}
var s3Config models.S3
if err := configEntry.getObject(&s3Config); err != nil {
return models.S3{Enabled: false}
}
return s3Config
}
// SetS3Config will set the external storage configuration.
func SetS3Config(config models.S3) error {
configEntry := ConfigEntry{Key: s3StorageConfigKey, Value: config}
return _datastore.Save(configEntry)
}
// GetStreamLatencyLevel will return the stream latency level.
func GetStreamLatencyLevel() models.LatencyLevel {
level, err := _datastore.GetNumber(videoLatencyLevel)
if err != nil {
level = 2 // default
} else if level > 4 {
level = 4 // highest
}
return models.GetLatencyLevel(int(level))
}
// SetStreamLatencyLevel will set the stream latency level.
func SetStreamLatencyLevel(level float64) error {
return _datastore.SetNumber(videoLatencyLevel, level)
}
// GetStreamOutputVariants will return all of the stream output variants.
func GetStreamOutputVariants() []models.StreamOutputVariant {
configEntry, err := _datastore.Get(videoStreamOutputVariantsKey)
if err != nil {
return config.GetDefaults().StreamVariants
}
var streamOutputVariants []models.StreamOutputVariant
if err := configEntry.getObject(&streamOutputVariants); err != nil {
return config.GetDefaults().StreamVariants
}
if len(streamOutputVariants) == 0 {
return config.GetDefaults().StreamVariants
}
return streamOutputVariants
}
// SetStreamOutputVariants will set the stream output variants.
func SetStreamOutputVariants(variants []models.StreamOutputVariant) error {
configEntry := ConfigEntry{Key: videoStreamOutputVariantsKey, Value: variants}
return _datastore.Save(configEntry)
}
// SetChatDisabled will disable chat if set to true.
func SetChatDisabled(disabled bool) error {
return _datastore.SetBool(chatDisabledKey, disabled)
}
// GetChatDisabled will return if chat is disabled.
func GetChatDisabled() bool {
disabled, err := _datastore.GetBool(chatDisabledKey)
if err == nil {
return disabled
}
return false
}
// SetChatEstablishedUsersOnlyMode sets the state of established user only mode.
func SetChatEstablishedUsersOnlyMode(enabled bool) error {
return _datastore.SetBool(chatEstablishedUsersOnlyModeKey, enabled)
}
// GetChatEstbalishedUsersOnlyMode returns the state of established user only mode.
func GetChatEstbalishedUsersOnlyMode() bool {
enabled, err := _datastore.GetBool(chatEstablishedUsersOnlyModeKey)
if err == nil {
return enabled
}
return false
}
// GetExternalActions will return the registered external actions.
func GetExternalActions() []models.ExternalAction {
configEntry, err := _datastore.Get(externalActionsKey)
if err != nil {
return []models.ExternalAction{}
}
var externalActions []models.ExternalAction
if err := configEntry.getObject(&externalActions); err != nil {
return []models.ExternalAction{}
}
return externalActions
}
// SetExternalActions will save external actions.
func SetExternalActions(actions []models.ExternalAction) error {
configEntry := ConfigEntry{Key: externalActionsKey, Value: actions}
return _datastore.Save(configEntry)
}
// SetCustomStyles will save a string with CSS to insert into the page.
func SetCustomStyles(styles string) error {
return _datastore.SetString(customStylesKey, styles)
}
// GetCustomStyles will return a string with CSS to insert into the page.
func GetCustomStyles() string {
style, err := _datastore.GetString(customStylesKey)
if err != nil {
return ""
}
return style
}
// SetCustomJavascript will save a string with Javascript to insert into the page.
func SetCustomJavascript(styles string) error {
return _datastore.SetString(customJavascriptKey, styles)
}
// GetCustomJavascript will return a string with Javascript to insert into the page.
func GetCustomJavascript() string {
style, err := _datastore.GetString(customJavascriptKey)
if err != nil {
return ""
}
return style
}
// SetVideoCodec will set the codec used for video encoding.
func SetVideoCodec(codec string) error {
return _datastore.SetString(videoCodecKey, codec)
}
// GetVideoCodec returns the codec to use for transcoding video.
func GetVideoCodec() string {
codec, err := _datastore.GetString(videoCodecKey)
if codec == "" || err != nil {
return "libx264" // Default value
}
return codec
}
// VerifySettings will perform a sanity check for specific settings values.
func VerifySettings() error {
if len(GetStreamKeys()) == 0 && config.TemporaryStreamKey == "" {
log.Errorln("No stream key set. Streaming is disabled. Please set one via the admin or command line arguments")
}
if GetAdminPassword() == "" {
return errors.New("no admin password set. Please set one via the admin or command line arguments")
}
logoPath := GetLogoPath()
if !utils.DoesFileExists(filepath.Join(config.DataDirectory, logoPath)) {
log.Traceln(logoPath, "not found in the data directory. copying a default logo.")
logo := static.GetLogo()
if err := os.WriteFile(filepath.Join(config.DataDirectory, "logo.png"), logo, 0o600); err != nil {
return errors.Wrap(err, "failed to write logo to disk")
}
if err := SetLogoPath("logo.png"); err != nil {
return errors.Wrap(err, "failed to save logo filename")
}
}
return nil
}
// FindHighestVideoQualityIndex will return the highest quality from a slice of variants.
func FindHighestVideoQualityIndex(qualities []models.StreamOutputVariant) int {
type IndexedQuality struct {
index int
quality models.StreamOutputVariant
}
if len(qualities) < 2 {
return 0
}
indexedQualities := make([]IndexedQuality, 0)
for index, quality := range qualities {
indexedQuality := IndexedQuality{index, quality}
indexedQualities = append(indexedQualities, indexedQuality)
}
sort.Slice(indexedQualities, func(a, b int) bool {
if indexedQualities[a].quality.IsVideoPassthrough && !indexedQualities[b].quality.IsVideoPassthrough {
return true
}
if !indexedQualities[a].quality.IsVideoPassthrough && indexedQualities[b].quality.IsVideoPassthrough {
return false
}
return indexedQualities[a].quality.VideoBitrate > indexedQualities[b].quality.VideoBitrate
})
return indexedQualities[0].index
}
// GetForbiddenUsernameList will return the blocked usernames as a comma separated string.
func GetForbiddenUsernameList() []string {
usernames, err := _datastore.GetStringSlice(blockedUsernamesKey)
if err != nil {
return config.DefaultForbiddenUsernames
}
if len(usernames) == 0 {
return config.DefaultForbiddenUsernames
}
return usernames
}
// SetForbiddenUsernameList set the username blocklist as a comma separated string.
func SetForbiddenUsernameList(usernames []string) error {
return _datastore.SetStringSlice(blockedUsernamesKey, usernames)
}
// GetSuggestedUsernamesList will return the suggested usernames.
// If the number of suggested usernames is smaller than 10, the number pool is
// not used (see code in the CreateAnonymousUser function).
func GetSuggestedUsernamesList() []string {
usernames, err := _datastore.GetStringSlice(suggestedUsernamesKey)
if err != nil || len(usernames) == 0 {
return []string{}
}
return usernames
}
// SetSuggestedUsernamesList sets the username suggestion list.
func SetSuggestedUsernamesList(usernames []string) error {
return _datastore.SetStringSlice(suggestedUsernamesKey, usernames)
}
// GetServerInitTime will return when the server was first setup.
func GetServerInitTime() (*utils.NullTime, error) {
var t utils.NullTime
configEntry, err := _datastore.Get(serverInitDateKey)
if err != nil {
return nil, err
}
if err := configEntry.getObject(&t); err != nil {
return nil, err
}
if !t.Valid {
return nil, err
}
return &t, nil
}
// SetServerInitTime will set when the server was first created.
func SetServerInitTime(t time.Time) error {
nt := utils.NullTime{Time: t, Valid: true}
configEntry := ConfigEntry{Key: serverInitDateKey, Value: nt}
return _datastore.Save(configEntry)
}
// SetFederationEnabled will enable federation if set to true.
func SetFederationEnabled(enabled bool) error {
return _datastore.SetBool(federationEnabledKey, enabled)
}
// GetFederationEnabled will return if federation is enabled.
func GetFederationEnabled() bool {
enabled, err := _datastore.GetBool(federationEnabledKey)
if err == nil {
return enabled
}
return false
}
// SetFederationUsername will set the username used in federated activities.
func SetFederationUsername(username string) error {
return _datastore.SetString(federationUsernameKey, username)
}
// GetFederationUsername will return the username used in federated activities.
func GetFederationUsername() string {
username, err := _datastore.GetString(federationUsernameKey)
if username == "" || err != nil {
return config.GetDefaults().FederationUsername
}
return username
}
// SetFederationGoLiveMessage will set the message sent when going live.
func SetFederationGoLiveMessage(message string) error {
return _datastore.SetString(federationGoLiveMessageKey, message)
}
// GetFederationGoLiveMessage will return the message sent when going live.
func GetFederationGoLiveMessage() string {
// Empty message means it's disabled.
message, err := _datastore.GetString(federationGoLiveMessageKey)
if err != nil {
log.Traceln("unable to fetch go live message.", err)
}
return message
}
// SetFederationIsPrivate will set if federation activity is private.
func SetFederationIsPrivate(isPrivate bool) error {
return _datastore.SetBool(federationPrivateKey, isPrivate)
}
// GetFederationIsPrivate will return if federation is private.
func GetFederationIsPrivate() bool {
isPrivate, err := _datastore.GetBool(federationPrivateKey)
if err == nil {
return isPrivate
}
return false
}
// SetFederationShowEngagement will set if fediverse engagement shows in chat.
func SetFederationShowEngagement(showEngagement bool) error {
return _datastore.SetBool(federationShowEngagementKey, showEngagement)
}
// GetFederationShowEngagement will return if fediverse engagement shows in chat.
func GetFederationShowEngagement() bool {
showEngagement, err := _datastore.GetBool(federationShowEngagementKey)
if err == nil {
return showEngagement
}
return true
}
// SetBlockedFederatedDomains will set the blocked federated domains.
func SetBlockedFederatedDomains(domains []string) error {
return _datastore.SetString(federationBlockedDomainsKey, strings.Join(domains, ","))
}
// GetBlockedFederatedDomains will return a list of blocked federated domains.
func GetBlockedFederatedDomains() []string {
domains, err := _datastore.GetString(federationBlockedDomainsKey)
if err != nil {
return []string{}
}
if domains == "" {
return []string{}
}
return strings.Split(domains, ",")
}
// SetChatJoinMessagesEnabled will set if chat join messages are enabled.
func SetChatJoinMessagesEnabled(enabled bool) error {
return _datastore.SetBool(chatJoinMessagesEnabledKey, enabled)
}
// GetChatJoinMessagesEnabled will return if chat join messages are enabled.
func GetChatJoinMessagesEnabled() bool {
enabled, err := _datastore.GetBool(chatJoinMessagesEnabledKey)
if err != nil {
return true
}
return enabled
}
// SetNotificationsEnabled will save the enabled state of notifications.
func SetNotificationsEnabled(enabled bool) error {
return _datastore.SetBool(notificationsEnabledKey, enabled)
}
// GetNotificationsEnabled will return the enabled state of notifications.
func GetNotificationsEnabled() bool {
enabled, _ := _datastore.GetBool(notificationsEnabledKey)
return enabled
}
// GetDiscordConfig will return the Discord configuration.
func GetDiscordConfig() models.DiscordConfiguration {
configEntry, err := _datastore.Get(discordConfigurationKey)
if err != nil {
return models.DiscordConfiguration{Enabled: false}
}
var config models.DiscordConfiguration
if err := configEntry.getObject(&config); err != nil {
return models.DiscordConfiguration{Enabled: false}
}
return config
}
// SetDiscordConfig will set the Discord configuration.
func SetDiscordConfig(config models.DiscordConfiguration) error {
configEntry := ConfigEntry{Key: discordConfigurationKey, Value: config}
return _datastore.Save(configEntry)
}
// GetBrowserPushConfig will return the browser push configuration.
func GetBrowserPushConfig() models.BrowserNotificationConfiguration {
configEntry, err := _datastore.Get(browserPushConfigurationKey)
if err != nil {
return models.BrowserNotificationConfiguration{Enabled: false}
}
var config models.BrowserNotificationConfiguration
if err := configEntry.getObject(&config); err != nil {
return models.BrowserNotificationConfiguration{Enabled: false}
}
return config
}
// SetBrowserPushConfig will set the browser push configuration.
func SetBrowserPushConfig(config models.BrowserNotificationConfiguration) error {
configEntry := ConfigEntry{Key: browserPushConfigurationKey, Value: config}
return _datastore.Save(configEntry)
}
// SetBrowserPushPublicKey will set the public key for browser pushes.
func SetBrowserPushPublicKey(key string) error {
return _datastore.SetString(browserPushPublicKeyKey, key)
}
// GetBrowserPushPublicKey will return the public key for browser pushes.
func GetBrowserPushPublicKey() (string, error) {
return _datastore.GetString(browserPushPublicKeyKey)
}
// SetBrowserPushPrivateKey will set the private key for browser pushes.
func SetBrowserPushPrivateKey(key string) error {
return _datastore.SetString(browserPushPrivateKeyKey, key)
}
// GetBrowserPushPrivateKey will return the private key for browser pushes.
func GetBrowserPushPrivateKey() (string, error) {
return _datastore.GetString(browserPushPrivateKeyKey)
}
// SetHasPerformedInitialNotificationsConfig sets when performed initial setup.
func SetHasPerformedInitialNotificationsConfig(hasConfigured bool) error {
return _datastore.SetBool(hasConfiguredInitialNotificationsKey, true)
}
// GetHasPerformedInitialNotificationsConfig gets when performed initial setup.
func GetHasPerformedInitialNotificationsConfig() bool {
configured, _ := _datastore.GetBool(hasConfiguredInitialNotificationsKey)
return configured
}
// GetHideViewerCount will return if the viewer count shold be hidden.
func GetHideViewerCount() bool {
hide, _ := _datastore.GetBool(hideViewerCountKey)
return hide
}
// SetHideViewerCount will set if the viewer count should be hidden.
func SetHideViewerCount(hide bool) error {
return _datastore.SetBool(hideViewerCountKey, hide)
}
// GetCustomOfflineMessage will return the custom offline message.
func GetCustomOfflineMessage() string {
message, _ := _datastore.GetString(customOfflineMessageKey)
return message
}
// SetCustomOfflineMessage will set the custom offline message.
func SetCustomOfflineMessage(message string) error {
return _datastore.SetString(customOfflineMessageKey, message)
}
// SetCustomColorVariableValues sets CSS variable names and values.
func SetCustomColorVariableValues(variables map[string]string) error {
return _datastore.SetStringMap(customColorVariableValuesKey, variables)
}
// GetCustomColorVariableValues gets CSS variable names and values.
func GetCustomColorVariableValues() map[string]string {
values, _ := _datastore.GetStringMap(customColorVariableValuesKey)
return values
}
// GetStreamKeys will return valid stream keys.
func GetStreamKeys() []models.StreamKey {
configEntry, err := _datastore.Get(streamKeysKey)
if err != nil {
return []models.StreamKey{}
}
var streamKeys []models.StreamKey
if err := configEntry.getObject(&streamKeys); err != nil {
return []models.StreamKey{}
}
return streamKeys
}
// SetStreamKeys will set valid stream keys.
func SetStreamKeys(actions []models.StreamKey) error {
configEntry := ConfigEntry{Key: streamKeysKey, Value: actions}
return _datastore.Save(configEntry)
}
// SetDisableSearchIndexing will set if the web server should be indexable.
func SetDisableSearchIndexing(disableSearchIndexing bool) error {
return _datastore.SetBool(disableSearchIndexingKey, disableSearchIndexing)
}
// GetDisableSearchIndexing will return if the web server should be indexable.
func GetDisableSearchIndexing() bool {
disableSearchIndexing, err := _datastore.GetBool(disableSearchIndexingKey)
if err != nil {
return false
}
return disableSearchIndexing
}
// GetVideoServingEndpoint returns the custom video endpont.
func GetVideoServingEndpoint() string {
message, _ := _datastore.GetString(videoServingEndpointKey)
return message
}
// SetVideoServingEndpoint sets the custom video endpoint.
func SetVideoServingEndpoint(message string) error {
return _datastore.SetString(videoServingEndpointKey, message)
}

60
core/data/configEntry.go

@ -1,60 +0,0 @@ @@ -1,60 +0,0 @@
package data
import (
"bytes"
"encoding/gob"
)
// ConfigEntry is the actual object saved to the database.
// The Value is encoded using encoding/gob.
type ConfigEntry struct {
Key string
Value interface{}
}
func (c *ConfigEntry) getStringSlice() ([]string, error) {
decoder := c.getDecoder()
var result []string
err := decoder.Decode(&result)
return result, err
}
func (c *ConfigEntry) getStringMap() (map[string]string, error) {
decoder := c.getDecoder()
var result map[string]string
err := decoder.Decode(&result)
return result, err
}
func (c *ConfigEntry) getString() (string, error) {
decoder := c.getDecoder()
var result string
err := decoder.Decode(&result)
return result, err
}
func (c *ConfigEntry) getNumber() (float64, error) {
decoder := c.getDecoder()
var result float64
err := decoder.Decode(&result)
return result, err
}
func (c *ConfigEntry) getBool() (bool, error) {
decoder := c.getDecoder()
var result bool
err := decoder.Decode(&result)
return result, err
}
func (c *ConfigEntry) getObject(result interface{}) error {
decoder := c.getDecoder()
err := decoder.Decode(result)
return err
}
func (c *ConfigEntry) getDecoder() *gob.Decoder {
valueBytes := c.Value.([]byte)
decoder := gob.NewDecoder(bytes.NewBuffer(valueBytes))
return decoder
}

23
core/data/crypto.go

@ -1,23 +0,0 @@ @@ -1,23 +0,0 @@
package data
// GetPublicKey will return the public key.
func GetPublicKey() string {
value, _ := _datastore.GetString(publicKeyKey)
return value
}
// SetPublicKey will save the public key.
func SetPublicKey(key string) error {
return _datastore.SetString(publicKeyKey, key)
}
// GetPrivateKey will return the private key.
func GetPrivateKey() string {
value, _ := _datastore.GetString(privateKeyKey)
return value
}
// SetPrivateKey will save the private key.
func SetPrivateKey(key string) error {
return _datastore.SetString(privateKeyKey, key)
}

75
core/data/datastoreMigrations.go

@ -1,75 +0,0 @@ @@ -1,75 +0,0 @@
package data
import (
"strings"
"github.com/owncast/owncast/models"
log "github.com/sirupsen/logrus"
)
const (
datastoreValuesVersion = 3
datastoreValueVersionKey = "DATA_STORE_VERSION"
)
func migrateDatastoreValues(datastore *Datastore) {
currentVersion, _ := _datastore.GetNumber(datastoreValueVersionKey)
if currentVersion == 0 {
currentVersion = datastoreValuesVersion
}
for v := currentVersion; v < datastoreValuesVersion; v++ {
log.Infof("Migration datastore values from %d to %d\n", int(v), int(v+1))
switch v {
case 0:
migrateToDatastoreValues1(datastore)
case 1:
migrateToDatastoreValues2(datastore)
case 2:
migrateToDatastoreValues3ServingEndpoint3(datastore)
default:
log.Fatalln("missing datastore values migration step")
}
}
if err := _datastore.SetNumber(datastoreValueVersionKey, datastoreValuesVersion); err != nil {
log.Errorln("error setting datastore value version:", err)
}
}
func migrateToDatastoreValues1(datastore *Datastore) {
// Migrate the forbidden usernames to be a slice instead of a string.
forbiddenUsernamesString, _ := datastore.GetString(blockedUsernamesKey)
if forbiddenUsernamesString != "" {
forbiddenUsernamesSlice := strings.Split(forbiddenUsernamesString, ",")
if err := datastore.SetStringSlice(blockedUsernamesKey, forbiddenUsernamesSlice); err != nil {
log.Errorln("error migrating blocked username list:", err)
}
}
// Migrate the suggested usernames to be a slice instead of a string.
suggestedUsernamesString, _ := datastore.GetString(suggestedUsernamesKey)
if suggestedUsernamesString != "" {
suggestedUsernamesSlice := strings.Split(suggestedUsernamesString, ",")
if err := datastore.SetStringSlice(suggestedUsernamesKey, suggestedUsernamesSlice); err != nil {
log.Errorln("error migrating suggested username list:", err)
}
}
}
func migrateToDatastoreValues2(datastore *Datastore) {
oldAdminPassword, _ := datastore.GetString("stream_key")
_ = SetAdminPassword(oldAdminPassword)
_ = SetStreamKeys([]models.StreamKey{
{Key: oldAdminPassword, Comment: "Default stream key"},
})
}
func migrateToDatastoreValues3ServingEndpoint3(_ *Datastore) {
s3Config := GetS3Config()
if !s3Config.Enabled {
return
}
_ = SetVideoServingEndpoint(s3Config.ServingEndpoint)
}

54
core/data/defaults.go

@ -1,54 +0,0 @@ @@ -1,54 +0,0 @@
package data
import (
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/models"
)
// HasPopulatedDefaults will determine if the defaults have been inserted into the database.
func HasPopulatedDefaults() bool {
hasPopulated, err := _datastore.GetBool("HAS_POPULATED_DEFAULTS")
if err != nil {
return false
}
return hasPopulated
}
func hasPopulatedFederationDefaults() bool {
hasPopulated, err := _datastore.GetBool("HAS_POPULATED_FEDERATION_DEFAULTS")
if err != nil {
return false
}
return hasPopulated
}
// PopulateDefaults will set default values in the database.
func PopulateDefaults() {
_datastore.warmCache()
defaults := config.GetDefaults()
if HasPopulatedDefaults() {
return
}
_ = SetAdminPassword(defaults.AdminPassword)
_ = SetStreamKeys(defaults.StreamKeys)
_ = SetHTTPPortNumber(float64(defaults.WebServerPort))
_ = SetRTMPPortNumber(float64(defaults.RTMPServerPort))
_ = SetLogoPath(defaults.Logo)
_ = SetServerMetadataTags([]string{"owncast", "streaming"})
_ = SetServerSummary(defaults.Summary)
_ = SetServerWelcomeMessage("")
_ = SetServerName(defaults.Name)
_ = SetExtraPageBodyContent(defaults.PageBodyContent)
_ = SetFederationGoLiveMessage(defaults.FederationGoLiveMessage)
_ = SetSocialHandles([]models.SocialHandle{
{
Platform: "github",
URL: "https://github.com/owncast/owncast",
},
})
_ = _datastore.SetBool("HAS_POPULATED_DEFAULTS", true)
}

172
core/data/emoji.go

@ -1,172 +0,0 @@ @@ -1,172 +0,0 @@
package data
import (
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"sync"
"time"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/static"
"github.com/owncast/owncast/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
var emojiCacheMu sync.Mutex
var emojiCacheData = make([]models.CustomEmoji, 0)
var emojiCacheModTime time.Time
// UpdateEmojiList will update the cache (if required) and
// return the modifiation time.
func UpdateEmojiList(force bool) (time.Time, error) {
var modTime time.Time
emojiPathInfo, err := os.Stat(config.CustomEmojiPath)
if err != nil {
return modTime, err
}
modTime = emojiPathInfo.ModTime()
if modTime.After(emojiCacheModTime) || force {
emojiCacheMu.Lock()
defer emojiCacheMu.Unlock()
// double-check that another thread didn't update this while waiting.
if modTime.After(emojiCacheModTime) || force {
emojiCacheModTime = modTime
if force {
emojiCacheModTime = time.Now()
}
emojiFS := os.DirFS(config.CustomEmojiPath)
emojiCacheData = make([]models.CustomEmoji, 0)
walkFunction := func(path string, d os.DirEntry, err error) error {
if d.IsDir() {
return nil
}
emojiPath := filepath.Join(config.EmojiDir, path)
fileName := d.Name()
fileBase := fileName[:len(fileName)-len(filepath.Ext(fileName))]
singleEmoji := models.CustomEmoji{Name: fileBase, URL: emojiPath}
emojiCacheData = append(emojiCacheData, singleEmoji)
return nil
}
if err := fs.WalkDir(emojiFS, ".", walkFunction); err != nil {
log.Errorln("unable to fetch emojis: " + err.Error())
}
}
}
return modTime, nil
}
// GetEmojiList returns a list of custom emoji from the emoji directory.
func GetEmojiList() []models.CustomEmoji {
_, err := UpdateEmojiList(false)
if err != nil {
return nil
}
// Lock to make sure this doesn't get updated in the middle of reading
emojiCacheMu.Lock()
defer emojiCacheMu.Unlock()
// return a copy of cache data, ensures underlying slice isn't affected
// by future update
emojiData := make([]models.CustomEmoji, len(emojiCacheData))
copy(emojiData, emojiCacheData)
return emojiData
}
// SetupEmojiDirectory sets up the custom emoji directory by copying all built-in
// emojis if the directory does not yet exist.
func SetupEmojiDirectory() (err error) {
type emojiDirectory struct {
path string
isDir bool
}
if utils.DoesFileExists(config.CustomEmojiPath) {
return nil
}
if err = os.MkdirAll(config.CustomEmojiPath, 0o750); err != nil {
return fmt.Errorf("unable to create custom emoji directory: %w", err)
}
staticFS := static.GetEmoji()
files := []emojiDirectory{}
walkFunction := func(path string, d os.DirEntry, err error) error {
if path == "." {
return nil
}
if d.Name() == "LICENSE.md" {
return nil
}
files = append(files, emojiDirectory{path: path, isDir: d.IsDir()})
return nil
}
if err := fs.WalkDir(staticFS, ".", walkFunction); err != nil {
log.Errorln("unable to fetch emojis: " + err.Error())
return errors.Wrap(err, "unable to fetch embedded emoji files")
}
if err != nil {
return fmt.Errorf("unable to read built-in emoji files: %w", err)
}
// Now copy all built-in emojis to the custom emoji directory
for _, path := range files {
emojiPath := filepath.Join(config.CustomEmojiPath, path.path)
if path.isDir {
if err := os.Mkdir(emojiPath, 0o700); err != nil {
return errors.Wrap(err, "unable to create emoji directory, check permissions?: "+path.path)
}
continue
}
memFile, staticOpenErr := staticFS.Open(path.path)
if staticOpenErr != nil {
return errors.Wrap(staticOpenErr, "unable to open emoji file from embedded filesystem")
}
// nolint:gosec
diskFile, err := os.Create(emojiPath)
if err != nil {
return fmt.Errorf("unable to create custom emoji file on disk: %w", err)
}
if err != nil {
_ = diskFile.Close()
return fmt.Errorf("unable to open built-in emoji file: %w", err)
}
if _, err = io.Copy(diskFile, memFile); err != nil {
_ = diskFile.Close()
_ = os.Remove(emojiPath)
return fmt.Errorf("unable to copy built-in emoji file to disk: %w", err)
}
if err = diskFile.Close(); err != nil {
_ = os.Remove(emojiPath)
return fmt.Errorf("unable to close custom emoji file on disk: %w", err)
}
}
return nil
}

108
core/data/messages.go

@ -1,108 +0,0 @@ @@ -1,108 +0,0 @@
package data
import (
"context"
"database/sql"
"github.com/owncast/owncast/db"
"github.com/owncast/owncast/models"
log "github.com/sirupsen/logrus"
)
// CreateMessagesTable will create the chat messages table if needed.
func CreateMessagesTable(db *sql.DB) {
createTableSQL := `CREATE TABLE IF NOT EXISTS messages (
"id" string NOT NULL,
"user_id" TEXT,
"body" TEXT,
"eventType" TEXT,
"hidden_at" DATETIME,
"timestamp" DATETIME,
"title" TEXT,
"subtitle" TEXT,
"image" TEXT,
"link" TEXT,
PRIMARY KEY (id)
);`
MustExec(createTableSQL, db)
// Create indexes
MustExec(`CREATE INDEX IF NOT EXISTS user_id_hidden_at_timestamp ON messages (id, user_id, hidden_at, timestamp);`, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_id ON messages (id);`, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id ON messages (user_id);`, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_hidden_at ON messages (hidden_at);`, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_timestamp ON messages (timestamp);`, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_messages_hidden_at_timestamp on messages(hidden_at, timestamp);`, db)
}
// GetMessagesCount will return the number of messages in the database.
func GetMessagesCount() int64 {
query := `SELECT COUNT(*) FROM messages`
rows, err := _db.Query(query)
if err != nil || rows.Err() != nil {
return 0
}
defer rows.Close()
var count int64
for rows.Next() {
if err := rows.Scan(&count); err != nil {
return 0
}
}
return count
}
// CreateBanIPTable will create the IP ban table if needed.
func CreateBanIPTable(db *sql.DB) {
createTableSQL := ` CREATE TABLE IF NOT EXISTS ip_bans (
"ip_address" TEXT NOT NULL PRIMARY KEY,
"notes" TEXT,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
stmt, err := db.Prepare(createTableSQL)
if err != nil {
log.Fatal("error creating ip ban table", err)
}
defer stmt.Close()
if _, err := stmt.Exec(); err != nil {
log.Fatal("error creating ip ban table", err)
}
}
// BanIPAddress will persist a new IP address ban to the datastore.
func BanIPAddress(address, note string) error {
return _datastore.GetQueries().BanIPAddress(context.Background(), db.BanIPAddressParams{
IpAddress: address,
Notes: sql.NullString{String: note, Valid: true},
})
}
// IsIPAddressBanned will return if an IP address has been previously blocked.
func IsIPAddressBanned(address string) (bool, error) {
blocked, error := _datastore.GetQueries().IsIPAddressBlocked(context.Background(), address)
return blocked > 0, error
}
// GetIPAddressBans will return all the banned IP addresses.
func GetIPAddressBans() ([]models.IPAddress, error) {
result, err := _datastore.GetQueries().GetIPAddressBans(context.Background())
if err != nil {
return nil, err
}
response := []models.IPAddress{}
for _, ip := range result {
response = append(response, models.IPAddress{
IPAddress: ip.IpAddress,
Notes: ip.Notes.String,
CreatedAt: ip.CreatedAt.Time,
})
}
return response, err
}
// RemoveIPAddressBan will remove a previously banned IP address.
func RemoveIPAddressBan(address string) error {
return _datastore.GetQueries().RemoveIPAddressBan(context.Background(), address)
}

179
core/data/persistence.go

@ -1,179 +0,0 @@ @@ -1,179 +0,0 @@
package data
import (
"bytes"
"database/sql"
"encoding/gob"
"sync"
"time"
// sqlite requires a blank import.
_ "github.com/mattn/go-sqlite3"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/db"
log "github.com/sirupsen/logrus"
)
// Datastore is the global key/value store for configuration values.
type Datastore struct {
DB *sql.DB
cache map[string][]byte
DbLock *sync.Mutex
}
func (ds *Datastore) warmCache() {
log.Traceln("Warming config value cache")
res, err := ds.DB.Query("SELECT key, value FROM datastore")
if err != nil || res.Err() != nil {
log.Errorln("error warming config cache", err, res.Err())
}
defer res.Close()
for res.Next() {
var rowKey string
var rowValue []byte
if err := res.Scan(&rowKey, &rowValue); err != nil {
log.Errorln("error pre-caching config row", err)
}
ds.cache[rowKey] = rowValue
}
}
// 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) (ConfigEntry, error) {
cachedValue, err := ds.GetCachedValue(key)
if err == nil {
return ConfigEntry{
Key: key,
Value: cachedValue,
}, nil
}
var resultKey string
var resultValue []byte
row := ds.DB.QueryRow("SELECT key, value FROM datastore WHERE key = ? LIMIT 1", key)
if err := row.Scan(&resultKey, &resultValue); err != nil {
return ConfigEntry{}, err
}
result := ConfigEntry{
Key: resultKey,
Value: resultValue,
}
ds.SetCachedValue(resultKey, resultValue)
return result, nil
}
// Save will save the ConfigEntry to the database.
func (ds *Datastore) Save(e 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
}
// Setup will create the datastore table and perform initial initialization.
func (ds *Datastore) Setup() {
ds.cache = make(map[string][]byte)
ds.DB = GetDatabase()
ds.DbLock = &sync.Mutex{}
createTableSQL := `CREATE TABLE IF NOT EXISTS datastore (
"key" string NOT NULL PRIMARY KEY,
"value" BLOB,
"timestamp" DATE DEFAULT CURRENT_TIMESTAMP NOT NULL
);`
ds.MustExec(createTableSQL)
if !HasPopulatedDefaults() {
PopulateDefaults()
}
if !hasPopulatedFederationDefaults() {
if err := SetFederationGoLiveMessage(config.GetDefaults().FederationGoLiveMessage); err != nil {
log.Errorln(err)
}
if err := _datastore.SetBool("HAS_POPULATED_FEDERATION_DEFAULTS", true); err != nil {
log.Errorln(err)
}
}
// Set the server initialization date if needed.
if hasSetInitDate, _ := GetServerInitTime(); hasSetInitDate == nil || !hasSetInitDate.Valid {
_ = SetServerInitTime(time.Now())
}
migrateDatastoreValues(_datastore)
}
// Reset will delete all config entries in the datastore and start over.
func (ds *Datastore) Reset() {
sql := "DELETE FROM datastore"
stmt, err := ds.DB.Prepare(sql)
if err != nil {
log.Fatalln(err)
}
defer stmt.Close()
if _, err = stmt.Exec(); err != nil {
log.Fatalln(err)
}
PopulateDefaults()
}
// GetDatastore returns the shared instance of the owncast datastore.
func GetDatastore() *Datastore {
return _datastore
}
// 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)
}
}

67
core/data/users.go

@ -1,67 +0,0 @@ @@ -1,67 +0,0 @@
package data
import (
"database/sql"
log "github.com/sirupsen/logrus"
)
func createAccessTokenTable(db *sql.DB) {
createTableSQL := `CREATE TABLE IF NOT EXISTS user_access_tokens (
"token" TEXT NOT NULL PRIMARY KEY,
"user_id" TEXT NOT NULL,
"timestamp" DATE DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
);`
stmt, err := db.Prepare(createTableSQL)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
_, err = stmt.Exec()
if err != nil {
log.Warnln(err)
}
}
func createUsersTable(db *sql.DB) {
log.Traceln("Creating users table...")
createTableSQL := `CREATE TABLE IF NOT EXISTS users (
"id" TEXT,
"display_name" TEXT NOT NULL,
"display_color" NUMBER NOT NULL,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"disabled_at" TIMESTAMP,
"previous_names" TEXT DEFAULT '',
"namechanged_at" TIMESTAMP,
"authenticated_at" TIMESTAMP,
"scopes" TEXT,
"type" TEXT DEFAULT 'STANDARD',
"last_used" DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);`
MustExec(createTableSQL, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id ON users (id);`, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id_disabled ON users (id, disabled_at);`, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_disabled_at ON users (disabled_at);`, db)
}
// GetUsersCount will return the number of users in the database.
func GetUsersCount() int64 {
query := `SELECT COUNT(*) FROM users`
rows, err := _db.Query(query)
if err != nil || rows.Err() != nil {
return 0
}
defer rows.Close()
var count int64
for rows.Next() {
if err := rows.Scan(&count); err != nil {
return 0
}
}
return count
}

13
core/offlineState.go

@ -6,7 +6,7 @@ import ( @@ -6,7 +6,7 @@ import (
"path/filepath"
"github.com/grafov/m3u8"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/services/config"
"github.com/owncast/owncast/static"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
@ -19,8 +19,9 @@ func appendOfflineToVariantPlaylist(index int, playlistFilePath string) { @@ -19,8 +19,9 @@ func appendOfflineToVariantPlaylist(index int, playlistFilePath string) {
return
}
c := config.Get()
tmpFileName := fmt.Sprintf("tmp-stream-%d.m3u8", index)
atomicWriteTmpPlaylistFile, err := os.CreateTemp(config.TempDir, tmpFileName)
atomicWriteTmpPlaylistFile, err := os.CreateTemp(c.TempDir, tmpFileName)
if err != nil {
log.Errorln("error creating tmp playlist file to write to", playlistFilePath, err)
return
@ -49,8 +50,9 @@ func appendOfflineToVariantPlaylist(index int, playlistFilePath string) { @@ -49,8 +50,9 @@ func appendOfflineToVariantPlaylist(index int, playlistFilePath string) {
}
func makeVariantIndexOffline(index int, offlineFilePath string, offlineFilename string) {
playlistFilePath := fmt.Sprintf(filepath.Join(config.HLSStoragePath, "%d/stream.m3u8"), index)
segmentFilePath := fmt.Sprintf(filepath.Join(config.HLSStoragePath, "%d/%s"), index, offlineFilename)
c := config.Get()
playlistFilePath := fmt.Sprintf(filepath.Join(c.HLSStoragePath, "%d/stream.m3u8"), index)
segmentFilePath := fmt.Sprintf(filepath.Join(c.HLSStoragePath, "%d/%s"), index, offlineFilename)
if err := utils.Copy(offlineFilePath, segmentFilePath); err != nil {
log.Warnln(err)
@ -94,7 +96,8 @@ func createEmptyOfflinePlaylist(playlistFilePath string, offlineFilename string) @@ -94,7 +96,8 @@ func createEmptyOfflinePlaylist(playlistFilePath string, offlineFilename string)
func saveOfflineClipToDisk(offlineFilename string) (string, error) {
offlineFileData := static.GetOfflineSegment()
offlineTmpFile, err := os.CreateTemp(config.TempDir, offlineFilename)
c := config.Get()
offlineTmpFile, err := os.CreateTemp(c.TempDir, offlineFilename)
if err != nil {
log.Errorln("unable to create temp file for offline video segment", err)
}

26
core/stats.go

@ -7,9 +7,9 @@ import ( @@ -7,9 +7,9 @@ import (
log "github.com/sirupsen/logrus"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/geoip"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/services/geoip"
"github.com/owncast/owncast/storage/configrepository"
)
var (
@ -45,10 +45,12 @@ func IsStreamConnected() bool { @@ -45,10 +45,12 @@ func IsStreamConnected() bool {
return false
}
configRepository := configrepository.Get()
// 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.
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 {
return false
}
@ -75,7 +77,7 @@ func SetViewerActive(viewer *models.Viewer) { @@ -75,7 +77,7 @@ func SetViewerActive(viewer *models.Viewer) {
l.Lock()
defer l.Unlock()
// Asynchronously, optionally, fetch GeoIP data.
// Asynchronously, optionally, fetch GeoIP configRepository.
go func(viewer *models.Viewer) {
viewer.Geo = _geoIPClient.GetGeoFromIP(viewer.IPAddress)
}(viewer)
@ -111,27 +113,31 @@ func pruneViewerCount() { @@ -111,27 +113,31 @@ func pruneViewerCount() {
}
func saveStats() {
if err := data.SetPeakOverallViewerCount(_stats.OverallMaxViewerCount); err != nil {
configRepository := configrepository.Get()
if err := configRepository.SetPeakOverallViewerCount(_stats.OverallMaxViewerCount); err != nil {
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)
}
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)
}
}
}
func getSavedStats() models.Stats {
savedLastDisconnectTime, _ := data.GetLastDisconnectTime()
configRepository := configrepository.Get()
savedLastDisconnectTime, _ := configRepository.GetLastDisconnectTime()
result := models.Stats{
ChatClients: make(map[string]models.Client),
Viewers: make(map[string]*models.Viewer),
SessionMaxViewerCount: data.GetPeakSessionViewerCount(),
OverallMaxViewerCount: data.GetPeakOverallViewerCount(),
SessionMaxViewerCount: configRepository.GetPeakSessionViewerCount(),
OverallMaxViewerCount: configRepository.GetPeakOverallViewerCount(),
LastDisconnectTime: savedLastDisconnectTime,
}

45
core/status.go

@ -1,45 +0,0 @@ @@ -1,45 +0,0 @@
package core
import (
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
)
// GetStatus gets the status of the system.
func GetStatus() models.Status {
if _stats == nil {
return models.Status{}
}
viewerCount := 0
if IsStreamConnected() {
viewerCount = len(_stats.Viewers)
}
return models.Status{
Online: IsStreamConnected(),
ViewerCount: viewerCount,
OverallMaxViewerCount: _stats.OverallMaxViewerCount,
SessionMaxViewerCount: _stats.SessionMaxViewerCount,
LastDisconnectTime: _stats.LastDisconnectTime,
LastConnectTime: _stats.LastConnectTime,
VersionNumber: config.VersionNumber,
StreamTitle: data.GetStreamTitle(),
}
}
// GetCurrentBroadcast will return the currently active broadcast.
func GetCurrentBroadcast() *models.CurrentBroadcast {
return _currentBroadcast
}
// setBroadcaster will store the current inbound broadcasting details.
func setBroadcaster(broadcaster models.Broadcaster) {
_broadcaster = &broadcaster
}
// GetBroadcaster will return the details of the currently active broadcaster.
func GetBroadcaster() *models.Broadcaster {
return _broadcaster
}

7
core/storage.go

@ -1,12 +1,13 @@ @@ -1,12 +1,13 @@
package core
import (
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/storageproviders"
"github.com/owncast/owncast/storage/configrepository"
"github.com/owncast/owncast/video/storageproviders"
)
func setupStorage() error {
s3Config := data.GetS3Config()
config := configrepository.Get()
s3Config := config.GetS3Config()
if s3Config.Enabled {
_storage = storageproviders.NewS3Storage()

200
core/streamState.go

@ -1,200 +0,0 @@ @@ -1,200 +0,0 @@
package core
import (
"context"
"io"
"time"
log "github.com/sirupsen/logrus"
"github.com/owncast/owncast/activitypub"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/rtmp"
"github.com/owncast/owncast/core/transcoder"
"github.com/owncast/owncast/core/webhooks"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/notifications"
"github.com/owncast/owncast/utils"
)
// After the stream goes offline this timer fires a full cleanup after N min.
var _offlineCleanupTimer *time.Timer
// While a stream takes place cleanup old HLS content every N min.
var _onlineCleanupTicker *time.Ticker
var _currentBroadcast *models.CurrentBroadcast
var _onlineTimerCancelFunc context.CancelFunc
var _lastNotified *time.Time
// setStreamAsConnected sets the stream as connected.
func setStreamAsConnected(rtmpOut *io.PipeReader) {
now := utils.NullTime{Time: time.Now(), Valid: true}
_stats.StreamConnected = true
_stats.LastDisconnectTime = nil
_stats.LastConnectTime = &now
_stats.SessionMaxViewerCount = 0
_currentBroadcast = &models.CurrentBroadcast{
LatencyLevel: data.GetStreamLatencyLevel(),
OutputSettings: data.GetStreamOutputVariants(),
}
StopOfflineCleanupTimer()
startOnlineCleanupTimer()
if _yp != nil {
go _yp.Start()
}
segmentPath := config.HLSStoragePath
if err := setupStorage(); err != nil {
log.Fatalln("failed to setup the storage", err)
}
go func() {
_transcoder = transcoder.NewTranscoder()
_transcoder.TranscoderCompleted = func(error) {
SetStreamAsDisconnected()
_transcoder = nil
_currentBroadcast = nil
}
_transcoder.SetStdin(rtmpOut)
_transcoder.Start(true)
}()
go webhooks.SendStreamStatusEvent(models.StreamStarted)
transcoder.StartThumbnailGenerator(segmentPath, data.FindHighestVideoQualityIndex(_currentBroadcast.OutputSettings))
_ = chat.SendSystemAction("Stay tuned, the stream is **starting**!", true)
chat.SendAllWelcomeMessage()
// Send delayed notification messages.
_onlineTimerCancelFunc = startLiveStreamNotificationsTimer()
}
// SetStreamAsDisconnected sets the stream as disconnected.
func SetStreamAsDisconnected() {
_ = chat.SendSystemAction("The stream is ending.", true)
now := utils.NullTime{Time: time.Now(), Valid: true}
if _onlineTimerCancelFunc != nil {
_onlineTimerCancelFunc()
}
_stats.StreamConnected = false
_stats.LastDisconnectTime = &now
_stats.LastConnectTime = nil
_broadcaster = nil
offlineFilename := "offline.ts"
offlineFilePath, err := saveOfflineClipToDisk(offlineFilename)
if err != nil {
log.Errorln(err)
return
}
transcoder.StopThumbnailGenerator()
rtmp.Disconnect()
if _yp != nil {
_yp.Stop()
}
// If there is no current broadcast available the previous stream
// likely failed for some reason. Don't try to append to it.
// Just transition to offline.
if _currentBroadcast == nil {
stopOnlineCleanupTimer()
transitionToOfflineVideoStreamContent()
log.Errorln("unexpected nil _currentBroadcast")
return
}
for index := range _currentBroadcast.OutputSettings {
makeVariantIndexOffline(index, offlineFilePath, offlineFilename)
}
StartOfflineCleanupTimer()
stopOnlineCleanupTimer()
saveStats()
go webhooks.SendStreamStatusEvent(models.StreamStopped)
}
// StartOfflineCleanupTimer will fire a cleanup after n minutes being disconnected.
func StartOfflineCleanupTimer() {
_offlineCleanupTimer = time.NewTimer(5 * time.Minute)
go func() {
for range _offlineCleanupTimer.C {
// Set video to offline state
resetDirectories()
transitionToOfflineVideoStreamContent()
}
}()
}
// StopOfflineCleanupTimer will stop the previous cleanup timer.
func StopOfflineCleanupTimer() {
if _offlineCleanupTimer != nil {
_offlineCleanupTimer.Stop()
}
}
func startOnlineCleanupTimer() {
_onlineCleanupTicker = time.NewTicker(1 * time.Minute)
go func() {
for range _onlineCleanupTicker.C {
if err := _storage.Cleanup(); err != nil {
log.Errorln(err)
}
}
}()
}
func stopOnlineCleanupTimer() {
if _onlineCleanupTicker != nil {
_onlineCleanupTicker.Stop()
}
}
func startLiveStreamNotificationsTimer() context.CancelFunc {
// Send delayed notification messages.
c, cancelFunc := context.WithCancel(context.Background())
_onlineTimerCancelFunc = cancelFunc
go func(c context.Context) {
select {
case <-time.After(time.Minute * 2.0):
if _lastNotified != nil && time.Since(*_lastNotified) < 10*time.Minute {
return
}
// Send Fediverse message.
if data.GetFederationEnabled() {
log.Traceln("Sending Federated Go Live message.")
if err := activitypub.SendLive(); err != nil {
log.Errorln(err)
}
}
// Send notification to those who have registered for them.
if notifier, err := notifications.New(data.GetDatastore()); err != nil {
log.Errorln(err)
} else {
notifier.Notify()
}
now := time.Now()
_lastNotified = &now
case <-c.Done():
}
}(c)
return cancelFunc
}

311
core/user/externalAPIUser.go

@ -1,311 +0,0 @@ @@ -1,311 +0,0 @@
package user
import (
"context"
"database/sql"
"strings"
"time"
"github.com/owncast/owncast/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/teris-io/shortid"
)
// ExternalAPIUser represents a single 3rd party integration that uses an access token.
// This struct mostly matches the User struct so they can be used interchangeably.
type ExternalAPIUser struct {
CreatedAt time.Time `json:"createdAt"`
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
ID string `json:"id"`
AccessToken string `json:"accessToken"`
DisplayName string `json:"displayName"`
Type string `json:"type,omitempty"` // Should be API
Scopes []string `json:"scopes"`
DisplayColor int `json:"displayColor"`
IsBot bool `json:"isBot"`
}
const (
// ScopeCanSendChatMessages will allow sending chat messages as itself.
ScopeCanSendChatMessages = "CAN_SEND_MESSAGES"
// ScopeCanSendSystemMessages will allow sending chat messages as the system.
ScopeCanSendSystemMessages = "CAN_SEND_SYSTEM_MESSAGES"
// ScopeHasAdminAccess will allow performing administrative actions on the server.
ScopeHasAdminAccess = "HAS_ADMIN_ACCESS"
)
// For a scope to be seen as "valid" it must live in this slice.
var validAccessTokenScopes = []string{
ScopeCanSendChatMessages,
ScopeCanSendSystemMessages,
ScopeHasAdminAccess,
}
// InsertExternalAPIUser will add a new API user to the database.
func InsertExternalAPIUser(token string, name string, color int, scopes []string) error {
log.Traceln("Adding new API user")
_datastore.DbLock.Lock()
defer _datastore.DbLock.Unlock()
scopesString := strings.Join(scopes, ",")
id := shortid.MustGenerate()
tx, err := _datastore.DB.Begin()
if err != nil {
return err
}
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, scopes, type, previous_names) values(?, ?, ?, ?, ?, ?)")
if err != nil {
return err
}
defer stmt.Close()
if _, err = stmt.Exec(id, name, color, scopesString, "API", name); err != nil {
return err
}
if err = tx.Commit(); err != nil {
return err
}
if err := addAccessTokenForUser(token, id); err != nil {
return errors.Wrap(err, "unable to save access token for new external api user")
}
return nil
}
// DeleteExternalAPIUser will delete a token from the database.
func DeleteExternalAPIUser(token string) error {
log.Traceln("Deleting access token")
_datastore.DbLock.Lock()
defer _datastore.DbLock.Unlock()
tx, err := _datastore.DB.Begin()
if err != nil {
return err
}
stmt, err := tx.Prepare("UPDATE users SET disabled_at = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)")
if err != nil {
return err
}
defer stmt.Close()
result, err := stmt.Exec(token)
if err != nil {
return err
}
if rowsDeleted, _ := result.RowsAffected(); rowsDeleted == 0 {
tx.Rollback() //nolint
return errors.New(token + " not found")
}
if err = tx.Commit(); err != nil {
return err
}
return nil
}
// GetExternalAPIUserForAccessTokenAndScope will determine if a specific token has access to perform a scoped action.
func GetExternalAPIUserForAccessTokenAndScope(token string, scope string) (*ExternalAPIUser, error) {
// This will split the scopes from comma separated to individual rows
// so we can efficiently find if a token supports a single scope.
// This is SQLite specific, so if we ever support other database
// backends we need to support other methods.
query := `SELECT
id,
scopes,
display_name,
display_color,
created_at,
last_used
FROM
user_access_tokens
INNER JOIN (
WITH RECURSIVE split(
id,
scopes,
display_name,
display_color,
created_at,
last_used,
disabled_at,
scope,
rest
) AS (
SELECT
id,
scopes,
display_name,
display_color,
created_at,
last_used,
disabled_at,
'',
scopes || ','
FROM
users AS u
UNION ALL
SELECT
id,
scopes,
display_name,
display_color,
created_at,
last_used,
disabled_at,
substr(rest, 0, instr(rest, ',')),
substr(rest, instr(rest, ',') + 1)
FROM
split
WHERE
rest <> ''
)
SELECT
id,
display_name,
display_color,
created_at,
last_used,
disabled_at,
scopes,
scope
FROM
split
WHERE
scope <> ''
) ON user_access_tokens.user_id = id
WHERE
disabled_at IS NULL
AND token = ?
AND scope = ?;`
row := _datastore.DB.QueryRow(query, token, scope)
integration, err := makeExternalAPIUserFromRow(row)
return integration, err
}
// GetIntegrationNameForAccessToken will return the integration name associated with a specific access token.
func GetIntegrationNameForAccessToken(token string) *string {
name, err := _datastore.GetQueries().GetUserDisplayNameByToken(context.Background(), token)
if err != nil {
return nil
}
return &name
}
// GetExternalAPIUser will return all API users with access tokens.
func GetExternalAPIUser() ([]ExternalAPIUser, error) { //nolint
query := "SELECT id, token, display_name, display_color, scopes, created_at, last_used FROM users, user_access_tokens WHERE user_access_tokens.user_id = id AND type IS 'API' AND disabled_at IS NULL"
rows, err := _datastore.DB.Query(query)
if err != nil {
return []ExternalAPIUser{}, err
}
defer rows.Close()
integrations, err := makeExternalAPIUsersFromRows(rows)
return integrations, err
}
// SetExternalAPIUserAccessTokenAsUsed will update the last used timestamp for a token.
func SetExternalAPIUserAccessTokenAsUsed(token string) error {
tx, err := _datastore.DB.Begin()
if err != nil {
return err
}
stmt, err := tx.Prepare("UPDATE users SET last_used = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)")
if err != nil {
return err
}
defer stmt.Close()
if _, err := stmt.Exec(token); err != nil {
return err
}
if err = tx.Commit(); err != nil {
return err
}
return nil
}
func makeExternalAPIUserFromRow(row *sql.Row) (*ExternalAPIUser, error) {
var id string
var displayName string
var displayColor int
var scopes string
var createdAt time.Time
var lastUsedAt *time.Time
err := row.Scan(&id, &scopes, &displayName, &displayColor, &createdAt, &lastUsedAt)
if err != nil {
log.Debugln("unable to convert row to api user", err)
return nil, err
}
integration := ExternalAPIUser{
ID: id,
DisplayName: displayName,
DisplayColor: displayColor,
CreatedAt: createdAt,
Scopes: strings.Split(scopes, ","),
LastUsedAt: lastUsedAt,
}
return &integration, nil
}
func makeExternalAPIUsersFromRows(rows *sql.Rows) ([]ExternalAPIUser, error) {
integrations := make([]ExternalAPIUser, 0)
for rows.Next() {
var id string
var accessToken string
var displayName string
var displayColor int
var scopes string
var createdAt time.Time
var lastUsedAt *time.Time
err := rows.Scan(&id, &accessToken, &displayName, &displayColor, &scopes, &createdAt, &lastUsedAt)
if err != nil {
log.Errorln(err)
return nil, err
}
integration := ExternalAPIUser{
ID: id,
AccessToken: accessToken,
DisplayName: displayName,
DisplayColor: displayColor,
CreatedAt: createdAt,
Scopes: strings.Split(scopes, ","),
LastUsedAt: lastUsedAt,
IsBot: true,
}
integrations = append(integrations, integration)
}
return integrations, nil
}
// HasValidScopes will verify that all the scopes provided are valid.
func HasValidScopes(scopes []string) bool {
for _, scope := range scopes {
_, foundInSlice := utils.FindInSlice(validAccessTokenScopes, scope)
if !foundInSlice {
return false
}
}
return true
}

473
core/user/user.go

@ -1,473 +0,0 @@ @@ -1,473 +0,0 @@
package user
import (
"context"
"database/sql"
"fmt"
"sort"
"strings"
"time"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/db"
"github.com/owncast/owncast/utils"
"github.com/pkg/errors"
"github.com/teris-io/shortid"
log "github.com/sirupsen/logrus"
)
var _datastore *data.Datastore
const (
moderatorScopeKey = "MODERATOR"
minSuggestedUsernamePoolLength = 10
)
// User represents a single chat user.
type User struct {
CreatedAt time.Time `json:"createdAt"`
DisabledAt *time.Time `json:"disabledAt,omitempty"`
NameChangedAt *time.Time `json:"nameChangedAt,omitempty"`
AuthenticatedAt *time.Time `json:"-"`
ID string `json:"id"`
DisplayName string `json:"displayName"`
PreviousNames []string `json:"previousNames"`
Scopes []string `json:"scopes,omitempty"`
DisplayColor int `json:"displayColor"`
IsBot bool `json:"isBot"`
Authenticated bool `json:"authenticated"`
}
// IsEnabled will return if this single user is enabled.
func (u *User) IsEnabled() bool {
return u.DisabledAt == nil
}
// IsModerator will return if the user has moderation privileges.
func (u *User) IsModerator() bool {
_, hasModerationScope := utils.FindInSlice(u.Scopes, moderatorScopeKey)
return hasModerationScope
}
// SetupUsers will perform the initial initialization of the user package.
func SetupUsers() {
_datastore = data.GetDatastore()
}
func generateDisplayName() string {
suggestedUsernamesList := data.GetSuggestedUsernamesList()
if len(suggestedUsernamesList) >= minSuggestedUsernamePoolLength {
index := utils.RandomIndex(len(suggestedUsernamesList))
return suggestedUsernamesList[index]
} else {
return utils.GeneratePhrase()
}
}
// CreateAnonymousUser will create a new anonymous user with the provided display name.
func CreateAnonymousUser(displayName string) (*User, string, error) {
// Try to assign a name that was requested.
if displayName != "" {
// If name isn't available then generate a random one.
if available, _ := IsDisplayNameAvailable(displayName); !available {
displayName = generateDisplayName()
}
} else {
displayName = generateDisplayName()
}
displayColor := utils.GenerateRandomDisplayColor(config.MaxUserColor)
id := shortid.MustGenerate()
user := &User{
ID: id,
DisplayName: displayName,
DisplayColor: displayColor,
CreatedAt: time.Now(),
}
// Create new user.
if err := create(user); err != nil {
return nil, "", err
}
// Assign it an access token.
accessToken, err := utils.GenerateAccessToken()
if err != nil {
log.Errorln("Unable to create access token for new user")
return nil, "", err
}
if err := addAccessTokenForUser(accessToken, id); err != nil {
return nil, "", errors.Wrap(err, "unable to save access token for new user")
}
return user, accessToken, nil
}
// IsDisplayNameAvailable will check if the proposed name is available for use.
func IsDisplayNameAvailable(displayName string) (bool, error) {
if available, err := _datastore.GetQueries().IsDisplayNameAvailable(context.Background(), displayName); err != nil {
return false, errors.Wrap(err, "unable to check if display name is available")
} else if available != 0 {
return false, nil
}
return true, nil
}
// ChangeUsername will change the user associated to userID from one display name to another.
func ChangeUsername(userID string, username string) error {
_datastore.DbLock.Lock()
defer _datastore.DbLock.Unlock()
if err := _datastore.GetQueries().ChangeDisplayName(context.Background(), db.ChangeDisplayNameParams{
DisplayName: username,
ID: userID,
PreviousNames: sql.NullString{String: fmt.Sprintf(",%s", username), Valid: true},
NamechangedAt: sql.NullTime{Time: time.Now(), Valid: true},
}); err != nil {
return errors.Wrap(err, "unable to change display name")
}
return nil
}
// ChangeUserColor will change the user associated to userID from one display name to another.
func ChangeUserColor(userID string, color int) error {
_datastore.DbLock.Lock()
defer _datastore.DbLock.Unlock()
if err := _datastore.GetQueries().ChangeDisplayColor(context.Background(), db.ChangeDisplayColorParams{
DisplayColor: int32(color),
ID: userID,
}); err != nil {
return errors.Wrap(err, "unable to change display color")
}
return nil
}
func addAccessTokenForUser(accessToken, userID string) error {
return _datastore.GetQueries().AddAccessTokenForUser(context.Background(), db.AddAccessTokenForUserParams{
Token: accessToken,
UserID: userID,
})
}
func create(user *User) error {
_datastore.DbLock.Lock()
defer _datastore.DbLock.Unlock()
tx, err := _datastore.DB.Begin()
if err != nil {
log.Debugln(err)
}
defer func() {
_ = tx.Rollback()
}()
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, previous_names, created_at) values(?, ?, ?, ?, ?)")
if err != nil {
log.Debugln(err)
}
defer stmt.Close()
_, err = stmt.Exec(user.ID, user.DisplayName, user.DisplayColor, user.DisplayName, user.CreatedAt)
if err != nil {
log.Errorln("error creating new user", err)
return err
}
return tx.Commit()
}
// SetEnabled will set the enabled status of a single user by ID.
func SetEnabled(userID string, enabled bool) error {
_datastore.DbLock.Lock()
defer _datastore.DbLock.Unlock()
tx, err := _datastore.DB.Begin()
if err != nil {
return err
}
defer tx.Rollback() //nolint
var stmt *sql.Stmt
if !enabled {
stmt, err = tx.Prepare("UPDATE users SET disabled_at=DATETIME('now', 'localtime') WHERE id IS ?")
} else {
stmt, err = tx.Prepare("UPDATE users SET disabled_at=null WHERE id IS ?")
}
if err != nil {
return err
}
defer stmt.Close()
if _, err := stmt.Exec(userID); err != nil {
return err
}
return tx.Commit()
}
// GetUserByToken will return a user by an access token.
func GetUserByToken(token string) *User {
u, err := _datastore.GetQueries().GetUserByAccessToken(context.Background(), token)
if err != nil {
return nil
}
var scopes []string
if u.Scopes.Valid {
scopes = strings.Split(u.Scopes.String, ",")
}
var disabledAt *time.Time
if u.DisabledAt.Valid {
disabledAt = &u.DisabledAt.Time
}
var authenticatedAt *time.Time
if u.AuthenticatedAt.Valid {
authenticatedAt = &u.AuthenticatedAt.Time
}
return &User{
ID: u.ID,
DisplayName: u.DisplayName,
DisplayColor: int(u.DisplayColor),
CreatedAt: u.CreatedAt.Time,
DisabledAt: disabledAt,
PreviousNames: strings.Split(u.PreviousNames.String, ","),
NameChangedAt: &u.NamechangedAt.Time,
AuthenticatedAt: authenticatedAt,
Authenticated: authenticatedAt != nil,
Scopes: scopes,
}
}
// SetAccessTokenToOwner will reassign an access token to be owned by a
// different user. Used for logging in with external auth.
func SetAccessTokenToOwner(token, userID string) error {
return _datastore.GetQueries().SetAccessTokenToOwner(context.Background(), db.SetAccessTokenToOwnerParams{
UserID: userID,
Token: token,
})
}
// SetUserAsAuthenticated will mark that a user has been authenticated
// in some way.
func SetUserAsAuthenticated(userID string) error {
return errors.Wrap(_datastore.GetQueries().SetUserAsAuthenticated(context.Background(), userID), "unable to set user as authenticated")
}
// SetModerator will add or remove moderator status for a single user by ID.
func SetModerator(userID string, isModerator bool) error {
if isModerator {
return addScopeToUser(userID, moderatorScopeKey)
}
return removeScopeFromUser(userID, moderatorScopeKey)
}
func addScopeToUser(userID string, scope string) error {
u := GetUserByID(userID)
if u == nil {
return errors.New("user not found when modifying scope")
}
scopesString := u.Scopes
scopes := utils.StringSliceToMap(scopesString)
scopes[scope] = true
scopesSlice := utils.StringMapKeys(scopes)
return setScopesOnUser(userID, scopesSlice)
}
func removeScopeFromUser(userID string, scope string) error {
u := GetUserByID(userID)
scopesString := u.Scopes
scopes := utils.StringSliceToMap(scopesString)
delete(scopes, scope)
scopesSlice := utils.StringMapKeys(scopes)
return setScopesOnUser(userID, scopesSlice)
}
func setScopesOnUser(userID string, scopes []string) error {
_datastore.DbLock.Lock()
defer _datastore.DbLock.Unlock()
tx, err := _datastore.DB.Begin()
if err != nil {
return err
}
defer tx.Rollback() //nolint
scopesSliceString := strings.TrimSpace(strings.Join(scopes, ","))
stmt, err := tx.Prepare("UPDATE users SET scopes=? WHERE id IS ?")
if err != nil {
return err
}
defer stmt.Close()
var val *string
if scopesSliceString == "" {
val = nil
} else {
val = &scopesSliceString
}
if _, err := stmt.Exec(val, userID); err != nil {
return err
}
return tx.Commit()
}
// GetUserByID will return a user by a user ID.
func GetUserByID(id string) *User {
_datastore.DbLock.Lock()
defer _datastore.DbLock.Unlock()
query := "SELECT id, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, scopes FROM users WHERE id = ?"
row := _datastore.DB.QueryRow(query, id)
if row == nil {
log.Errorln(row)
return nil
}
return getUserFromRow(row)
}
// GetDisabledUsers will return back all the currently disabled users that are not API users.
func GetDisabledUsers() []*User {
query := "SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM users WHERE disabled_at IS NOT NULL AND type IS NOT 'API'"
rows, err := _datastore.DB.Query(query)
if err != nil {
log.Errorln(err)
return nil
}
defer rows.Close()
users := getUsersFromRows(rows)
sort.Slice(users, func(i, j int) bool {
return users[i].DisabledAt.Before(*users[j].DisabledAt)
})
return users
}
// GetModeratorUsers will return a list of users with moderator access.
func GetModeratorUsers() []*User {
query := `SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM (
WITH RECURSIVE split(id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope, rest) AS (
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, '', scopes || ',' FROM users
UNION ALL
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at,
substr(rest, 0, instr(rest, ',')),
substr(rest, instr(rest, ',')+1)
FROM split
WHERE rest <> '')
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope
FROM split
WHERE scope <> ''
ORDER BY created_at
) AS token WHERE token.scope = ?`
rows, err := _datastore.DB.Query(query, moderatorScopeKey)
if err != nil {
log.Errorln(err)
return nil
}
defer rows.Close()
users := getUsersFromRows(rows)
return users
}
func getUsersFromRows(rows *sql.Rows) []*User {
users := make([]*User, 0)
for rows.Next() {
var id string
var displayName string
var displayColor int
var createdAt time.Time
var disabledAt *time.Time
var previousUsernames string
var userNameChangedAt *time.Time
var scopesString *string
if err := rows.Scan(&id, &displayName, &scopesString, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt); err != nil {
log.Errorln("error creating collection of users from results", err)
return nil
}
var scopes []string
if scopesString != nil {
scopes = strings.Split(*scopesString, ",")
}
user := &User{
ID: id,
DisplayName: displayName,
DisplayColor: displayColor,
CreatedAt: createdAt,
DisabledAt: disabledAt,
PreviousNames: strings.Split(previousUsernames, ","),
NameChangedAt: userNameChangedAt,
Scopes: scopes,
}
users = append(users, user)
}
sort.Slice(users, func(i, j int) bool {
return users[i].CreatedAt.Before(users[j].CreatedAt)
})
return users
}
func getUserFromRow(row *sql.Row) *User {
var id string
var displayName string
var displayColor int
var createdAt time.Time
var disabledAt *time.Time
var previousUsernames string
var userNameChangedAt *time.Time
var scopesString *string
if err := row.Scan(&id, &displayName, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt, &scopesString); err != nil {
return nil
}
var scopes []string
if scopesString != nil {
scopes = strings.Split(*scopesString, ",")
}
return &User{
ID: id,
DisplayName: displayName,
DisplayColor: displayColor,
CreatedAt: createdAt,
DisabledAt: disabledAt,
PreviousNames: strings.Split(previousUsernames, ","),
NameChangedAt: userNameChangedAt,
Scopes: scopes,
}
}

28
core/webhooks/stream.go

@ -1,28 +0,0 @@ @@ -1,28 +0,0 @@
package webhooks
import (
"time"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/teris-io/shortid"
)
// SendStreamStatusEvent will send all webhook destinations the current stream status.
func SendStreamStatusEvent(eventType models.EventType) {
sendStreamStatusEvent(eventType, shortid.MustGenerate(), time.Now())
}
func sendStreamStatusEvent(eventType models.EventType, id string, timestamp time.Time) {
SendEventToWebhooks(WebhookEvent{
Type: eventType,
EventData: map[string]interface{}{
"id": id,
"name": data.GetServerName(),
"summary": data.GetServerSummary(),
"streamTitle": data.GetStreamTitle(),
"status": getStatus(),
"timestamp": timestamp,
},
})
}

44
core/webhooks/webhooks.go

@ -1,44 +0,0 @@ @@ -1,44 +0,0 @@
package webhooks
import (
"sync"
"time"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/models"
)
// WebhookEvent represents an event sent as a webhook.
type WebhookEvent struct {
EventData interface{} `json:"eventData,omitempty"`
Type models.EventType `json:"type"` // messageSent | userJoined | userNameChange
}
// WebhookChatMessage represents a single chat message sent as a webhook payload.
type WebhookChatMessage struct {
User *user.User `json:"user,omitempty"`
Timestamp *time.Time `json:"timestamp,omitempty"`
Body string `json:"body,omitempty"`
RawBody string `json:"rawBody,omitempty"`
ID string `json:"id,omitempty"`
ClientID uint `json:"clientId,omitempty"`
Visible bool `json:"visible"`
}
// SendEventToWebhooks will send a single webhook event to all webhook destinations.
func SendEventToWebhooks(payload WebhookEvent) {
sendEventToWebhooks(payload, nil)
}
func sendEventToWebhooks(payload WebhookEvent, wg *sync.WaitGroup) {
webhooks := data.GetWebhooksForEvent(payload.Type)
for _, webhook := range webhooks {
// Use wg to track the number of notifications to be sent.
if wg != nil {
wg.Add(1)
}
addToQueue(webhook, payload, wg)
}
}

12
logging/logging.go

@ -30,20 +30,20 @@ type OCLogger struct { @@ -30,20 +30,20 @@ type OCLogger struct {
var Logger *OCLogger
// Setup configures our custom logging destinations.
func Setup(enableDebugOptions bool, enableVerboseLogging bool) {
func Setup(enableDebugOptions bool, enableVerboseLogging bool, logDirectory string) {
// Create the logging directory if needed
loggingDirectory := filepath.Dir(getLogFilePath())
loggingDirectory := filepath.Dir(logDirectory)
if !utils.DoesFileExists(loggingDirectory) {
if err := os.Mkdir(loggingDirectory, 0700); err != nil {
if err := os.Mkdir(loggingDirectory, 0o700); err != nil {
logger.Errorln("unable to create logs directory", loggingDirectory, err)
}
}
// Write logs to a file
path := getLogFilePath()
logFile := filepath.Join(logDirectory, "owncast.log")
writer, _ := rotatelogs.New(
path+".%Y%m%d%H%M",
rotatelogs.WithLinkName(path),
logFile+".%Y%m%d%H%M",
rotatelogs.WithLinkName(logFile),
rotatelogs.WithMaxAge(time.Duration(86400)*time.Second),
rotatelogs.WithRotationTime(time.Duration(604800)*time.Second),
)

16
logging/paths.go

@ -1,16 +0,0 @@ @@ -1,16 +0,0 @@
package logging
import (
"path/filepath"
"github.com/owncast/owncast/config"
)
// GetTranscoderLogFilePath returns the logging path for the transcoder log output.
func GetTranscoderLogFilePath() string {
return filepath.Join(config.LogDirectory, "transcoder.log")
}
func getLogFilePath() string {
return filepath.Join(config.LogDirectory, "owncast.log")
}

181
main.go

@ -1,166 +1,43 @@ @@ -1,166 +1,43 @@
package main
import (
"flag"
"os"
"strconv"
import "github.com/owncast/owncast/cmd"
"github.com/owncast/owncast/logging"
log "github.com/sirupsen/logrus"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/metrics"
"github.com/owncast/owncast/router"
"github.com/owncast/owncast/utils"
)
var (
dbFile = flag.String("database", "", "Path to the database file.")
logDirectory = flag.String("logdir", "", "Directory where logs will be written to")
backupDirectory = flag.String("backupdir", "", "Directory where backups will be written to")
enableDebugOptions = flag.Bool("enableDebugFeatures", false, "Enable additional debugging options.")
enableVerboseLogging = flag.Bool("enableVerboseLogging", false, "Enable additional logging.")
restoreDatabaseFile = flag.String("restoreDatabase", "", "Restore an Owncast database backup")
newAdminPassword = flag.String("adminpassword", "", "Set your admin password")
newStreamKey = flag.String("streamkey", "", "Set a temporary stream key for this session")
webServerPortOverride = flag.String("webserverport", "", "Force the web server to listen on a specific port")
webServerIPOverride = flag.String("webserverip", "", "Force web server to listen on this IP address")
rtmpPortOverride = flag.Int("rtmpport", 0, "Set listen port for the RTMP server")
)
// nolint:cyclop
func main() {
flag.Parse()
if *logDirectory != "" {
config.LogDirectory = *logDirectory
}
if *backupDirectory != "" {
config.BackupDirectory = *backupDirectory
}
// Create the data directory if needed
if !utils.DoesFileExists("data") {
if err := os.Mkdir("./data", 0o700); err != nil {
log.Fatalln("Cannot create data directory", err)
}
}
// Migrate old (pre 0.1.0) emoji to new location if they exist.
utils.MigrateCustomEmojiLocations()
// Otherwise save the default emoji to the data directory.
if err := data.SetupEmojiDirectory(); err != nil {
log.Fatalln("Cannot set up emoji directory", err)
}
// Recreate the temp dir
if utils.DoesFileExists(config.TempDir) {
err := os.RemoveAll(config.TempDir)
if err != nil {
log.Fatalln("Unable to remove temp dir! Check permissions.", config.TempDir, err)
}
}
if err := os.Mkdir(config.TempDir, 0o700); err != nil {
log.Fatalln("Unable to create temp dir!", err)
}
configureLogging(*enableDebugOptions, *enableVerboseLogging)
log.Infoln(config.GetReleaseString())
// Allows a user to restore a specific database backup
if *restoreDatabaseFile != "" {
databaseFile := config.DatabaseFilePath
if *dbFile != "" {
databaseFile = *dbFile
}
if err := utils.Restore(*restoreDatabaseFile, databaseFile); err != nil {
log.Fatalln(err)
}
log.Println("Database has been restored. Restart Owncast.")
log.Exit(0)
}
config.EnableDebugFeatures = *enableDebugOptions
if *dbFile != "" {
config.DatabaseFilePath = *dbFile
}
if err := data.SetupPersistence(config.DatabaseFilePath); err != nil {
log.Fatalln("failed to open database", err)
}
app := &cmd.Application{}
app.Start()
}
handleCommandLineFlags()
// var configRepository = configrepository.Get()
// starts the core
if err := core.Start(); err != nil {
log.Fatalln("failed to start the core package", err)
}
// // nolint:cyclop
// func main() {
// flag.Parse()
go metrics.Start(core.GetStatus)
// config = configservice.NewConfig()
if err := router.Start(); err != nil {
log.Fatalln("failed to start/run the router", err)
}
}
// // Otherwise save the default emoji to the data directory.
// if err := data.SetupEmojiDirectory(); err != nil {
// log.Fatalln("Cannot set up emoji directory", err)
// }
func handleCommandLineFlags() {
if *newAdminPassword != "" {
if err := data.SetAdminPassword(*newAdminPassword); err != nil {
log.Errorln("Error setting your admin password.", err)
log.Exit(1)
} else {
log.Infoln("Admin password changed")
}
}
// if err := data.SetupPersistence(config.DatabaseFilePath); err != nil {
// log.Fatalln("failed to open database", err)
// }
if *newStreamKey != "" {
log.Println("Temporary stream key is set for this session.")
config.TemporaryStreamKey = *newStreamKey
}
// handleCommandLineFlags()
// Set the web server port
if *webServerPortOverride != "" {
portNumber, err := strconv.Atoi(*webServerPortOverride)
if err != nil {
log.Warnln(err)
return
}
// // starts the core
// if err := core.Start(); err != nil {
// log.Fatalln("failed to start the core package", err)
// }
log.Println("Saving new web server port number to", portNumber)
if err := data.SetHTTPPortNumber(float64(portNumber)); err != nil {
log.Errorln(err)
}
}
config.WebServerPort = data.GetHTTPPortNumber()
// go metrics.Start(core.GetStatus)
// Set the web server ip
if *webServerIPOverride != "" {
log.Println("Saving new web server listen IP address to", *webServerIPOverride)
if err := data.SetHTTPListenAddress(*webServerIPOverride); err != nil {
log.Errorln(err)
}
}
config.WebServerIP = data.GetHTTPListenAddress()
// webserver := webserver.New()
// if err := webserver.Start(config.WebServerIP, config.WebServerPort); err != nil {
// log.Fatalln("failed to start/run the web server", err)
// }
// }
// Set the rtmp server port
if *rtmpPortOverride > 0 {
log.Println("Saving new RTMP server port number to", *rtmpPortOverride)
if err := data.SetRTMPPortNumber(float64(*rtmpPortOverride)); err != nil {
log.Errorln(err)
}
}
}
func configureLogging(enableDebugFeatures bool, enableVerboseLogging bool) {
logging.Setup(enableDebugFeatures, enableVerboseLogging)
log.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
})
}
// func handleCommandLineFlags() {
// }

59
metrics/hardware.go

@ -1,59 +0,0 @@ @@ -1,59 +0,0 @@
package metrics
import (
"time"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/mem"
log "github.com/sirupsen/logrus"
)
// Max number of metrics we want to keep.
const maxCollectionValues = 300
func collectCPUUtilization() {
if len(metrics.CPUUtilizations) > maxCollectionValues {
metrics.CPUUtilizations = metrics.CPUUtilizations[1:]
}
v, err := cpu.Percent(0, false)
if err != nil {
log.Errorln(err)
return
}
// Default to zero but try to use the cumulative values of all the CPUs
// if values exist.
value := 0.0
if len(v) > 0 {
value = v[0]
}
metricValue := TimestampedValue{time.Now(), value}
metrics.CPUUtilizations = append(metrics.CPUUtilizations, metricValue)
cpuUsage.Set(metricValue.Value)
}
func collectRAMUtilization() {
if len(metrics.RAMUtilizations) > maxCollectionValues {
metrics.RAMUtilizations = metrics.RAMUtilizations[1:]
}
memoryUsage, _ := mem.VirtualMemory()
metricValue := TimestampedValue{time.Now(), memoryUsage.UsedPercent}
metrics.RAMUtilizations = append(metrics.RAMUtilizations, metricValue)
}
func collectDiskUtilization() {
path := "./"
diskUse, _ := disk.Usage(path)
if len(metrics.DiskUtilizations) > maxCollectionValues {
metrics.DiskUtilizations = metrics.DiskUtilizations[1:]
}
metricValue := TimestampedValue{time.Now(), diskUse.UsedPercent}
metrics.DiskUtilizations = append(metrics.DiskUtilizations, metricValue)
}

101
metrics/metrics.go

@ -1,101 +0,0 @@ @@ -1,101 +0,0 @@
package metrics
import (
"sync"
"time"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
)
// How often we poll for updates.
const hardwareMetricsPollingInterval = 2 * time.Minute
const playbackMetricsPollingInterval = 2 * time.Minute
const (
// How often we poll for updates.
viewerMetricsPollingInterval = 2 * time.Minute
activeChatClientCountKey = "chat_client_count"
activeViewerCountKey = "viewer_count"
)
// CollectedMetrics stores different collected + timestamped values.
type CollectedMetrics struct {
streamHealthOverview *models.StreamHealthOverview
medianSegmentDownloadSeconds []TimestampedValue `json:"-"`
maximumSegmentDownloadSeconds []TimestampedValue `json:"-"`
DiskUtilizations []TimestampedValue `json:"disk"`
errorCount []TimestampedValue `json:"-"`
lowestBitrate []TimestampedValue `json:"-"`
medianBitrate []TimestampedValue `json:"-"`
RAMUtilizations []TimestampedValue `json:"memory"`
CPUUtilizations []TimestampedValue `json:"cpu"`
highestBitrate []TimestampedValue `json:"-"`
minimumSegmentDownloadSeconds []TimestampedValue `json:"-"`
minimumLatency []TimestampedValue `json:"-"`
maximumLatency []TimestampedValue `json:"-"`
medianLatency []TimestampedValue `json:"-"`
qualityVariantChanges []TimestampedValue `json:"-"`
m sync.Mutex `json:"-"`
}
// Metrics is the shared Metrics instance.
var metrics *CollectedMetrics
var _getStatus func() models.Status
// Start will begin the metrics collection and alerting.
func Start(getStatus func() models.Status) {
_getStatus = getStatus
host := data.GetServerURL()
if host == "" {
host = "unknown"
}
labels = map[string]string{
"version": config.VersionNumber,
"host": host,
}
setupPrometheusCollectors()
metrics = new(CollectedMetrics)
go startViewerCollectionMetrics()
go func() {
for range time.Tick(hardwareMetricsPollingInterval) {
handlePolling()
}
}()
go func() {
for range time.Tick(playbackMetricsPollingInterval) {
handlePlaybackPolling()
}
}()
}
func handlePolling() {
metrics.m.Lock()
defer metrics.m.Unlock()
// Collect hardware stats
collectCPUUtilization()
collectRAMUtilization()
collectDiskUtilization()
// Alerting
handleAlerting()
}
// GetMetrics will return the collected metrics.
func GetMetrics() *CollectedMetrics {
return metrics
}

337
metrics/playback.go

@ -1,337 +0,0 @@ @@ -1,337 +0,0 @@
package metrics
import (
"math"
"time"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/utils"
)
// Playback error counts reported since the last time we collected metrics.
var (
windowedErrorCounts = map[string]float64{}
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.
if _getStatus().Online {
generateStreamHealthOverview()
}
collectPlaybackErrorCount()
collectLatencyValues()
collectSegmentDownloadDuration()
collectLowestBandwidth()
collectQualityVariantChanges()
}
// RegisterPlaybackErrorCount will add to the windowed playback error count.
func RegisterPlaybackErrorCount(clientID string, count float64) {
metrics.m.Lock()
defer metrics.m.Unlock()
windowedErrorCounts[clientID] = count
// windowedErrorCounts = append(windowedErrorCounts, count)
}
// RegisterQualityVariantChangesCount will add to the windowed quality variant
// change count.
func RegisterQualityVariantChangesCount(clientID string, count float64) {
metrics.m.Lock()
defer metrics.m.Unlock()
windowedQualityVariantChanges[clientID] = count
}
// RegisterPlayerBandwidth will add to the windowed playback bandwidth.
func RegisterPlayerBandwidth(clientID string, kbps float64) {
metrics.m.Lock()
defer metrics.m.Unlock()
windowedBandwidths[clientID] = kbps
}
// RegisterPlayerLatency will add to the windowed player latency values.
func RegisterPlayerLatency(clientID string, seconds float64) {
metrics.m.Lock()
defer metrics.m.Unlock()
windowedLatencies[clientID] = seconds
}
// RegisterPlayerSegmentDownloadDuration will add to the windowed player segment
// download duration values.
func RegisterPlayerSegmentDownloadDuration(clientID string, seconds float64) {
metrics.m.Lock()
defer metrics.m.Unlock()
windowedDownloadDurations[clientID] = seconds
}
// collectPlaybackErrorCount will take all of the error counts each individual
// 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
// horrible for everyone.
func collectPlaybackErrorCount() {
valueSlice := utils.Float64MapToSlice(windowedErrorCounts)
count := utils.Sum(valueSlice)
windowedErrorCounts = map[string]float64{}
metrics.errorCount = append(metrics.errorCount, TimestampedValue{
Time: time.Now(),
Value: count,
})
if len(metrics.errorCount) > maxCollectionValues {
metrics.errorCount = metrics.errorCount[1:]
}
// Save to Prometheus collector.
playbackErrorCount.Set(count)
}
func collectSegmentDownloadDuration() {
median := 0.0
max := 0.0
min := 0.0
valueSlice := utils.Float64MapToSlice(windowedDownloadDurations)
if len(valueSlice) > 0 {
median = utils.Median(valueSlice)
min, max = utils.MinMax(valueSlice)
windowedDownloadDurations = map[string]float64{}
}
metrics.medianSegmentDownloadSeconds = append(metrics.medianSegmentDownloadSeconds, TimestampedValue{
Time: time.Now(),
Value: median,
})
if len(metrics.medianSegmentDownloadSeconds) > maxCollectionValues {
metrics.medianSegmentDownloadSeconds = metrics.medianSegmentDownloadSeconds[1:]
}
metrics.minimumSegmentDownloadSeconds = append(metrics.minimumSegmentDownloadSeconds, TimestampedValue{
Time: time.Now(),
Value: min,
})
if len(metrics.minimumSegmentDownloadSeconds) > maxCollectionValues {
metrics.minimumSegmentDownloadSeconds = metrics.minimumSegmentDownloadSeconds[1:]
}
metrics.maximumSegmentDownloadSeconds = append(metrics.maximumSegmentDownloadSeconds, TimestampedValue{
Time: time.Now(),
Value: max,
})
if len(metrics.maximumSegmentDownloadSeconds) > maxCollectionValues {
metrics.maximumSegmentDownloadSeconds = metrics.maximumSegmentDownloadSeconds[1:]
}
}
// GetMedianDownloadDurationsOverTime will return a window of durations errors over time.
func GetMedianDownloadDurationsOverTime() []TimestampedValue {
return metrics.medianSegmentDownloadSeconds
}
// GetMaximumDownloadDurationsOverTime will return a maximum durations errors over time.
func GetMaximumDownloadDurationsOverTime() []TimestampedValue {
return metrics.maximumSegmentDownloadSeconds
}
// GetMinimumDownloadDurationsOverTime will return a maximum durations errors over time.
func GetMinimumDownloadDurationsOverTime() []TimestampedValue {
return metrics.minimumSegmentDownloadSeconds
}
// GetPlaybackErrorCountOverTime will return a window of playback errors over time.
func GetPlaybackErrorCountOverTime() []TimestampedValue {
return metrics.errorCount
}
func collectLatencyValues() {
median := 0.0
min := 0.0
max := 0.0
valueSlice := utils.Float64MapToSlice(windowedLatencies)
windowedLatencies = map[string]float64{}
if len(valueSlice) > 0 {
median = utils.Median(valueSlice)
min, max = utils.MinMax(valueSlice)
windowedLatencies = map[string]float64{}
}
metrics.medianLatency = append(metrics.medianLatency, TimestampedValue{
Time: time.Now(),
Value: median,
})
if len(metrics.medianLatency) > maxCollectionValues {
metrics.medianLatency = metrics.medianLatency[1:]
}
metrics.minimumLatency = append(metrics.minimumLatency, TimestampedValue{
Time: time.Now(),
Value: min,
})
if len(metrics.minimumLatency) > maxCollectionValues {
metrics.minimumLatency = metrics.minimumLatency[1:]
}
metrics.maximumLatency = append(metrics.maximumLatency, TimestampedValue{
Time: time.Now(),
Value: max,
})
if len(metrics.maximumLatency) > maxCollectionValues {
metrics.maximumLatency = metrics.maximumLatency[1:]
}
}
// GetMedianLatencyOverTime will return the median latency values over time.
func GetMedianLatencyOverTime() []TimestampedValue {
if len(metrics.medianLatency) == 0 {
return []TimestampedValue{}
}
return metrics.medianLatency
}
// GetMinimumLatencyOverTime will return the min latency values over time.
func GetMinimumLatencyOverTime() []TimestampedValue {
if len(metrics.minimumLatency) == 0 {
return []TimestampedValue{}
}
return metrics.minimumLatency
}
// GetMaximumLatencyOverTime will return the max latency values over time.
func GetMaximumLatencyOverTime() []TimestampedValue {
if len(metrics.maximumLatency) == 0 {
return []TimestampedValue{}
}
return metrics.maximumLatency
}
// collectLowestBandwidth will collect the bandwidth currently collected
// so we can report to the streamer the worst possible streaming condition
// being experienced.
func collectLowestBandwidth() {
min := 0.0
median := 0.0
max := 0.0
valueSlice := utils.Float64MapToSlice(windowedBandwidths)
if len(windowedBandwidths) > 0 {
min, max = utils.MinMax(valueSlice)
min = math.Round(min)
max = math.Round(max)
median = utils.Median(valueSlice)
windowedBandwidths = map[string]float64{}
}
metrics.lowestBitrate = append(metrics.lowestBitrate, TimestampedValue{
Time: time.Now(),
Value: math.Round(min),
})
if len(metrics.lowestBitrate) > maxCollectionValues {
metrics.lowestBitrate = metrics.lowestBitrate[1:]
}
metrics.medianBitrate = append(metrics.medianBitrate, TimestampedValue{
Time: time.Now(),
Value: math.Round(median),
})
if len(metrics.medianBitrate) > maxCollectionValues {
metrics.medianBitrate = metrics.medianBitrate[1:]
}
metrics.highestBitrate = append(metrics.highestBitrate, TimestampedValue{
Time: time.Now(),
Value: math.Round(max),
})
if len(metrics.highestBitrate) > maxCollectionValues {
metrics.highestBitrate = metrics.highestBitrate[1:]
}
}
// GetSlowestDownloadRateOverTime will return the collected lowest bandwidth values
// over time.
func GetSlowestDownloadRateOverTime() []TimestampedValue {
if len(metrics.lowestBitrate) == 0 {
return []TimestampedValue{}
}
return metrics.lowestBitrate
}
// GetMedianDownloadRateOverTime will return the collected median bandwidth values.
func GetMedianDownloadRateOverTime() []TimestampedValue {
if len(metrics.medianBitrate) == 0 {
return []TimestampedValue{}
}
return metrics.medianBitrate
}
// GetMaximumDownloadRateOverTime will return the collected maximum bandwidth values.
func GetMaximumDownloadRateOverTime() []TimestampedValue {
if len(metrics.maximumLatency) == 0 {
return []TimestampedValue{}
}
return metrics.maximumLatency
}
// GetMinimumDownloadRateOverTime will return the collected minimum bandwidth values.
func GetMinimumDownloadRateOverTime() []TimestampedValue {
if len(metrics.minimumLatency) == 0 {
return []TimestampedValue{}
}
return metrics.minimumLatency
}
// GetMaxDownloadRateOverTime will return the collected highest bandwidth values.
func GetMaxDownloadRateOverTime() []TimestampedValue {
if len(metrics.highestBitrate) == 0 {
return []TimestampedValue{}
}
return metrics.highestBitrate
}
func collectQualityVariantChanges() {
valueSlice := utils.Float64MapToSlice(windowedQualityVariantChanges)
count := utils.Sum(valueSlice)
windowedQualityVariantChanges = map[string]float64{}
metrics.qualityVariantChanges = append(metrics.qualityVariantChanges, TimestampedValue{
Time: time.Now(),
Value: count,
})
}
// GetQualityVariantChangesOverTime will return the collected quality variant
// changes.
func GetQualityVariantChangesOverTime() []TimestampedValue {
return metrics.qualityVariantChanges
}
// GetPlaybackMetricsRepresentation returns what percentage of all known players
// the metrics represent.
func GetPlaybackMetricsRepresentation() int {
totalPlayerCount := len(core.GetActiveViewers())
representation := utils.IntPercentage(len(windowedBandwidths), totalPlayerCount)
return representation
}

2
core/chat/events/actionEvent.go → models/actionEvent.go

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
package events
package models
// ActionEvent represents an action that took place, not a chat message.
type ActionEvent struct {

8
auth/auth.go → models/auth.go

@ -1,11 +1,11 @@ @@ -1,11 +1,11 @@
package auth
package models
// Type represents a form of authentication.
type Type string
type AuthType string
// The different auth types we support.
const (
// IndieAuth https://indieauth.spec.indieweb.org/.
IndieAuth Type = "indieauth"
Fediverse Type = "fediverse"
IndieAuth AuthType = "indieauth"
Fediverse AuthType = "fediverse"
)

12
models/chatAccessScopes.go

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
package models
const (
// ScopeCanSendChatMessages will allow sending chat messages as itself.
ScopeCanSendChatMessages = "CAN_SEND_MESSAGES"
// ScopeCanSendSystemMessages will allow sending chat messages as the system.
ScopeCanSendSystemMessages = "CAN_SEND_SYSTEM_MESSAGES"
// ScopeHasAdminAccess will allow performing administrative actions on the server.
ScopeHasAdminAccess = "HAS_ADMIN_ACCESS"
ModeratorScopeKey = "MODERATOR"
)

4
core/chat/events/eventtype.go → models/chatEventTypes.go

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
package events
package models
// EventType is the type of a websocket event.
type EventType = string
@ -14,6 +14,8 @@ const ( @@ -14,6 +14,8 @@ const (
UserColorChanged EventType = "COLOR_CHANGE"
// VisibiltyUpdate is the event sent when a chat message's visibility changes.
VisibiltyUpdate EventType = "VISIBILITY-UPDATE"
// VisibiltyToggled is the event sent when a chat message's visibility changes.
VisibiltyToggled EventType = "VISIBILITY-UPDATE"
// PING is a ping message.
PING EventType = "PING"
// PONG is a pong message.

2
models/client.go

@ -4,7 +4,7 @@ import ( @@ -4,7 +4,7 @@ import (
"net/http"
"time"
"github.com/owncast/owncast/geoip"
"github.com/owncast/owncast/services/geoip"
"github.com/owncast/owncast/utils"
)

67
models/configEntry.go

@ -0,0 +1,67 @@ @@ -0,0 +1,67 @@
package models
import (
"bytes"
"encoding/gob"
)
// ConfigEntry is the actual object saved to the database.
// The Value is encoded using encoding/gob.
type ConfigEntry struct {
Key string
Value interface{}
}
// GetStringSlice will return the value as a string slice.
func (c *ConfigEntry) GetStringSlice() ([]string, error) {
decoder := c.GetDecoder()
var result []string
err := decoder.Decode(&result)
return result, err
}
// GetStringMap will return the value as a string map.
func (c *ConfigEntry) GetStringMap() (map[string]string, error) {
decoder := c.GetDecoder()
var result map[string]string
err := decoder.Decode(&result)
return result, err
}
// GetString will return the value as a string.
func (c *ConfigEntry) GetString() (string, error) {
decoder := c.GetDecoder()
var result string
err := decoder.Decode(&result)
return result, err
}
// GetNumber will return the value as a float64.
func (c *ConfigEntry) GetNumber() (float64, error) {
decoder := c.GetDecoder()
var result float64
err := decoder.Decode(&result)
return result, err
}
// GetBool will return the value as a bool.
func (c *ConfigEntry) GetBool() (bool, error) {
decoder := c.GetDecoder()
var result bool
err := decoder.Decode(&result)
return result, err
}
// GetObject will return the value as an object.
func (c *ConfigEntry) GetObject(result interface{}) error {
decoder := c.GetDecoder()
err := decoder.Decode(result)
return err
}
// GetDecoder will return a decoder for the value.
func (c *ConfigEntry) GetDecoder() *gob.Decoder {
valueBytes := c.Value.([]byte)
decoder := gob.NewDecoder(bytes.NewBuffer(valueBytes))
return decoder
}

6
core/chat/events/connectedClientInfo.go → models/connectedClientInfo.go

@ -1,9 +1,7 @@ @@ -1,9 +1,7 @@
package events
import "github.com/owncast/owncast/core/user"
package models
// ConnectedClientInfo represents the information about a connected client.
type ConnectedClientInfo struct {
Event
User *user.User `json:"user"`
User *User `json:"user"`
}

29
models/eventType.go

@ -1,29 +0,0 @@ @@ -1,29 +0,0 @@
package models
// EventType is the type of a websocket event.
type EventType = string
const (
// MessageSent is the event sent when a chat event takes place.
MessageSent EventType = "CHAT"
// UserJoined is the event sent when a chat user join action takes place.
UserJoined EventType = "USER_JOINED"
// UserNameChanged is the event sent when a chat username change takes place.
UserNameChanged EventType = "NAME_CHANGE"
// VisibiltyToggled is the event sent when a chat message's visibility changes.
VisibiltyToggled EventType = "VISIBILITY-UPDATE"
// PING is a ping message.
PING EventType = "PING"
// PONG is a pong message.
PONG EventType = "PONG"
// StreamStarted represents a stream started event.
StreamStarted EventType = "STREAM_STARTED"
// StreamStopped represents a stream stopped event.
StreamStopped EventType = "STREAM_STOPPED"
// StreamTitleUpdated is the event sent when a stream's title changes.
StreamTitleUpdated EventType = "STREAM_TITLE_UPDATED"
// SystemMessageSent is the event sent when a system message is sent.
SystemMessageSent EventType = "SYSTEM"
// ChatActionSent is a generic chat action that can be used for anything that doesn't need specific handling or formatting.
ChatActionSent EventType = "CHAT_ACTION"
)

19
models/externalAPIUser.go

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
package models
import (
"time"
)
// ExternalAPIUser represents a single 3rd party integration that uses an access token.
// This struct mostly matches the User struct so they can be used interchangeably.
type ExternalAPIUser struct {
CreatedAt time.Time `json:"createdAt"`
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
ID string `json:"id"`
AccessToken string `json:"accessToken"`
DisplayName string `json:"displayName"`
Type string `json:"type,omitempty"` // Should be API
Scopes []string `json:"scopes"`
DisplayColor int `json:"displayColor"`
IsBot bool `json:"isBot"`
}

6
core/chat/events/fediverseEngagementEvent.go → models/fediverseEngagementEvent.go

@ -1,6 +1,4 @@ @@ -1,6 +1,4 @@
package events
import "github.com/owncast/owncast/core/data"
package models
// FediverseEngagementEvent is a message displayed in chat on representing an action on the Fediverse.
type FediverseEngagementEvent struct {
@ -22,7 +20,7 @@ func (e *FediverseEngagementEvent) GetBroadcastPayload() EventPayload { @@ -22,7 +20,7 @@ func (e *FediverseEngagementEvent) GetBroadcastPayload() EventPayload {
"title": e.UserAccountName,
"link": e.Link,
"user": EventPayload{
"displayName": data.GetServerName(),
"displayName": "Owncast",
},
}
}

5
core/chat/events/events.go → models/messageEvents.go

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
package events
package models
import (
"bytes"
@ -20,7 +20,6 @@ import ( @@ -20,7 +20,6 @@ import (
"mvdan.cc/xurls"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
log "github.com/sirupsen/logrus"
)
@ -42,7 +41,7 @@ type Event struct { @@ -42,7 +41,7 @@ type Event struct {
// UserEvent is an event with an associated user.
type UserEvent struct {
User *user.User `json:"user"`
User *User `json:"user"`
HiddenAt *time.Time `json:"hiddenAt,omitempty"`
ClientID uint `json:"clientId,omitempty"`
}

2
core/chat/events/nameChangeEvent.go → models/nameChangeEvent.go

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
package events
package models
// NameChangeEvent is received when a user changes their chat display name.
type NameChangeEvent struct {

2
core/chat/events/setMessageVisibilityEvent.go → models/setMessageVisibilityEvent.go

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
package events
package models
// SetMessageVisibilityEvent is the event fired when one or more message
// visibilities are changed.

2
models/stats.go

@ -6,9 +6,9 @@ import ( @@ -6,9 +6,9 @@ import (
// Stats holds the stats for the system.
type Stats struct {
LastConnectTime *utils.NullTime `json:"lastConnectTime"`
LastDisconnectTime *utils.NullTime `json:"lastDisconnectTime"`
LastConnectTime *utils.NullTime `json:"-"`
ChatClients map[string]Client `json:"-"`
Viewers map[string]*Viewer `json:"-"`
SessionMaxViewerCount int `json:"sessionMaxViewerCount"`

7
core/chat/events/systemMessageEvent.go → models/systemMessageEvent.go

@ -1,11 +1,10 @@ @@ -1,11 +1,10 @@
package events
import "github.com/owncast/owncast/core/data"
package models
// SystemMessageEvent is a message displayed in chat on behalf of the server.
type SystemMessageEvent struct {
Event
MessageEvent
DisplayName string
}
// GetBroadcastPayload will return the object to send to all chat users.
@ -16,7 +15,7 @@ func (e *SystemMessageEvent) GetBroadcastPayload() EventPayload { @@ -16,7 +15,7 @@ func (e *SystemMessageEvent) GetBroadcastPayload() EventPayload {
"body": e.Body,
"type": SystemMessageSent,
"user": EventPayload{
"displayName": data.GetServerName(),
"displayName": e.DisplayName,
},
}
}

4
metrics/timestampedValue.go → models/timestampedValue.go

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
package metrics
package models
import (
"time"
@ -12,7 +12,7 @@ type TimestampedValue struct { @@ -12,7 +12,7 @@ type TimestampedValue struct {
Value float64 `json:"value"`
}
func makeTimestampedValuesFromDatapoints(dp []*tstorage.DataPoint) []TimestampedValue {
func MakeTimestampedValuesFromDatapoints(dp []*tstorage.DataPoint) []TimestampedValue {
tv := []TimestampedValue{}
for _, d := range dp {
tv = append(tv, TimestampedValue{Time: time.Unix(d.Timestamp, 0), Value: d.Value})

36
models/user.go

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
package models
import (
"time"
"github.com/owncast/owncast/utils"
)
const (
moderatorScopeKey = "MODERATOR"
)
type User struct {
CreatedAt time.Time `json:"createdAt"`
DisabledAt *time.Time `json:"disabledAt,omitempty"`
NameChangedAt *time.Time `json:"nameChangedAt,omitempty"`
AuthenticatedAt *time.Time `json:"-"`
ID string `json:"id"`
DisplayName string `json:"displayName"`
PreviousNames []string `json:"previousNames"`
Scopes []string `json:"scopes,omitempty"`
DisplayColor int `json:"displayColor"`
IsBot bool `json:"isBot"`
Authenticated bool `json:"authenticated"`
}
// IsEnabled will return if this single user is enabled.
func (u *User) IsEnabled() bool {
return u.DisabledAt == nil
}
// IsModerator will return if the user has moderation privileges.
func (u *User) IsModerator() bool {
_, hasModerationScope := utils.FindInSlice(u.Scopes, moderatorScopeKey)
return hasModerationScope
}

2
core/chat/events/userDisabledEvent.go → models/userDisabledEvent.go

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
package events
package models
// UserDisabledEvent is the event fired when a user is banned/blocked and disconnected from chat.
type UserDisabledEvent struct {

20
models/userJoinedEvent.go

@ -1,11 +1,17 @@ @@ -1,11 +1,17 @@
package models
import "time"
// UserJoinedEvent represents an event when a user joins the chat.
// UserJoinedEvent is the event fired when a user joins chat.
type UserJoinedEvent struct {
Timestamp time.Time `json:"timestamp,omitempty"`
Username string `json:"username"`
Type EventType `json:"type"`
ID string `json:"id"`
Event
UserEvent
}
// GetBroadcastPayload will return the object to send to all chat users.
func (e *UserJoinedEvent) GetBroadcastPayload() EventPayload {
return EventPayload{
"type": UserJoined,
"id": e.ID,
"timestamp": e.Timestamp,
"user": e.User,
}
}

2
core/chat/events/userMessageEvent.go → models/userMessageEvent.go

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
package events
package models
// UserMessageEvent is an inbound message from a user.
type UserMessageEvent struct {

2
models/viewer.go

@ -4,7 +4,7 @@ import ( @@ -4,7 +4,7 @@ import (
"net/http"
"time"
"github.com/owncast/owncast/geoip"
"github.com/owncast/owncast/services/geoip"
"github.com/owncast/owncast/utils"
)

52
notifications/persistence.go

@ -1,52 +0,0 @@ @@ -1,52 +0,0 @@
package notifications
import (
"context"
"database/sql"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/db"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
func createNotificationsTable(db *sql.DB) {
log.Traceln("Creating federation followers table...")
createTableSQL := `CREATE TABLE IF NOT EXISTS notifications (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"channel" TEXT NOT NULL,
"destination" TEXT NOT NULL,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP);`
data.MustExec(createTableSQL, db)
data.MustExec(`CREATE INDEX IF NOT EXISTS idx_channel ON notifications (channel);`, db)
}
// 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
}

444
router/router.go

@ -1,444 +0,0 @@ @@ -1,444 +0,0 @@
package router
import (
"fmt"
"net/http"
"time"
"github.com/CAFxX/httpcompression"
"github.com/prometheus/client_golang/prometheus/promhttp"
log "github.com/sirupsen/logrus"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"github.com/owncast/owncast/activitypub"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/controllers/admin"
fediverseauth "github.com/owncast/owncast/controllers/auth/fediverse"
"github.com/owncast/owncast/controllers/auth/indieauth"
"github.com/owncast/owncast/controllers/moderation"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/router/middleware"
"github.com/owncast/owncast/utils"
"github.com/owncast/owncast/yp"
)
// Start starts the router for the http, ws, and rtmp.
func Start() error {
// The primary web app.
http.HandleFunc("/", controllers.IndexHandler)
// The admin web app.
http.HandleFunc("/admin/", middleware.RequireAdminAuth(controllers.IndexHandler))
// Images
http.HandleFunc("/thumbnail.jpg", controllers.GetThumbnail)
http.HandleFunc("/preview.gif", controllers.GetPreview)
http.HandleFunc("/logo", controllers.GetLogo)
// Custom Javascript
http.HandleFunc("/customjavascript", controllers.ServeCustomJavascript)
// Return a single emoji image.
http.HandleFunc(config.EmojiDir, controllers.GetCustomEmojiImage)
// return the logo
// return a logo that's compatible with external social networks
http.HandleFunc("/logo/external", controllers.GetCompatibleLogo)
// robots.txt
http.HandleFunc("/robots.txt", controllers.GetRobotsDotTxt)
// status of the system
http.HandleFunc("/api/status", controllers.GetStatus)
// custom emoji supported in the chat
http.HandleFunc("/api/emoji", controllers.GetCustomEmojiList)
// chat rest api
http.HandleFunc("/api/chat", middleware.RequireUserAccessToken(controllers.GetChatMessages))
// web config api
http.HandleFunc("/api/config", controllers.GetWebConfig)
// return the YP protocol data
http.HandleFunc("/api/yp", yp.GetYPResponse)
// list of all social platforms
http.HandleFunc("/api/socialplatforms", controllers.GetAllSocialPlatforms)
// return the list of video variants available
http.HandleFunc("/api/video/variants", controllers.GetVideoStreamOutputVariants)
// tell the backend you're an active viewer
http.HandleFunc("/api/ping", controllers.Ping)
// register a new chat user
http.HandleFunc("/api/chat/register", controllers.RegisterAnonymousChatUser)
// return remote follow details
http.HandleFunc("/api/remotefollow", controllers.RemoteFollow)
// return followers
http.HandleFunc("/api/followers", middleware.HandlePagination(controllers.GetFollowers))
// save client video playback metrics
http.HandleFunc("/api/metrics/playback", controllers.ReportPlaybackMetrics)
// Register for notifications
http.HandleFunc("/api/notifications/register", middleware.RequireUserAccessToken(controllers.RegisterForLiveNotifications))
// Authenticated admin requests
// Current inbound broadcaster
http.HandleFunc("/api/admin/status", middleware.RequireAdminAuth(admin.Status))
// Return HLS video
http.HandleFunc("/hls/", controllers.HandleHLSRequest)
// Disconnect inbound stream
http.HandleFunc("/api/admin/disconnect", middleware.RequireAdminAuth(admin.DisconnectInboundConnection))
// Server config
http.HandleFunc("/api/admin/serverconfig", middleware.RequireAdminAuth(admin.GetServerConfig))
// Get viewer count over time
http.HandleFunc("/api/admin/viewersOverTime", middleware.RequireAdminAuth(admin.GetViewersOverTime))
// Get active viewers
http.HandleFunc("/api/admin/viewers", middleware.RequireAdminAuth(admin.GetActiveViewers))
// Get hardware stats
http.HandleFunc("/api/admin/hardwarestats", middleware.RequireAdminAuth(admin.GetHardwareStats))
// Get a a detailed list of currently connected chat clients
http.HandleFunc("/api/admin/chat/clients", middleware.RequireAdminAuth(admin.GetConnectedChatClients))
// Get all logs
http.HandleFunc("/api/admin/logs", middleware.RequireAdminAuth(admin.GetLogs))
// Get warning/error logs
http.HandleFunc("/api/admin/logs/warnings", middleware.RequireAdminAuth(admin.GetWarnings))
// Get all chat messages for the admin, unfiltered.
http.HandleFunc("/api/admin/chat/messages", middleware.RequireAdminAuth(admin.GetChatMessages))
// Update chat message visibility
http.HandleFunc("/api/admin/chat/messagevisibility", middleware.RequireAdminAuth(admin.UpdateMessageVisibility))
// Enable/disable a user
http.HandleFunc("/api/admin/chat/users/setenabled", middleware.RequireAdminAuth(admin.UpdateUserEnabled))
// Ban/unban an IP address
http.HandleFunc("/api/admin/chat/users/ipbans/create", middleware.RequireAdminAuth(admin.BanIPAddress))
// Remove an IP address ban
http.HandleFunc("/api/admin/chat/users/ipbans/remove", middleware.RequireAdminAuth(admin.UnBanIPAddress))
// Return all the banned IP addresses
http.HandleFunc("/api/admin/chat/users/ipbans", middleware.RequireAdminAuth(admin.GetIPAddressBans))
// Get a list of disabled users
http.HandleFunc("/api/admin/chat/users/disabled", middleware.RequireAdminAuth(admin.GetDisabledUsers))
// Set moderator status for a user
http.HandleFunc("/api/admin/chat/users/setmoderator", middleware.RequireAdminAuth(admin.UpdateUserModerator))
// Get a list of moderator users
http.HandleFunc("/api/admin/chat/users/moderators", middleware.RequireAdminAuth(admin.GetModerators))
// return followers
http.HandleFunc("/api/admin/followers", middleware.RequireAdminAuth(middleware.HandlePagination(controllers.GetFollowers)))
// Get a list of pending follow requests
http.HandleFunc("/api/admin/followers/pending", middleware.RequireAdminAuth(admin.GetPendingFollowRequests))
// Get a list of rejected or blocked follows
http.HandleFunc("/api/admin/followers/blocked", middleware.RequireAdminAuth(admin.GetBlockedAndRejectedFollowers))
// Set the following state of a follower or follow request.
http.HandleFunc("/api/admin/followers/approve", middleware.RequireAdminAuth(admin.ApproveFollower))
// Upload custom emoji
http.HandleFunc("/api/admin/emoji/upload", middleware.RequireAdminAuth(admin.UploadCustomEmoji))
// Delete custom emoji
http.HandleFunc("/api/admin/emoji/delete", middleware.RequireAdminAuth(admin.DeleteCustomEmoji))
// Update config values
// Change the current streaming key in memory
http.HandleFunc("/api/admin/config/adminpass", middleware.RequireAdminAuth(admin.SetAdminPassword))
// Set an array of valid stream keys
http.HandleFunc("/api/admin/config/streamkeys", middleware.RequireAdminAuth(admin.SetStreamKeys))
// Change the extra page content in memory
http.HandleFunc("/api/admin/config/pagecontent", middleware.RequireAdminAuth(admin.SetExtraPageContent))
// Stream title
http.HandleFunc("/api/admin/config/streamtitle", middleware.RequireAdminAuth(admin.SetStreamTitle))
// Server name
http.HandleFunc("/api/admin/config/name", middleware.RequireAdminAuth(admin.SetServerName))
// Server summary
http.HandleFunc("/api/admin/config/serversummary", middleware.RequireAdminAuth(admin.SetServerSummary))
// Offline message
http.HandleFunc("/api/admin/config/offlinemessage", middleware.RequireAdminAuth(admin.SetCustomOfflineMessage))
// Server welcome message
http.HandleFunc("/api/admin/config/welcomemessage", middleware.RequireAdminAuth(admin.SetServerWelcomeMessage))
// Disable chat
http.HandleFunc("/api/admin/config/chat/disable", middleware.RequireAdminAuth(admin.SetChatDisabled))
// Disable chat user join messages
http.HandleFunc("/api/admin/config/chat/joinmessagesenabled", middleware.RequireAdminAuth(admin.SetChatJoinMessagesEnabled))
// Enable/disable chat established user mode
http.HandleFunc("/api/admin/config/chat/establishedusermode", middleware.RequireAdminAuth(admin.SetEnableEstablishedChatUserMode))
// Set chat usernames that are not allowed
http.HandleFunc("/api/admin/config/chat/forbiddenusernames", middleware.RequireAdminAuth(admin.SetForbiddenUsernameList))
// Set the suggested chat usernames that will be assigned automatically
http.HandleFunc("/api/admin/config/chat/suggestedusernames", middleware.RequireAdminAuth(admin.SetSuggestedUsernameList))
// Set video codec
http.HandleFunc("/api/admin/config/video/codec", middleware.RequireAdminAuth(admin.SetVideoCodec))
// Set style/color/css values
http.HandleFunc("/api/admin/config/appearance", middleware.RequireAdminAuth(admin.SetCustomColorVariableValues))
// Return all webhooks
http.HandleFunc("/api/admin/webhooks", middleware.RequireAdminAuth(admin.GetWebhooks))
// Delete a single webhook
http.HandleFunc("/api/admin/webhooks/delete", middleware.RequireAdminAuth(admin.DeleteWebhook))
// Create a single webhook
http.HandleFunc("/api/admin/webhooks/create", middleware.RequireAdminAuth(admin.CreateWebhook))
// Get all access tokens
http.HandleFunc("/api/admin/accesstokens", middleware.RequireAdminAuth(admin.GetExternalAPIUsers))
// Delete a single access token
http.HandleFunc("/api/admin/accesstokens/delete", middleware.RequireAdminAuth(admin.DeleteExternalAPIUser))
// Create a single access token
http.HandleFunc("/api/admin/accesstokens/create", middleware.RequireAdminAuth(admin.CreateExternalAPIUser))
// Return the auto-update features that are supported for this instance.
http.HandleFunc("/api/admin/update/options", middleware.RequireAdminAuth(admin.AutoUpdateOptions))
// Begin the auto update
http.HandleFunc("/api/admin/update/start", middleware.RequireAdminAuth(admin.AutoUpdateStart))
// Force quit the service to restart it
http.HandleFunc("/api/admin/update/forcequit", middleware.RequireAdminAuth(admin.AutoUpdateForceQuit))
// Send a system message to chat
http.HandleFunc("/api/integrations/chat/system", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendSystemMessage))
// Send a system message to a single client
http.HandleFunc(utils.RestEndpoint("/api/integrations/chat/system/client/{clientId}", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendSystemMessageToConnectedClient)))
// Send a user message to chat *NO LONGER SUPPORTED
http.HandleFunc("/api/integrations/chat/user", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendChatMessages, admin.SendUserMessage))
// Send a message to chat as a specific 3rd party bot/integration based on its access token
http.HandleFunc("/api/integrations/chat/send", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendChatMessages, admin.SendIntegrationChatMessage))
// Send a user action to chat
http.HandleFunc("/api/integrations/chat/action", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, admin.SendChatAction))
// Hide chat message
http.HandleFunc("/api/integrations/chat/messagevisibility", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, admin.ExternalUpdateMessageVisibility))
// Stream title
http.HandleFunc("/api/integrations/streamtitle", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, admin.ExternalSetStreamTitle))
// Get chat history
http.HandleFunc("/api/integrations/chat", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, controllers.ExternalGetChatMessages))
// Connected clients
http.HandleFunc("/api/integrations/clients", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, admin.ExternalGetConnectedChatClients))
// Logo path
http.HandleFunc("/api/admin/config/logo", middleware.RequireAdminAuth(admin.SetLogo))
// Server tags
http.HandleFunc("/api/admin/config/tags", middleware.RequireAdminAuth(admin.SetTags))
// ffmpeg
http.HandleFunc("/api/admin/config/ffmpegpath", middleware.RequireAdminAuth(admin.SetFfmpegPath))
// Server http port
http.HandleFunc("/api/admin/config/webserverport", middleware.RequireAdminAuth(admin.SetWebServerPort))
// Server http listen address
http.HandleFunc("/api/admin/config/webserverip", middleware.RequireAdminAuth(admin.SetWebServerIP))
// Server rtmp port
http.HandleFunc("/api/admin/config/rtmpserverport", middleware.RequireAdminAuth(admin.SetRTMPServerPort))
// Websocket host override
http.HandleFunc("/api/admin/config/sockethostoverride", middleware.RequireAdminAuth(admin.SetSocketHostOverride))
// Custom video serving endpoint
http.HandleFunc("/api/admin/config/videoservingendpoint", middleware.RequireAdminAuth(admin.SetVideoServingEndpoint))
// Is server marked as NSFW
http.HandleFunc("/api/admin/config/nsfw", middleware.RequireAdminAuth(admin.SetNSFW))
// directory enabled
http.HandleFunc("/api/admin/config/directoryenabled", middleware.RequireAdminAuth(admin.SetDirectoryEnabled))
// social handles
http.HandleFunc("/api/admin/config/socialhandles", middleware.RequireAdminAuth(admin.SetSocialHandles))
// set the number of video segments and duration per segment in a playlist
http.HandleFunc("/api/admin/config/video/streamlatencylevel", middleware.RequireAdminAuth(admin.SetStreamLatencyLevel))
// set an array of video output configurations
http.HandleFunc("/api/admin/config/video/streamoutputvariants", middleware.RequireAdminAuth(admin.SetStreamOutputVariants))
// set s3 configuration
http.HandleFunc("/api/admin/config/s3", middleware.RequireAdminAuth(admin.SetS3Configuration))
// set server url
http.HandleFunc("/api/admin/config/serverurl", middleware.RequireAdminAuth(admin.SetServerURL))
// reset the YP registration
http.HandleFunc("/api/admin/yp/reset", middleware.RequireAdminAuth(admin.ResetYPRegistration))
// set external action links
http.HandleFunc("/api/admin/config/externalactions", middleware.RequireAdminAuth(admin.SetExternalActions))
// set custom style css
http.HandleFunc("/api/admin/config/customstyles", middleware.RequireAdminAuth(admin.SetCustomStyles))
// set custom style javascript
http.HandleFunc("/api/admin/config/customjavascript", middleware.RequireAdminAuth(admin.SetCustomJavascript))
// Video playback metrics
http.HandleFunc("/api/admin/metrics/video", middleware.RequireAdminAuth(admin.GetVideoPlaybackMetrics))
// Is the viewer count hidden from viewers
http.HandleFunc("/api/admin/config/hideviewercount", middleware.RequireAdminAuth(admin.SetHideViewerCount))
// set disabling of search indexing
http.HandleFunc("/api/admin/config/disablesearchindexing", middleware.RequireAdminAuth(admin.SetDisableSearchIndexing))
// Inline chat moderation actions
// Update chat message visibility
http.HandleFunc("/api/chat/messagevisibility", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateMessageVisibility))
// Enable/disable a user
http.HandleFunc("/api/chat/users/setenabled", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateUserEnabled))
// Get a user's details
http.HandleFunc("/api/moderation/chat/user/", middleware.RequireUserModerationScopeAccesstoken(moderation.GetUserDetails))
// Configure Federation features
// enable/disable federation features
http.HandleFunc("/api/admin/config/federation/enable", middleware.RequireAdminAuth(admin.SetFederationEnabled))
// set if federation activities are private
http.HandleFunc("/api/admin/config/federation/private", middleware.RequireAdminAuth(admin.SetFederationActivityPrivate))
// set if fediverse engagement appears in chat
http.HandleFunc("/api/admin/config/federation/showengagement", middleware.RequireAdminAuth(admin.SetFederationShowEngagement))
// set local federated username
http.HandleFunc("/api/admin/config/federation/username", middleware.RequireAdminAuth(admin.SetFederationUsername))
// set federated go live message
http.HandleFunc("/api/admin/config/federation/livemessage", middleware.RequireAdminAuth(admin.SetFederationGoLiveMessage))
// Federation blocked domains
http.HandleFunc("/api/admin/config/federation/blockdomains", middleware.RequireAdminAuth(admin.SetFederationBlockDomains))
// send a public message to the Fediverse from the server's user
http.HandleFunc("/api/admin/federation/send", middleware.RequireAdminAuth(admin.SendFederatedMessage))
// Return federated activities
http.HandleFunc("/api/admin/federation/actions", middleware.RequireAdminAuth(middleware.HandlePagination(admin.GetFederatedActions)))
// Prometheus metrics
http.Handle("/api/admin/prometheus", middleware.RequireAdminAuth(func(rw http.ResponseWriter, r *http.Request) {
promhttp.Handler().ServeHTTP(rw, r)
}))
// Configure outbound notification channels.
http.HandleFunc("/api/admin/config/notifications/discord", middleware.RequireAdminAuth(admin.SetDiscordNotificationConfiguration))
http.HandleFunc("/api/admin/config/notifications/browser", middleware.RequireAdminAuth(admin.SetBrowserNotificationConfiguration))
// Auth
// Start auth flow
http.HandleFunc("/api/auth/indieauth", middleware.RequireUserAccessToken(indieauth.StartAuthFlow))
http.HandleFunc("/api/auth/indieauth/callback", indieauth.HandleRedirect)
http.HandleFunc("/api/auth/provider/indieauth", indieauth.HandleAuthEndpoint)
http.HandleFunc("/api/auth/fediverse", middleware.RequireUserAccessToken(fediverseauth.RegisterFediverseOTPRequest))
http.HandleFunc("/api/auth/fediverse/verify", fediverseauth.VerifyFediverseOTPRequest)
// ActivityPub has its own router
activitypub.Start(data.GetDatastore())
// websocket
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
chat.HandleClientConnection(w, r)
})
// Optional public static files
http.Handle("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir(config.PublicFilesPath))))
port := config.WebServerPort
ip := config.WebServerIP
h2s := &http2.Server{}
// Create a custom mux handler to intercept the /debug/vars endpoint.
// This is a hack because Prometheus enables this endpoint by default
// due to its use of expvar and we do not want this exposed.
defaultMux := h2c.NewHandler(http.DefaultServeMux, h2s)
m := http.NewServeMux()
m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/debug/vars" {
w.WriteHeader(http.StatusNotFound)
return
} else if r.URL.Path == "/embed/chat/" || r.URL.Path == "/embed/chat" {
// Redirect /embed/chat
http.Redirect(w, r, "/embed/chat/readonly", http.StatusTemporaryRedirect)
} else {
defaultMux.ServeHTTP(w, r)
}
})
compress, _ := httpcompression.DefaultAdapter() // Use the default configuration
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", ip, port),
ReadHeaderTimeout: 4 * time.Second,
Handler: compress(m),
}
if ip != "0.0.0.0" {
log.Infof("Web server is listening at %s:%d.", ip, port)
} else {
log.Infof("Web server is listening on port %d.", port)
}
log.Infoln("Configure this server by visiting /admin.")
return server.ListenAndServe()
}

82
services/apfederation/activitypub.go

@ -0,0 +1,82 @@ @@ -0,0 +1,82 @@
package apfederation
import (
"github.com/owncast/owncast/services/apfederation/crypto"
"github.com/owncast/owncast/services/apfederation/outbox"
"github.com/owncast/owncast/services/apfederation/workerpool"
"github.com/owncast/owncast/storage/configrepository"
"github.com/owncast/owncast/storage/data"
"github.com/owncast/owncast/storage/federationrepository"
"github.com/owncast/owncast/models"
log "github.com/sirupsen/logrus"
)
type APFederation struct {
workers *workerpool.WorkerPool
outbox *outbox.APOutbox
}
func New() *APFederation {
ds := data.GetDatastore()
apf := &APFederation{
outbox: outbox.Get(),
}
apf.Start(ds)
return apf
}
var temporaryGlobalInstance *APFederation
func Get() *APFederation {
if temporaryGlobalInstance == nil {
temporaryGlobalInstance = New()
}
return temporaryGlobalInstance
}
// Start will initialize and start the federation support.
func (ap *APFederation) Start(datastore *data.Store) {
configRepository := configrepository.Get()
// workerpool.InitOutboundWorkerPool()
// ap.InitInboxWorkerPool()
// Generate the keys for signing federated activity if needed.
if configRepository.GetPrivateKey() == "" {
privateKey, publicKey, err := crypto.GenerateKeys()
_ = configRepository.SetPrivateKey(string(privateKey))
_ = configRepository.SetPublicKey(string(publicKey))
if err != nil {
log.Errorln("Unable to get private key", err)
}
}
}
// SendLive will send a "Go Live" message to followers.
func (ap *APFederation) SendLive() error {
return ap.SendLive()
}
// SendPublicFederatedMessage will send an arbitrary provided message to followers.
func (ap *APFederation) SendPublicFederatedMessage(message string) error {
return ap.outbox.SendPublicMessage(message)
}
// SendDirectFederatedMessage will send a direct message to a single account.
func (ap *APFederation) SendDirectFederatedMessage(message, account string) error {
return ap.outbox.SendDirectMessageToAccount(message, account)
}
// GetFollowerCount will return the local tracked follower count.
func (ap *APFederation) GetFollowerCount() (int64, error) {
federationRepository := federationrepository.Get()
return federationRepository.GetFollowerCount()
}
// GetPendingFollowRequests will return the pending follow requests.
func (ap *APFederation) GetPendingFollowRequests() ([]models.Follower, error) {
federationRepository := federationrepository.Get()
return federationRepository.GetPendingFollowRequests()
}

10
activitypub/apmodels/activity.go → services/apfederation/apmodels/activity.go

@ -6,7 +6,7 @@ import ( @@ -6,7 +6,7 @@ import (
"github.com/go-fed/activity/streams"
"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.
@ -87,8 +87,10 @@ func MakeActivityDirect(activity vocab.ActivityStreamsCreate, toIRI *url.URL) vo @@ -87,8 +87,10 @@ func MakeActivityDirect(activity vocab.ActivityStreamsCreate, toIRI *url.URL) vo
// MakeActivityPublic sets the required properties to make this activity
// seen as public.
func MakeActivityPublic(activity vocab.ActivityStreamsCreate) vocab.ActivityStreamsCreate {
configRepository := configrepository.Get()
// TO the public if we're not treating ActivityPub as "private".
if !data.GetFederationIsPrivate() {
if !configRepository.GetFederationIsPrivate() {
public, _ := url.Parse(PUBLIC)
to := streams.NewActivityStreamsToProperty()
@ -115,13 +117,15 @@ func MakeCreateActivity(activityID *url.URL) vocab.ActivityStreamsCreate { @@ -115,13 +117,15 @@ func MakeCreateActivity(activityID *url.URL) vocab.ActivityStreamsCreate {
// MakeUpdateActivity will return a new Update activity with the provided aID.
func MakeUpdateActivity(activityID *url.URL) vocab.ActivityStreamsUpdate {
configRepository := configrepository.Get()
activity := streams.NewActivityStreamsUpdate()
id := streams.NewJSONLDIdProperty()
id.Set(activityID)
activity.SetJSONLDId(id)
// CC the public if we're not treating ActivityPub as "private".
if !data.GetFederationIsPrivate() {
if !configRepository.GetFederationIsPrivate() {
public, _ := url.Parse(PUBLIC)
cc := streams.NewActivityStreamsCcProperty()
cc.AppendIRI(public)

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save