Compare commits
18 Commits
develop
...
backend-re
Author | SHA1 | Date |
---|---|---|
|
c572fb76ce | 2 years ago |
|
659a19bf2c | 2 years ago |
|
c7b3aa7450 | 2 years ago |
|
580de78625 | 2 years ago |
|
fa1b128187 | 2 years ago |
|
4c69f3b1dd | 2 years ago |
|
78f942b88e | 2 years ago |
|
309bc72bda | 2 years ago |
|
35b15605b5 | 2 years ago |
|
0417eacde8 | 2 years ago |
|
ff14a176fd | 2 years ago |
|
d9482e8701 | 2 years ago |
|
650159d8c9 | 2 years ago |
|
ea4871bdf3 | 2 years ago |
|
4e54f18378 | 2 years ago |
|
694854cd90 | 2 years ago |
|
084e3427d8 | 2 years ago |
|
201ccc190d | 2 years ago |
299 changed files with 10300 additions and 9111 deletions
@ -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() |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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()) |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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)) |
||||
} |
@ -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, |
||||
} |
||||
} |
@ -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() |
||||
} |
@ -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.") |
||||
} |
@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
|
||||
//go:build freebsd
|
||||
// +build freebsd
|
||||
|
||||
package chat |
||||
package cmd |
||||
|
||||
import ( |
||||
"syscall" |
@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package chat |
||||
package cmd |
||||
|
||||
func setSystemConcurrentConnectionLimit(limit int64) {} |
@ -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) |
||||
} |
||||
} |
||||
} |
@ -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()) |
||||
} |
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
package cmd |
||||
|
||||
func initializeData() { |
||||
} |
@ -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() |
||||
} |
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
package cmd |
||||
|
||||
func (app *Application) startServices() { |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
@ -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,8 +0,0 @@
@@ -1,8 +0,0 @@
|
||||
//go:build enable_updates
|
||||
// +build enable_updates
|
||||
|
||||
package config |
||||
|
||||
func init() { |
||||
EnableAutoUpdate = true |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
@ -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") |
||||
} |
@ -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") |
||||
} |
@ -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") |
||||
} |
@ -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) |
||||
} |
@ -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") |
||||
} |
@ -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)) |
||||
} |
@ -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") |
||||
} |
@ -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) |
||||
} |
@ -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) |
||||
} |
@ -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") |
||||
} |
@ -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"` |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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") |
||||
} |
@ -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") |
||||
} |
@ -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, "") |
||||
} |
@ -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) |
||||
} |
@ -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" |
@ -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)) |
||||
} |
@ -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) |
||||
} |
@ -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) |
||||
} |
@ -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, |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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() |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
@ -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) |
||||
} |
@ -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) |
||||
} |
@ -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 |
||||
} |
@ -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) |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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, |
||||
} |
||||
} |
@ -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, |
||||
}, |
||||
}) |
||||
} |
@ -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) |
||||
} |
||||
} |
@ -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") |
||||
} |
@ -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() {
|
||||
// }
|
||||
|
@ -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) |
||||
} |
@ -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 |
||||
} |
@ -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 |
||||
} |
@ -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 { |
@ -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" |
||||
) |
@ -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" |
||||
) |
@ -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 |
||||
} |
@ -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"` |
||||
} |
@ -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" |
||||
) |
@ -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"` |
||||
} |
@ -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 { |
@ -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.
|
@ -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 |
||||
} |
@ -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 { |
@ -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, |
||||
} |
||||
} |
||||
|
@ -1,4 +1,4 @@
@@ -1,4 +1,4 @@
|
||||
package events |
||||
package models |
||||
|
||||
// UserMessageEvent is an inbound message from a user.
|
||||
type UserMessageEvent struct { |
@ -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 |
||||
} |
@ -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() |
||||
} |
@ -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() |
||||
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue