From b80ccc496675032506d8e5f14ac8fe961d10f778 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Fri, 21 Jul 2023 22:25:59 -0700 Subject: [PATCH] WIP --- activitypub/activitypub.go | 61 ----- activitypub/inbox/chat.go | 64 ----- activitypub/inbox/update.go | 25 -- activitypub/router.go | 35 --- cmd/application.go | 48 ++++ cmd/backuprestore.go | 20 ++ {core/chat => cmd}/concurrentConnections.go | 14 +- .../concurrentConnections_freebsd.go | 2 +- .../concurrentConnections_windows.go | 2 +- cmd/config.go | 71 ++++++ cmd/console.go | 7 + cmd/data.go | 4 + cmd/flags.go | 23 ++ cmd/services.go | 4 + cmd/setup.go | 162 +++++++++++++ {core/chat => cmd}/utils_windows.go | 0 core/chat/chat.go | 187 --------------- core/chat/events/systemMessageEvent.go | 25 -- core/chat/events/userJoinedEvent.go | 17 -- core/chat/events/userMessageEvent.go | 25 -- core/chat/utils.go | 22 -- core/core.go | 75 +----- core/offlineState.go | 6 +- core/stats.go | 7 + core/status.go | 46 ---- core/streamState.go | 203 ---------------- logging/logging.go | 10 +- logging/paths.go | 18 -- main.go | 192 +++------------ models/chatAccessScopes.go | 12 + .../eventtype.go => models/chatEventTypes.go | 4 +- .../events => models}/connectedClientInfo.go | 6 +- models/event.go | 20 ++ models/eventPayload.go | 4 + models/eventType.go | 29 --- .../chat/events => models}/nameChangeEvent.go | 2 +- .../setMessageVisibilityEvent.go | 2 +- models/stats.go | 2 +- .../events => models}/userDisabledEvent.go | 2 +- models/userEvent.go | 10 + models/userJoinedEvent.go | 20 +- services/apfederation/activitypub.go | 82 +++++++ .../apfederation}/apmodels/activity.go | 0 .../apfederation}/apmodels/actor.go | 2 +- .../apfederation}/apmodels/actor_test.go | 0 .../apfederation}/apmodels/hashtag.go | 0 .../apfederation}/apmodels/inboxRequest.go | 0 .../apfederation}/apmodels/message.go | 0 .../apfederation}/apmodels/utils.go | 0 .../apfederation}/apmodels/webfinger.go | 0 .../apfederation}/crypto/keys.go | 0 .../apfederation}/crypto/publicKey.go | 0 .../apfederation}/crypto/sign.go | 2 +- .../apfederation}/inbox/announce.go | 13 +- services/apfederation/inbox/chat.go | 59 +++++ .../apfederation}/inbox/constants.go | 0 .../apfederation}/inbox/create.go | 2 +- .../apfederation}/inbox/follow.go | 32 +-- services/apfederation/inbox/inbox.go | 37 +++ .../apfederation}/inbox/like.go | 13 +- .../apfederation}/inbox/undo.go | 4 +- services/apfederation/inbox/update.go | 23 ++ .../apfederation}/inbox/worker.go | 26 +- .../apfederation}/inbox/worker_test.go | 16 +- .../apfederation}/inbox/workerpool.go | 9 +- .../apfederation/outbox}/acceptFollow.go | 15 +- .../apfederation}/outbox/outbox.go | 124 ++++++---- .../apfederation}/requests/http.go | 16 +- services/apfederation/requests/requests.go | 20 ++ .../apfederation}/resolvers/follow.go | 14 +- .../apfederation}/resolvers/resolve.go | 22 +- services/apfederation/resolvers/resolvers.go | 24 ++ .../apfederation}/webfinger/webfinger.go | 17 +- .../apfederation}/workerpool/outbound.go | 35 ++- services/auth/indieauth/client.go | 10 +- .../events => services/chat}/actionEvent.go | 14 +- services/chat/chat.go | 227 ++++++++++++++++++ {core => services}/chat/chatclient.go | 11 +- services/chat/emoji.go | 131 ++++++++++ {core => services}/chat/events.go | 33 +-- .../chat}/fediverseEngagementEvent.go | 20 +- .../chat/messageEvents.go | 149 +----------- .../chat/messageRendering_test.go | 3 +- {core => services}/chat/messages.go | 13 +- {core => services}/chat/server.go | 185 +++++++------- services/chat/systemMessageEvent.go | 28 +++ services/chat/userMessageEvent.go | 39 +++ .../events => services/chat}/userPartEvent.go | 14 +- services/config/config.go | 11 +- services/metrics/healthOverview.go | 17 +- services/metrics/metrics.go | 9 +- services/metrics/playback.go | 5 +- services/metrics/viewers.go | 13 +- services/notifications/notifications.go | 7 +- services/status/status.go | 61 +++++ services/webhooks/chat.go | 9 +- services/webhooks/chat_test.go | 42 ++-- services/webhooks/manager.go | 6 +- services/webhooks/stream.go | 4 +- services/webhooks/stream_test.go | 6 +- services/webhooks/webhooks.go | 3 +- services/webhooks/webhooks_test.go | 27 ++- services/webhooks/workerpool.go | 3 + services/yp/yp.go | 8 +- static/emoji.go | 89 ------- storage/chatrepository/chatrepository.go | 4 +- .../chatrepository}/persistence.go | 133 +++++----- .../chat => storage/chatrepository}/pruner.go | 23 +- storage/configrepository/config.go | 10 +- storage/configrepository/configrepository.go | 5 +- storage/data/datastore.go | 2 +- .../federationrepository.go | 8 +- storage/sqlstorage/initialize.go | 2 +- storage/sqlstorage/migrations.go | 2 +- storage/userrepository/userrepository.go | 23 +- utils/emojiMigration.go | 23 -- video/rtmp/broadcaster.go | 2 +- video/rtmp/rtmp.go | 12 +- video/state/offline.go | 123 ++++++++++ video/state/online.go | 124 ++++++++++ video/state/state.go | 35 +++ video/storageproviders/local.go | 5 +- .../storageproviders/rewriteLocalPlaylist.go | 2 +- video/storageproviders/s3Storage.go | 9 +- video/transcoder/fileWriterReceiverService.go | 4 +- video/transcoder/thumbnailGenerator.go | 12 +- video/transcoder/transcoder.go | 27 ++- video/transcoder/utils.go | 5 +- web/style-definitions/config.js | 35 ++- webserver/handlers/adminApiChatHandlers.go | 42 ++-- webserver/handlers/adminApiConfigHandlers.go | 31 ++- .../handlers/adminApiConnectedClients.go | 3 +- webserver/handlers/adminApiEmojiConfig.go | 4 +- .../handlers/adminApiFederationConfig.go | 18 +- webserver/handlers/adminApiFollowers.go | 22 +- webserver/handlers/adminApiHardwareStats.go | 2 +- webserver/handlers/adminApiServerConfig.go | 2 +- webserver/handlers/adminApiSoftwareUpdate.go | 2 +- webserver/handlers/adminApiSystemStatus.go | 22 +- webserver/handlers/adminApiVideoConfig.go | 65 ++--- webserver/handlers/adminApiViewers.go | 4 +- .../handlers/auth/fediverse/fediverse.go | 43 ++-- webserver/handlers/auth/indieauth/client.go | 37 ++- webserver/handlers/auth/indieauth/server.go | 10 +- webserver/handlers/chat.go | 9 +- webserver/handlers/config.go | 8 +- webserver/handlers/disconnect.go | 6 +- webserver/handlers/emoji.go | 5 +- .../handlers/federation}/actors.go | 15 +- .../handlers/federation}/followers.go | 26 +- .../handlers/federation}/inbox.go | 12 +- .../handlers/federation}/nodeinfo.go | 41 +++- .../handlers/federation}/object.go | 19 +- .../handlers/federation}/outbox.go | 28 ++- .../handlers/federation}/webfinger.go | 7 +- webserver/handlers/followers.go | 5 +- webserver/handlers/handlers.go | 18 +- webserver/handlers/hls.go | 2 +- webserver/handlers/images.go | 4 +- webserver/handlers/index.go | 2 +- webserver/handlers/moderation.go | 13 +- webserver/handlers/notifications.go | 5 +- webserver/handlers/playbackMetrics.go | 11 +- webserver/handlers/remoteFollow.go | 6 +- webserver/handlers/status.go | 16 +- webserver/handlers/ypApi.go | 14 +- webserver/middleware/auth.go | 2 +- webserver/router.go | 70 ++++-- webserver/webserver.go | 19 +- webserver/webserver_test.go | 7 +- 170 files changed, 2573 insertions(+), 2026 deletions(-) delete mode 100644 activitypub/activitypub.go delete mode 100644 activitypub/inbox/chat.go delete mode 100644 activitypub/inbox/update.go delete mode 100644 activitypub/router.go create mode 100644 cmd/application.go create mode 100644 cmd/backuprestore.go rename {core/chat => cmd}/concurrentConnections.go (61%) rename {core/chat => cmd}/concurrentConnections_freebsd.go (97%) rename {core/chat => cmd}/concurrentConnections_windows.go (87%) create mode 100644 cmd/config.go create mode 100644 cmd/console.go create mode 100644 cmd/data.go create mode 100644 cmd/flags.go create mode 100644 cmd/services.go create mode 100644 cmd/setup.go rename {core/chat => cmd}/utils_windows.go (100%) delete mode 100644 core/chat/chat.go delete mode 100644 core/chat/events/systemMessageEvent.go delete mode 100644 core/chat/events/userJoinedEvent.go delete mode 100644 core/chat/events/userMessageEvent.go delete mode 100644 core/chat/utils.go delete mode 100644 core/status.go delete mode 100644 core/streamState.go delete mode 100644 logging/paths.go create mode 100644 models/chatAccessScopes.go rename core/chat/events/eventtype.go => models/chatEventTypes.go (95%) rename {core/chat/events => models}/connectedClientInfo.go (56%) create mode 100644 models/event.go create mode 100644 models/eventPayload.go delete mode 100644 models/eventType.go rename {core/chat/events => models}/nameChangeEvent.go (98%) rename {core/chat/events => models}/setMessageVisibilityEvent.go (97%) rename {core/chat/events => models}/userDisabledEvent.go (96%) create mode 100644 models/userEvent.go create mode 100644 services/apfederation/activitypub.go rename {activitypub => services/apfederation}/apmodels/activity.go (100%) rename {activitypub => services/apfederation}/apmodels/actor.go (99%) rename {activitypub => services/apfederation}/apmodels/actor_test.go (100%) rename {activitypub => services/apfederation}/apmodels/hashtag.go (100%) rename {activitypub => services/apfederation}/apmodels/inboxRequest.go (100%) rename {activitypub => services/apfederation}/apmodels/message.go (100%) rename {activitypub => services/apfederation}/apmodels/utils.go (100%) rename {activitypub => services/apfederation}/apmodels/webfinger.go (100%) rename {activitypub => services/apfederation}/crypto/keys.go (100%) rename {activitypub => services/apfederation}/crypto/publicKey.go (100%) rename {activitypub => services/apfederation}/crypto/sign.go (99%) rename {activitypub => services/apfederation}/inbox/announce.go (52%) create mode 100644 services/apfederation/inbox/chat.go rename {activitypub => services/apfederation}/inbox/constants.go (100%) rename {activitypub => services/apfederation}/inbox/create.go (67%) rename {activitypub => services/apfederation}/inbox/follow.go (55%) create mode 100644 services/apfederation/inbox/inbox.go rename {activitypub => services/apfederation}/inbox/like.go (57%) rename {activitypub => services/apfederation}/inbox/undo.go (77%) create mode 100644 services/apfederation/inbox/update.go rename {activitypub => services/apfederation}/inbox/worker.go (78%) rename {activitypub => services/apfederation}/inbox/worker_test.go (84%) rename {activitypub => services/apfederation}/inbox/workerpool.go (77%) rename {activitypub/requests => services/apfederation/outbox}/acceptFollow.go (67%) rename {activitypub => services/apfederation}/outbox/outbox.go (64%) rename {activitypub => services/apfederation}/requests/http.go (73%) create mode 100644 services/apfederation/requests/requests.go rename {activitypub => services/apfederation}/resolvers/follow.go (65%) rename {activitypub => services/apfederation}/resolvers/resolve.go (85%) create mode 100644 services/apfederation/resolvers/resolvers.go rename {activitypub => services/apfederation}/webfinger/webfinger.go (81%) rename {activitypub => services/apfederation}/workerpool/outbound.go (61%) rename {core/chat/events => services/chat}/actionEvent.go (58%) create mode 100644 services/chat/chat.go rename {core => services}/chat/chatclient.go (96%) create mode 100644 services/chat/emoji.go rename {core => services}/chat/events.go (87%) rename {core/chat/events => services/chat}/fediverseEngagementEvent.go (63%) rename core/chat/events/events.go => services/chat/messageEvents.go (52%) rename {core => services}/chat/messageRendering_test.go (96%) rename {core => services}/chat/messages.go (63%) rename {core => services}/chat/server.go (70%) create mode 100644 services/chat/systemMessageEvent.go create mode 100644 services/chat/userMessageEvent.go rename {core/chat/events => services/chat}/userPartEvent.go (52%) create mode 100644 services/status/status.go rename {core/chat => storage/chatrepository}/persistence.go (74%) rename {core/chat => storage/chatrepository}/pruner.go (61%) delete mode 100644 utils/emojiMigration.go create mode 100644 video/state/offline.go create mode 100644 video/state/online.go create mode 100644 video/state/state.go rename {activitypub/controllers => webserver/handlers/federation}/actors.go (81%) rename {activitypub/controllers => webserver/handlers/federation}/followers.go (83%) rename {activitypub/controllers => webserver/handlers/federation}/inbox.go (81%) rename {activitypub/controllers => webserver/handlers/federation}/nodeinfo.go (87%) rename {activitypub/controllers => webserver/handlers/federation}/object.go (59%) rename {activitypub/controllers => webserver/handlers/federation}/outbox.go (82%) rename {activitypub/controllers => webserver/handlers/federation}/webfinger.go (92%) diff --git a/activitypub/activitypub.go b/activitypub/activitypub.go deleted file mode 100644 index 4b3dedaca..000000000 --- a/activitypub/activitypub.go +++ /dev/null @@ -1,61 +0,0 @@ -package activitypub - -import ( - "net/http" - - "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/storage/configrepository" - "github.com/owncast/owncast/storage/data" - - "github.com/owncast/owncast/models" - log "github.com/sirupsen/logrus" -) - -var configRepository = configrepository.Get() - -// Start will initialize and start the federation support. -func Start(datastore *data.Store, router *http.ServeMux) { - persistence.Setup(datastore) - workerpool.InitOutboundWorkerPool() - inbox.InitInboxWorkerPool() - StartRouter(router) - - // 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 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() -} diff --git a/activitypub/inbox/chat.go b/activitypub/inbox/chat.go deleted file mode 100644 index 70b1c3fef..000000000 --- a/activitypub/inbox/chat.go +++ /dev/null @@ -1,64 +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/storage/configrepository" -) - -var configRepository = configrepository.Get() - -func handleEngagementActivity(eventType events.EventType, isLiveNotification bool, actorReference vocab.ActivityStreamsActorProperty, action string) error { - // Do nothing if displaying engagement actions has been turned off. - if !configRepository.GetFederationShowEngagement() { - return nil - } - - // Do nothing if chat is disabled - if configRepository.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.", configRepository.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.", configRepository.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 -} diff --git a/activitypub/inbox/update.go b/activitypub/inbox/update.go deleted file mode 100644 index 047ed3398..000000000 --- a/activitypub/inbox/update.go +++ /dev/null @@ -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()) -} diff --git a/activitypub/router.go b/activitypub/router.go deleted file mode 100644 index 0d13b1115..000000000 --- a/activitypub/router.go +++ /dev/null @@ -1,35 +0,0 @@ -package activitypub - -import ( - "net/http" - - "github.com/owncast/owncast/activitypub/controllers" - "github.com/owncast/owncast/webserver/middleware" -) - -// StartRouter will start the federation specific http router. -func StartRouter(router *http.ServeMux) { - // WebFinger - router.HandleFunc("/.well-known/webfinger", controllers.WebfingerHandler) - - // Host Metadata - router.HandleFunc("/.well-known/host-meta", controllers.HostMetaController) - - // Nodeinfo v1 - router.HandleFunc("/.well-known/nodeinfo", controllers.NodeInfoController) - - // x-nodeinfo v2 - router.HandleFunc("/.well-known/x-nodeinfo2", controllers.XNodeInfo2Controller) - - // Nodeinfo v2 - router.HandleFunc("/nodeinfo/2.0", controllers.NodeInfoV2Controller) - - // Instance details - router.HandleFunc("/api/v1/instance", controllers.InstanceV1Controller) - - // Single ActivityPub Actor - router.HandleFunc("/federation/user/", middleware.RequireActivityPubOrRedirect(controllers.ActorHandler)) - - // Single AP object - router.HandleFunc("/federation/", middleware.RequireActivityPubOrRedirect(controllers.ObjectHandler)) -} diff --git a/cmd/application.go b/cmd/application.go new file mode 100644 index 000000000..93afaed81 --- /dev/null +++ b/cmd/application.go @@ -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() +} diff --git a/cmd/backuprestore.go b/cmd/backuprestore.go new file mode 100644 index 000000000..1d10cf2aa --- /dev/null +++ b/cmd/backuprestore.go @@ -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.") +} diff --git a/core/chat/concurrentConnections.go b/cmd/concurrentConnections.go similarity index 61% rename from core/chat/concurrentConnections.go rename to cmd/concurrentConnections.go index cbbea81c3..03b053cb9 100644 --- a/core/chat/concurrentConnections.go +++ b/cmd/concurrentConnections.go @@ -2,7 +2,7 @@ //go:build !freebsd && !windows // +build !freebsd,!windows -package chat +package cmd import ( "syscall" @@ -24,3 +24,15 @@ func setSystemConcurrentConnectionLimit(limit int64) { log.Traceln("Max process connection count changed from system limit of", originalLimit, "to", limit) } + +func getMaximumConcurrentConnectionLimit() int64 { + var rLimit syscall.Rlimit + if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil { + log.Fatalln(err) + } + + // Return the limit to 70% of max so the machine doesn't die even if it's maxed out for some reason. + proposedLimit := int64(float32(rLimit.Max) * 0.7) + + return proposedLimit +} diff --git a/core/chat/concurrentConnections_freebsd.go b/cmd/concurrentConnections_freebsd.go similarity index 97% rename from core/chat/concurrentConnections_freebsd.go rename to cmd/concurrentConnections_freebsd.go index 68d4955b3..d948c01eb 100644 --- a/core/chat/concurrentConnections_freebsd.go +++ b/cmd/concurrentConnections_freebsd.go @@ -1,7 +1,7 @@ //go:build freebsd // +build freebsd -package chat +package cmd import ( "syscall" diff --git a/core/chat/concurrentConnections_windows.go b/cmd/concurrentConnections_windows.go similarity index 87% rename from core/chat/concurrentConnections_windows.go rename to cmd/concurrentConnections_windows.go index ef61843b3..7ccbb51de 100644 --- a/core/chat/concurrentConnections_windows.go +++ b/cmd/concurrentConnections_windows.go @@ -1,6 +1,6 @@ //go:build windows // +build windows -package chat +package cmd func setSystemConcurrentConnectionLimit(limit int64) {} diff --git a/cmd/config.go b/cmd/config.go new file mode 100644 index 000000000..178ef7868 --- /dev/null +++ b/cmd/config.go @@ -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) + } + } +} diff --git a/cmd/console.go b/cmd/console.go new file mode 100644 index 000000000..1a3ddb059 --- /dev/null +++ b/cmd/console.go @@ -0,0 +1,7 @@ +package cmd + +import log "github.com/sirupsen/logrus" + +func (app *Application) showStartupMessage() { + log.Infoln(app.configservice.GetReleaseString()) +} diff --git a/cmd/data.go b/cmd/data.go new file mode 100644 index 000000000..a21ed05c4 --- /dev/null +++ b/cmd/data.go @@ -0,0 +1,4 @@ +package cmd + +func initializeData() { +} diff --git a/cmd/flags.go b/cmd/flags.go new file mode 100644 index 000000000..c99c3bea4 --- /dev/null +++ b/cmd/flags.go @@ -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() +} diff --git a/cmd/services.go b/cmd/services.go new file mode 100644 index 000000000..24144ba3c --- /dev/null +++ b/cmd/services.go @@ -0,0 +1,4 @@ +package cmd + +func (app *Application) startServices() { +} diff --git a/cmd/setup.go b/cmd/setup.go new file mode 100644 index 000000000..2ed8b314d --- /dev/null +++ b/cmd/setup.go @@ -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) + } + } +} diff --git a/core/chat/utils_windows.go b/cmd/utils_windows.go similarity index 100% rename from core/chat/utils_windows.go rename to cmd/utils_windows.go diff --git a/core/chat/chat.go b/core/chat/chat.go deleted file mode 100644 index 6b27d4a85..000000000 --- a/core/chat/chat.go +++ /dev/null @@ -1,187 +0,0 @@ -package chat - -import ( - "errors" - "net/http" - "sort" - - "github.com/owncast/owncast/core/chat/events" - "github.com/owncast/owncast/models" - "github.com/owncast/owncast/services/config" - "github.com/owncast/owncast/storage/configrepository" - "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 -) - -var configRepository = configrepository.Get() - -// 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) - c := config.GetConfig() - - chatMessagesSentCounter = promauto.NewGauge(prometheus.GaugeOpts{ - Name: "total_chat_message_count", - Help: "The number of chat messages incremented over time.", - ConstLabels: map[string]string{ - "version": c.VersionNumber, - "host": configRepository.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) -} diff --git a/core/chat/events/systemMessageEvent.go b/core/chat/events/systemMessageEvent.go deleted file mode 100644 index 9c9ec25df..000000000 --- a/core/chat/events/systemMessageEvent.go +++ /dev/null @@ -1,25 +0,0 @@ -package events - -// SystemMessageEvent is a message displayed in chat on behalf of the server. -type SystemMessageEvent struct { - Event - MessageEvent -} - -// GetBroadcastPayload will return the object to send to all chat users. -func (e *SystemMessageEvent) GetBroadcastPayload() EventPayload { - return EventPayload{ - "id": e.ID, - "timestamp": e.Timestamp, - "body": e.Body, - "type": SystemMessageSent, - "user": EventPayload{ - "displayName": configRepository.GetServerName(), - }, - } -} - -// GetMessageType will return the event type for this message. -func (e *SystemMessageEvent) GetMessageType() EventType { - return SystemMessageSent -} diff --git a/core/chat/events/userJoinedEvent.go b/core/chat/events/userJoinedEvent.go deleted file mode 100644 index 475b74ce5..000000000 --- a/core/chat/events/userJoinedEvent.go +++ /dev/null @@ -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, - } -} diff --git a/core/chat/events/userMessageEvent.go b/core/chat/events/userMessageEvent.go deleted file mode 100644 index f0dd95e91..000000000 --- a/core/chat/events/userMessageEvent.go +++ /dev/null @@ -1,25 +0,0 @@ -package events - -// UserMessageEvent is an inbound message from a user. -type UserMessageEvent struct { - Event - UserEvent - MessageEvent -} - -// GetBroadcastPayload will return the object to send to all chat users. -func (e *UserMessageEvent) GetBroadcastPayload() EventPayload { - return EventPayload{ - "id": e.ID, - "timestamp": e.Timestamp, - "body": e.Body, - "user": e.User, - "type": MessageSent, - "visible": e.HiddenAt == nil, - } -} - -// GetMessageType will return the event type for this message. -func (e *UserMessageEvent) GetMessageType() EventType { - return MessageSent -} diff --git a/core/chat/utils.go b/core/chat/utils.go deleted file mode 100644 index fae7beb29..000000000 --- a/core/chat/utils.go +++ /dev/null @@ -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 -} diff --git a/core/core.go b/core/core.go index 6c53c33dc..39465030e 100644 --- a/core/core.go +++ b/core/core.go @@ -9,9 +9,9 @@ import ( "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/models" - "github.com/owncast/owncast/services/auth" "github.com/owncast/owncast/services/config" "github.com/owncast/owncast/services/notifications" + "github.com/owncast/owncast/services/status" "github.com/owncast/owncast/services/webhooks" "github.com/owncast/owncast/services/yp" "github.com/owncast/owncast/storage/configrepository" @@ -31,11 +31,10 @@ var ( fileWriter = transcoder.FileWriterReceiverService{} ) -var configRepository = configrepository.Get() - // Start starts up the core processing. func Start() error { resetDirectories() + configRepository := configrepository.Get() configRepository.PopulateDefaults() @@ -59,7 +58,7 @@ func Start() error { } // user.SetupUsers() - auth.Setup(data.GetDatastore()) + // auth.Setup(data.GetDatastore()) fileWriter.SetupFileWriterReceiverService(&handler) @@ -68,77 +67,29 @@ func Start() error { return err } - _yp = yp.NewYP(GetStatus) + s := status.Get() + gsf := func() *models.Status { + s := status.Get() + return &s.Status + } - if err := chat.Start(GetStatus); err != nil { + _yp = yp.NewYP(gsf) + + if err := chat.Start(gsf); err != nil { log.Errorln(err) } // start the rtmp server - go rtmp.Start(setStreamAsConnected, setBroadcaster) + go rtmp.Start(setStreamAsConnected, s.SetBroadcaster) rtmpPort := configRepository.GetRTMPPortNumber() if rtmpPort != 1935 { log.Infof("RTMP is accepting inbound streams on port %d.", rtmpPort) } - webhooks.InitTemporarySingleton(GetStatus) + webhooks.InitTemporarySingleton(gsf) notifications.Setup(data.GetDatastore()) return nil } - -func createInitialOfflineState() error { - transitionToOfflineVideoStreamContent() - - return nil -} - -// transitionToOfflineVideoStreamContent will overwrite the current stream with the -// offline video stream state only. No live stream HLS segments will continue to be -// referenced. -func transitionToOfflineVideoStreamContent() { - log.Traceln("Firing transcoder with offline stream state") - - _transcoder := transcoder.NewTranscoder() - _transcoder.SetIdentifier("offline") - _transcoder.SetLatencyLevel(models.GetLatencyLevel(4)) - _transcoder.SetIsEvent(true) - - offlineFilePath, err := saveOfflineClipToDisk("offline.ts") - if err != nil { - log.Fatalln("unable to save offline clip:", err) - } - - _transcoder.SetInput(offlineFilePath) - go _transcoder.Start(false) - - // Copy the logo to be the thumbnail - logo := configRepository.GetLogoPath() - c := config.GetConfig() - dst := filepath.Join(c.TempDir, "thumbnail.jpg") - if err = utils.Copy(filepath.Join("data", logo), dst); err != nil { - log.Warnln(err) - } - - // Delete the preview Gif - _ = os.Remove(path.Join(config.DataDirectory, "preview.gif")) -} - -func resetDirectories() { - log.Trace("Resetting file directories to a clean slate.") - - // Wipe hls data directory - c := config.GetConfig() - utils.CleanupDirectory(c.HLSStoragePath) - - // Remove the previous thumbnail - logo := 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) - } - } -} diff --git a/core/offlineState.go b/core/offlineState.go index 19e7d874e..09242032f 100644 --- a/core/offlineState.go +++ b/core/offlineState.go @@ -19,7 +19,7 @@ func appendOfflineToVariantPlaylist(index int, playlistFilePath string) { return } - c := config.GetConfig() + c := config.Get() tmpFileName := fmt.Sprintf("tmp-stream-%d.m3u8", index) atomicWriteTmpPlaylistFile, err := os.CreateTemp(c.TempDir, tmpFileName) if err != nil { @@ -50,7 +50,7 @@ func appendOfflineToVariantPlaylist(index int, playlistFilePath string) { } func makeVariantIndexOffline(index int, offlineFilePath string, offlineFilename string) { - c := config.GetConfig() + c := config.Get() playlistFilePath := fmt.Sprintf(filepath.Join(c.HLSStoragePath, "%d/stream.m3u8"), index) segmentFilePath := fmt.Sprintf(filepath.Join(c.HLSStoragePath, "%d/%s"), index, offlineFilename) @@ -96,7 +96,7 @@ func createEmptyOfflinePlaylist(playlistFilePath string, offlineFilename string) func saveOfflineClipToDisk(offlineFilename string) (string, error) { offlineFileData := static.GetOfflineSegment() - c := config.GetConfig() + c := config.Get() offlineTmpFile, err := os.CreateTemp(c.TempDir, offlineFilename) if err != nil { log.Errorln("unable to create temp file for offline video segment", err) diff --git a/core/stats.go b/core/stats.go index cee571251..a447bbae4 100644 --- a/core/stats.go +++ b/core/stats.go @@ -9,6 +9,7 @@ import ( "github.com/owncast/owncast/models" "github.com/owncast/owncast/services/geoip" + "github.com/owncast/owncast/storage/configrepository" ) var ( @@ -44,6 +45,8 @@ func IsStreamConnected() bool { return false } + configRepository := configrepository.Get() + // Kind of a hack. It takes a handful of seconds between a RTMP connection and when HLS data is available. // So account for that with an artificial buffer of four segments. timeSinceLastConnected := time.Since(_stats.LastConnectTime.Time).Seconds() @@ -110,6 +113,8 @@ func pruneViewerCount() { } func saveStats() { + configRepository := configrepository.Get() + if err := configRepository.SetPeakOverallViewerCount(_stats.OverallMaxViewerCount); err != nil { log.Errorln("error saving viewer count", err) } @@ -124,6 +129,8 @@ func saveStats() { } func getSavedStats() models.Stats { + configRepository := configrepository.Get() + savedLastDisconnectTime, _ := configRepository.GetLastDisconnectTime() result := models.Stats{ diff --git a/core/status.go b/core/status.go deleted file mode 100644 index 6e8542467..000000000 --- a/core/status.go +++ /dev/null @@ -1,46 +0,0 @@ -package core - -import ( - "github.com/owncast/owncast/models" - "github.com/owncast/owncast/services/config" -) - -// 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) - } - - c := config.GetConfig() - - return models.Status{ - Online: IsStreamConnected(), - ViewerCount: viewerCount, - OverallMaxViewerCount: _stats.OverallMaxViewerCount, - SessionMaxViewerCount: _stats.SessionMaxViewerCount, - LastDisconnectTime: _stats.LastDisconnectTime, - LastConnectTime: _stats.LastConnectTime, - VersionNumber: c.VersionNumber, - StreamTitle: configRepository.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 -} diff --git a/core/streamState.go b/core/streamState.go deleted file mode 100644 index 8cd64c97f..000000000 --- a/core/streamState.go +++ /dev/null @@ -1,203 +0,0 @@ -package core - -import ( - "context" - "io" - "time" - - log "github.com/sirupsen/logrus" - - "github.com/owncast/owncast/activitypub" - "github.com/owncast/owncast/core/chat" - "github.com/owncast/owncast/models" - "github.com/owncast/owncast/services/config" - "github.com/owncast/owncast/services/notifications" - "github.com/owncast/owncast/services/webhooks" - "github.com/owncast/owncast/storage/data" - "github.com/owncast/owncast/utils" - "github.com/owncast/owncast/video/rtmp" - "github.com/owncast/owncast/video/transcoder" -) - -// 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: configRepository.GetStreamLatencyLevel(), - OutputSettings: configRepository.GetStreamOutputVariants(), - } - - StopOfflineCleanupTimer() - startOnlineCleanupTimer() - - if _yp != nil { - go _yp.Start() - } - - c := config.GetConfig() - segmentPath := c.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) - }() - - webhookManager := webhooks.Get() - go webhookManager.SendStreamStatusEvent(models.StreamStarted) - transcoder.StartThumbnailGenerator(segmentPath, configRepository.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() - - webhookManager := webhooks.Get() - go webhookManager.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 configRepository.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 -} diff --git a/logging/logging.go b/logging/logging.go index aa9867be5..81e01595b 100644 --- a/logging/logging.go +++ b/logging/logging.go @@ -30,9 +30,9 @@ type OCLogger struct { var Logger *OCLogger // Setup configures our custom logging destinations. -func Setup(enableDebugOptions bool, enableVerboseLogging bool) { +func Setup(enableDebugOptions bool, enableVerboseLogging bool, logDirectory string) { // Create the logging directory if needed - loggingDirectory := filepath.Dir(getLogFilePath()) + loggingDirectory := filepath.Dir(logDirectory) if !utils.DoesFileExists(loggingDirectory) { if err := os.Mkdir(loggingDirectory, 0o700); err != nil { logger.Errorln("unable to create logs directory", loggingDirectory, err) @@ -40,10 +40,10 @@ func Setup(enableDebugOptions bool, enableVerboseLogging bool) { } // Write logs to a file - path := getLogFilePath() + logFile := filepath.Join(logDirectory, "owncast.log") writer, _ := rotatelogs.New( - path+".%Y%m%d%H%M", - rotatelogs.WithLinkName(path), + logFile+".%Y%m%d%H%M", + rotatelogs.WithLinkName(logFile), rotatelogs.WithMaxAge(time.Duration(86400)*time.Second), rotatelogs.WithRotationTime(time.Duration(604800)*time.Second), ) diff --git a/logging/paths.go b/logging/paths.go deleted file mode 100644 index a3b747648..000000000 --- a/logging/paths.go +++ /dev/null @@ -1,18 +0,0 @@ -package logging - -import ( - "path/filepath" - - "github.com/owncast/owncast/services/config" -) - -// GetTranscoderLogFilePath returns the logging path for the transcoder log output. -func GetTranscoderLogFilePath() string { - c := config.GetConfig() - return filepath.Join(c.LogDirectory, "transcoder.log") -} - -func getLogFilePath() string { - c := config.GetConfig() - return filepath.Join(c.LogDirectory, "owncast.log") -} diff --git a/main.go b/main.go index d8a7e025f..33e99ed69 100644 --- a/main.go +++ b/main.go @@ -1,173 +1,43 @@ package main -import ( - "flag" - "os" - "strconv" +import "github.com/owncast/owncast/cmd" - "github.com/owncast/owncast/logging" - "github.com/owncast/owncast/storage/configrepository" - "github.com/owncast/owncast/webserver" - log "github.com/sirupsen/logrus" - - "github.com/owncast/owncast/core" - configservice "github.com/owncast/owncast/services/config" - "github.com/owncast/owncast/services/metrics" - - "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") - config *configservice.Config -) - -var configRepository = configrepository.Get() - -// nolint:cyclop func main() { - flag.Parse() - - config = configservice.NewConfig() - - 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) - } - - handleCommandLineFlags() - - // starts the core - if err := core.Start(); err != nil { - log.Fatalln("failed to start the core package", err) - } - - go metrics.Start(core.GetStatus) - - webserver := webserver.New() - if err := webserver.Start(config.WebServerIP, config.WebServerPort); err != nil { - log.Fatalln("failed to start/run the web server", err) - } + app := &cmd.Application{} + app.Start() } -func handleCommandLineFlags() { - 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") - } - } +// var configRepository = configrepository.Get() - if *newStreamKey != "" { - log.Println("Temporary stream key is set for this session.") - config.TemporaryStreamKey = *newStreamKey - } +// // nolint:cyclop +// func main() { +// flag.Parse() - // Set the web server port - if *webServerPortOverride != "" { - portNumber, err := strconv.Atoi(*webServerPortOverride) - if err != nil { - log.Warnln(err) - return - } +// config = configservice.NewConfig() - log.Println("Saving new web server port number to", portNumber) - if err := configRepository.SetHTTPPortNumber(float64(portNumber)); err != nil { - log.Errorln(err) - } - } - config.WebServerPort = configRepository.GetHTTPPortNumber() +// // Otherwise save the default emoji to the data directory. +// if err := data.SetupEmojiDirectory(); err != nil { +// log.Fatalln("Cannot set up emoji directory", err) +// } - // 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) - } - } - config.WebServerIP = configRepository.GetHTTPListenAddress() +// if err := data.SetupPersistence(config.DatabaseFilePath); err != nil { +// log.Fatalln("failed to open database", err) +// } - // 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) - } - } -} +// handleCommandLineFlags() -func configureLogging(enableDebugFeatures bool, enableVerboseLogging bool) { - logging.Setup(enableDebugFeatures, enableVerboseLogging) - log.SetFormatter(&log.TextFormatter{ - FullTimestamp: true, - }) -} +// // starts the core +// if err := core.Start(); err != nil { +// log.Fatalln("failed to start the core package", err) +// } + +// go metrics.Start(core.GetStatus) + +// webserver := webserver.New() +// if err := webserver.Start(config.WebServerIP, config.WebServerPort); err != nil { +// log.Fatalln("failed to start/run the web server", err) +// } +// } + +// func handleCommandLineFlags() { +// } diff --git a/models/chatAccessScopes.go b/models/chatAccessScopes.go new file mode 100644 index 000000000..2ec754dc2 --- /dev/null +++ b/models/chatAccessScopes.go @@ -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" +) diff --git a/core/chat/events/eventtype.go b/models/chatEventTypes.go similarity index 95% rename from core/chat/events/eventtype.go rename to models/chatEventTypes.go index 2cc05b58f..2ee0ef8a8 100644 --- a/core/chat/events/eventtype.go +++ b/models/chatEventTypes.go @@ -1,4 +1,4 @@ -package events +package models // EventType is the type of a websocket event. type EventType = string @@ -16,6 +16,8 @@ const ( UserColorChanged EventType = "COLOR_CHANGE" // VisibiltyUpdate is the event sent when a chat message's visibility changes. VisibiltyUpdate EventType = "VISIBILITY-UPDATE" + // VisibiltyToggled is the event sent when a chat message's visibility changes. + VisibiltyToggled EventType = "VISIBILITY-UPDATE" // PING is a ping message. PING EventType = "PING" // PONG is a pong message. diff --git a/core/chat/events/connectedClientInfo.go b/models/connectedClientInfo.go similarity index 56% rename from core/chat/events/connectedClientInfo.go rename to models/connectedClientInfo.go index 2223af361..55126b3dc 100644 --- a/core/chat/events/connectedClientInfo.go +++ b/models/connectedClientInfo.go @@ -1,9 +1,7 @@ -package events - -import "github.com/owncast/owncast/models" +package models // ConnectedClientInfo represents the information about a connected client. type ConnectedClientInfo struct { Event - User *models.User `json:"user"` + User *User `json:"user"` } diff --git a/models/event.go b/models/event.go new file mode 100644 index 000000000..e835fd03c --- /dev/null +++ b/models/event.go @@ -0,0 +1,20 @@ +package models + +import ( + "time" + + "github.com/teris-io/shortid" +) + +// Event is any kind of event. A type is required to be specified. +type Event struct { + Timestamp time.Time `json:"timestamp"` + Type EventType `json:"type,omitempty"` + ID string `json:"id"` +} + +// SetDefaults will set default properties of all inbound events. +func (e *Event) SetDefaults() { + e.ID = shortid.MustGenerate() + e.Timestamp = time.Now() +} diff --git a/models/eventPayload.go b/models/eventPayload.go new file mode 100644 index 000000000..b234969fb --- /dev/null +++ b/models/eventPayload.go @@ -0,0 +1,4 @@ +package models + +// EventPayload is a generic key/value map for sending out to chat clients. +type EventPayload map[string]interface{} diff --git a/models/eventType.go b/models/eventType.go deleted file mode 100644 index dd36295ba..000000000 --- a/models/eventType.go +++ /dev/null @@ -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" -) diff --git a/core/chat/events/nameChangeEvent.go b/models/nameChangeEvent.go similarity index 98% rename from core/chat/events/nameChangeEvent.go rename to models/nameChangeEvent.go index e5659a18f..de1e80cb6 100644 --- a/core/chat/events/nameChangeEvent.go +++ b/models/nameChangeEvent.go @@ -1,4 +1,4 @@ -package events +package models // NameChangeEvent is received when a user changes their chat display name. type NameChangeEvent struct { diff --git a/core/chat/events/setMessageVisibilityEvent.go b/models/setMessageVisibilityEvent.go similarity index 97% rename from core/chat/events/setMessageVisibilityEvent.go rename to models/setMessageVisibilityEvent.go index 6d82d6b0a..10a79ce62 100644 --- a/core/chat/events/setMessageVisibilityEvent.go +++ b/models/setMessageVisibilityEvent.go @@ -1,4 +1,4 @@ -package events +package models // SetMessageVisibilityEvent is the event fired when one or more message // visibilities are changed. diff --git a/models/stats.go b/models/stats.go index 6a69321d2..4bf7d18da 100644 --- a/models/stats.go +++ b/models/stats.go @@ -6,9 +6,9 @@ import ( // Stats holds the stats for the system. type Stats struct { + LastConnectTime *utils.NullTime `json:"lastConnectTime"` LastDisconnectTime *utils.NullTime `json:"lastDisconnectTime"` - LastConnectTime *utils.NullTime `json:"-"` ChatClients map[string]Client `json:"-"` Viewers map[string]*Viewer `json:"-"` SessionMaxViewerCount int `json:"sessionMaxViewerCount"` diff --git a/core/chat/events/userDisabledEvent.go b/models/userDisabledEvent.go similarity index 96% rename from core/chat/events/userDisabledEvent.go rename to models/userDisabledEvent.go index d869210ef..53ad3bacb 100644 --- a/core/chat/events/userDisabledEvent.go +++ b/models/userDisabledEvent.go @@ -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 { diff --git a/models/userEvent.go b/models/userEvent.go new file mode 100644 index 000000000..be421119e --- /dev/null +++ b/models/userEvent.go @@ -0,0 +1,10 @@ +package models + +import "time" + +// UserEvent is an event with an associated user. +type UserEvent struct { + User *User `json:"user"` + HiddenAt *time.Time `json:"hiddenAt,omitempty"` + ClientID uint `json:"clientId,omitempty"` +} diff --git a/models/userJoinedEvent.go b/models/userJoinedEvent.go index de7c55170..1d344d98c 100644 --- a/models/userJoinedEvent.go +++ b/models/userJoinedEvent.go @@ -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, + } } diff --git a/services/apfederation/activitypub.go b/services/apfederation/activitypub.go new file mode 100644 index 000000000..bad648943 --- /dev/null +++ b/services/apfederation/activitypub.go @@ -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() +} diff --git a/activitypub/apmodels/activity.go b/services/apfederation/apmodels/activity.go similarity index 100% rename from activitypub/apmodels/activity.go rename to services/apfederation/apmodels/activity.go diff --git a/activitypub/apmodels/actor.go b/services/apfederation/apmodels/actor.go similarity index 99% rename from activitypub/apmodels/actor.go rename to services/apfederation/apmodels/actor.go index becf15ab6..7ca9d00be 100644 --- a/activitypub/apmodels/actor.go +++ b/services/apfederation/apmodels/actor.go @@ -8,8 +8,8 @@ import ( "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" - "github.com/owncast/owncast/activitypub/crypto" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/apfederation/crypto" "github.com/owncast/owncast/storage/configrepository" log "github.com/sirupsen/logrus" ) diff --git a/activitypub/apmodels/actor_test.go b/services/apfederation/apmodels/actor_test.go similarity index 100% rename from activitypub/apmodels/actor_test.go rename to services/apfederation/apmodels/actor_test.go diff --git a/activitypub/apmodels/hashtag.go b/services/apfederation/apmodels/hashtag.go similarity index 100% rename from activitypub/apmodels/hashtag.go rename to services/apfederation/apmodels/hashtag.go diff --git a/activitypub/apmodels/inboxRequest.go b/services/apfederation/apmodels/inboxRequest.go similarity index 100% rename from activitypub/apmodels/inboxRequest.go rename to services/apfederation/apmodels/inboxRequest.go diff --git a/activitypub/apmodels/message.go b/services/apfederation/apmodels/message.go similarity index 100% rename from activitypub/apmodels/message.go rename to services/apfederation/apmodels/message.go diff --git a/activitypub/apmodels/utils.go b/services/apfederation/apmodels/utils.go similarity index 100% rename from activitypub/apmodels/utils.go rename to services/apfederation/apmodels/utils.go diff --git a/activitypub/apmodels/webfinger.go b/services/apfederation/apmodels/webfinger.go similarity index 100% rename from activitypub/apmodels/webfinger.go rename to services/apfederation/apmodels/webfinger.go diff --git a/activitypub/crypto/keys.go b/services/apfederation/crypto/keys.go similarity index 100% rename from activitypub/crypto/keys.go rename to services/apfederation/crypto/keys.go diff --git a/activitypub/crypto/publicKey.go b/services/apfederation/crypto/publicKey.go similarity index 100% rename from activitypub/crypto/publicKey.go rename to services/apfederation/crypto/publicKey.go diff --git a/activitypub/crypto/sign.go b/services/apfederation/crypto/sign.go similarity index 99% rename from activitypub/crypto/sign.go rename to services/apfederation/crypto/sign.go index cc1cc481c..44154848b 100644 --- a/activitypub/crypto/sign.go +++ b/services/apfederation/crypto/sign.go @@ -77,7 +77,7 @@ func CreateSignedRequest(payload []byte, url *url.URL, fromActorIRI *url.URL) (* req, _ := http.NewRequest("POST", url.String(), bytes.NewBuffer(payload)) - c := config.GetConfig() + c := config.Get() ua := fmt.Sprintf("%s; https://owncast.online", c.GetReleaseString()) req.Header.Set("User-Agent", ua) diff --git a/activitypub/inbox/announce.go b/services/apfederation/inbox/announce.go similarity index 52% rename from activitypub/inbox/announce.go rename to services/apfederation/inbox/announce.go index db79393f7..fe760458b 100644 --- a/activitypub/inbox/announce.go +++ b/services/apfederation/inbox/announce.go @@ -5,23 +5,22 @@ import ( "time" "github.com/go-fed/activity/streams/vocab" - "github.com/owncast/owncast/activitypub/persistence" - "github.com/owncast/owncast/core/chat/events" + "github.com/owncast/owncast/models" "github.com/pkg/errors" ) -func handleAnnounceRequest(c context.Context, activity vocab.ActivityStreamsAnnounce) error { +func (api *APInbox) handleAnnounceRequest(c context.Context, activity vocab.ActivityStreamsAnnounce) error { object := activity.GetActivityStreamsObject() actorReference := activity.GetActivityStreamsActor() objectIRI := object.At(0).GetIRI().String() actorIRI := actorReference.At(0).GetIRI().String() - if hasPreviouslyhandled, err := persistence.HasPreviouslyHandledInboundActivity(objectIRI, actorIRI, events.FediverseEngagementRepost); hasPreviouslyhandled || err != nil { + if hasPreviouslyhandled, err := api.federationRepository.HasPreviouslyHandledInboundActivity(objectIRI, actorIRI, models.FediverseEngagementRepost); hasPreviouslyhandled || err != nil { return errors.Wrap(err, "inbound activity of share/re-post has already been handled") } // Shares need to match a post we had already sent. - _, isLiveNotification, timestamp, err := persistence.GetObjectByIRI(objectIRI) + _, isLiveNotification, timestamp, err := api.federationRepository.GetObjectByIRI(objectIRI) if err != nil { return errors.Wrap(err, "Could not find post locally") } @@ -32,9 +31,9 @@ func handleAnnounceRequest(c context.Context, activity vocab.ActivityStreamsAnno } // Save as an accepted activity - if err := persistence.SaveInboundFediverseActivity(objectIRI, actorIRI, events.FediverseEngagementRepost, time.Now()); err != nil { + if err := api.federationRepository.SaveInboundFediverseActivity(objectIRI, actorIRI, models.FediverseEngagementRepost, time.Now()); err != nil { return errors.Wrap(err, "unable to save inbound share/re-post activity") } - return handleEngagementActivity(events.FediverseEngagementRepost, isLiveNotification, actorReference, events.FediverseEngagementRepost) + return api.handleEngagementActivity(models.FediverseEngagementRepost, isLiveNotification, actorReference, models.FediverseEngagementRepost) } diff --git a/services/apfederation/inbox/chat.go b/services/apfederation/inbox/chat.go new file mode 100644 index 000000000..a47a63ff5 --- /dev/null +++ b/services/apfederation/inbox/chat.go @@ -0,0 +1,59 @@ +package inbox + +import ( + "fmt" + + "github.com/go-fed/activity/streams/vocab" + "github.com/owncast/owncast/models" +) + +func (api *APInbox) handleEngagementActivity(eventType models.EventType, isLiveNotification bool, actorReference vocab.ActivityStreamsActorProperty, action string) error { + // Do nothing if displaying engagement actions has been turned off. + if !api.configRepository.GetFederationShowEngagement() { + return nil + } + + // Do nothing if chat is disabled + if api.configRepository.GetChatDisabled() { + return nil + } + + // Get actor of the action + actor, _ := api.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 == models.FediverseEngagementLike { + suffix = "liked that this stream went live." + } else if action == models.FediverseEngagementLike { + suffix = fmt.Sprintf("liked a post from %s.", api.configRepository.GetServerName()) + } else if isLiveNotification && action == models.FediverseEngagementRepost { + suffix = "shared this stream with their followers." + } else if action == models.FediverseEngagementRepost { + suffix = fmt.Sprintf("shared a post from %s.", api.configRepository.GetServerName()) + } else if action == models.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 := api.chatService.SendFediverseAction(eventType, actor.FullUsername, image, body, actorIRI); err != nil { + return err + } + + return nil +} diff --git a/activitypub/inbox/constants.go b/services/apfederation/inbox/constants.go similarity index 100% rename from activitypub/inbox/constants.go rename to services/apfederation/inbox/constants.go diff --git a/activitypub/inbox/create.go b/services/apfederation/inbox/create.go similarity index 67% rename from activitypub/inbox/create.go rename to services/apfederation/inbox/create.go index 3eab72a5e..a60f16569 100644 --- a/activitypub/inbox/create.go +++ b/services/apfederation/inbox/create.go @@ -7,7 +7,7 @@ import ( "github.com/pkg/errors" ) -func handleCreateRequest(c context.Context, activity vocab.ActivityStreamsCreate) error { +func (api *APInbox) handleCreateRequest(c context.Context, activity vocab.ActivityStreamsCreate) error { iri := activity.GetJSONLDId().GetIRI().String() return errors.New("not handling create request of: " + iri) } diff --git a/activitypub/inbox/follow.go b/services/apfederation/inbox/follow.go similarity index 55% rename from activitypub/inbox/follow.go rename to services/apfederation/inbox/follow.go index e9e4ec0af..ed32115f9 100644 --- a/activitypub/inbox/follow.go +++ b/services/apfederation/inbox/follow.go @@ -6,17 +6,15 @@ import ( "time" "github.com/go-fed/activity/streams/vocab" - "github.com/owncast/owncast/activitypub/persistence" - "github.com/owncast/owncast/activitypub/requests" - "github.com/owncast/owncast/activitypub/resolvers" - "github.com/owncast/owncast/core/chat/events" + "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/apfederation/outbox" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) -func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsFollow) error { - follow, err := resolvers.MakeFollowRequest(c, activity) +func (api *APInbox) handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsFollow) error { + follow, err := api.resolvers.MakeFollowRequest(c, activity) if err != nil { log.Errorln("unable to create follow inbox request", err) return err @@ -26,19 +24,21 @@ func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsF return fmt.Errorf("unable to handle request") } - approved := !configRepository.GetFederationIsPrivate() + approved := !api.configRepository.GetFederationIsPrivate() followRequest := *follow - if err := persistence.AddFollow(followRequest, approved); err != nil { + if err := api.federationRepository.AddFollow(followRequest, approved); err != nil { log.Errorln("unable to save follow request", err) return err } - localAccountName := configRepository.GetDefaultFederationUsername() + localAccountName := api.configRepository.GetDefaultFederationUsername() + + ob := outbox.Get() if approved { - if err := requests.SendFollowAccept(follow.Inbox, activity, localAccountName); err != nil { + if err := ob.SendFollowAccept(follow.Inbox, activity, localAccountName); err != nil { log.Errorln("unable to send follow accept", err) return err } @@ -54,27 +54,27 @@ func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsF // chat due to a previous follow request, then do so. hasPreviouslyhandled := true // Default so we don't send anything if it fails. if approved { - hasPreviouslyhandled, err = persistence.HasPreviouslyHandledInboundActivity(objectIRI, actorIRI, events.FediverseEngagementFollow) + hasPreviouslyhandled, err = api.federationRepository.HasPreviouslyHandledInboundActivity(objectIRI, actorIRI, models.FediverseEngagementFollow) if err != nil { log.Errorln("error checking for previously handled follow activity", err) } } // Save this follow action to our activities table. - if err := persistence.SaveInboundFediverseActivity(objectIRI, actorIRI, events.FediverseEngagementFollow, time.Now()); err != nil { + if err := api.federationRepository.SaveInboundFediverseActivity(objectIRI, actorIRI, models.FediverseEngagementFollow, time.Now()); err != nil { return errors.Wrap(err, "unable to save inbound share/re-post activity") } // Send action to chat if it has not been previously handled. if !hasPreviouslyhandled { - return handleEngagementActivity(events.FediverseEngagementFollow, false, actorReference, events.FediverseEngagementFollow) + return api.handleEngagementActivity(models.FediverseEngagementFollow, false, actorReference, models.FediverseEngagementFollow) } return nil } -func handleUnfollowRequest(c context.Context, activity vocab.ActivityStreamsUndo) error { - request := resolvers.MakeUnFollowRequest(c, activity) +func (api *APInbox) handleUnfollowRequest(c context.Context, activity vocab.ActivityStreamsUndo) error { + request := api.resolvers.MakeUnFollowRequest(c, activity) if request == nil { log.Errorf("unable to handle unfollow request") return errors.New("unable to handle unfollow request") @@ -83,5 +83,5 @@ func handleUnfollowRequest(c context.Context, activity vocab.ActivityStreamsUndo unfollowRequest := *request log.Traceln("unfollow request:", unfollowRequest) - return persistence.RemoveFollow(unfollowRequest) + return api.federationRepository.RemoveFollow(unfollowRequest) } diff --git a/services/apfederation/inbox/inbox.go b/services/apfederation/inbox/inbox.go new file mode 100644 index 000000000..77d73d02b --- /dev/null +++ b/services/apfederation/inbox/inbox.go @@ -0,0 +1,37 @@ +package inbox + +import ( + "github.com/owncast/owncast/services/apfederation/requests" + "github.com/owncast/owncast/services/apfederation/resolvers" + "github.com/owncast/owncast/services/chat" + + "github.com/owncast/owncast/storage/configrepository" + "github.com/owncast/owncast/storage/federationrepository" +) + +type APInbox struct { + configRepository configrepository.ConfigRepository + federationRepository *federationrepository.FederationRepository + resolvers *resolvers.APResolvers + requests *requests.Requests + chatService *chat.Chat +} + +func New() *APInbox { + return &APInbox{ + configRepository: configrepository.Get(), + federationRepository: federationrepository.Get(), + resolvers: resolvers.Get(), + requests: requests.Get(), + chatService: chat.Get(), + } +} + +var temporaryGlobalInstance *APInbox + +func Get() *APInbox { + if temporaryGlobalInstance == nil { + temporaryGlobalInstance = New() + } + return temporaryGlobalInstance +} diff --git a/activitypub/inbox/like.go b/services/apfederation/inbox/like.go similarity index 57% rename from activitypub/inbox/like.go rename to services/apfederation/inbox/like.go index 4028d3d30..dd1313f06 100644 --- a/activitypub/inbox/like.go +++ b/services/apfederation/inbox/like.go @@ -5,12 +5,11 @@ import ( "time" "github.com/go-fed/activity/streams/vocab" - "github.com/owncast/owncast/activitypub/persistence" - "github.com/owncast/owncast/core/chat/events" + "github.com/owncast/owncast/models" "github.com/pkg/errors" ) -func handleLikeRequest(c context.Context, activity vocab.ActivityStreamsLike) error { +func (api *APInbox) handleLikeRequest(c context.Context, activity vocab.ActivityStreamsLike) error { object := activity.GetActivityStreamsObject() actorReference := activity.GetActivityStreamsActor() if object.Len() < 1 { @@ -24,12 +23,12 @@ func handleLikeRequest(c context.Context, activity vocab.ActivityStreamsLike) er objectIRI := object.At(0).GetIRI().String() actorIRI := actorReference.At(0).GetIRI().String() - if hasPreviouslyhandled, err := persistence.HasPreviouslyHandledInboundActivity(objectIRI, actorIRI, events.FediverseEngagementLike); hasPreviouslyhandled || err != nil { + if hasPreviouslyhandled, err := api.federationRepository.HasPreviouslyHandledInboundActivity(objectIRI, actorIRI, models.FediverseEngagementLike); hasPreviouslyhandled || err != nil { return errors.Wrap(err, "inbound activity of like has already been handled") } // Likes need to match a post we had already sent. - _, isLiveNotification, timestamp, err := persistence.GetObjectByIRI(objectIRI) + _, isLiveNotification, timestamp, err := api.federationRepository.GetObjectByIRI(objectIRI) if err != nil { return errors.Wrap(err, "Could not find post locally") } @@ -40,9 +39,9 @@ func handleLikeRequest(c context.Context, activity vocab.ActivityStreamsLike) er } // Save as an accepted activity - if err := persistence.SaveInboundFediverseActivity(objectIRI, actorIRI, events.FediverseEngagementLike, time.Now()); err != nil { + if err := api.federationRepository.SaveInboundFediverseActivity(objectIRI, actorIRI, models.FediverseEngagementLike, time.Now()); err != nil { return errors.Wrap(err, "unable to save inbound like activity") } - return handleEngagementActivity(events.FediverseEngagementLike, isLiveNotification, actorReference, events.FediverseEngagementLike) + return api.handleEngagementActivity(models.FediverseEngagementLike, isLiveNotification, actorReference, models.FediverseEngagementLike) } diff --git a/activitypub/inbox/undo.go b/services/apfederation/inbox/undo.go similarity index 77% rename from activitypub/inbox/undo.go rename to services/apfederation/inbox/undo.go index fd18fc6bb..9cbf166f0 100644 --- a/activitypub/inbox/undo.go +++ b/services/apfederation/inbox/undo.go @@ -8,13 +8,13 @@ import ( "github.com/go-fed/activity/streams/vocab" ) -func handleUndoInboxRequest(c context.Context, activity vocab.ActivityStreamsUndo) error { +func (api *APInbox) handleUndoInboxRequest(c context.Context, activity vocab.ActivityStreamsUndo) error { // Determine if this is an undo of a follow, favorite, announce, etc. o := activity.GetActivityStreamsObject() for iter := o.Begin(); iter != o.End(); iter = iter.Next() { if iter.IsActivityStreamsFollow() { // This is an Unfollow request - if err := handleUnfollowRequest(c, activity); err != nil { + if err := api.handleUnfollowRequest(c, activity); err != nil { return err } } else { diff --git a/services/apfederation/inbox/update.go b/services/apfederation/inbox/update.go new file mode 100644 index 000000000..879f07fe8 --- /dev/null +++ b/services/apfederation/inbox/update.go @@ -0,0 +1,23 @@ +package inbox + +import ( + "context" + + "github.com/go-fed/activity/streams/vocab" + log "github.com/sirupsen/logrus" +) + +func (api *APInbox) 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 := api.resolvers.GetResolvedActorFromActorProperty(activity.GetActivityStreamsActor()) + if err != nil { + log.Errorln(err) + return err + } + + return api.federationRepository.UpdateFollower(actor.ActorIri.String(), actor.Inbox.String(), actor.Name, actor.FullUsername, actor.Image.String()) +} diff --git a/activitypub/inbox/worker.go b/services/apfederation/inbox/worker.go similarity index 78% rename from activitypub/inbox/worker.go rename to services/apfederation/inbox/worker.go index 008d6d5a7..9695a5bf7 100644 --- a/activitypub/inbox/worker.go +++ b/services/apfederation/inbox/worker.go @@ -12,15 +12,13 @@ import ( "github.com/pkg/errors" "github.com/go-fed/httpsig" - "github.com/owncast/owncast/activitypub/apmodels" - "github.com/owncast/owncast/activitypub/persistence" - "github.com/owncast/owncast/activitypub/resolvers" + "github.com/owncast/owncast/services/apfederation/apmodels" log "github.com/sirupsen/logrus" ) -func handle(request apmodels.InboxRequest) { - if verified, err := Verify(request.Request); err != nil { +func (api *APInbox) handle(request apmodels.InboxRequest) { + if verified, err := api.Verify(request.Request); err != nil { log.Debugln("Error in attempting to verify request", err) return } else if !verified { @@ -28,7 +26,7 @@ func handle(request apmodels.InboxRequest) { return } - if err := resolvers.Resolve(context.Background(), request.Body, handleUpdateRequest, handleFollowInboxRequest, handleLikeRequest, handleAnnounceRequest, handleUndoInboxRequest, handleCreateRequest); err != nil { + if err := api.resolvers.Resolve(context.Background(), request.Body, api.handleUpdateRequest, api.handleFollowInboxRequest, api.handleLikeRequest, api.handleAnnounceRequest, api.handleUndoInboxRequest, api.handleCreateRequest); err != nil { log.Debugln("resolver error:", err) } } @@ -36,7 +34,7 @@ func handle(request apmodels.InboxRequest) { // Verify will Verify the http signature of an inbound request as well as // check it against the list of blocked domains. // nolint: cyclop -func Verify(request *http.Request) (bool, error) { +func (api *APInbox) Verify(request *http.Request) (bool, error) { verifier, err := httpsig.NewVerifier(request) if err != nil { return false, errors.Wrap(err, "failed to create key verifier for request") @@ -71,7 +69,7 @@ func Verify(request *http.Request) (bool, error) { return false, errors.New("Unable to determine algorithm to verify request") } - publicKey, err := resolvers.GetResolvedPublicKeyFromIRI(pubKeyID.String()) + publicKey, err := api.resolvers.GetResolvedPublicKeyFromIRI(pubKeyID.String()) if err != nil { return false, errors.Wrap(err, "failed to resolve actor from IRI to fetch key") } @@ -86,12 +84,12 @@ func Verify(request *http.Request) (bool, error) { } // Test to see if the actor is in the list of blocked federated domains. - if isBlockedDomain(publicKeyActorIRI.Hostname()) { + if api.isBlockedDomain(publicKeyActorIRI.Hostname()) { return false, errors.New("domain is blocked") } // If actor is specifically blocked, then fail validation. - if blocked, err := isBlockedActor(publicKeyActorIRI); err != nil || blocked { + if blocked, err := api.isBlockedActor(publicKeyActorIRI); err != nil || blocked { return false, err } @@ -129,8 +127,8 @@ func Verify(request *http.Request) (bool, error) { return false, fmt.Errorf("http signature verification error(s) for: %s: %+v", pubKeyID.String(), triedAlgos) } -func isBlockedDomain(domain string) bool { - blockedDomains := configRepository.GetBlockedFederatedDomains() +func (api *APInbox) isBlockedDomain(domain string) bool { + blockedDomains := api.configRepository.GetBlockedFederatedDomains() for _, blockedDomain := range blockedDomains { if strings.Contains(domain, blockedDomain) { @@ -141,8 +139,8 @@ func isBlockedDomain(domain string) bool { return false } -func isBlockedActor(actorIRI *url.URL) (bool, error) { - blockedactor, err := persistence.GetFollower(actorIRI.String()) +func (api *APInbox) isBlockedActor(actorIRI *url.URL) (bool, error) { + blockedactor, err := api.federationRepository.GetFollower(actorIRI.String()) if blockedactor != nil && blockedactor.DisabledAt != nil { return true, errors.Wrap(err, "remote actor is blocked") diff --git a/activitypub/inbox/worker_test.go b/services/apfederation/inbox/worker_test.go similarity index 84% rename from activitypub/inbox/worker_test.go rename to services/apfederation/inbox/worker_test.go index 2b4b8d956..98dec41f3 100644 --- a/activitypub/inbox/worker_test.go +++ b/services/apfederation/inbox/worker_test.go @@ -6,10 +6,10 @@ import ( "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" - "github.com/owncast/owncast/activitypub/apmodels" - "github.com/owncast/owncast/activitypub/persistence" + "github.com/owncast/owncast/services/apfederation/apmodels" "github.com/owncast/owncast/storage/configrepository" "github.com/owncast/owncast/storage/data" + "github.com/owncast/owncast/storage/federationrepository" ) func makeFakePerson() vocab.ActivityStreamsPerson { @@ -54,6 +54,7 @@ func TestMain(m *testing.M) { panic(err) } + _ = federationrepository.New(ds) configRepository := configrepository.New(ds) configRepository.PopulateDefaults() configRepository.SetServerURL("https://my.cool.site.biz") @@ -82,17 +83,20 @@ func TestBlockedDomains(t *testing.T) { } func TestBlockedActors(t *testing.T) { + federationRepository := federationrepository.Get() person := makeFakePerson() fakeRequest := streams.NewActivityStreamsFollow() - persistence.AddFollow(apmodels.ActivityPubActor{ + ib := Get() + + federationRepository.AddFollow(apmodels.ActivityPubActor{ ActorIri: person.GetJSONLDId().GetIRI(), Inbox: person.GetJSONLDId().GetIRI(), FollowRequestIri: person.GetJSONLDId().GetIRI(), RequestObject: fakeRequest, }, false) - persistence.BlockOrRejectFollower(person.GetJSONLDId().GetIRI().String()) + federationRepository.BlockOrRejectFollower(person.GetJSONLDId().GetIRI().String()) - blocked, err := isBlockedActor(person.GetJSONLDId().GetIRI()) + blocked, err := ib.isBlockedActor(person.GetJSONLDId().GetIRI()) if err != nil { t.Error(err) return @@ -103,7 +107,7 @@ func TestBlockedActors(t *testing.T) { } failedBlockIRI, _ := url.Parse("https://freedom.eagle/user/mrbar") - failedBlock, err := isBlockedActor(failedBlockIRI) + failedBlock, err := ib.isBlockedActor(failedBlockIRI) if failedBlock { t.Error("Invalid blocking of unblocked actor IRI") diff --git a/activitypub/inbox/workerpool.go b/services/apfederation/inbox/workerpool.go similarity index 77% rename from activitypub/inbox/workerpool.go rename to services/apfederation/inbox/workerpool.go index 38bc2100e..fcf9de120 100644 --- a/activitypub/inbox/workerpool.go +++ b/services/apfederation/inbox/workerpool.go @@ -4,6 +4,7 @@ import ( "runtime" "github.com/owncast/owncast/activitypub/apmodels" + "github.com/owncast/owncast/services/apfederation/apmodels" log "github.com/sirupsen/logrus" ) @@ -18,7 +19,7 @@ type Job struct { var queue chan Job // InitInboxWorkerPool starts n go routines that await ActivityPub jobs. -func InitInboxWorkerPool() { +func (api *APInbox) InitInboxWorkerPool() { queue = make(chan Job) // start workers @@ -28,16 +29,16 @@ func InitInboxWorkerPool() { } // AddToQueue will queue up an outbound http request. -func AddToQueue(req apmodels.InboxRequest) { +func (api *APInbox) AddToQueue(req apmodels.InboxRequest) { log.Tracef("Queued request for ActivityPub inbox handler") queue <- Job{req} } -func worker(workerID int, queue <-chan Job) { +func (api *APInbox) worker(workerID int, queue <-chan Job) { log.Debugf("Started ActivityPub worker %d", workerID) for job := range queue { - handle(job.request) + api.handle(job.request) log.Tracef("Done with ActivityPub inbox handler using worker %d", workerID) } diff --git a/activitypub/requests/acceptFollow.go b/services/apfederation/outbox/acceptFollow.go similarity index 67% rename from activitypub/requests/acceptFollow.go rename to services/apfederation/outbox/acceptFollow.go index add89e24f..1c0d9c8e5 100644 --- a/activitypub/requests/acceptFollow.go +++ b/services/apfederation/outbox/acceptFollow.go @@ -1,4 +1,4 @@ -package requests +package outbox import ( "encoding/json" @@ -6,16 +6,15 @@ import ( "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" - "github.com/owncast/owncast/activitypub/apmodels" - "github.com/owncast/owncast/activitypub/crypto" - "github.com/owncast/owncast/activitypub/workerpool" + "github.com/owncast/owncast/services/apfederation/apmodels" + "github.com/owncast/owncast/services/apfederation/crypto" "github.com/teris-io/shortid" ) // SendFollowAccept will send an accept activity to a follow request from a specified local user. -func SendFollowAccept(inbox *url.URL, originalFollowActivity vocab.ActivityStreamsFollow, fromLocalAccountName string) error { - followAccept := makeAcceptFollow(originalFollowActivity, fromLocalAccountName) +func (apo *APOutbox) SendFollowAccept(inbox *url.URL, originalFollowActivity vocab.ActivityStreamsFollow, fromLocalAccountName string) error { + followAccept := apo.makeAcceptFollow(originalFollowActivity, fromLocalAccountName) localAccountIRI := apmodels.MakeLocalIRIForAccount(fromLocalAccountName) var jsonmap map[string]interface{} @@ -26,12 +25,12 @@ func SendFollowAccept(inbox *url.URL, originalFollowActivity vocab.ActivityStrea return err } - workerpool.AddToOutboundQueue(req) + apo.workerpool.AddToOutboundQueue(req) return nil } -func makeAcceptFollow(originalFollowActivity vocab.ActivityStreamsFollow, fromAccountName string) vocab.ActivityStreamsAccept { +func (r *APOutbox) makeAcceptFollow(originalFollowActivity vocab.ActivityStreamsFollow, fromAccountName string) vocab.ActivityStreamsAccept { acceptIDString := shortid.MustGenerate() acceptID := apmodels.MakeLocalIRIForResource(acceptIDString) actorID := apmodels.MakeLocalIRIForAccount(fromAccountName) diff --git a/activitypub/outbox/outbox.go b/services/apfederation/outbox/outbox.go similarity index 64% rename from activitypub/outbox/outbox.go rename to services/apfederation/outbox/outbox.go index eeced48e1..0170d731b 100644 --- a/activitypub/outbox/outbox.go +++ b/services/apfederation/outbox/outbox.go @@ -9,6 +9,7 @@ import ( "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" +<<<<<<< HEAD:activitypub/outbox/outbox.go "github.com/owncast/owncast/activitypub/apmodels" "github.com/owncast/owncast/activitypub/crypto" "github.com/owncast/owncast/activitypub/persistence" @@ -17,20 +18,55 @@ import ( "github.com/owncast/owncast/activitypub/webfinger" "github.com/owncast/owncast/activitypub/workerpool" "github.com/owncast/owncast/core/data" +======= + +>>>>>>> 4f9fbfba1 (WIP):services/apfederation/outbox/outbox.go "github.com/owncast/owncast/storage/configrepository" + "github.com/owncast/owncast/storage/federationrepository" "github.com/pkg/errors" + "github.com/owncast/owncast/services/apfederation/apmodels" + "github.com/owncast/owncast/services/apfederation/crypto" + "github.com/owncast/owncast/services/apfederation/requests" + "github.com/owncast/owncast/services/apfederation/resolvers" + "github.com/owncast/owncast/services/apfederation/webfinger" + "github.com/owncast/owncast/services/apfederation/workerpool" "github.com/owncast/owncast/services/config" "github.com/owncast/owncast/utils" log "github.com/sirupsen/logrus" "github.com/teris-io/shortid" ) -var configRepository = configrepository.Get() +type APOutbox struct { + configRepository configrepository.ConfigRepository + federationRepository *federationrepository.FederationRepository + resolvers *resolvers.APResolvers + workerpool *workerpool.WorkerPool + requests *requests.Requests +} + +func New() *APOutbox { + return &APOutbox{ + configRepository: configrepository.Get(), + federationRepository: federationrepository.Get(), + resolvers: resolvers.Get(), + workerpool: workerpool.Get(), + requests: requests.Get(), + } +} + +var temporaryGlobalInstance *APOutbox + +func Get() *APOutbox { + if temporaryGlobalInstance == nil { + temporaryGlobalInstance = New() + } + return temporaryGlobalInstance +} // SendLive will send all followers the message saying you started a live stream. -func SendLive() error { - textContent := configRepository.GetFederationGoLiveMessage() +func (apo *APOutbox) SendLive() error { + textContent := apo.configRepository.GetFederationGoLiveMessage() // If the message is empty then do not send it. if textContent == "" { @@ -41,11 +77,11 @@ func SendLive() error { reg := regexp.MustCompile("[^a-zA-Z0-9]+") tagProp := streams.NewActivityStreamsTagProperty() - for _, tagString := range configRepository.GetServerMetadataTags() { + for _, tagString := range apo.configRepository.GetServerMetadataTags() { tagWithoutSpecialCharacters := reg.ReplaceAllString(tagString, "") hashtag := apmodels.MakeHashtag(tagWithoutSpecialCharacters) tagProp.AppendTootHashtag(hashtag) - tagString := getHashtagLinkHTMLFromTagString(tagWithoutSpecialCharacters) + tagString := apo.getHashtagLinkHTMLFromTagString(tagWithoutSpecialCharacters) tagStrings = append(tagStrings, tagString) } @@ -60,15 +96,19 @@ func SendLive() error { tagsString := strings.Join(tagStrings, " ") var streamTitle string - if title := configRepository.GetStreamTitle(); title != "" { + if title := apo.configRepository.GetStreamTitle(); title != "" { streamTitle = fmt.Sprintf("

%s

", title) } +<<<<<<< HEAD:activitypub/outbox/outbox.go textContent = fmt.Sprintf("

%s

%s

%s

%s

", textContent, streamTitle, tagsString, data.GetServerURL(), data.GetServerURL()) +======= + textContent = fmt.Sprintf("

%s

%s

%s

%s", textContent, streamTitle, tagsString, apo.configRepository.GetServerURL(), apo.configRepository.GetServerURL()) +>>>>>>> 4f9fbfba1 (WIP):services/apfederation/outbox/outbox.go - activity, _, note, noteID := createBaseOutboundMessage(textContent) + activity, _, note, noteID := apo.createBaseOutboundMessage(textContent) // To the public if we're not treating ActivityPub as "private". - if !configRepository.GetFederationIsPrivate() { + if !apo.configRepository.GetFederationIsPrivate() { note = apmodels.MakeNotePublic(note) activity = apmodels.MakeActivityPublic(activity) } @@ -76,11 +116,11 @@ func SendLive() error { note.SetActivityStreamsTag(tagProp) // Attach an image along with the Federated message. - previewURL, err := url.Parse(configRepository.GetServerURL()) + previewURL, err := url.Parse(apo.configRepository.GetServerURL()) if err == nil { var imageToAttach string var mediaType string - c := config.GetConfig() + c := config.Get() previewGif := filepath.Join(c.TempDir, "preview.gif") thumbnailJpg := filepath.Join(c.TempDir, "thumbnail.jpg") uniquenessString := shortid.MustGenerate() @@ -98,7 +138,7 @@ func SendLive() error { } } - if configRepository.GetNSFW() { + if apo.configRepository.GetNSFW() { // Mark content as sensitive. sensitive := streams.NewActivityStreamsSensitiveProperty() sensitive.AppendXMLSchemaBoolean(true) @@ -111,11 +151,11 @@ func SendLive() error { return errors.New("unable to serialize go live message activity " + err.Error()) } - if err := SendToFollowers(b); err != nil { + if err := apo.SendToFollowers(b); err != nil { return err } - if err := Add(note, noteID, true); err != nil { + if err := apo.Add(note, noteID, true); err != nil { return err } @@ -123,20 +163,22 @@ func SendLive() error { } // SendDirectMessageToAccount will send a direct message to a single account. -func SendDirectMessageToAccount(textContent, account string) error { - links, err := webfinger.GetWebfingerLinks(account) +func (apo *APOutbox) SendDirectMessageToAccount(textContent, account string) error { + wf := webfinger.Get() + + links, err := wf.GetWebfingerLinks(account) if err != nil { return errors.Wrap(err, "unable to get webfinger links when sending private message") } user := apmodels.MakeWebFingerRequestResponseFromData(links) iri := user.Self - actor, err := resolvers.GetResolvedActorFromIRI(iri) + actor, err := apo.resolvers.GetResolvedActorFromIRI(iri) if err != nil { return errors.Wrap(err, "unable to resolve actor to send message to") } - activity, _, note, _ := createBaseOutboundMessage(textContent) + activity, _, note, _ := apo.createBaseOutboundMessage(textContent) // Set direct message visibility activity = apmodels.MakeActivityDirect(activity, actor.ActorIri) @@ -150,11 +192,11 @@ func SendDirectMessageToAccount(textContent, account string) error { return errors.Wrap(err, "unable to serialize custom fediverse message activity") } - return SendToUser(actor.Inbox, b) + return apo.SendToUser(actor.Inbox, b) } // SendPublicMessage will send a public message to all followers. -func SendPublicMessage(textContent string) error { +func (apo *APOutbox) SendPublicMessage(textContent string) error { originalContent := textContent textContent = utils.RenderSimpleMarkdown(textContent) @@ -166,7 +208,7 @@ func SendPublicMessage(textContent string) error { tagWithoutHashtag := strings.TrimPrefix(hashtag, "#") // Replace the instances of the tag with a link to the tag page. - tagHTML := getHashtagLinkHTMLFromTagString(tagWithoutHashtag) + tagHTML := apo.getHashtagLinkHTMLFromTagString(tagWithoutHashtag) textContent = strings.ReplaceAll(textContent, hashtag, tagHTML) // Create Hashtag object for the tag. @@ -174,10 +216,10 @@ func SendPublicMessage(textContent string) error { tagProp.AppendTootHashtag(hashtag) } - activity, _, note, noteID := createBaseOutboundMessage(textContent) + activity, _, note, noteID := apo.createBaseOutboundMessage(textContent) note.SetActivityStreamsTag(tagProp) - if !configRepository.GetFederationIsPrivate() { + if !apo.configRepository.GetFederationIsPrivate() { note = apmodels.MakeNotePublic(note) activity = apmodels.MakeActivityPublic(activity) } @@ -188,11 +230,11 @@ func SendPublicMessage(textContent string) error { return errors.New("unable to serialize custom fediverse message activity " + err.Error()) } - if err := SendToFollowers(b); err != nil { + if err := apo.SendToFollowers(b); err != nil { return err } - if err := Add(note, noteID, false); err != nil { + if err := apo.Add(note, noteID, false); err != nil { return err } @@ -200,8 +242,8 @@ func SendPublicMessage(textContent string) error { } // nolint: unparam -func createBaseOutboundMessage(textContent string) (vocab.ActivityStreamsCreate, string, vocab.ActivityStreamsNote, string) { - localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername()) +func (apo *APOutbox) createBaseOutboundMessage(textContent string) (vocab.ActivityStreamsCreate, string, vocab.ActivityStreamsNote, string) { + localActor := apmodels.MakeLocalIRIForAccount(apo.configRepository.GetDefaultFederationUsername()) noteID := shortid.MustGenerate() noteIRI := apmodels.MakeLocalIRIForResource(noteID) id := shortid.MustGenerate() @@ -216,15 +258,15 @@ func createBaseOutboundMessage(textContent string) (vocab.ActivityStreamsCreate, } // Get Hashtag HTML link for a given tag (without # prefix). -func getHashtagLinkHTMLFromTagString(baseHashtag string) string { +func (apo *APOutbox) getHashtagLinkHTMLFromTagString(baseHashtag string) string { return fmt.Sprintf("#%s", baseHashtag, baseHashtag) } // SendToFollowers will send an arbitrary payload to all follower inboxes. -func SendToFollowers(payload []byte) error { - localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername()) +func (apo *APOutbox) SendToFollowers(payload []byte) error { + localActor := apmodels.MakeLocalIRIForAccount(apo.configRepository.GetDefaultFederationUsername()) - followers, _, err := persistence.GetFederationFollowers(-1, 0) + followers, _, err := apo.federationRepository.GetFederationFollowers(-1, 0) if err != nil { log.Errorln("unable to fetch followers to send to", err) return errors.New("unable to fetch followers to send payload to") @@ -238,29 +280,29 @@ func SendToFollowers(payload []byte) error { return errors.New("unable to create outbox request: " + follower.Inbox) } - workerpool.AddToOutboundQueue(req) + apo.workerpool.AddToOutboundQueue(req) } return nil } // SendToUser will send a payload to a single specific inbox. -func SendToUser(inbox *url.URL, payload []byte) error { - localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername()) +func (apo *APOutbox) SendToUser(inbox *url.URL, payload []byte) error { + localActor := apmodels.MakeLocalIRIForAccount(apo.configRepository.GetDefaultFederationUsername()) - req, err := requests.CreateSignedRequest(payload, inbox, localActor) + req, err := apo.requests.CreateSignedRequest(payload, inbox, localActor) if err != nil { return errors.Wrap(err, "unable to create outbox request") } - workerpool.AddToOutboundQueue(req) + apo.workerpool.AddToOutboundQueue(req) return nil } // UpdateFollowersWithAccountUpdates will send an update to all followers alerting of a profile update. -func UpdateFollowersWithAccountUpdates() error { +func (apo *APOutbox) UpdateFollowersWithAccountUpdates() error { // Don't do anything if federation is disabled. - if !configRepository.GetFederationEnabled() { + if !apo.configRepository.GetFederationEnabled() { return nil } @@ -269,7 +311,7 @@ func UpdateFollowersWithAccountUpdates() error { activity := apmodels.MakeUpdateActivity(objectID) actor := streams.NewActivityStreamsPerson() - actorID := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername()) + actorID := apmodels.MakeLocalIRIForAccount(apo.configRepository.GetDefaultFederationUsername()) actorIDProperty := streams.NewJSONLDIdProperty() actorIDProperty.Set(actorID) actor.SetJSONLDId(actorIDProperty) @@ -287,11 +329,11 @@ func UpdateFollowersWithAccountUpdates() error { log.Errorln("unable to serialize send update actor activity", err) return errors.New("unable to serialize send update actor activity") } - return SendToFollowers(b) + return apo.SendToFollowers(b) } // Add will save an ActivityPub object to the datastore. -func Add(item vocab.Type, id string, isLiveNotification bool) error { +func (apo *APOutbox) Add(item vocab.Type, id string, isLiveNotification bool) error { iri := item.GetJSONLDId().GetIRI().String() typeString := item.GetTypeName() @@ -306,5 +348,5 @@ func Add(item vocab.Type, id string, isLiveNotification bool) error { return err } - return persistence.AddToOutbox(iri, b, typeString, isLiveNotification) + return apo.federationRepository.AddToOutbox(iri, b, typeString, isLiveNotification) } diff --git a/activitypub/requests/http.go b/services/apfederation/requests/http.go similarity index 73% rename from activitypub/requests/http.go rename to services/apfederation/requests/http.go index c23c0eeac..360468b6c 100644 --- a/activitypub/requests/http.go +++ b/services/apfederation/requests/http.go @@ -9,14 +9,14 @@ import ( "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" - "github.com/owncast/owncast/activitypub/crypto" + "github.com/owncast/owncast/services/apfederation/crypto" "github.com/owncast/owncast/services/config" log "github.com/sirupsen/logrus" ) // WriteStreamResponse will write a ActivityPub object to the provided ResponseWriter and sign with the provided key. -func WriteStreamResponse(item vocab.Type, w http.ResponseWriter, publicKey crypto.PublicKey) error { +func (r *Requests) WriteStreamResponse(item vocab.Type, w http.ResponseWriter, publicKey crypto.PublicKey) error { var jsonmap map[string]interface{} jsonmap, _ = streams.Serialize(item) b, err := json.Marshal(jsonmap) @@ -24,21 +24,21 @@ func WriteStreamResponse(item vocab.Type, w http.ResponseWriter, publicKey crypt return err } - return WriteResponse(b, w, publicKey) + return r.WriteResponse(b, w, publicKey) } // WritePayloadResponse will write any arbitrary object to the provided ResponseWriter and sign with the provided key. -func WritePayloadResponse(payload interface{}, w http.ResponseWriter, publicKey crypto.PublicKey) error { +func (r *Requests) WritePayloadResponse(payload interface{}, w http.ResponseWriter, publicKey crypto.PublicKey) error { b, err := json.Marshal(payload) if err != nil { return err } - return WriteResponse(b, w, publicKey) + return r.WriteResponse(b, w, publicKey) } // WriteResponse will write any arbitrary payload to the provided ResponseWriter and sign with the provided key. -func WriteResponse(payload []byte, w http.ResponseWriter, publicKey crypto.PublicKey) error { +func (r *Requests) WriteResponse(payload []byte, w http.ResponseWriter, publicKey crypto.PublicKey) error { w.Header().Set("Content-Type", "application/activity+json") if err := crypto.SignResponse(w, payload, publicKey); err != nil { @@ -56,11 +56,11 @@ func WriteResponse(payload []byte, w http.ResponseWriter, publicKey crypto.Publi } // CreateSignedRequest will create a signed POST request of a payload to the provided destination. -func CreateSignedRequest(payload []byte, url *url.URL, fromActorIRI *url.URL) (*http.Request, error) { +func (r *Requests) CreateSignedRequest(payload []byte, url *url.URL, fromActorIRI *url.URL) (*http.Request, error) { log.Debugln("Sending", string(payload), "to", url) req, _ := http.NewRequest(http.MethodPost, url.String(), bytes.NewBuffer(payload)) - c := config.GetConfig() + c := config.Get() ua := fmt.Sprintf("%s; https://owncast.online", c.GetReleaseString()) req.Header.Set("User-Agent", ua) req.Header.Set("Content-Type", "application/activity+json") diff --git a/services/apfederation/requests/requests.go b/services/apfederation/requests/requests.go new file mode 100644 index 000000000..c2769918e --- /dev/null +++ b/services/apfederation/requests/requests.go @@ -0,0 +1,20 @@ +package requests + +import "github.com/owncast/owncast/services/apfederation/workerpool" + +type Requests struct { + outboundWorkerPool *workerpool.WorkerPool +} + +func New() *Requests { + return &Requests{} +} + +var temporaryGlobalInstance *Requests + +func Get() *Requests { + if temporaryGlobalInstance == nil { + temporaryGlobalInstance = New() + } + return temporaryGlobalInstance +} diff --git a/activitypub/resolvers/follow.go b/services/apfederation/resolvers/follow.go similarity index 65% rename from activitypub/resolvers/follow.go rename to services/apfederation/resolvers/follow.go index 42211c9cd..c61b2a327 100644 --- a/activitypub/resolvers/follow.go +++ b/services/apfederation/resolvers/follow.go @@ -5,18 +5,18 @@ import ( "fmt" "github.com/go-fed/activity/streams/vocab" - "github.com/owncast/owncast/activitypub/apmodels" + "github.com/owncast/owncast/services/apfederation/apmodels" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) -func getPersonFromFollow(activity vocab.ActivityStreamsFollow) (apmodels.ActivityPubActor, error) { - return GetResolvedActorFromActorProperty(activity.GetActivityStreamsActor()) +func (apr *APResolvers) getPersonFromFollow(activity vocab.ActivityStreamsFollow) (apmodels.ActivityPubActor, error) { + return apr.GetResolvedActorFromActorProperty(activity.GetActivityStreamsActor()) } // MakeFollowRequest will convert an inbound Follow request to our internal actor model. -func MakeFollowRequest(c context.Context, activity vocab.ActivityStreamsFollow) (*apmodels.ActivityPubActor, error) { - person, err := getPersonFromFollow(activity) +func (apr *APResolvers) MakeFollowRequest(c context.Context, activity vocab.ActivityStreamsFollow) (*apmodels.ActivityPubActor, error) { + person, err := apr.getPersonFromFollow(activity) if err != nil { return nil, errors.New("unable to resolve person from follow request: " + err.Error()) } @@ -39,8 +39,8 @@ func MakeFollowRequest(c context.Context, activity vocab.ActivityStreamsFollow) } // MakeUnFollowRequest will convert an inbound Unfollow request to our internal actor model. -func MakeUnFollowRequest(c context.Context, activity vocab.ActivityStreamsUndo) *apmodels.ActivityPubActor { - person, err := GetResolvedActorFromActorProperty(activity.GetActivityStreamsActor()) +func (apr *APResolvers) MakeUnFollowRequest(c context.Context, activity vocab.ActivityStreamsUndo) *apmodels.ActivityPubActor { + person, err := apr.GetResolvedActorFromActorProperty(activity.GetActivityStreamsActor()) if err != nil { log.Errorln("unable to resolve person from actor iri", person.ActorIri, err) return nil diff --git a/activitypub/resolvers/resolve.go b/services/apfederation/resolvers/resolve.go similarity index 85% rename from activitypub/resolvers/resolve.go rename to services/apfederation/resolvers/resolve.go index 73be0405b..444068642 100644 --- a/activitypub/resolvers/resolve.go +++ b/services/apfederation/resolvers/resolve.go @@ -8,15 +8,15 @@ import ( "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" - "github.com/owncast/owncast/activitypub/apmodels" - "github.com/owncast/owncast/activitypub/crypto" + "github.com/owncast/owncast/services/apfederation/apmodels" + "github.com/owncast/owncast/services/apfederation/crypto" "github.com/owncast/owncast/storage/configrepository" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) // Resolve will translate a raw ActivityPub payload and fire the callback associated with that activity type. -func Resolve(c context.Context, data []byte, callbacks ...interface{}) error { +func (apr *APResolvers) Resolve(c context.Context, data []byte, callbacks ...interface{}) error { jsonResolver, err := streams.NewJSONResolver(callbacks...) if err != nil { // Something in the setup was wrong. For example, a callback has an @@ -46,7 +46,7 @@ func Resolve(c context.Context, data []byte, callbacks ...interface{}) error { } // ResolveIRI will resolve an IRI ahd call the correct callback for the resolved type. -func ResolveIRI(c context.Context, iri string, callbacks ...interface{}) error { +func (apr *APResolvers) ResolveIRI(c context.Context, iri string, callbacks ...interface{}) error { log.Debugln("Resolving", iri) req, _ := http.NewRequest(http.MethodGet, iri, nil) @@ -71,12 +71,12 @@ func ResolveIRI(c context.Context, iri string, callbacks ...interface{}) error { } // fmt.Println(string(data)) - return Resolve(c, data, callbacks...) + return apr.Resolve(c, data, callbacks...) } // GetResolvedActorFromActorProperty resolve an external actor property to a // fully populated internal actor representation. -func GetResolvedActorFromActorProperty(actor vocab.ActivityStreamsActorProperty) (apmodels.ActivityPubActor, error) { +func (apr *APResolvers) GetResolvedActorFromActorProperty(actor vocab.ActivityStreamsActorProperty) (apmodels.ActivityPubActor, error) { var err error var apActor apmodels.ActivityPubActor resolved := false @@ -89,7 +89,7 @@ func GetResolvedActorFromActorProperty(actor vocab.ActivityStreamsActorProperty) // If the actor is an unresolved IRI then we need to resolve it. if actorObjectOrIRI.IsIRI() { iri := actorObjectOrIRI.GetIRI().String() - return GetResolvedActorFromIRI(iri) + return apr.GetResolvedActorFromIRI(iri) } if actorObjectOrIRI.IsActivityStreamsPerson() { @@ -125,7 +125,7 @@ func GetResolvedActorFromActorProperty(actor vocab.ActivityStreamsActorProperty) } // GetResolvedPublicKeyFromIRI will resolve a publicKey IRI string to a vocab.W3IDSecurityV1PublicKey. -func GetResolvedPublicKeyFromIRI(publicKeyIRI string) (vocab.W3IDSecurityV1PublicKey, error) { +func (apr *APResolvers) GetResolvedPublicKeyFromIRI(publicKeyIRI string) (vocab.W3IDSecurityV1PublicKey, error) { var err error var pubkey vocab.W3IDSecurityV1PublicKey resolved := false @@ -175,7 +175,7 @@ func GetResolvedPublicKeyFromIRI(publicKeyIRI string) (vocab.W3IDSecurityV1Publi return nil } - if e := ResolveIRI(context.Background(), publicKeyIRI, personCallback, serviceCallback, applicationCallback, pubkeyCallback); e != nil { + if e := apr.ResolveIRI(context.Background(), publicKeyIRI, personCallback, serviceCallback, applicationCallback, pubkeyCallback); e != nil { err = e } @@ -191,7 +191,7 @@ func GetResolvedPublicKeyFromIRI(publicKeyIRI string) (vocab.W3IDSecurityV1Publi } // GetResolvedActorFromIRI will resolve an IRI string to a fully populated actor. -func GetResolvedActorFromIRI(personOrServiceIRI string) (apmodels.ActivityPubActor, error) { +func (apr *APResolvers) GetResolvedActorFromIRI(personOrServiceIRI string) (apmodels.ActivityPubActor, error) { var err error var apActor apmodels.ActivityPubActor resolved := false @@ -222,7 +222,7 @@ func GetResolvedActorFromIRI(personOrServiceIRI string) (apmodels.ActivityPubAct return e } - if e := ResolveIRI(context.Background(), personOrServiceIRI, personCallback, serviceCallback, applicationCallback); e != nil { + if e := apr.ResolveIRI(context.Background(), personOrServiceIRI, personCallback, serviceCallback, applicationCallback); e != nil { err = e } diff --git a/services/apfederation/resolvers/resolvers.go b/services/apfederation/resolvers/resolvers.go new file mode 100644 index 000000000..caeb4b19b --- /dev/null +++ b/services/apfederation/resolvers/resolvers.go @@ -0,0 +1,24 @@ +package resolvers + +import ( + "github.com/owncast/owncast/storage/configrepository" +) + +type APResolvers struct { + configRepository configrepository.ConfigRepository +} + +func New() *APResolvers { + return &APResolvers{ + configRepository: configrepository.Get(), + } +} + +var temporaryGlobalInstance *APResolvers + +func Get() *APResolvers { + if temporaryGlobalInstance == nil { + temporaryGlobalInstance = New() + } + return temporaryGlobalInstance +} diff --git a/activitypub/webfinger/webfinger.go b/services/apfederation/webfinger/webfinger.go similarity index 81% rename from activitypub/webfinger/webfinger.go rename to services/apfederation/webfinger/webfinger.go index 14e4cfffb..d8ce1fa78 100644 --- a/activitypub/webfinger/webfinger.go +++ b/services/apfederation/webfinger/webfinger.go @@ -11,8 +11,23 @@ import ( "github.com/owncast/owncast/utils" ) +type Webfinger struct{} + +var temporaryGlobalInstance *Webfinger + +func Get() *Webfinger { + if temporaryGlobalInstance == nil { + temporaryGlobalInstance = New() + } + return temporaryGlobalInstance +} + +func New() *Webfinger { + return &Webfinger{} +} + // GetWebfingerLinks will return webfinger data for an account. -func GetWebfingerLinks(account string) ([]map[string]interface{}, error) { +func (w *Webfinger) GetWebfingerLinks(account string) ([]map[string]interface{}, error) { type webfingerResponse struct { Links []map[string]interface{} `json:"links"` } diff --git a/activitypub/workerpool/outbound.go b/services/apfederation/workerpool/outbound.go similarity index 61% rename from activitypub/workerpool/outbound.go rename to services/apfederation/workerpool/outbound.go index a3d0fadea..402e91040 100644 --- a/activitypub/workerpool/outbound.go +++ b/services/apfederation/workerpool/outbound.go @@ -15,12 +15,29 @@ type Job struct { request *http.Request } -var queue chan Job +type WorkerPool struct { + queue chan Job +} + +func New() *WorkerPool { + wp := &WorkerPool{ + queue: make(chan Job), + } + wp.initOutboundWorkerPool() + return wp +} + +var temporaryGlobalInstance *WorkerPool + +func Get() *WorkerPool { + if temporaryGlobalInstance == nil { + temporaryGlobalInstance = New() + } + return temporaryGlobalInstance +} // InitOutboundWorkerPool starts n go routines that await ActivityPub jobs. -func InitOutboundWorkerPool() { - queue = make(chan Job) - +func (wp *WorkerPool) initOutboundWorkerPool() { // start workers for i := 1; i <= workerPoolSize; i++ { go worker(i, queue) @@ -28,23 +45,23 @@ func InitOutboundWorkerPool() { } // AddToOutboundQueue will queue up an outbound http request. -func AddToOutboundQueue(req *http.Request) { +func (wp *WorkerPool) AddToOutboundQueue(req *http.Request) { log.Tracef("Queued request for ActivityPub destination %s", req.RequestURI) - queue <- Job{req} + wp.queue <- Job{req} } -func worker(workerID int, queue <-chan Job) { +func (wp *WorkerPool) worker(workerID int, queue <-chan Job) { log.Debugf("Started ActivityPub worker %d", workerID) for job := range queue { - if err := sendActivityPubMessageToInbox(job); err != nil { + if err := wp.sendActivityPubMessageToInbox(job); err != nil { log.Errorf("ActivityPub destination %s failed to send Error: %s", job.request.RequestURI, err) } log.Tracef("Done with ActivityPub destination %s using worker %d", job.request.RequestURI, workerID) } } -func sendActivityPubMessageToInbox(job Job) error { +func (wp *WorkerPool) sendActivityPubMessageToInbox(job Job) error { client := &http.Client{} resp, err := client.Do(job.request) diff --git a/services/auth/indieauth/client.go b/services/auth/indieauth/client.go index 2842e0861..3fbc55ceb 100644 --- a/services/auth/indieauth/client.go +++ b/services/auth/indieauth/client.go @@ -10,12 +10,8 @@ import ( "strings" "time" -<<<<<<< HEAD - "github.com/owncast/owncast/core/data" - "github.com/owncast/owncast/utils" -======= "github.com/owncast/owncast/storage/configrepository" ->>>>>>> 659a19bf2 (WIP refactored all storage into repos. Tests pass.) + "github.com/owncast/owncast/utils" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) @@ -45,7 +41,6 @@ func (c *IndieAuthClient) StartAuthFlow(authHost, userID, accessToken, displayNa return nil, errors.New("Please try again later. Too many pending requests.") } -<<<<<<< HEAD // Reject any requests to our internal network or loopback if utils.IsHostnameInternal(authHost) { return nil, errors.New("unable to use provided host") @@ -62,11 +57,8 @@ func (c *IndieAuthClient) StartAuthFlow(authHost, userID, accessToken, displayNa return nil, errors.New("only servers secured with https are supported") } - serverURL := data.GetServerURL() -======= configRepository := configrepository.Get() serverURL := configRepository.GetServerURL() ->>>>>>> 659a19bf2 (WIP refactored all storage into repos. Tests pass.) if serverURL == "" { return nil, errors.New("Owncast server URL must be set when using auth") } diff --git a/core/chat/events/actionEvent.go b/services/chat/actionEvent.go similarity index 58% rename from core/chat/events/actionEvent.go rename to services/chat/actionEvent.go index b251b8511..d16e039f3 100644 --- a/core/chat/events/actionEvent.go +++ b/services/chat/actionEvent.go @@ -1,14 +1,16 @@ -package events +package chat + +import "github.com/owncast/owncast/models" // ActionEvent represents an action that took place, not a chat message. type ActionEvent struct { - Event + models.Event MessageEvent } // GetBroadcastPayload will return the object to send to all chat users. -func (e *ActionEvent) GetBroadcastPayload() EventPayload { - return EventPayload{ +func (e *ActionEvent) GetBroadcastPayload() models.EventPayload { + return models.EventPayload{ "id": e.ID, "timestamp": e.Timestamp, "body": e.Body, @@ -17,6 +19,6 @@ func (e *ActionEvent) GetBroadcastPayload() EventPayload { } // GetMessageType will return the type of message. -func (e *ActionEvent) GetMessageType() EventType { - return ChatActionSent +func (e *ActionEvent) GetMessageType() models.EventType { + return models.ChatActionSent } diff --git a/services/chat/chat.go b/services/chat/chat.go new file mode 100644 index 000000000..fc44407f4 --- /dev/null +++ b/services/chat/chat.go @@ -0,0 +1,227 @@ +package chat + +import ( + "fmt" + "net/http" + "sort" + + "github.com/owncast/owncast/models" + "github.com/owncast/owncast/storage/chatrepository" + "github.com/owncast/owncast/storage/configrepository" + "github.com/owncast/owncast/storage/userrepository" + log "github.com/sirupsen/logrus" +) + +type Chat struct { + getStatus func() *models.Status + server *Server + configRepository *configrepository.SqlConfigRepository + emojis *emojis +} + +func New() *Chat { + return &Chat{ + configRepository: configrepository.Get(), + emojis: newEmojis(), + } +} + +var temporaryGlobalInstance *Chat + +// GetConfig returns the temporary global instance. +// Remove this after dependency injection is implemented. +func Get() *Chat { + if temporaryGlobalInstance == nil { + temporaryGlobalInstance = New() + } + + return temporaryGlobalInstance +} + +// Start begins the chat server. +func (c *Chat) Start(getStatusFunc func() *models.Status) error { + c.getStatus = getStatusFunc + c.server = NewChat() + + go c.server.Run() + + log.Traceln("Chat server started with max connection count of", c.server.maxSocketConnectionLimit) + + return nil +} + +// FindClientByID will return a single connected client by ID. +func (c *Chat) FindClientByID(clientID uint) (*Client, bool) { + client, found := c.server.clients[clientID] + return client, found +} + +// GetClients will return all the current chat clients connected. +func (c *Chat) GetClients() []*Client { + clients := []*Client{} + + if c.server == nil { + return clients + } + + // Convert the keyed map to a slice. + for _, client := range c.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 (c *Chat) SendSystemMessage(text string, ephemeral bool) error { + message := SystemMessageEvent{ + MessageEvent: MessageEvent{ + Body: text, + }, + } + message.SetDefaults() + message.RenderBody() + message.DisplayName = c.configRepository.GetServerName() + + if err := c.Broadcast(&message); err != nil { + log.Errorln("error sending system message", err) + } + + if !ephemeral { + cr := chatrepository.Get() + cr.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 (c *Chat) SendFediverseAction(eventType string, userAccountName string, image *string, body string, link string) error { + message := FediverseEngagementEvent{ + Event: models.Event{ + Type: eventType, + }, + MessageEvent: MessageEvent{ + Body: body, + }, + UserAccountName: userAccountName, + Image: image, + Link: link, + } + + message.SetDefaults() + message.RenderBody() + + if err := c.Broadcast(&message); err != nil { + log.Errorln("error sending system message", err) + return err + } + + cr := chatrepository.Get() + cr.SaveFederatedAction(message) + + return nil +} + +// SendSystemAction will send a system action string as an action event to all clients. +func (c *Chat) SendSystemAction(text string, ephemeral bool) error { + message := ActionEvent{ + MessageEvent: MessageEvent{ + Body: text, + }, + } + + message.SetDefaults() + message.RenderBody() + + if err := c.Broadcast(&message); err != nil { + log.Errorln("error sending system chat action") + } + + if !ephemeral { + cr := chatrepository.Get() + cr.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 (c *Chat) SendAllWelcomeMessage() { + c.server.sendAllWelcomeMessage() +} + +// SendSystemMessageToClient will send a single message to a single connected chat client. +func (c *Chat) SendSystemMessageToClient(clientID uint, text string) { + if client, foundClient := c.FindClientByID(clientID); foundClient { + c.server.sendSystemMessageToClient(client, text) + } +} + +// Broadcast will send all connected clients the outbound object provided. +func (c *Chat) Broadcast(event OutboundEvent) error { + return c.server.Broadcast(event.GetBroadcastPayload()) +} + +// HandleClientConnection handles a single inbound websocket connection. +func (c *Chat) HandleClientConnection(w http.ResponseWriter, r *http.Request) { + c.server.HandleClientConnection(w, r) +} + +// DisconnectClients will forcefully disconnect all clients belonging to a user by ID. +func (c *Chat) DisconnectClients(clients []*Client) { + c.server.DisconnectClients(clients) +} + +func (c *Chat) GetClientsForUser(userID string) ([]*Client, error) { + return c.server.GetClientsForUser(userID) +} + +// SendConnectedClientInfoToUser will find all the connected clients assigned to a user +// and re-send each the connected client info. +func (c *Chat) SendConnectedClientInfoToUser(userID string) error { + clients, err := c.server.GetClientsForUser(userID) + if err != nil { + return err + } + + userRepository := userrepository.Get() + + // Get an updated reference to the user. + user := userRepository.GetUserByID(userID) + if user == nil { + return fmt.Errorf("user not found") + } + + if err != nil { + return err + } + + for _, client := range clients { + // Update the client's reference to its user. + client.User = user + // Send the update to the client. + client.sendConnectedClientInfo() + } + + return nil +} + +// SendActionToUser will send system action text to all connected clients +// assigned to a user ID. +func (c *Chat) SendActionToUser(userID string, text string) error { + clients, err := c.server.GetClientsForUser(userID) + if err != nil { + return err + } + + for _, client := range clients { + c.server.sendActionToClient(client, text) + } + + return nil +} diff --git a/core/chat/chatclient.go b/services/chat/chatclient.go similarity index 96% rename from core/chat/chatclient.go rename to services/chat/chatclient.go index 8825d75cb..25a986979 100644 --- a/core/chat/chatclient.go +++ b/services/chat/chatclient.go @@ -11,7 +11,6 @@ import ( "golang.org/x/time/rate" "github.com/gorilla/websocket" - "github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/models" "github.com/owncast/owncast/services/config" "github.com/owncast/owncast/services/geoip" @@ -75,9 +74,9 @@ var ( ) func (c *Client) sendConnectedClientInfo() { - payload := events.ConnectedClientInfo{ - Event: events.Event{ - Type: events.ConnectedUserInfo, + payload := models.ConnectedClientInfo{ + Event: models.Event{ + Type: models.ConnectedUserInfo, }, User: c.User, } @@ -237,8 +236,8 @@ func (c *Client) sendPayload(payload interface{}) { } func (c *Client) sendAction(message string) { - clientMessage := events.ActionEvent{ - MessageEvent: events.MessageEvent{ + clientMessage := ActionEvent{ + MessageEvent: MessageEvent{ Body: message, }, } diff --git a/services/chat/emoji.go b/services/chat/emoji.go new file mode 100644 index 000000000..b058ffaae --- /dev/null +++ b/services/chat/emoji.go @@ -0,0 +1,131 @@ +package chat + +import ( + "bytes" + "strings" + "sync" + "text/template" + "time" + + emojiDef "github.com/yuin/goldmark-emoji/definition" +) + +// implements the emojiDef.Emojis interface but uses case-insensitive search. +// the .children field isn't currently used, but could be used in a future +// implementation of say, emoji packs where a child represents a pack. +type emojis struct { + list []emojiDef.Emoji + names map[string]*emojiDef.Emoji + children []emojiDef.Emojis + + emojiMu sync.Mutex + emojiDefs emojiDef.Emojis + emojiHTML map[string]string + emojiModTime time.Time + emojiHTMLFormat string + emojiHTMLTemplate *template.Template +} + +// return a new Emojis set. +func newEmojis(emotes ...emojiDef.Emoji) *emojis { + loadEmoji() + + e := &emojis{ + list: emotes, + names: map[string]*emojiDef.Emoji{}, + children: []emojiDef.Emojis{}, + + emojiMu: sync.Mutex{}, + emojiHTML: make(map[string]string), + emojiModTime: time.Now(), + emojiHTMLFormat: `:{{ .Name }}:`, + emojiHTMLTemplate: template.Must(template.New("emojiHTML").Parse(emojiHTMLFormat)), + } + + for i := range e.list { + emoji := &e.list[i] + for _, s := range emoji.ShortNames { + e.names[s] = emoji + } + } + + return e +} + +func (self *emojis) Get(shortName string) (*emojiDef.Emoji, bool) { + v, ok := self.names[strings.ToLower(shortName)] + if ok { + return v, ok + } + + for _, child := range self.children { + v, ok := child.Get(shortName) + if ok { + return v, ok + } + } + + return nil, false +} + +func (self *emojis) Add(emotes emojiDef.Emojis) { + self.children = append(self.children, emotes) +} + +func (self *emojis) Clone() emojiDef.Emojis { + clone := &emojis{ + list: self.list, + names: self.names, + children: make([]emojiDef.Emojis, len(self.children)), + } + + copy(clone.children, self.children) + + return clone +} + +var ( + emojiMu sync.Mutex + // emojiDefs = newEmojis() + // emojiHTML = make(map[string]string) + emojiModTime time.Time + emojiHTMLFormat = `:{{ .Name }}:` + emojiHTMLTemplate = template.Must(template.New("emojiHTML").Parse(emojiHTMLFormat)) +) + +type emojiMeta struct { + emojiDefs emojiDef.Emojis + emojiHTML map[string]string +} + +func loadEmoji() emojiDef.Emojis { + modTime, err := data.UpdateEmojiList(false) + if err != nil { + return + } + emojiArr := make([]emojiDef.Emoji, 0) + + if modTime.After(emojiModTime) { + emojiMu.Lock() + defer emojiMu.Unlock() + + emojiHTML := make(map[string]string) + + emojiList := data.GetEmojiList() + + for i := 0; i < len(emojiList); i++ { + var buf bytes.Buffer + err := emojiHTMLTemplate.Execute(&buf, emojiList[i]) + if err != nil { + return + } + emojiHTML[strings.ToLower(emojiList[i].Name)] = buf.String() + + emoji := emojiDef.NewEmoji(emojiList[i].Name, nil, strings.ToLower(emojiList[i].Name)) + emojiArr = append(emojiArr, emoji) + } + + } + emojiDefs := newEmojis(emojiArr...) + return emojiDefs +} diff --git a/core/chat/events.go b/services/chat/events.go similarity index 87% rename from core/chat/events.go rename to services/chat/events.go index c0889c5e2..8d3ffe252 100644 --- a/core/chat/events.go +++ b/services/chat/events.go @@ -6,16 +6,18 @@ import ( "strings" "time" - "github.com/owncast/owncast/core/chat/events" + "github.com/owncast/owncast/models" "github.com/owncast/owncast/services/config" + "github.com/owncast/owncast/services/status" "github.com/owncast/owncast/services/webhooks" - "github.com/owncast/owncast/storage" + "github.com/owncast/owncast/storage/chatrepository" + "github.com/owncast/owncast/storage/userrepository" "github.com/owncast/owncast/utils" log "github.com/sirupsen/logrus" ) func (s *Server) userNameChanged(eventData chatClientEvent) { - var receivedEvent events.NameChangeEvent + var receivedEvent models.NameChangeEvent if err := json.Unmarshal(eventData.data, &receivedEvent); err != nil { log.Errorln("error unmarshalling to NameChangeEvent", err) return @@ -24,8 +26,8 @@ func (s *Server) userNameChanged(eventData chatClientEvent) { proposedUsername := receivedEvent.NewName // Check if name is on the blocklist - blocklist := configRepository.GetForbiddenUsernameList() - userRepository := storage.GetUserRepository() + blocklist := s.configRepository.GetForbiddenUsernameList() + userRepository := userrepository.Get() // Names have a max length proposedUsername = utils.MakeSafeStringOfLength(proposedUsername, config.MaxChatDisplayNameLength) @@ -82,7 +84,7 @@ func (s *Server) userNameChanged(eventData chatClientEvent) { // Send chat event letting everyone about about the name change savedUser.DisplayName = proposedUsername - broadcastEvent := events.NameChangeBroadcast{ + broadcastEvent := models.NameChangeBroadcast{ Oldname: oldName, } broadcastEvent.User = savedUser @@ -104,7 +106,7 @@ func (s *Server) userNameChanged(eventData chatClientEvent) { } func (s *Server) userColorChanged(eventData chatClientEvent) { - var receivedEvent events.ColorChangeEvent + var receivedEvent models.ColorChangeEvent if err := json.Unmarshal(eventData.data, &receivedEvent); err != nil { log.Errorln("error unmarshalling to ColorChangeEvent", err) return @@ -115,7 +117,7 @@ func (s *Server) userColorChanged(eventData chatClientEvent) { log.Errorln("invalid color requested when changing user display color") return } - userRepository := storage.GetUserRepository() + userRepository := userrepository.Get() // Save the new color if err := userRepository.ChangeUserColor(eventData.client.User.ID, receivedEvent.NewColor); err != nil { @@ -128,7 +130,7 @@ func (s *Server) userColorChanged(eventData chatClientEvent) { } func (s *Server) userMessageSent(eventData chatClientEvent) { - var event events.UserMessageEvent + var event UserMessageEvent if err := json.Unmarshal(eventData.data, &event); err != nil { log.Errorln("error unmarshalling to UserMessageEvent", err) return @@ -142,15 +144,17 @@ func (s *Server) userMessageSent(eventData chatClientEvent) { return } + st := status.Get() + // Ignore if the stream has been offline - if !getStatus().Online && getStatus().LastDisconnectTime != nil { - disconnectedTime := getStatus().LastDisconnectTime.Time + if st.Online && st.Status.LastDisconnectTime != nil { + disconnectedTime := st.Status.LastDisconnectTime.Time if time.Since(disconnectedTime) > 5*time.Minute { return } } - userRepository := storage.GetUserRepository() + userRepository := userrepository.Get() event.User = userRepository.GetUserByToken(eventData.client.accessToken) @@ -168,9 +172,10 @@ func (s *Server) userMessageSent(eventData chatClientEvent) { // Send chat message sent webhook webhookManager := webhooks.Get() webhookManager.SendChatEvent(&event) - chatMessagesSentCounter.Inc() + s.chatMessagesSentCounter.Inc() - SaveUserMessage(event) + cr := chatrepository.Get() + cr.SaveUserMessage(event) eventData.client.MessageCount++ } diff --git a/core/chat/events/fediverseEngagementEvent.go b/services/chat/fediverseEngagementEvent.go similarity index 63% rename from core/chat/events/fediverseEngagementEvent.go rename to services/chat/fediverseEngagementEvent.go index ceae96d16..0a6f6eb5f 100644 --- a/core/chat/events/fediverseEngagementEvent.go +++ b/services/chat/fediverseEngagementEvent.go @@ -1,23 +1,19 @@ -package events +package chat -import ( - "github.com/owncast/owncast/storage/configrepository" -) +import "github.com/owncast/owncast/models" // FediverseEngagementEvent is a message displayed in chat on representing an action on the Fediverse. type FediverseEngagementEvent struct { - Event + models.Event MessageEvent Image *string `json:"image"` Link string `json:"link"` UserAccountName string `json:"title"` } -var configRepository = configrepository.Get() - // GetBroadcastPayload will return the object to send to all chat users. -func (e *FediverseEngagementEvent) GetBroadcastPayload() EventPayload { - return EventPayload{ +func (e *FediverseEngagementEvent) GetBroadcastPayload() models.EventPayload { + return models.EventPayload{ "id": e.ID, "timestamp": e.Timestamp, "body": e.Body, @@ -25,13 +21,13 @@ func (e *FediverseEngagementEvent) GetBroadcastPayload() EventPayload { "type": e.Event.Type, "title": e.UserAccountName, "link": e.Link, - "user": EventPayload{ - "displayName": configRepository.GetServerName(), + "user": models.EventPayload{ + "displayName": "Owncast", }, } } // GetMessageType will return the event type for this message. -func (e *FediverseEngagementEvent) GetMessageType() EventType { +func (e *FediverseEngagementEvent) GetMessageType() models.EventType { return e.Event.Type } diff --git a/core/chat/events/events.go b/services/chat/messageEvents.go similarity index 52% rename from core/chat/events/events.go rename to services/chat/messageEvents.go index 75083bf3d..4c6769254 100644 --- a/core/chat/events/events.go +++ b/services/chat/messageEvents.go @@ -1,50 +1,27 @@ -package events +package chat import ( "bytes" "regexp" "strings" - "sync" - "text/template" - "time" "github.com/microcosm-cc/bluemonday" "github.com/owncast/owncast/models" - "github.com/teris-io/shortid" "github.com/yuin/goldmark" emoji "github.com/yuin/goldmark-emoji" emojiAst "github.com/yuin/goldmark-emoji/ast" - emojiDef "github.com/yuin/goldmark-emoji/definition" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/util" "mvdan.cc/xurls" - "github.com/owncast/owncast/core/data" log "github.com/sirupsen/logrus" ) -// EventPayload is a generic key/value map for sending out to chat clients. -type EventPayload map[string]interface{} - // OutboundEvent represents an event that is sent out to all listeners of the chat server. type OutboundEvent interface { - GetBroadcastPayload() EventPayload - GetMessageType() EventType -} - -// Event is any kind of event. A type is required to be specified. -type Event struct { - Timestamp time.Time `json:"timestamp"` - Type EventType `json:"type,omitempty"` - ID string `json:"id"` -} - -// UserEvent is an event with an associated user. -type UserEvent struct { - User *models.User `json:"user"` - HiddenAt *time.Time `json:"hiddenAt,omitempty"` - ClientID uint `json:"clientId,omitempty"` + GetBroadcastPayload() models.EventPayload + GetMessageType() models.EventType } // MessageEvent is an event that has a message body. @@ -56,122 +33,10 @@ type MessageEvent struct { // SystemActionEvent is an event that represents an action that took place, not a chat message. type SystemActionEvent struct { - Event + models.Event MessageEvent } -// SetDefaults will set default properties of all inbound events. -func (e *Event) SetDefaults() { - e.ID = shortid.MustGenerate() - e.Timestamp = time.Now() -} - -// SetDefaults will set default properties of all inbound events. -func (e *UserMessageEvent) SetDefaults() { - e.ID = shortid.MustGenerate() - e.Timestamp = time.Now() - e.RenderAndSanitizeMessageBody() -} - -// implements the emojiDef.Emojis interface but uses case-insensitive search. -// the .children field isn't currently used, but could be used in a future -// implementation of say, emoji packs where a child represents a pack. -type emojis struct { - list []emojiDef.Emoji - names map[string]*emojiDef.Emoji - children []emojiDef.Emojis -} - -// return a new Emojis set. -func newEmojis(emotes ...emojiDef.Emoji) emojiDef.Emojis { - self := &emojis{ - list: emotes, - names: map[string]*emojiDef.Emoji{}, - children: []emojiDef.Emojis{}, - } - - for i := range self.list { - emoji := &self.list[i] - for _, s := range emoji.ShortNames { - self.names[s] = emoji - } - } - - return self -} - -func (self *emojis) Get(shortName string) (*emojiDef.Emoji, bool) { - v, ok := self.names[strings.ToLower(shortName)] - if ok { - return v, ok - } - - for _, child := range self.children { - v, ok := child.Get(shortName) - if ok { - return v, ok - } - } - - return nil, false -} - -func (self *emojis) Add(emotes emojiDef.Emojis) { - self.children = append(self.children, emotes) -} - -func (self *emojis) Clone() emojiDef.Emojis { - clone := &emojis{ - list: self.list, - names: self.names, - children: make([]emojiDef.Emojis, len(self.children)), - } - - copy(clone.children, self.children) - - return clone -} - -var ( - emojiMu sync.Mutex - emojiDefs = newEmojis() - emojiHTML = make(map[string]string) - emojiModTime time.Time - emojiHTMLFormat = `:{{ .Name }}:` - emojiHTMLTemplate = template.Must(template.New("emojiHTML").Parse(emojiHTMLFormat)) -) - -func loadEmoji() { - modTime, err := data.UpdateEmojiList(false) - if err != nil { - return - } - - if modTime.After(emojiModTime) { - emojiMu.Lock() - defer emojiMu.Unlock() - - emojiHTML = make(map[string]string) - - emojiList := data.GetEmojiList() - emojiArr := make([]emojiDef.Emoji, 0) - - for i := 0; i < len(emojiList); i++ { - var buf bytes.Buffer - err := emojiHTMLTemplate.Execute(&buf, emojiList[i]) - if err != nil { - return - } - emojiHTML[strings.ToLower(emojiList[i].Name)] = buf.String() - - emoji := emojiDef.NewEmoji(emojiList[i].Name, nil, strings.ToLower(emojiList[i].Name)) - emojiArr = append(emojiArr, emoji) - } - - emojiDefs = newEmojis(emojiArr...) - } -} - // RenderAndSanitizeMessageBody will turn markdown into HTML, sanitize raw user-supplied HTML and standardize // the message into something safe and renderable for clients. func (m *MessageEvent) RenderAndSanitizeMessageBody() { @@ -204,10 +69,8 @@ func RenderAndSanitize(raw string) string { // RenderMarkdown will return HTML rendered from the string body of a chat message. func RenderMarkdown(raw string) string { - loadEmoji() - - emojiMu.Lock() - defer emojiMu.Unlock() + // emojiMu.Lock() + // defer emojiMu.Unlock() markdown := goldmark.New( goldmark.WithRendererOptions( diff --git a/core/chat/messageRendering_test.go b/services/chat/messageRendering_test.go similarity index 96% rename from core/chat/messageRendering_test.go rename to services/chat/messageRendering_test.go index 382d83cdb..5a8ea0a96 100644 --- a/core/chat/messageRendering_test.go +++ b/services/chat/messageRendering_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/owncast/owncast/core/chat/events" + "github.com/owncast/owncast/models" ) // Test a bunch of arbitrary markup and markdown to make sure we get sanitized @@ -54,7 +55,7 @@ func TestAllowEmojiImages(t *testing.T) { func TestAllowHTML(t *testing.T) { messageContent := `` expected := "

\n" - result := events.RenderMarkdown(messageContent) + result := models.RenderMarkdown(messageContent) if result != expected { t.Errorf("message rendering does not match expected. Got\n%s, \n\n want:\n%s", result, expected) diff --git a/core/chat/messages.go b/services/chat/messages.go similarity index 63% rename from core/chat/messages.go rename to services/chat/messages.go index 91316b4b3..ddb63f19d 100644 --- a/core/chat/messages.go +++ b/services/chat/messages.go @@ -3,29 +3,32 @@ package chat import ( "errors" - "github.com/owncast/owncast/core/chat/events" + "github.com/owncast/owncast/models" "github.com/owncast/owncast/services/webhooks" + "github.com/owncast/owncast/storage/chatrepository" log "github.com/sirupsen/logrus" ) // SetMessagesVisibility will set the visibility of multiple messages by ID. -func SetMessagesVisibility(messageIDs []string, visibility bool) error { +func (c *Chat) SetMessagesVisibility(messageIDs []string, visibility bool) error { + cr := chatrepository.Get() + // Save new message visibility - if err := saveMessageVisibility(messageIDs, visibility); err != nil { + if err := cr.SaveMessageVisibility(messageIDs, visibility); err != nil { log.Errorln(err) return err } // Send an event letting the chat clients know to hide or show // the messages. - event := events.SetMessageVisibilityEvent{ + event := models.SetMessageVisibilityEvent{ MessageIDs: messageIDs, Visible: visibility, } event.Event.SetDefaults() payload := event.GetBroadcastPayload() - if err := _server.Broadcast(payload); err != nil { + if err := c.server.Broadcast(payload); err != nil { return errors.New("error broadcasting message visibility payload " + err.Error()) } diff --git a/core/chat/server.go b/services/chat/server.go similarity index 70% rename from core/chat/server.go rename to services/chat/server.go index 361c66fcf..de3ef47fc 100644 --- a/core/chat/server.go +++ b/services/chat/server.go @@ -2,21 +2,26 @@ package chat import ( "encoding/json" + "errors" "fmt" "net/http" "sync" "time" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" log "github.com/sirupsen/logrus" "github.com/gorilla/websocket" - "github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/models" "github.com/owncast/owncast/services/config" "github.com/owncast/owncast/services/geoip" + "github.com/owncast/owncast/services/status" "github.com/owncast/owncast/services/webhooks" - "github.com/owncast/owncast/storage" + "github.com/owncast/owncast/storage/chatrepository" + "github.com/owncast/owncast/storage/configrepository" + "github.com/owncast/owncast/storage/userrepository" "github.com/owncast/owncast/utils" ) @@ -41,25 +46,42 @@ type Server struct { userPartedTimers map[string]*time.Ticker seq uint maxSocketConnectionLimit int64 + chatMessagesSentCounter prometheus.Gauge - mu sync.RWMutex + // a map of user IDs and when they last were active. + lastSeenCache map[string]time.Time + + mu sync.RWMutex + config *config.Config + configRepository *configrepository.SqlConfigRepository + chatRepository *chatrepository.ChatRepository } // NewChat will return a new instance of the chat server. func NewChat() *Server { - maximumConcurrentConnectionLimit := getMaximumConcurrentConnectionLimit() - setSystemConcurrentConnectionLimit(maximumConcurrentConnectionLimit) - server := &Server{ clients: map[uint]*Client{}, outbound: make(chan []byte), inbound: make(chan chatClientEvent), unregister: make(chan uint), - maxSocketConnectionLimit: maximumConcurrentConnectionLimit, + maxSocketConnectionLimit: 100, // TODO: Set this properly! + lastSeenCache: map[string]time.Time{}, geoipClient: geoip.NewClient(), userPartedTimers: map[string]*time.Ticker{}, + config: config.Get(), + configRepository: configrepository.Get(), + chatRepository: chatrepository.Get(), } + server.chatMessagesSentCounter = promauto.NewGauge(prometheus.GaugeOpts{ + Name: "total_chat_message_count", + Help: "The number of chat messages incremented over time.", + ConstLabels: map[string]string{ + "version": server.config.VersionNumber, + "host": server.configRepository.GetServerURL(), + }, + }) + return server } @@ -94,11 +116,7 @@ func (s *Server) Addclient(conn *websocket.Conn, user *models.User, accessToken ConnectedAt: time.Now(), } - // Do not send user re-joined broadcast message if they've been active within 10 minutes. - shouldSendJoinedMessages := configRepository.GetChatJoinPartMessagesEnabled() - if previouslyLastSeen, ok := _lastSeenCache[user.ID]; ok && time.Since(previouslyLastSeen) < time.Minute*10 { - shouldSendJoinedMessages = false - } + shouldSendJoinedMessages := s.configRepository.GetChatJoinPartMessagesEnabled() s.mu.Lock() { @@ -108,7 +126,6 @@ func (s *Server) Addclient(conn *websocket.Conn, user *models.User, accessToken if ticker, ok := s.userPartedTimers[user.ID]; ok { ticker.Stop() delete(s.userPartedTimers, user.ID) - shouldSendJoinedMessages = false } client.Id = s.seq @@ -124,7 +141,9 @@ func (s *Server) Addclient(conn *websocket.Conn, user *models.User, accessToken client.sendConnectedClientInfo() - if getStatus().Online { + st := status.Get() + + if st.Online { if shouldSendJoinedMessages { s.sendUserJoinedMessage(client) } @@ -140,7 +159,7 @@ func (s *Server) Addclient(conn *websocket.Conn, user *models.User, accessToken } func (s *Server) sendUserJoinedMessage(c *Client) { - userJoinedEvent := events.UserJoinedEvent{} + userJoinedEvent := models.UserJoinedEvent{} userJoinedEvent.SetDefaults() userJoinedEvent.User = c.User userJoinedEvent.ClientID = c.Id @@ -154,13 +173,31 @@ func (s *Server) sendUserJoinedMessage(c *Client) { webhookManager.SendChatEventUserJoined(userJoinedEvent) } +// getClientsForUser will return chat connections that are owned by a specific user. +func (s *Server) GetClientsForUser(userID string) ([]*Client, error) { + s.mu.Lock() + defer s.mu.Unlock() + + clients := map[string][]*Client{} + + for _, client := range s.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 +} + func (s *Server) handleClientDisconnected(c *Client) { if _, ok := s.clients[c.Id]; ok { log.Debugln("Deleting", c.Id) delete(s.clients, c.Id) } - additionalClientCheck, _ := GetClientsForUser(c.User.ID) + additionalClientCheck, _ := s.GetClientsForUser(c.User.ID) if len(additionalClientCheck) > 0 { // This user is still connected to chat with another client. return @@ -178,13 +215,13 @@ func (s *Server) sendUserPartedMessage(c *Client) { s.userPartedTimers[c.User.ID].Stop() delete(s.userPartedTimers, c.User.ID) - userPartEvent := events.UserPartEvent{} + userPartEvent := UserPartEvent{} userPartEvent.SetDefaults() userPartEvent.User = c.User userPartEvent.ClientID = c.Id // If part messages are disabled. - if data.GetChatJoinPartMessagesEnabled() { + if s.configRepository.GetChatJoinPartMessagesEnabled() { if err := s.Broadcast(userPartEvent.GetBroadcastPayload()); err != nil { log.Errorln("error sending chat part message", err) } @@ -195,14 +232,17 @@ func (s *Server) sendUserPartedMessage(c *Client) { // HandleClientConnection is fired when a single client connects to the websocket. func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request) { - if configRepository.GetChatDisabled() { - _, _ = w.Write([]byte(events.ChatDisabled)) + cr := configrepository.Get() + chatRepository := chatrepository.Get() + + if cr.GetChatDisabled() { + _, _ = w.Write([]byte(models.ChatDisabled)) return } ipAddress := utils.GetIPAddressFromRequest(r) // Check if this client's IP address is banned. If so send a rejection. - if blocked, err := configRepository.IsIPAddressBanned(ipAddress); blocked { + if blocked, err := chatRepository.IsIPAddressBanned(ipAddress); blocked { log.Debugln("Client ip address has been blocked. Rejecting.") w.WriteHeader(http.StatusForbidden) @@ -214,7 +254,7 @@ func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request) // Limit concurrent chat connections if int64(len(s.clients)) >= s.maxSocketConnectionLimit { log.Warnln("rejecting incoming client connection as it exceeds the max client count of", s.maxSocketConnectionLimit) - _, _ = w.Write([]byte(events.ErrorMaxConnectionsExceeded)) + _, _ = w.Write([]byte(models.ErrorMaxConnectionsExceeded)) return } @@ -237,14 +277,14 @@ func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request) return } - userRepository := storage.GetUserRepository() + userRepository := userrepository.Get() // A user is required to use the websocket user := userRepository.GetUserByToken(accessToken) if user == nil { // Send error that registration is required - _ = conn.WriteJSON(events.EventPayload{ - "type": events.ErrorNeedsRegistration, + _ = conn.WriteJSON(models.EventPayload{ + "type": models.ErrorNeedsRegistration, }) _ = conn.Close() return @@ -253,8 +293,8 @@ func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request) // User is disabled therefore we should disconnect. if user.DisabledAt != nil { log.Traceln("Disabled user", user.ID, user.DisplayName, "rejected") - _ = conn.WriteJSON(events.EventPayload{ - "type": events.ErrorUserDisabled, + _ = conn.WriteJSON(models.EventPayload{ + "type": models.ErrorUserDisabled, }) _ = conn.Close() return @@ -266,7 +306,7 @@ func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request) } // Broadcast sends message to all connected clients. -func (s *Server) Broadcast(payload events.EventPayload) error { +func (s *Server) Broadcast(payload models.EventPayload) error { data, err := json.Marshal(payload) if err != nil { return err @@ -291,7 +331,7 @@ func (s *Server) Broadcast(payload events.EventPayload) error { } // Send will send a single payload to a single connected client. -func (s *Server) Send(payload events.EventPayload, client *Client) { +func (s *Server) Send(payload models.EventPayload, client *Client) { data, err := json.Marshal(payload) if err != nil { log.Errorln(err) @@ -307,12 +347,12 @@ func (s *Server) DisconnectClients(clients []*Client) { log.Traceln("Disconnecting client", client.User.ID, "owned by", client.User.DisplayName) go func(client *Client) { - event := events.UserDisabledEvent{} + event := models.UserDisabledEvent{} event.SetDefaults() // Send this disabled event specifically to this single connected client // to let them know they've been banned. - _server.Send(event.GetBroadcastPayload(), client) + s.Send(event.GetBroadcastPayload(), client) // Give the socket time to send out the above message. // Unfortunately I don't know of any way to get a real callback to know when @@ -327,58 +367,15 @@ func (s *Server) DisconnectClients(clients []*Client) { } } -// SendConnectedClientInfoToUser will find all the connected clients assigned to a user -// and re-send each the connected client info. -func SendConnectedClientInfoToUser(userID string) error { - clients, err := GetClientsForUser(userID) - if err != nil { - return err - } - - userRepository := storage.GetUserRepository() - - // Get an updated reference to the user. - user := userRepository.GetUserByID(userID) - if user == nil { - return fmt.Errorf("user not found") - } - - if err != nil { - return err - } - - for _, client := range clients { - // Update the client's reference to its user. - client.User = user - // Send the update to the client. - client.sendConnectedClientInfo() - } - - return nil -} - -// SendActionToUser will send system action text to all connected clients -// assigned to a user ID. -func SendActionToUser(userID string, text string) error { - clients, err := GetClientsForUser(userID) - if err != nil { - return err - } - - for _, client := range clients { - _server.sendActionToClient(client, text) - } - - return nil -} - func (s *Server) eventReceived(event chatClientEvent) { c := event.client u := c.User + cr := configrepository.Get() + // If established chat user only mode is enabled and the user is not old // enough then reject this event and send them an informative message. - if u != nil && configRepository.GetChatEstbalishedUsersOnlyMode() && time.Since(event.client.User.CreatedAt) < config.GetDefaults().ChatEstablishedUserModeTimeDuration && !u.IsModerator() { + if u != nil && cr.GetChatEstbalishedUsersOnlyMode() && time.Since(event.client.User.CreatedAt) < config.GetDefaults().ChatEstablishedUserModeTimeDuration && !u.IsModerator() { s.sendActionToClient(c, "You have not been an established chat participant long enough to take part in chat. Please enjoy the stream and try again later.") return } @@ -391,13 +388,13 @@ func (s *Server) eventReceived(event chatClientEvent) { eventType := typecheck["type"] switch eventType { - case events.MessageSent: + case models.MessageSent: s.userMessageSent(event) - case events.UserNameChanged: + case models.UserNameChanged: s.userNameChanged(event) - case events.UserColorChanged: + case models.UserColorChanged: s.userColorChanged(event) default: log.Debugln(logSanitize(fmt.Sprint(eventType)), "event not found:", logSanitize(fmt.Sprint(typecheck))) @@ -407,8 +404,9 @@ func (s *Server) eventReceived(event chatClientEvent) { func (s *Server) sendWelcomeMessageToClient(c *Client) { // Add an artificial delay so people notice this message come in. time.Sleep(7 * time.Second) + cr := configrepository.Get() - welcomeMessage := utils.RenderSimpleMarkdown(configRepository.GetServerWelcomeMessage()) + welcomeMessage := utils.RenderSimpleMarkdown(cr.GetServerWelcomeMessage()) if welcomeMessage != "" { s.sendSystemMessageToClient(c, welcomeMessage) @@ -416,39 +414,42 @@ func (s *Server) sendWelcomeMessageToClient(c *Client) { } func (s *Server) sendAllWelcomeMessage() { - welcomeMessage := utils.RenderSimpleMarkdown(configRepository.GetServerWelcomeMessage()) + cr := configrepository.Get() + welcomeMessage := utils.RenderSimpleMarkdown(cr.GetServerWelcomeMessage()) if welcomeMessage != "" { - clientMessage := events.SystemMessageEvent{ - Event: events.Event{}, - MessageEvent: events.MessageEvent{ + clientMessage := SystemMessageEvent{ + Event: models.Event{}, + MessageEvent: MessageEvent{ Body: welcomeMessage, }, } clientMessage.SetDefaults() + clientMessage.DisplayName = s.configRepository.GetServerName() _ = s.Broadcast(clientMessage.GetBroadcastPayload()) } } func (s *Server) sendSystemMessageToClient(c *Client, message string) { - clientMessage := events.SystemMessageEvent{ - Event: events.Event{}, - MessageEvent: events.MessageEvent{ + clientMessage := SystemMessageEvent{ + Event: models.Event{}, + MessageEvent: MessageEvent{ Body: message, }, } clientMessage.SetDefaults() clientMessage.RenderBody() + clientMessage.DisplayName = s.configRepository.GetServerName() s.Send(clientMessage.GetBroadcastPayload(), c) } func (s *Server) sendActionToClient(c *Client, message string) { - clientMessage := events.ActionEvent{ - MessageEvent: events.MessageEvent{ + clientMessage := ActionEvent{ + MessageEvent: MessageEvent{ Body: message, }, - Event: events.Event{ - Type: events.ChatActionSent, + Event: models.Event{ + Type: models.ChatActionSent, }, } clientMessage.SetDefaults() diff --git a/services/chat/systemMessageEvent.go b/services/chat/systemMessageEvent.go new file mode 100644 index 000000000..ee53a71ff --- /dev/null +++ b/services/chat/systemMessageEvent.go @@ -0,0 +1,28 @@ +package chat + +import "github.com/owncast/owncast/models" + +// SystemMessageEvent is a message displayed in chat on behalf of the server. +type SystemMessageEvent struct { + models.Event + MessageEvent + DisplayName string +} + +// GetBroadcastPayload will return the object to send to all chat users. +func (e *SystemMessageEvent) GetBroadcastPayload() models.EventPayload { + return models.EventPayload{ + "id": e.ID, + "timestamp": e.Timestamp, + "body": e.Body, + "type": models.SystemMessageSent, + "user": models.EventPayload{ + "displayName": e.DisplayName, + }, + } +} + +// GetMessageType will return the event type for this message. +func (e *SystemMessageEvent) GetMessageType() models.EventType { + return models.SystemMessageSent +} diff --git a/services/chat/userMessageEvent.go b/services/chat/userMessageEvent.go new file mode 100644 index 000000000..ed27663fc --- /dev/null +++ b/services/chat/userMessageEvent.go @@ -0,0 +1,39 @@ +package chat + +import ( + "time" + + "github.com/owncast/owncast/models" + "github.com/teris-io/shortid" +) + +// UserMessageEvent is an inbound message from a user. +type UserMessageEvent struct { + models.Event + models.UserEvent + MessageEvent +} + +// GetBroadcastPayload will return the object to send to all chat users. +func (e *UserMessageEvent) GetBroadcastPayload() models.EventPayload { + return models.EventPayload{ + "id": e.ID, + "timestamp": e.Timestamp, + "body": e.Body, + "user": e.User, + "type": models.MessageSent, + "visible": e.HiddenAt == nil, + } +} + +// GetMessageType will return the event type for this message. +func (e *UserMessageEvent) GetMessageType() models.EventType { + return models.MessageSent +} + +// SetDefaults will set default properties of all inbound events. +func (e *UserMessageEvent) SetDefaults() { + e.ID = shortid.MustGenerate() + e.Timestamp = time.Now() + e.RenderAndSanitizeMessageBody() +} diff --git a/core/chat/events/userPartEvent.go b/services/chat/userPartEvent.go similarity index 52% rename from core/chat/events/userPartEvent.go rename to services/chat/userPartEvent.go index f0ef14b7d..23cb4c0b9 100644 --- a/core/chat/events/userPartEvent.go +++ b/services/chat/userPartEvent.go @@ -1,15 +1,17 @@ -package events +package chat + +import "github.com/owncast/owncast/models" // UserPartEvent is the event fired when a user leaves chat. type UserPartEvent struct { - Event - UserEvent + models.Event + models.UserEvent } // GetBroadcastPayload will return the object to send to all chat users. -func (e *UserPartEvent) GetBroadcastPayload() EventPayload { - return EventPayload{ - "type": UserParted, +func (e *UserPartEvent) GetBroadcastPayload() models.EventPayload { + return models.EventPayload{ + "type": models.UserParted, "id": e.ID, "timestamp": e.Timestamp, "user": e.User, diff --git a/services/config/config.go b/services/config/config.go index c58a17483..0188dcd7c 100644 --- a/services/config/config.go +++ b/services/config/config.go @@ -44,7 +44,7 @@ type Config struct { } // NewFediAuth creates a new FediAuth instance. -func NewConfig() *Config { +func New() *Config { // Default config values. c := &Config{ DatabaseFilePath: "data/owncast.db", @@ -71,9 +71,9 @@ var temporaryGlobalInstance *Config // GetConfig returns the temporary global instance. // Remove this after dependency injection is implemented. -func GetConfig() *Config { +func Get() *Config { if temporaryGlobalInstance == nil { - temporaryGlobalInstance = NewConfig() + temporaryGlobalInstance = New() } return temporaryGlobalInstance @@ -104,3 +104,8 @@ func (c *Config) GetReleaseString() string { return fmt.Sprintf("Owncast v%s-%s (%s)", versionNumber, buildPlatform, gitCommit) } + +// GetTranscoderLogFilePath returns the logging path for the transcoder log output. +func (c *Config) GetTranscoderLogFilePath() string { + return filepath.Join(c.LogDirectory, "transcoder.log") +} diff --git a/services/metrics/healthOverview.go b/services/metrics/healthOverview.go index 0318874dd..64ddf5827 100644 --- a/services/metrics/healthOverview.go +++ b/services/metrics/healthOverview.go @@ -6,6 +6,7 @@ import ( "github.com/owncast/owncast/core" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/status" "github.com/owncast/owncast/storage/configrepository" "github.com/owncast/owncast/utils" ) @@ -16,8 +17,6 @@ const ( minClientCountForDetails = 3 ) -var configRepository = configrepository.Get() - // GetStreamHealthOverview will return the stream health overview. func (m *Metrics) GetStreamHealthOverview() *models.StreamHealthOverview { return m.metrics.streamHealthOverview @@ -71,6 +70,8 @@ func (m *Metrics) networkSpeedHealthOverviewMessage() string { bitrate int } + configRepository := configrepository.Get() + outputVariants := configRepository.GetStreamOutputVariants() streamSortVariants := make([]singleVariant, len(outputVariants)) @@ -137,12 +138,14 @@ func (m *Metrics) wastefulBitrateOverviewMessage() string { return "" } - currentBroadcast := core.GetCurrentBroadcast() + stat := status.Get() + + currentBroadcast := stat.GetCurrentBroadcast() if currentBroadcast == nil { return "" } - currentBroadcaster := core.GetBroadcaster() + currentBroadcaster := stat.GetBroadcaster() if currentBroadcast == nil { return "" } @@ -156,6 +159,7 @@ func (m *Metrics) wastefulBitrateOverviewMessage() string { if inboundBitrate == 0 { return "" } + configRepository := configrepository.Get() outputVariants := configRepository.GetStreamOutputVariants() @@ -230,6 +234,8 @@ func (m *Metrics) errorCountHealthOverviewMessage() string { if totalNumberOfClients >= minClientCountForDetails { healthyPercentage := utils.IntPercentage(clientsWithErrors, totalNumberOfClients) + configRepository := configrepository.Get() + isUsingPassthrough := false outputVariants := configRepository.GetStreamOutputVariants() for _, variant := range outputVariants { @@ -242,7 +248,8 @@ func (m *Metrics) errorCountHealthOverviewMessage() string { return fmt.Sprintf("%d of %d viewers (%d%%) are experiencing errors. You're currently using a video passthrough output, often known for causing playback issues for people. It is suggested you turn it off.", clientsWithErrors, totalNumberOfClients, healthyPercentage) } - currentBroadcast := core.GetCurrentBroadcast() + stat := status.Get() + currentBroadcast := stat.GetCurrentBroadcast() if currentBroadcast != nil && currentBroadcast.LatencyLevel.SecondsPerSegment < 3 { return fmt.Sprintf("%d of %d viewers (%d%%) may be experiencing some issues. You may want to increase your latency buffer level in your video configuration to see if it helps.", clientsWithErrors, totalNumberOfClients, healthyPercentage) } diff --git a/services/metrics/metrics.go b/services/metrics/metrics.go index f17c0b092..a6417576c 100644 --- a/services/metrics/metrics.go +++ b/services/metrics/metrics.go @@ -5,7 +5,9 @@ import ( "time" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/chat" "github.com/owncast/owncast/services/config" + "github.com/owncast/owncast/storage/configrepository" "github.com/prometheus/client_golang/prometheus" ) @@ -26,6 +28,8 @@ type Metrics struct { chatUserCount prometheus.Gauge currentChatMessageCount prometheus.Gauge playbackErrorCount prometheus.Gauge + + chatService *chat.Chat } // How often we poll for updates. @@ -76,6 +80,7 @@ func New() *Metrics { windowedBandwidths: map[string]float64{}, windowedLatencies: map[string]float64{}, windowedDownloadDurations: map[string]float64{}, + chatService: chat.Get(), } } @@ -84,12 +89,14 @@ func New() *Metrics { // Start will begin the metrics collection and alerting. func (m *Metrics) Start(getStatus func() models.Status) { m.getStatus = getStatus + configRepository := configrepository.Get() + host := configRepository.GetServerURL() if host == "" { host = "unknown" } - c := config.GetConfig() + c := config.Get() m.labels = map[string]string{ "version": c.VersionNumber, diff --git a/services/metrics/playback.go b/services/metrics/playback.go index 7263a463f..31ee36600 100644 --- a/services/metrics/playback.go +++ b/services/metrics/playback.go @@ -6,6 +6,7 @@ import ( "github.com/owncast/owncast/core" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/status" "github.com/owncast/owncast/utils" ) @@ -13,8 +14,10 @@ func (m *Metrics) handlePlaybackPolling() { m.metrics.m.Lock() defer m.metrics.m.Unlock() + s := status.Get() + // Make sure this is fired first before all the values get cleared below. - if m.getStatus().Online { + if s.Online { m.generateStreamHealthOverview() } diff --git a/services/metrics/viewers.go b/services/metrics/viewers.go index 912d4c1b1..5e243cbc9 100644 --- a/services/metrics/viewers.go +++ b/services/metrics/viewers.go @@ -4,9 +4,8 @@ import ( "time" "github.com/nakabonne/tstorage" - "github.com/owncast/owncast/core" - "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/status" "github.com/owncast/owncast/storage/chatrepository" "github.com/owncast/owncast/storage/userrepository" log "github.com/sirupsen/logrus" @@ -30,12 +29,14 @@ func (m *Metrics) startViewerCollectionMetrics() { } func (m *Metrics) collectViewerCount() { + s := status.Get() + // Don't collect metrics for viewers if there's no stream active. - if !core.GetStatus().Online { + if !s.Online { return } - count := core.GetStatus().ViewerCount + count := s.ViewerCount // Save active viewer count to our Prometheus collector. m.activeViewerCount.Set(float64(count)) @@ -52,9 +53,9 @@ func (m *Metrics) collectViewerCount() { } func (m *Metrics) collectChatClientCount() { - count := len(chat.GetClients()) + count := len(m.chatService.GetClients()) m.activeChatClientCount.Set(float64(count)) - chatRepository := chatrepository.GetChatRepository() + chatRepository := chatrepository.Get() usersRepository := userrepository.Get() // Total message count diff --git a/services/notifications/notifications.go b/services/notifications/notifications.go index efb650770..f8dd313bc 100644 --- a/services/notifications/notifications.go +++ b/services/notifications/notifications.go @@ -22,12 +22,15 @@ type Notifier struct { } var ( - configRepository = configrepository.Get() - notificationsRepository = notificationsrepository.Get() + configRepository *configrepository.SqlConfigRepository + notificationsRepository *notificationsrepository.SqlNotificationsRepository ) // Setup will perform any pre-use setup for the notifier. func Setup(datastore *data.Store) { + configRepository = configrepository.Get() + notificationsRepository = notificationsrepository.Get() + initializeBrowserPushIfNeeded() } diff --git a/services/status/status.go b/services/status/status.go new file mode 100644 index 000000000..611ebee4d --- /dev/null +++ b/services/status/status.go @@ -0,0 +1,61 @@ +package status + +import ( + "github.com/owncast/owncast/models" + "github.com/owncast/owncast/storage/configrepository" +) + +type Status struct { + models.Stats + models.Status + + broadcast *models.CurrentBroadcast + broadcaster *models.Broadcaster + StreamConnected bool + + VersionNumber string `json:"versionNumber"` + StreamTitle string `json:"streamTitle"` + ViewerCount int `json:"viewerCount"` + OverallMaxViewerCount int `json:"overallMaxViewerCount"` + SessionMaxViewerCount int `json:"sessionMaxViewerCount"` + + Online bool `json:"online"` +} + +var temporaryGlobalInstance *Status + +func New() *Status { + configRepository := configrepository.Get() + + return &Status{ + StreamTitle: configRepository.GetStreamTitle(), + } +} + +// Get will return the global instance of the status service. +func Get() *Status { + if temporaryGlobalInstance == nil { + temporaryGlobalInstance = &Status{} + } + + return temporaryGlobalInstance +} + +// GetCurrentBroadcast will return the currently active broadcast. +func (s *Status) GetCurrentBroadcast() *models.CurrentBroadcast { + return s.broadcast +} + +func (s *Status) SetCurrentBroadcast(broadcast *models.CurrentBroadcast) { + s.broadcast = broadcast +} + +// SetBroadcaster will store the current inbound broadcasting details. +func (s *Status) SetBroadcaster(broadcaster *models.Broadcaster) { + s.broadcaster = broadcaster +} + +// GetBroadcaster will return the details of the currently active broadcaster. +func (s *Status) GetBroadcaster() *models.Broadcaster { + return s.broadcaster +} diff --git a/services/webhooks/chat.go b/services/webhooks/chat.go index 1cc96e3a2..8e9928030 100644 --- a/services/webhooks/chat.go +++ b/services/webhooks/chat.go @@ -1,12 +1,11 @@ package webhooks import ( - "github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/models" ) // SendChatEvent will send a chat event to webhook destinations. -func (w *LiveWebhookManager) SendChatEvent(chatEvent *events.UserMessageEvent) { +func (w *LiveWebhookManager) SendChatEvent(chatEvent *models.UserMessageEvent) { webhookEvent := WebhookEvent{ Type: chatEvent.GetMessageType(), EventData: &WebhookChatMessage{ @@ -24,7 +23,7 @@ func (w *LiveWebhookManager) SendChatEvent(chatEvent *events.UserMessageEvent) { } // SendChatEventUsernameChanged will send a username changed event to webhook destinations. -func (w *LiveWebhookManager) SendChatEventUsernameChanged(event events.NameChangeEvent) { +func (w *LiveWebhookManager) SendChatEventUsernameChanged(event models.NameChangeEvent) { webhookEvent := WebhookEvent{ Type: models.UserNameChanged, EventData: event, @@ -34,7 +33,7 @@ func (w *LiveWebhookManager) SendChatEventUsernameChanged(event events.NameChang } // SendChatEventUserJoined sends a webhook notifying that a user has joined. -func (w *LiveWebhookManager) SendChatEventUserJoined(event events.UserJoinedEvent) { +func (w *LiveWebhookManager) SendChatEventUserJoined(event models.UserJoinedEvent) { webhookEvent := WebhookEvent{ Type: models.UserJoined, EventData: event, @@ -55,7 +54,7 @@ func SendChatEventUserParted(event events.UserPartEvent) { // SendChatEventSetMessageVisibility sends a webhook notifying that the visibility of one or more // messages has changed. -func (w *LiveWebhookManager) SendChatEventSetMessageVisibility(event events.SetMessageVisibilityEvent) { +func (w *LiveWebhookManager) SendChatEventSetMessageVisibility(event models.SetMessageVisibilityEvent) { webhookEvent := WebhookEvent{ Type: models.VisibiltyToggled, EventData: event, diff --git a/services/webhooks/chat_test.go b/services/webhooks/chat_test.go index 9ebc09ca2..91a2e1ca6 100644 --- a/services/webhooks/chat_test.go +++ b/services/webhooks/chat_test.go @@ -4,14 +4,12 @@ import ( "testing" "time" - "github.com/owncast/owncast/core/chat/events" - "github.com/owncast/owncast/core/user" "github.com/owncast/owncast/models" ) func TestSendChatEvent(t *testing.T) { timestamp := time.Unix(72, 6).UTC() - user := user.User{ + user := models.User{ ID: "user id", DisplayName: "display name", DisplayColor: 4, @@ -26,18 +24,18 @@ func TestSendChatEvent(t *testing.T) { } checkPayload(t, models.MessageSent, func() { - manager.SendChatEvent(&events.UserMessageEvent{ - Event: events.Event{ - Type: events.MessageSent, + manager.SendChatEvent(&models.UserMessageEvent{ + Event: models.Event{ + Type: models.MessageSent, ID: "id", Timestamp: timestamp, }, - UserEvent: events.UserEvent{ + UserEvent: models.UserEvent{ User: &user, ClientID: 51, HiddenAt: nil, }, - MessageEvent: events.MessageEvent{ + MessageEvent: models.MessageEvent{ OutboundEvent: nil, Body: "body", RawBody: "raw body", @@ -64,7 +62,7 @@ func TestSendChatEvent(t *testing.T) { func TestSendChatEventUsernameChanged(t *testing.T) { timestamp := time.Unix(72, 6).UTC() - user := user.User{ + user := models.User{ ID: "user id", DisplayName: "display name", DisplayColor: 4, @@ -79,13 +77,13 @@ func TestSendChatEventUsernameChanged(t *testing.T) { } checkPayload(t, models.UserNameChanged, func() { - manager.SendChatEventUsernameChanged(events.NameChangeEvent{ - Event: events.Event{ - Type: events.UserNameChanged, + manager.SendChatEventUsernameChanged(models.NameChangeEvent{ + Event: models.Event{ + Type: models.UserNameChanged, ID: "id", Timestamp: timestamp, }, - UserEvent: events.UserEvent{ + UserEvent: models.UserEvent{ User: &user, ClientID: 51, HiddenAt: nil, @@ -112,7 +110,7 @@ func TestSendChatEventUsernameChanged(t *testing.T) { func TestSendChatEventUserJoined(t *testing.T) { timestamp := time.Unix(72, 6).UTC() - user := user.User{ + user := models.User{ ID: "user id", DisplayName: "display name", DisplayColor: 4, @@ -127,13 +125,13 @@ func TestSendChatEventUserJoined(t *testing.T) { } checkPayload(t, models.UserJoined, func() { - manager.SendChatEventUserJoined(events.UserJoinedEvent{ - Event: events.Event{ - Type: events.UserJoined, + manager.SendChatEventUserJoined(models.UserJoinedEvent{ + Event: models.Event{ + Type: models.UserJoined, ID: "id", Timestamp: timestamp, }, - UserEvent: events.UserEvent{ + UserEvent: models.UserEvent{ User: &user, ClientID: 51, HiddenAt: nil, @@ -160,13 +158,13 @@ func TestSendChatEventSetMessageVisibility(t *testing.T) { timestamp := time.Unix(72, 6).UTC() checkPayload(t, models.VisibiltyToggled, func() { - manager.SendChatEventSetMessageVisibility(events.SetMessageVisibilityEvent{ - Event: events.Event{ - Type: events.VisibiltyUpdate, + manager.SendChatEventSetMessageVisibility(models.SetMessageVisibilityEvent{ + Event: models.Event{ + Type: models.VisibiltyUpdate, ID: "id", Timestamp: timestamp, }, - UserMessageEvent: events.UserMessageEvent{}, + UserMessageEvent: models.UserMessageEvent{}, MessageIDs: []string{"message1", "message2"}, Visible: false, }) diff --git a/services/webhooks/manager.go b/services/webhooks/manager.go index 59a4de244..055c6fb78 100644 --- a/services/webhooks/manager.go +++ b/services/webhooks/manager.go @@ -10,11 +10,11 @@ type Manager interface { // to be sent out to all registered webhook destinations. type LiveWebhookManager struct { queue chan Job - getStatus func() models.Status + getStatus func() *models.Status } // New creates a new webhook manager. -func New(getStatusFunc func() models.Status) *LiveWebhookManager { +func New(getStatusFunc func() *models.Status) *LiveWebhookManager { m := &LiveWebhookManager{ getStatus: getStatusFunc, } @@ -24,7 +24,7 @@ func New(getStatusFunc func() models.Status) *LiveWebhookManager { // InitTemporarySingleton initializes the the temporary global instance of the webhook manager // to be deleted once dependency injection is implemented. -func InitTemporarySingleton(getStatusFunc func() models.Status) { +func InitTemporarySingleton(getStatusFunc func() *models.Status) { temporaryGlobalInstance = New(getStatusFunc) } diff --git a/services/webhooks/stream.go b/services/webhooks/stream.go index 75ed82b4d..bc111ff84 100644 --- a/services/webhooks/stream.go +++ b/services/webhooks/stream.go @@ -8,14 +8,14 @@ import ( "github.com/teris-io/shortid" ) -var configRepository = configrepository.Get() - // SendStreamStatusEvent will send all webhook destinations the current stream status. func (w *LiveWebhookManager) SendStreamStatusEvent(eventType models.EventType) { w.sendStreamStatusEvent(eventType, shortid.MustGenerate(), time.Now()) } func (w *LiveWebhookManager) sendStreamStatusEvent(eventType models.EventType, id string, timestamp time.Time) { + configRepository := configrepository.Get() + w.SendEventToWebhooks(WebhookEvent{ Type: eventType, EventData: map[string]interface{}{ diff --git a/services/webhooks/stream_test.go b/services/webhooks/stream_test.go index b4df5fcc2..0dbca3482 100644 --- a/services/webhooks/stream_test.go +++ b/services/webhooks/stream_test.go @@ -4,17 +4,19 @@ import ( "testing" "time" - "github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/storage/configrepository" ) func TestSendStreamStatusEvent(t *testing.T) { + configRepository := configrepository.Get() + configRepository.SetServerName("my server") configRepository.SetServerSummary("my server where I stream") configRepository.SetStreamTitle("my stream") checkPayload(t, models.StreamStarted, func() { - manager.sendStreamStatusEvent(events.StreamStarted, "id", time.Unix(72, 6).UTC()) + manager.sendStreamStatusEvent(models.StreamStarted, "id", time.Unix(72, 6).UTC()) }, `{ "id": "id", "name": "my server", diff --git a/services/webhooks/webhooks.go b/services/webhooks/webhooks.go index 2cd7dcf5f..439344915 100644 --- a/services/webhooks/webhooks.go +++ b/services/webhooks/webhooks.go @@ -25,14 +25,13 @@ type WebhookChatMessage struct { Visible bool `json:"visible"` } -var webhookRepository = storage.GetWebhookRepository() - // SendEventToWebhooks will send a single webhook event to all webhook destinations. func (w *LiveWebhookManager) SendEventToWebhooks(payload WebhookEvent) { w.sendEventToWebhooks(payload, nil) } func (w *LiveWebhookManager) sendEventToWebhooks(payload WebhookEvent, wg *sync.WaitGroup) { + webhookRepository := storage.GetWebhookRepository() webhooks := webhookRepository.GetWebhooksForEvent(payload.Type) for _, webhook := range webhooks { diff --git a/services/webhooks/webhooks_test.go b/services/webhooks/webhooks_test.go index 0b768c648..9358656c9 100644 --- a/services/webhooks/webhooks_test.go +++ b/services/webhooks/webhooks_test.go @@ -6,7 +6,6 @@ import ( "fmt" "net/http" "net/http/httptest" - "os" "sync" "sync/atomic" "testing" @@ -15,13 +14,15 @@ import ( "github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/storage" + "github.com/owncast/owncast/storage/data" jsonpatch "gopkg.in/evanphx/json-patch.v5" ) var manager *LiveWebhookManager -func fakeGetStatus() models.Status { - return models.Status{ +func fakeGetStatus() *models.Status { + return &models.Status{ Online: true, ViewerCount: 5, OverallMaxViewerCount: 420, @@ -32,16 +33,10 @@ func fakeGetStatus() models.Status { } func TestMain(m *testing.M) { - dbFile, err := os.CreateTemp(os.TempDir(), "owncast-test-db.db") + _, err := data.NewStore(":memory:") if err != nil { panic(err) } - dbFile.Close() - defer os.Remove(dbFile.Name()) - - if err := data.SetupPersistence(dbFile.Name()); err != nil { - panic(err) - } InitTemporarySingleton(fakeGetStatus) manager = Get() @@ -53,6 +48,8 @@ func TestMain(m *testing.M) { // Because the other tests use `sendEventToWebhooks` with a `WaitGroup` to know when the test completes, // this test ensures that `SendToWebhooks` without a `WaitGroup` doesn't panic. func TestPublicSend(t *testing.T) { + webhookRepository := storage.GetWebhookRepository() + // Send enough events to be sure at least one worker delivers a second event. eventsCount := webhookWorkerPoolSize + 1 @@ -87,6 +84,8 @@ func TestPublicSend(t *testing.T) { // Make sure that events are only sent to interested endpoints. func TestRouting(t *testing.T) { + webhookRepository := storage.GetWebhookRepository() + eventTypes := []models.EventType{models.ChatActionSent, models.UserJoined, events.UserParted} calls := map[models.EventType]int{} @@ -142,6 +141,8 @@ func TestRouting(t *testing.T) { // Make sure that events are sent to all interested endpoints. func TestMultiple(t *testing.T) { + webhookRepository := storage.GetWebhookRepository() + const times = 2 var calls uint32 @@ -179,6 +180,8 @@ func TestMultiple(t *testing.T) { // Make sure when a webhook is used its last used timestamp is updated. func TestTimestamps(t *testing.T) { + webhookRepository := storage.GetWebhookRepository() + const tolerance = time.Second start := time.Now() eventTypes := []models.EventType{models.StreamStarted, models.StreamStopped} @@ -260,6 +263,8 @@ func TestTimestamps(t *testing.T) { // Make sure up to the expected number of events can be fired in parallel. func TestParallel(t *testing.T) { + webhookRepository := storage.GetWebhookRepository() + var calls uint32 var wgStart sync.WaitGroup @@ -312,6 +317,8 @@ func TestParallel(t *testing.T) { // Send an event, capture it, and verify that it has the expected payload. func checkPayload(t *testing.T, eventType models.EventType, send func(), expectedJson string) { + webhookRepository := storage.GetWebhookRepository() + eventChannel := make(chan WebhookEvent) // Set up a server. diff --git a/services/webhooks/workerpool.go b/services/webhooks/workerpool.go index 7a9e2743c..54ad23e6e 100644 --- a/services/webhooks/workerpool.go +++ b/services/webhooks/workerpool.go @@ -10,6 +10,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/storage" ) // webhookWorkerPoolSize defines the number of concurrent HTTP webhook requests. @@ -75,6 +76,8 @@ func (w *LiveWebhookManager) sendWebhook(job Job) error { defer resp.Body.Close() + webhookRepository := storage.GetWebhookRepository() + if err := webhookRepository.SetWebhookAsUsed(job.webhook); err != nil { log.Warnln(err) } diff --git a/services/yp/yp.go b/services/yp/yp.go index 0b7d701f0..105aaac40 100644 --- a/services/yp/yp.go +++ b/services/yp/yp.go @@ -18,12 +18,10 @@ import ( const pingInterval = 4 * time.Minute var ( - getStatus func() models.Status + getStatus func() *models.Status _inErrorState = false ) -var configRepository = configrepository.Get() - // YP is a service for handling listing in the Owncast directory. type YP struct { timer *time.Ticker @@ -42,7 +40,7 @@ type ypPingRequest struct { } // NewYP creates a new instance of the YP service handler. -func NewYP(getStatusFunc func() models.Status) *YP { +func NewYP(getStatusFunc func() *models.Status) *YP { getStatus = getStatusFunc return &YP{} } @@ -63,6 +61,8 @@ func (yp *YP) Stop() { } func (yp *YP) ping() { + configRepository := configrepository.Get() + if !configRepository.GetDirectoryEnabled() { return } diff --git a/static/emoji.go b/static/emoji.go index 60752d9eb..0522488e6 100644 --- a/static/emoji.go +++ b/static/emoji.go @@ -1,8 +1,6 @@ package static import ( - "fmt" - "io" "io/fs" "os" "path/filepath" @@ -11,8 +9,6 @@ import ( "github.com/owncast/owncast/models" "github.com/owncast/owncast/services/config" - "github.com/owncast/owncast/utils" - "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) @@ -92,88 +88,3 @@ func GetEmojiList() []models.CustomEmoji { 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 - } - - c := config.GetConfig() - - if utils.DoesFileExists(c.CustomEmojiPath) { - return nil - } - - if err = os.MkdirAll(c.CustomEmojiPath, 0o750); err != nil { - return fmt.Errorf("unable to create custom emoji directory: %w", err) - } - - staticFS := 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(c.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 -} diff --git a/storage/chatrepository/chatrepository.go b/storage/chatrepository/chatrepository.go index 4b0d9948d..2d3916edd 100644 --- a/storage/chatrepository/chatrepository.go +++ b/storage/chatrepository/chatrepository.go @@ -11,6 +11,8 @@ func New(datastore *data.Store) *ChatRepository { datastore: datastore, } + r.startPruner() + return r } @@ -18,7 +20,7 @@ func New(datastore *data.Store) *ChatRepository { var temporaryGlobalInstance *ChatRepository // GetUserRepository will return the user repository. -func GetChatRepository() *ChatRepository { +func Get() *ChatRepository { if temporaryGlobalInstance == nil { i := New(data.GetDatastore()) temporaryGlobalInstance = i diff --git a/core/chat/persistence.go b/storage/chatrepository/persistence.go similarity index 74% rename from core/chat/persistence.go rename to storage/chatrepository/persistence.go index ea8d1afb5..a5e9430b1 100644 --- a/core/chat/persistence.go +++ b/storage/chatrepository/persistence.go @@ -1,4 +1,4 @@ -package chat +package chatrepository import ( "context" @@ -6,7 +6,6 @@ import ( "strings" "time" - "github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/models" "github.com/owncast/owncast/storage/data" log "github.com/sirupsen/logrus" @@ -14,36 +13,17 @@ import ( var _datastore *data.Store -const ( - maxBacklogHours = 2 // Keep backlog max hours worth of messages - maxBacklogNumber = 50 // Return max number of messages in history request -) - -func setupPersistence() { - _datastore = data.GetDatastore() - data.CreateMessagesTable(_datastore.DB) - data.CreateBanIPTable(_datastore.DB) - - chatDataPruner := time.NewTicker(5 * time.Minute) - go func() { - runPruner() - for range chatDataPruner.C { - runPruner() - } - }() -} - // SaveUserMessage will save a single chat event to the messages database. -func SaveUserMessage(event events.UserMessageEvent) { - saveEvent(event.ID, &event.User.ID, event.Body, event.Type, event.HiddenAt, event.Timestamp, nil, nil, nil, nil) +func (cr *ChatRepository) SaveUserMessage(event models.UserMessageEvent) { + cr.SaveEvent(event.ID, &event.User.ID, event.Body, event.Type, event.HiddenAt, event.Timestamp, nil, nil, nil, nil) } -func saveFederatedAction(event events.FediverseEngagementEvent) { - saveEvent(event.ID, nil, event.Body, event.Type, nil, event.Timestamp, event.Image, &event.Link, &event.UserAccountName, nil) +func (cr *ChatRepository) SaveFederatedAction(event models.FediverseEngagementEvent) { + cr.SaveEvent(event.ID, nil, event.Body, event.Type, nil, event.Timestamp, event.Image, &event.Link, &event.UserAccountName, nil) } // nolint: unparam -func saveEvent(id string, userID *string, body string, eventType string, hidden *time.Time, timestamp time.Time, image *string, link *string, title *string, subtitle *string) { +func (cr *ChatRepository) SaveEvent(id string, userID *string, body string, eventType string, hidden *time.Time, timestamp time.Time, image *string, link *string, title *string, subtitle *string) { defer func() { _historyCache = nil }() @@ -74,7 +54,7 @@ func saveEvent(id string, userID *string, body string, eventType string, hidden } } -func makeUserMessageEventFromRowData(row rowData) events.UserMessageEvent { +func (cr *ChatRepository) makeUserMessageEventFromRowData(row rowData) models.UserMessageEvent { scopes := "" if row.userScopes != nil { scopes = *row.userScopes @@ -117,17 +97,17 @@ func makeUserMessageEventFromRowData(row rowData) events.UserMessageEvent { IsBot: isBot, } - message := events.UserMessageEvent{ - Event: events.Event{ + message := models.UserMessageEvent{ + Event: models.Event{ Type: row.eventType, ID: row.id, Timestamp: row.timestamp, }, - UserEvent: events.UserEvent{ + UserEvent: models.UserEvent{ User: &u, HiddenAt: row.hiddenAt, }, - MessageEvent: events.MessageEvent{ + MessageEvent: models.MessageEvent{ Body: row.body, RawBody: row.body, }, @@ -136,14 +116,14 @@ func makeUserMessageEventFromRowData(row rowData) events.UserMessageEvent { return message } -func makeSystemMessageChatEventFromRowData(row rowData) events.SystemMessageEvent { - message := events.SystemMessageEvent{ - Event: events.Event{ +func (cr *ChatRepository) makeSystemMessageChatEventFromRowData(row rowData) models.SystemMessageEvent { + message := models.SystemMessageEvent{ + Event: models.Event{ Type: row.eventType, ID: row.id, Timestamp: row.timestamp, }, - MessageEvent: events.MessageEvent{ + MessageEvent: models.MessageEvent{ Body: row.body, RawBody: row.body, }, @@ -151,14 +131,14 @@ func makeSystemMessageChatEventFromRowData(row rowData) events.SystemMessageEven return message } -func makeActionMessageChatEventFromRowData(row rowData) events.ActionEvent { - message := events.ActionEvent{ - Event: events.Event{ +func (cr *ChatRepository) makeActionMessageChatEventFromRowData(row rowData) models.ActionEvent { + message := models.ActionEvent{ + Event: models.Event{ Type: row.eventType, ID: row.id, Timestamp: row.timestamp, }, - MessageEvent: events.MessageEvent{ + MessageEvent: models.MessageEvent{ Body: row.body, RawBody: row.body, }, @@ -166,14 +146,14 @@ func makeActionMessageChatEventFromRowData(row rowData) events.ActionEvent { return message } -func makeFederatedActionChatEventFromRowData(row rowData) events.FediverseEngagementEvent { - message := events.FediverseEngagementEvent{ - Event: events.Event{ +func (cr *ChatRepository) makeFederatedActionChatEventFromRowData(row rowData) models.FediverseEngagementEvent { + message := models.FediverseEngagementEvent{ + Event: models.Event{ Type: row.eventType, ID: row.id, Timestamp: row.timestamp, }, - MessageEvent: events.MessageEvent{ + MessageEvent: models.MessageEvent{ Body: row.body, RawBody: row.body, }, @@ -208,7 +188,7 @@ type rowData struct { id string } -func getChat(rows *sql.Rows) ([]interface{}, error) { +func (cr *ChatRepository) getChat(rows *sql.Rows) ([]interface{}, error) { history := make([]interface{}, 0) for rows.Next() { @@ -242,18 +222,18 @@ func getChat(rows *sql.Rows) ([]interface{}, error) { var message interface{} switch row.eventType { - case events.MessageSent: - message = makeUserMessageEventFromRowData(row) - case events.SystemMessageSent: - message = makeSystemMessageChatEventFromRowData(row) - case events.ChatActionSent: - message = makeActionMessageChatEventFromRowData(row) - case events.FediverseEngagementFollow: - message = makeFederatedActionChatEventFromRowData(row) - case events.FediverseEngagementLike: - message = makeFederatedActionChatEventFromRowData(row) - case events.FediverseEngagementRepost: - message = makeFederatedActionChatEventFromRowData(row) + case models.MessageSent: + message = cr.makeUserMessageEventFromRowData(row) + case models.SystemMessageSent: + message = cr.makeSystemMessageChatEventFromRowData(row) + case models.ChatActionSent: + message = cr.makeActionMessageChatEventFromRowData(row) + case models.FediverseEngagementFollow: + message = cr.makeFederatedActionChatEventFromRowData(row) + case models.FediverseEngagementLike: + message = cr.makeFederatedActionChatEventFromRowData(row) + case models.FediverseEngagementRepost: + message = cr.makeFederatedActionChatEventFromRowData(row) } history = append(history, message) @@ -265,7 +245,7 @@ func getChat(rows *sql.Rows) ([]interface{}, error) { var _historyCache *[]interface{} // GetChatModerationHistory will return all the chat messages suitable for moderation purposes. -func GetChatModerationHistory() []interface{} { +func (cr *ChatRepository) GetChatModerationHistory() []interface{} { if _historyCache != nil { return *_historyCache } @@ -295,7 +275,7 @@ func GetChatModerationHistory() []interface{} { defer stmt.Close() defer rows.Close() - result, err := getChat(rows) + result, err := cr.getChat(rows) if err != nil { log.Errorln(err) log.Errorln("There is a problem enumerating chat message rows. Please report this:", query) @@ -313,7 +293,7 @@ func GetChatModerationHistory() []interface{} { } // GetChatHistory will return all the chat messages suitable for returning as user-facing chat history. -func GetChatHistory() []interface{} { +func (cr *ChatRepository) GetChatHistory() []interface{} { tx, err := _datastore.DB.Begin() if err != nil { log.Errorln("error fetching chat history", err) @@ -340,7 +320,7 @@ func GetChatHistory() []interface{} { defer stmt.Close() defer rows.Close() - m, err := getChat(rows) + m, err := cr.getChat(rows) if err != nil { log.Errorln(err) log.Errorln("There is a problem enumerating chat message rows. Please report this:", query) @@ -361,20 +341,20 @@ func GetChatHistory() []interface{} { } // GetMessagesFromUser returns chat messages that were sent by a specific user. -func GetMessagesFromUser(userID string) ([]events.UserMessageEvent, error) { +func (cr *ChatRepository) GetMessagesFromUser(userID string) ([]models.UserMessageEvent, error) { query, err := _datastore.GetQueries().GetMessagesFromUser(context.Background(), sql.NullString{String: userID, Valid: true}) if err != nil { return nil, err } - results := make([]events.UserMessageEvent, len(query)) + results := make([]models.UserMessageEvent, len(query)) for i, row := range query { - results[i] = events.UserMessageEvent{ - Event: events.Event{ + results[i] = models.UserMessageEvent{ + Event: models.Event{ Timestamp: row.Timestamp.Time, ID: row.ID, }, - MessageEvent: events.MessageEvent{ + MessageEvent: models.MessageEvent{ Body: row.Body.String, }, } @@ -385,7 +365,7 @@ func GetMessagesFromUser(userID string) ([]events.UserMessageEvent, error) { // SetMessageVisibilityForUserID will bulk change the visibility of messages for a user // and then send out visibility changed events to chat clients. -func SetMessageVisibilityForUserID(userID string, visible bool) error { +func (cr *ChatRepository) SetMessageVisibilityForUserID(userID string, visible bool) ([]string, error) { defer func() { _historyCache = nil }() @@ -393,7 +373,7 @@ func SetMessageVisibilityForUserID(userID string, visible bool) error { tx, err := _datastore.DB.Begin() if err != nil { log.Errorln("error while setting message visibility", err) - return nil + return nil, err } defer tx.Rollback() // nolint @@ -402,13 +382,13 @@ func SetMessageVisibilityForUserID(userID string, visible bool) error { stmt, err := tx.Prepare(query) if err != nil { log.Errorln("error while setting message visibility", err) - return nil + return nil, err } rows, err := stmt.Query(userID) if err != nil { log.Errorln("error while setting message visibility", err) - return nil + return nil, err } defer stmt.Close() @@ -417,31 +397,30 @@ func SetMessageVisibilityForUserID(userID string, visible bool) error { // Get a list of IDs to send to the connected clients to hide ids := make([]string, 0) - messages, err := getChat(rows) + messages, err := cr.getChat(rows) if err != nil { log.Errorln(err) log.Errorln("There is a problem enumerating chat message rows. Please report this:", query) - return nil + return nil, err } if len(messages) == 0 { - return nil + return nil, nil } for _, message := range messages { - ids = append(ids, message.(events.UserMessageEvent).ID) + ids = append(ids, message.(models.UserMessageEvent).ID) } if err = tx.Commit(); err != nil { log.Errorln("error while setting message visibility ", err) - return nil + return nil, nil } - // Tell the clients to hide/show these messages. - return SetMessagesVisibility(ids, visible) + return nil, nil } -func saveMessageVisibility(messageIDs []string, visible bool) error { +func (cr *ChatRepository) SaveMessageVisibility(messageIDs []string, visible bool) error { defer func() { _historyCache = nil }() diff --git a/core/chat/pruner.go b/storage/chatrepository/pruner.go similarity index 61% rename from core/chat/pruner.go rename to storage/chatrepository/pruner.go index a9281b3df..0ab357c77 100644 --- a/core/chat/pruner.go +++ b/storage/chatrepository/pruner.go @@ -1,14 +1,33 @@ -package chat +package chatrepository import ( "fmt" + "time" + "github.com/owncast/owncast/storage/data" log "github.com/sirupsen/logrus" ) +const ( + maxBacklogHours = 2 // Keep backlog max hours worth of messages + maxBacklogNumber = 50 // Return max number of messages in history request +) + +func (cr *ChatRepository) startPruner() { + chatDataPruner := time.NewTicker(5 * time.Minute) + go func() { + cr.runPruner() + for range chatDataPruner.C { + cr.runPruner() + } + }() +} + // Only keep recent messages so we don't keep more chat data than needed // for privacy and efficiency reasons. -func runPruner() { +func (cr *ChatRepository) runPruner() { + _datastore := data.GetDatastore() + _datastore.DbLock.Lock() defer _datastore.DbLock.Unlock() diff --git a/storage/configrepository/config.go b/storage/configrepository/config.go index b5ee9ed7b..271f8d566 100644 --- a/storage/configrepository/config.go +++ b/storage/configrepository/config.go @@ -537,7 +537,7 @@ func (cr *SqlConfigRepository) GetVideoCodec() string { // VerifySettings will perform a sanity check for specific settings values. func (cr *SqlConfigRepository) VerifySettings() error { - c := config.GetConfig() + c := config.Get() if len(cr.GetStreamKeys()) == 0 && c.TemporaryStreamKey == "" { log.Errorln("No stream key set. Streaming is disabled. Please set one via the admin or command line arguments") } @@ -752,13 +752,13 @@ func (cr *SqlConfigRepository) GetBlockedFederatedDomains() []string { return strings.Split(domains, ",") } -// SetChatJoinMessagesEnabled will set if chat join messages are enabled. -func (cr *SqlConfigRepository) SetChatJoinMessagesEnabled(enabled bool) error { +// SetChatJoinPartMessagesEnabled will set if chat join messages are enabled. +func (cr *SqlConfigRepository) SetChatJoinPartMessagesEnabled(enabled bool) error { return cr.datastore.SetBool(chatJoinMessagesEnabledKey, enabled) } -// GetChatJoinMessagesEnabled will return if chat join messages are enabled. -func (cr *SqlConfigRepository) GetChatJoinMessagesEnabled() bool { +// GetChatJoinPartMessagesEnabled will return if chat join messages are enabled. +func (cr *SqlConfigRepository) GetChatJoinPartMessagesEnabled() bool { enabled, err := cr.datastore.GetBool(chatJoinMessagesEnabledKey) if err != nil { return true diff --git a/storage/configrepository/configrepository.go b/storage/configrepository/configrepository.go index 7d96f4c6d..67e476851 100644 --- a/storage/configrepository/configrepository.go +++ b/storage/configrepository/configrepository.go @@ -93,8 +93,8 @@ type ConfigRepository interface { GetFederationShowEngagement() bool SetBlockedFederatedDomains(domains []string) error GetBlockedFederatedDomains() []string - SetChatJoinMessagesEnabled(enabled bool) error - GetChatJoinMessagesEnabled() bool + SetChatJoinPartMessagesEnabled(enabled bool) error + GetChatJoinPartMessagesEnabled() bool SetNotificationsEnabled(enabled bool) error GetNotificationsEnabled() bool GetDiscordConfig() models.DiscordConfiguration @@ -119,6 +119,7 @@ type ConfigRepository interface { GetDisableSearchIndexing() bool GetVideoServingEndpoint() string SetVideoServingEndpoint(message string) error + GetDefaultFederationUsername() string } type SqlConfigRepository struct { diff --git a/storage/data/datastore.go b/storage/data/datastore.go index 6997e67dd..f34722525 100644 --- a/storage/data/datastore.go +++ b/storage/data/datastore.go @@ -47,7 +47,7 @@ var temporaryGlobalDatastoreInstance *Store // GetDatastore returns the shared instance of the owncast datastore. func GetDatastore() *Store { if temporaryGlobalDatastoreInstance == nil { - c := config.GetConfig() + c := config.Get() i, err := NewStore(c.DatabaseFilePath) if err != nil { log.Fatal(err) diff --git a/storage/federationrepository/federationrepository.go b/storage/federationrepository/federationrepository.go index d10eea10a..c010494f3 100644 --- a/storage/federationrepository/federationrepository.go +++ b/storage/federationrepository/federationrepository.go @@ -9,9 +9,9 @@ import ( "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" - "github.com/owncast/owncast/activitypub/apmodels" - "github.com/owncast/owncast/activitypub/resolvers" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/apfederation/apmodels" + "github.com/owncast/owncast/services/apfederation/resolvers" "github.com/owncast/owncast/storage/data" "github.com/owncast/owncast/storage/sqlstorage" "github.com/owncast/owncast/utils" @@ -306,6 +306,8 @@ func (f *FederationRepository) GetOutboxPostCount() (int64, error) { func (f *FederationRepository) GetOutbox(limit int, offset int) (vocab.ActivityStreamsOrderedCollection, error) { collection := streams.NewActivityStreamsOrderedCollection() orderedItems := streams.NewActivityStreamsOrderedItemsProperty() + r := resolvers.Get() + rows, err := f.datastore.GetQueries().GetOutboxWithOffset( context.Background(), sqlstorage.GetOutboxWithOffsetParams{Limit: int32(limit), Offset: int32(offset)}, @@ -319,7 +321,7 @@ func (f *FederationRepository) GetOutbox(limit int, offset int) (vocab.ActivityS orderedItems.AppendActivityStreamsCreate(activity) return nil } - if err := resolvers.Resolve(context.Background(), value, createCallback); err != nil { + if err := r.Resolve(context.Background(), value, createCallback); err != nil { return collection, err } } diff --git a/storage/sqlstorage/initialize.go b/storage/sqlstorage/initialize.go index a217ef144..4a5931b88 100644 --- a/storage/sqlstorage/initialize.go +++ b/storage/sqlstorage/initialize.go @@ -91,7 +91,7 @@ func InitializeDatabase(file string, schemaVersion int) (*sql.DB, error) { dbBackupTicker := time.NewTicker(1 * time.Hour) go func() { - c := config.GetConfig() + c := config.Get() backupFile := filepath.Join(c.BackupDirectory, "owncastdb.bak") for range dbBackupTicker.C { utils.Backup(db, backupFile) diff --git a/storage/sqlstorage/migrations.go b/storage/sqlstorage/migrations.go index 7a95a7859..d411a4777 100644 --- a/storage/sqlstorage/migrations.go +++ b/storage/sqlstorage/migrations.go @@ -21,7 +21,7 @@ func NewSqlMigrations(db *sql.DB) *SqlMigrations { } func (m *SqlMigrations) MigrateDatabaseSchema(db *sql.DB, from, to int) error { - c := config.GetConfig() + c := config.Get() log.Printf("Migrating database from version %d to %d", from, to) dbBackupFile := filepath.Join(c.BackupDirectory, fmt.Sprintf("owncast-v%d.bak", from)) diff --git a/storage/userrepository/userrepository.go b/storage/userrepository/userrepository.go index eea8144f3..2d080031d 100644 --- a/storage/userrepository/userrepository.go +++ b/storage/userrepository/userrepository.go @@ -59,17 +59,6 @@ func Get() *SqlUserRepository { return temporaryGlobalInstance } -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" -) - // User represents a single chat user. // SetupUsers will perform the initial initialization of the user package. @@ -285,10 +274,10 @@ func (r *SqlUserRepository) SetUserAsAuthenticated(userID string) error { // SetModerator will add or remove moderator status for a single user by ID. func (r *SqlUserRepository) SetModerator(userID string, isModerator bool) error { if isModerator { - return r.addScopeToUser(userID, moderatorScopeKey) + return r.addScopeToUser(userID, models.ModeratorScopeKey) } - return r.removeScopeFromUser(userID, moderatorScopeKey) + return r.removeScopeFromUser(userID, models.ModeratorScopeKey) } func (r *SqlUserRepository) addScopeToUser(userID string, scope string) error { @@ -401,7 +390,7 @@ func (r *SqlUserRepository) GetModeratorUsers() []*models.User { ORDER BY created_at ) AS token WHERE token.scope = ?` - rows, err := r.datastore.DB.Query(query, moderatorScopeKey) + rows, err := r.datastore.DB.Query(query, models.ModeratorScopeKey) if err != nil { log.Errorln(err) return nil @@ -748,9 +737,9 @@ func (r *SqlUserRepository) makeExternalAPIUsersFromRows(rows *sql.Rows) ([]mode func (r *SqlUserRepository) HasValidScopes(scopes []string) bool { // For a scope to be seen as "valid" it must live in this slice. validAccessTokenScopes := []string{ - ScopeCanSendChatMessages, - ScopeCanSendSystemMessages, - ScopeHasAdminAccess, + models.ScopeCanSendChatMessages, + models.ScopeCanSendSystemMessages, + models.ScopeHasAdminAccess, } for _, scope := range scopes { diff --git a/utils/emojiMigration.go b/utils/emojiMigration.go deleted file mode 100644 index 29700c729..000000000 --- a/utils/emojiMigration.go +++ /dev/null @@ -1,23 +0,0 @@ -package utils - -import ( - "path" - - log "github.com/sirupsen/logrus" -) - -// MigrateCustomEmojiLocations migrates custom emoji from the old location to the new location. -func MigrateCustomEmojiLocations() { - oldLocation := path.Join("webroot", "img", "emoji") - newLocation := path.Join("data", "emoji") - - if !DoesFileExists(oldLocation) { - return - } - - log.Println("Moving custom emoji directory from", oldLocation, "to", newLocation) - - if err := Move(oldLocation, newLocation); err != nil { - log.Errorln("error moving custom emoji directory", err) - } -} diff --git a/video/rtmp/broadcaster.go b/video/rtmp/broadcaster.go index a2d212926..57fa1b32f 100644 --- a/video/rtmp/broadcaster.go +++ b/video/rtmp/broadcaster.go @@ -30,5 +30,5 @@ func setCurrentBroadcasterInfo(t flvio.Tag, remoteAddr string) { }, } - _setBroadcaster(broadcaster) + _setBroadcaster(&broadcaster) } diff --git a/video/rtmp/rtmp.go b/video/rtmp/rtmp.go index fe93b8706..4f8388066 100644 --- a/video/rtmp/rtmp.go +++ b/video/rtmp/rtmp.go @@ -25,16 +25,16 @@ var ( var ( _setStreamAsConnected func(*io.PipeReader) - _setBroadcaster func(models.Broadcaster) + _setBroadcaster func(*models.Broadcaster) ) -var configRepository = configrepository.Get() - // Start starts the rtmp service, listening on specified RTMP port. -func Start(setStreamAsConnected func(*io.PipeReader), setBroadcaster func(models.Broadcaster)) { +func Start(setStreamAsConnected func(*io.PipeReader), setBroadcaster func(*models.Broadcaster)) { _setStreamAsConnected = setStreamAsConnected _setBroadcaster = setBroadcaster + configRepository := configrepository.Get() + port := configRepository.GetRTMPPortNumber() s := rtmp.NewServer() var lis net.Listener @@ -80,10 +80,12 @@ func HandleConn(c *rtmp.Conn, nc net.Conn) { return } + configRepository := configrepository.Get() + accessGranted := false validStreamingKeys := configRepository.GetStreamKeys() - configservice := config.GetConfig() + configservice := config.Get() // If a stream key override was specified then use that instead. if configservice.TemporaryStreamKey != "" { diff --git a/video/state/offline.go b/video/state/offline.go new file mode 100644 index 000000000..31cada7b1 --- /dev/null +++ b/video/state/offline.go @@ -0,0 +1,123 @@ +package state + +import ( + "os" + "path" + "path/filepath" + + "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/config" + "github.com/owncast/owncast/storage/configrepository" + "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/video/transcoder" + log "github.com/sirupsen/logrus" +) + +// transitionToOfflineVideoStreamContent will overwrite the current stream with the +// offline video stream state only. No live stream HLS segments will continue to be +// referenced. +func transitionToOfflineVideoStreamContent() { + log.Traceln("Firing transcoder with offline stream state") + + _transcoder := transcoder.NewTranscoder() + _transcoder.SetIdentifier("offline") + _transcoder.SetLatencyLevel(models.GetLatencyLevel(4)) + _transcoder.SetIsEvent(true) + + offlineFilePath, err := saveOfflineClipToDisk("offline.ts") + if err != nil { + log.Fatalln("unable to save offline clip:", err) + } + + _transcoder.SetInput(offlineFilePath) + go _transcoder.Start(false) + + configRepository := configrepository.Get() + + // Copy the logo to be the thumbnail + logo := configRepository.GetLogoPath() + c := config.Get() + dst := filepath.Join(c.TempDir, "thumbnail.jpg") + if err = utils.Copy(filepath.Join("data", logo), dst); err != nil { + log.Warnln(err) + } + + // Delete the preview Gif + _ = os.Remove(path.Join(config.DataDirectory, "preview.gif")) +} + +func createInitialOfflineState() error { + transitionToOfflineVideoStreamContent() + + return nil +} + +// 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() + + webhookManager := webhooks.Get() + go webhookManager.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() + } +} diff --git a/video/state/online.go b/video/state/online.go new file mode 100644 index 000000000..1b37887e2 --- /dev/null +++ b/video/state/online.go @@ -0,0 +1,124 @@ +package state + +import ( + "context" + "io" + "time" + + "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/apfederation/outbox" + "github.com/owncast/owncast/services/config" + "github.com/owncast/owncast/services/notifications" + "github.com/owncast/owncast/services/webhooks" + "github.com/owncast/owncast/storage/configrepository" + "github.com/owncast/owncast/storage/data" + "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/video/transcoder" + log "github.com/sirupsen/logrus" +) + +// setStreamAsConnected sets the stream as connected. +func (vs *VideoState) setStreamAsConnected(rtmpOut *io.PipeReader) { + now := utils.NullTime{Time: time.Now(), Valid: true} + vs.status.StreamConnected = true + vs.status.Status.LastDisconnectTime = nil + vs.status.Status.LastConnectTime = &now + vs.status.SessionMaxViewerCount = 0 + + configRepository := configrepository.Get() + + vs.status.SetCurrentBroadcast(&models.CurrentBroadcast{ + LatencyLevel: configRepository.GetStreamLatencyLevel(), + OutputSettings: configRepository.GetStreamOutputVariants(), + }) + + StopOfflineCleanupTimer() + startOnlineCleanupTimer() + + if _yp != nil { + go _yp.Start() + } + + c := config.Get() + segmentPath := c.HLSStoragePath + + if err := setupStorage(); err != nil { + log.Fatalln("failed to setup the storage", err) + } + + go func() { + vs.transcoder = transcoder.NewTranscoder() + vs.transcoder.TranscoderCompleted = func(error) { + SetStreamAsDisconnected() + vs.transcoder = nil + vs.status.SetCurrentBroadcast(nil) + } + vs.transcoder.SetStdin(rtmpOut) + vs.transcoder.Start(true) + }() + + webhookManager := webhooks.Get() + go webhookManager.SendStreamStatusEvent(models.StreamStarted) + transcoder.StartThumbnailGenerator(segmentPath, configRepository.FindHighestVideoQualityIndex(vs.status.GetCurrentBroadcast().OutputSettings)) + + _ = vs.chatService.SendSystemAction("Stay tuned, the stream is **starting**!", true) + vs.chatService.SendAllWelcomeMessage() + + // Send delayed notification messages. + _onlineTimerCancelFunc = startLiveStreamNotificationsTimer() +} + +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 + } + + configRepository := configrepository.Get() + + // Send Fediverse message. + if configRepository.GetFederationEnabled() { + ob := outbox.Get() + log.Traceln("Sending Federated Go Live message.") + if err := ob.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 +} diff --git a/video/state/state.go b/video/state/state.go new file mode 100644 index 000000000..baae26854 --- /dev/null +++ b/video/state/state.go @@ -0,0 +1,35 @@ +package state + +import ( + "github.com/owncast/owncast/services/chat" + "github.com/owncast/owncast/services/metrics" + "github.com/owncast/owncast/services/status" + "github.com/owncast/owncast/utils" + "github.com/owncast/owncast/video/transcoder" +) + +type VideoState struct { + transcoder *transcoder.Transcoder + metrics *metrics.Metrics + status *status.Status + lastNotified utils.NullTime + chatService *chat.Chat +} + +func New() *VideoState { + return &VideoState{ + transcoder: transcoder.Get(), + metrics: metrics.Get(), + status: status.Get(), + } +} + +var temporaryGlobalInstance *VideoState + +func Get() *VideoState { + if temporaryGlobalInstance == nil { + temporaryGlobalInstance = New() + } + + return temporaryGlobalInstance +} diff --git a/video/storageproviders/local.go b/video/storageproviders/local.go index c816553c2..9dbd5055e 100644 --- a/video/storageproviders/local.go +++ b/video/storageproviders/local.go @@ -9,6 +9,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/owncast/owncast/services/config" + "github.com/owncast/owncast/storage/configrepository" ) // LocalStorage represents an instance of the local storage provider for HLS video. @@ -62,10 +63,12 @@ func (s *LocalStorage) Save(filePath string, retryCount int) (string, error) { } func (s *LocalStorage) Cleanup() error { + configRepository := configrepository.Get() + // Determine how many files we should keep on disk maxNumber := configRepository.GetStreamLatencyLevel().SegmentCount buffer := 10 - c := config.GetConfig() + c := config.Get() baseDirectory := c.HLSStoragePath files, err := getAllFilesRecursive(baseDirectory) diff --git a/video/storageproviders/rewriteLocalPlaylist.go b/video/storageproviders/rewriteLocalPlaylist.go index 6afbf07d3..5499ace88 100644 --- a/video/storageproviders/rewriteLocalPlaylist.go +++ b/video/storageproviders/rewriteLocalPlaylist.go @@ -34,7 +34,7 @@ func rewritePlaylistLocations(localFilePath, remoteServingEndpoint, pathPrefix s } item.URI = remoteServingEndpoint + filepath.Join(finalPath, item.URI) } - c := config.GetConfig() + c := config.Get() publicPath := filepath.Join(c.HLSStoragePath, filepath.Base(localFilePath)) newPlaylist := p.String() diff --git a/video/storageproviders/s3Storage.go b/video/storageproviders/s3Storage.go index 83f7ceb42..328ba521b 100644 --- a/video/storageproviders/s3Storage.go +++ b/video/storageproviders/s3Storage.go @@ -53,8 +53,6 @@ type S3Storage struct { s3ForcePathStyle bool } -var configRepository = configrepository.Get() - // NewS3Storage returns a new S3Storage instance. func NewS3Storage() *S3Storage { return &S3Storage{ @@ -66,6 +64,7 @@ func NewS3Storage() *S3Storage { // Setup sets up the s3 storage for saving the video to s3. func (s *S3Storage) Setup() error { log.Trace("Setting up S3 for external storage of video...") + configRepository := configrepository.Get() s3Config := configRepository.GetS3Config() customVideoServingEndpoint := configRepository.GetVideoServingEndpoint() @@ -109,6 +108,8 @@ func (s *S3Storage) SegmentWritten(localFilePath string) { // Warn the user about long-running save operations if averagePerformance != 0 { + configRepository := configrepository.Get() + if averagePerformance > float64(configRepository.GetStreamLatencyLevel().SecondsPerSegment)*0.9 { log.Warnln("Possible slow uploads: average upload S3 save duration", averagePerformance, "s. troubleshoot this issue by visiting https://owncast.online/docs/troubleshooting/") } @@ -160,7 +161,7 @@ func (s *S3Storage) Save(filePath string, retryCount int) (string, error) { } defer file.Close() - c := config.GetConfig() + c := config.Get() // Convert the local path to the variant/file path by stripping the local storage location. normalizedPath := strings.TrimPrefix(filePath, c.HLSStoragePath) @@ -219,6 +220,8 @@ func (s *S3Storage) Save(filePath string, retryCount int) (string, error) { } func (s *S3Storage) Cleanup() error { + configRepository := configrepository.Get() + // Determine how many files we should keep on S3 storage maxNumber := configRepository.GetStreamLatencyLevel().SegmentCount buffer := 20 diff --git a/video/transcoder/fileWriterReceiverService.go b/video/transcoder/fileWriterReceiverService.go index 74fa96ca7..cb7e224c5 100644 --- a/video/transcoder/fileWriterReceiverService.go +++ b/video/transcoder/fileWriterReceiverService.go @@ -42,7 +42,7 @@ func (s *FileWriterReceiverService) SetupFileWriterReceiverService(callbacks Fil log.Fatalln("Unable to start internal video writing service", err) } - c := config.GetConfig() + c := config.Get() listenerPort := strings.Split(listener.Addr().String(), ":")[1] c.InternalHLSListenerPort = listenerPort log.Traceln("Transcoder response service listening on: " + listenerPort) @@ -60,7 +60,7 @@ func (s *FileWriterReceiverService) uploadHandler(w http.ResponseWriter, r *http return } - c := config.GetConfig() + c := config.Get() path := r.URL.Path writePath := filepath.Join(c.HLSStoragePath, path) f, err := os.Create(writePath) //nolint: gosec diff --git a/video/transcoder/thumbnailGenerator.go b/video/transcoder/thumbnailGenerator.go index 33d4f532d..a5d98d628 100644 --- a/video/transcoder/thumbnailGenerator.go +++ b/video/transcoder/thumbnailGenerator.go @@ -15,10 +15,7 @@ import ( "github.com/owncast/owncast/utils" ) -var ( - _timer *time.Ticker - configRepository = configrepository.Get() -) +var _timer *time.Ticker // StopThumbnailGenerator will stop the periodic generating of a thumbnail from video. func StopThumbnailGenerator() { @@ -55,7 +52,7 @@ func StartThumbnailGenerator(chunkPath string, variantIndex int, isVideoPassthro } func fireThumbnailGenerator(segmentPath string, variantIndex int) error { - c := config.GetConfig() + c := config.Get() // JPG takes less time to encode than PNG outputFile := path.Join(c.TempDir, "thumbnail.jpg") @@ -94,6 +91,8 @@ func fireThumbnailGenerator(segmentPath string, variantIndex int) error { return nil } + configRepository := configrepository.Get() + mostRecentFile := path.Join(framePath, names[0]) ffmpegPath := utils.ValidatedFfmpegPath(configRepository.GetFfMpegPath()) outputFileTemp := path.Join(c.TempDir, "tempthumbnail.jpg") @@ -125,7 +124,8 @@ func fireThumbnailGenerator(segmentPath string, variantIndex int) error { } func makeAnimatedGifPreview(sourceFile string, outputFile string) { - c := config.GetConfig() + configRepository := configrepository.Get() + c := config.Get() ffmpegPath := utils.ValidatedFfmpegPath(configRepository.GetFfMpegPath()) outputFileTemp := path.Join(c.TempDir, "temppreview.gif") diff --git a/video/transcoder/transcoder.go b/video/transcoder/transcoder.go index d59d3d1ab..8f8fb3b40 100644 --- a/video/transcoder/transcoder.go +++ b/video/transcoder/transcoder.go @@ -11,9 +11,9 @@ import ( log "github.com/sirupsen/logrus" "github.com/teris-io/shortid" - "github.com/owncast/owncast/logging" "github.com/owncast/owncast/models" "github.com/owncast/owncast/services/config" + "github.com/owncast/owncast/storage/configrepository" "github.com/owncast/owncast/utils" ) @@ -119,7 +119,7 @@ func (t *Transcoder) Start(shouldLog bool) { } createVariantDirectories() - c := config.GetConfig() + c := config.Get() if c.EnableDebugFeatures { log.Println(command) @@ -137,7 +137,7 @@ func (t *Transcoder) Start(shouldLog bool) { } if err := _commandExec.Start(); err != nil { - log.Errorln("Transcoder error. See", logging.GetTranscoderLogFilePath(), "for full output to debug.") + log.Errorln("Transcoder error. See", c.GetTranscoderLogFilePath(), "for full output to debug.") log.Panicln(err, command) } @@ -155,7 +155,7 @@ func (t *Transcoder) Start(shouldLog bool) { } if err != nil { - log.Errorln("transcoding error. look at", logging.GetTranscoderLogFilePath(), "to help debug. your copy of ffmpeg may not support your selected codec of", t.codec.Name(), "https://owncast.online/docs/codecs/") + log.Errorln("transcoding error. look at", c.GetTranscoderLogFilePath(), "to help debug. your copy of ffmpeg may not support your selected codec of", t.codec.Name(), "https://owncast.online/docs/codecs/") } } @@ -198,8 +198,11 @@ func (t *Transcoder) getString() string { if len(hlsOptionFlags) > 0 { hlsOptionsString = "-hls_flags " + strings.Join(hlsOptionFlags, "+") } + + c := config.Get() + ffmpegFlags := []string{ - fmt.Sprintf(`FFREPORT=file="%s":level=32`, logging.GetTranscoderLogFilePath()), + fmt.Sprintf(`FFREPORT=file="%s":level=32`, c.GetTranscoderLogFilePath()), t.ffmpegPath, "-hide_banner", "-loglevel warning", @@ -272,10 +275,22 @@ func getVariantFromConfigQuality(quality models.StreamOutputVariant, index int) return variant } +var temporaryGlobalInstance *Transcoder + +func Get() *Transcoder { + if temporaryGlobalInstance == nil { + temporaryGlobalInstance = NewTranscoder() + } + + return temporaryGlobalInstance +} + // NewTranscoder will return a new Transcoder, populated by the config. func NewTranscoder() *Transcoder { + configRepository := configrepository.Get() + ffmpegPath := utils.ValidatedFfmpegPath(configRepository.GetFfMpegPath()) - c := config.GetConfig() + c := config.Get() transcoder := new(Transcoder) transcoder.ffmpegPath = ffmpegPath diff --git a/video/transcoder/utils.go b/video/transcoder/utils.go index 936e3a0f8..d804b8adb 100644 --- a/video/transcoder/utils.go +++ b/video/transcoder/utils.go @@ -8,6 +8,7 @@ import ( "sync" "github.com/owncast/owncast/services/config" + "github.com/owncast/owncast/storage/configrepository" "github.com/owncast/owncast/utils" log "github.com/sirupsen/logrus" ) @@ -96,11 +97,13 @@ func handleTranscoderMessage(message string) { } func createVariantDirectories() { - c := config.GetConfig() + c := config.Get() // Create private hls data dirs utils.CleanupDirectory(c.HLSStoragePath) + configRepository := configrepository.Get() + if len(configRepository.GetStreamOutputVariants()) != 0 { for index := range configRepository.GetStreamOutputVariants() { if err := os.MkdirAll(path.Join(c.HLSStoragePath, strconv.Itoa(index)), 0o750); err != nil { diff --git a/web/style-definitions/config.js b/web/style-definitions/config.js index 7ab0ddf20..f9d52ce50 100644 --- a/web/style-definitions/config.js +++ b/web/style-definitions/config.js @@ -41,15 +41,40 @@ module.exports = { }, ], }, - less: { - transformGroup: 'less', + // less: { + // transformGroup: 'less', + // buildPath: 'build/', + // files: [ + // { + // destination: 'variables.less', + // format: 'less/variables', + // options: { + // fileHeader: 'myCustomHeader', + // }, + // }, + // ], + // }, + 'ios-swift': { + transforms: [ + 'attribute/cti', + 'name/cti/camel', + 'color/UIColorSwift', + 'content/swift/literal', + 'size/swift/remToCGFloat', + 'font/swift/literal', + ], buildPath: 'build/', files: [ { - destination: 'variables.less', - format: 'less/variables', + destination: 'Assets.swift', + format: 'ios-swift/enum.swift', + className: 'Assets', + filter: token => { + return token.attributes.category === 'asset'; + }, options: { - fileHeader: 'myCustomHeader', + outputReferences: true, + showFileHeader: false, }, }, ], diff --git a/webserver/handlers/adminApiChatHandlers.go b/webserver/handlers/adminApiChatHandlers.go index a442f0910..ec372580b 100644 --- a/webserver/handlers/adminApiChatHandlers.go +++ b/webserver/handlers/adminApiChatHandlers.go @@ -9,10 +9,8 @@ import ( "net/http" "strconv" - "github.com/owncast/owncast/core/chat" - "github.com/owncast/owncast/core/chat/events" - "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/chat" "github.com/owncast/owncast/storage/chatrepository" "github.com/owncast/owncast/storage/userrepository" "github.com/owncast/owncast/utils" @@ -23,7 +21,7 @@ import ( ) var ( - chatRepository = chatrepository.GetChatRepository() + chatRepository = chatrepository.Get() userRepository = userrepository.Get() ) @@ -53,7 +51,7 @@ func (h *Handlers) UpdateMessageVisibility(w http.ResponseWriter, r *http.Reques return } - if err := chat.SetMessagesVisibility(request.IDArray, request.Visible); err != nil { + if err := h.chatService.SetMessagesVisibility(request.IDArray, request.Visible); err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -148,16 +146,18 @@ func (h *Handlers) UpdateUserEnabled(w http.ResponseWriter, r *http.Request) { // 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 { + messageIds, err := h.chatRepository.SetMessageVisibilityForUserID(request.UserID, request.Enabled) + if err != nil { log.Errorln("error changing user messages visibility", err) responses.WriteSimpleResponse(w, false, err.Error()) return } + h.chatService.SetMessagesVisibility(messageIds, request.Enabled) } // Forcefully disconnect the user from the chat if !request.Enabled { - clients, err := chat.GetClientsForUser(request.UserID) + clients, err := h.chatService.GetClientsForUser(request.UserID) if len(clients) == 0 { // Nothing to do return @@ -169,9 +169,9 @@ func (h *Handlers) UpdateUserEnabled(w http.ResponseWriter, r *http.Request) { return } - chat.DisconnectClients(clients) + h.chatService.DisconnectClients(clients) disconnectedUser := userRepository.GetUserByID(request.UserID) - _ = chat.SendSystemAction(fmt.Sprintf("**%s** has been removed from chat.", disconnectedUser.DisplayName), true) + _ = h.chatService.SendSystemAction(fmt.Sprintf("**%s** has been removed from chat.", disconnectedUser.DisplayName), true) localIP4Address := "127.0.0.1" localIP6Address := "::1" @@ -181,7 +181,7 @@ func (h *Handlers) UpdateUserEnabled(w http.ResponseWriter, r *http.Request) { ipAddress := client.IPAddress if ipAddress != localIP4Address && ipAddress != localIP6Address { reason := fmt.Sprintf("Banning of %s", disconnectedUser.DisplayName) - if err := data.BanIPAddress(ipAddress, reason); err != nil { + if err := h.chatRepository.BanIPAddress(ipAddress, reason); err != nil { log.Errorln("error banning IP address: ", err) } } @@ -226,7 +226,7 @@ func (h *Handlers) UpdateUserModerator(w http.ResponseWriter, r *http.Request) { } // Update the clients for this user to know about the moderator access change. - if err := chat.SendConnectedClientInfoToUser(req.UserID); err != nil { + if err := h.chatService.SendConnectedClientInfoToUser(req.UserID); err != nil { log.Debugln(err) } @@ -245,7 +245,7 @@ func (h *Handlers) GetModerators(w http.ResponseWriter, r *http.Request) { func (h *Handlers) GetAdminChatMessages(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - messages := chat.GetChatModerationHistory() + messages := h.chatRepository.GetChatModerationHistory() responses.WriteResponse(w, messages) } @@ -253,13 +253,13 @@ func (h *Handlers) GetAdminChatMessages(w http.ResponseWriter, r *http.Request) func (h *Handlers) SendSystemMessage(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - var message events.SystemMessageEvent + var message chat.SystemMessageEvent if err := json.NewDecoder(r.Body).Decode(&message); err != nil { responses.InternalErrorHandler(w, err) return } - if err := chat.SendSystemMessage(message.Body, false); err != nil { + if err := h.chatService.SendSystemMessage(message.Body, false); err != nil { responses.BadRequestHandler(w, err) } @@ -281,13 +281,13 @@ func (h *Handlers) SendSystemMessageToConnectedClient(integration models.Externa return } - var message events.SystemMessageEvent + var message chat.SystemMessageEvent if err := json.NewDecoder(r.Body).Decode(&message); err != nil { responses.InternalErrorHandler(w, err) return } - chat.SendSystemMessageToClient(uint(clientIDNumeric), message.Body) + h.chatService.SendSystemMessageToClient(uint(clientIDNumeric), message.Body) responses.WriteSimpleResponse(w, true, "sent") } @@ -308,7 +308,7 @@ func (h *Handlers) SendIntegrationChatMessage(integration models.ExternalAPIUser return } - var event events.UserMessageEvent + var event chat.UserMessageEvent if err := json.NewDecoder(r.Body).Decode(&event); err != nil { responses.InternalErrorHandler(w, err) return @@ -330,12 +330,12 @@ func (h *Handlers) SendIntegrationChatMessage(integration models.ExternalAPIUser IsBot: true, } - if err := chat.Broadcast(&event); err != nil { + if err := h.chatService.Broadcast(&event); err != nil { responses.BadRequestHandler(w, err) return } - chat.SaveUserMessage(event) + h.chatRepository.SaveUserMessage(event) responses.WriteSimpleResponse(w, true, "sent") } @@ -344,7 +344,7 @@ func (h *Handlers) SendIntegrationChatMessage(integration models.ExternalAPIUser func (h *Handlers) SendChatAction(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - var message events.SystemActionEvent + var message chat.SystemActionEvent if err := json.NewDecoder(r.Body).Decode(&message); err != nil { responses.InternalErrorHandler(w, err) return @@ -353,7 +353,7 @@ func (h *Handlers) SendChatAction(integration models.ExternalAPIUser, w http.Res message.SetDefaults() message.RenderBody() - if err := chat.SendSystemAction(message.Body, false); err != nil { + if err := h.chatService.SendSystemAction(message.Body, false); err != nil { responses.BadRequestHandler(w, err) return } diff --git a/webserver/handlers/adminApiConfigHandlers.go b/webserver/handlers/adminApiConfigHandlers.go index 8e03f8a93..6b3442591 100644 --- a/webserver/handlers/adminApiConfigHandlers.go +++ b/webserver/handlers/adminApiConfigHandlers.go @@ -9,9 +9,8 @@ import ( "path/filepath" "strings" - "github.com/owncast/owncast/activitypub/outbox" - "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/apfederation/outbox" "github.com/owncast/owncast/services/webhooks" "github.com/owncast/owncast/utils" "github.com/owncast/owncast/webserver/requests" @@ -41,8 +40,10 @@ func (h *Handlers) SetTags(w http.ResponseWriter, r *http.Request) { return } + ob := outbox.Get() + // Update Fediverse followers about this change. - if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { + if err := ob.UpdateFollowersWithAccountUpdates(); err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -68,7 +69,7 @@ func (h *Handlers) SetStreamTitle(w http.ResponseWriter, r *http.Request) { return } if value != "" { - sendSystemChatAction(fmt.Sprintf("Stream title changed to **%s**", value), true) + h.sendSystemChatAction(fmt.Sprintf("Stream title changed to **%s**", value), true) webhookManager := webhooks.Get() go webhookManager.SendStreamStatusEvent(models.StreamTitleUpdated) } @@ -80,8 +81,8 @@ func (h *Handlers) ExternalSetStreamTitle(integration models.ExternalAPIUser, w h.SetStreamTitle(w, r) } -func sendSystemChatAction(messageText string, ephemeral bool) { - if err := chat.SendSystemAction(messageText, ephemeral); err != nil { +func (h *Handlers) sendSystemChatAction(messageText string, ephemeral bool) { + if err := h.chatService.SendSystemAction(messageText, ephemeral); err != nil { log.Errorln(err) } } @@ -102,8 +103,10 @@ func (h *Handlers) SetServerName(w http.ResponseWriter, r *http.Request) { return } + ob := outbox.Get() + // Update Fediverse followers about this change. - if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { + if err := ob.UpdateFollowersWithAccountUpdates(); err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -127,8 +130,10 @@ func (h *Handlers) SetServerSummary(w http.ResponseWriter, r *http.Request) { return } + ob := outbox.Get() + // Update Fediverse followers about this change. - if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { + if err := ob.UpdateFollowersWithAccountUpdates(); err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -249,8 +254,10 @@ func (h *Handlers) SetLogo(w http.ResponseWriter, r *http.Request) { log.Error("Error saving logo uniqueness string: ", err) } + ob := outbox.Get() + // Update Fediverse followers about this change. - if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { + if err := ob.UpdateFollowersWithAccountUpdates(); err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -560,8 +567,10 @@ func (h *Handlers) SetSocialHandles(w http.ResponseWriter, r *http.Request) { return } + ob := outbox.Get() + // Update Fediverse followers about this change. - if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { + if err := ob.UpdateFollowersWithAccountUpdates(); err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -717,7 +726,7 @@ func (h *Handlers) SetChatJoinMessagesEnabled(w http.ResponseWriter, r *http.Req return } - if err := configRepository.SetChatJoinMessagesEnabled(configValue.Value.(bool)); err != nil { + if err := configRepository.SetChatJoinPartMessagesEnabled(configValue.Value.(bool)); err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return } diff --git a/webserver/handlers/adminApiConnectedClients.go b/webserver/handlers/adminApiConnectedClients.go index bce672b32..38be09639 100644 --- a/webserver/handlers/adminApiConnectedClients.go +++ b/webserver/handlers/adminApiConnectedClients.go @@ -4,14 +4,13 @@ import ( "encoding/json" "net/http" - "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/models" "github.com/owncast/owncast/webserver/responses" ) // GetConnectedChatClients returns currently connected clients. func (h *Handlers) GetConnectedChatClients(w http.ResponseWriter, r *http.Request) { - clients := chat.GetClients() + clients := h.chatService.GetClients() w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(clients); err != nil { diff --git a/webserver/handlers/adminApiEmojiConfig.go b/webserver/handlers/adminApiEmojiConfig.go index 21fea849b..2a6562f4a 100644 --- a/webserver/handlers/adminApiEmojiConfig.go +++ b/webserver/handlers/adminApiEmojiConfig.go @@ -38,7 +38,7 @@ func (h *Handlers) UploadCustomEmoji(w http.ResponseWriter, r *http.Request) { } // Prevent path traversal attacks - c := config.GetConfig() + c := config.Get() emojiFileName := filepath.Base(emoji.Name) targetPath := filepath.Join(c.CustomEmojiPath, emojiFileName) @@ -78,7 +78,7 @@ func (h *Handlers) DeleteCustomEmoji(w http.ResponseWriter, r *http.Request) { return } - c := config.GetConfig() + c := config.Get() targetPath := filepath.Join(c.CustomEmojiPath, emoji.Name) if err := os.Remove(targetPath); err != nil { diff --git a/webserver/handlers/adminApiFederationConfig.go b/webserver/handlers/adminApiFederationConfig.go index ca0e3ce38..c2452899e 100644 --- a/webserver/handlers/adminApiFederationConfig.go +++ b/webserver/handlers/adminApiFederationConfig.go @@ -3,9 +3,9 @@ package handlers import ( "net/http" - "github.com/owncast/owncast/activitypub" - "github.com/owncast/owncast/activitypub/outbox" - "github.com/owncast/owncast/activitypub/persistence" + "github.com/owncast/owncast/services/apfederation" + "github.com/owncast/owncast/services/apfederation/outbox" + "github.com/owncast/owncast/storage/federationrepository" "github.com/owncast/owncast/webserver/requests" "github.com/owncast/owncast/webserver/responses" ) @@ -27,7 +27,9 @@ func (h *Handlers) SendFederatedMessage(w http.ResponseWriter, r *http.Request) return } - if err := activitypub.SendPublicFederatedMessage(message); err != nil { + ap := apfederation.Get() + + if err := ap.SendPublicFederatedMessage(message); err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -69,8 +71,10 @@ func (h *Handlers) SetFederationActivityPrivate(w http.ResponseWriter, r *http.R return } + ob := outbox.Get() + // Update Fediverse followers about this change. - if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { + if err := ob.UpdateFollowersWithAccountUpdates(); err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -164,7 +168,9 @@ func (h *Handlers) SetFederationBlockDomains(w http.ResponseWriter, r *http.Requ func (h *Handlers) GetFederatedActions(page int, pageSize int, w http.ResponseWriter, r *http.Request) { offset := pageSize * page - activities, total, err := persistence.GetInboundActivities(pageSize, offset) + federationRepository := federationrepository.Get() + + activities, total, err := federationRepository.GetInboundActivities(pageSize, offset) if err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return diff --git a/webserver/handlers/adminApiFollowers.go b/webserver/handlers/adminApiFollowers.go index 576559549..e64794952 100644 --- a/webserver/handlers/adminApiFollowers.go +++ b/webserver/handlers/adminApiFollowers.go @@ -4,8 +4,8 @@ import ( "encoding/json" "net/http" - "github.com/owncast/owncast/activitypub/persistence" - aprequests "github.com/owncast/owncast/activitypub/requests" + "github.com/owncast/owncast/services/apfederation/outbox" + "github.com/owncast/owncast/storage/federationrepository" "github.com/owncast/owncast/webserver/requests" "github.com/owncast/owncast/webserver/responses" ) @@ -28,29 +28,33 @@ func (h *Handlers) ApproveFollower(w http.ResponseWriter, r *http.Request) { return } + federationRepository := federationrepository.Get() + if approval.Approved { // Approve a follower - if err := persistence.ApprovePreviousFollowRequest(approval.ActorIRI); err != nil { + if err := federationRepository.ApprovePreviousFollowRequest(approval.ActorIRI); err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return } localAccountName := configRepository.GetDefaultFederationUsername() - followRequest, err := persistence.GetFollower(approval.ActorIRI) + followRequest, err := federationRepository.GetFollower(approval.ActorIRI) if err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return } + ob := outbox.Get() + // Send the approval to the follow requestor. - if err := aprequests.SendFollowAccept(followRequest.Inbox, followRequest.RequestObject, localAccountName); err != nil { + if err := ob.SendFollowAccept(followRequest.Inbox, followRequest.RequestObject, localAccountName); err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return } } else { // Remove/block a follower - if err := persistence.BlockOrRejectFollower(approval.ActorIRI); err != nil { + if err := federationRepository.BlockOrRejectFollower(approval.ActorIRI); err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return } @@ -61,7 +65,8 @@ func (h *Handlers) ApproveFollower(w http.ResponseWriter, r *http.Request) { // GetPendingFollowRequests will return a list of pending follow requests. func (h *Handlers) GetPendingFollowRequests(w http.ResponseWriter, r *http.Request) { - requests, err := persistence.GetPendingFollowRequests() + federationRepository := federationrepository.Get() + requests, err := federationRepository.GetPendingFollowRequests() if err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return @@ -72,7 +77,8 @@ func (h *Handlers) GetPendingFollowRequests(w http.ResponseWriter, r *http.Reque // GetBlockedAndRejectedFollowers will return blocked and rejected followers. func (h *Handlers) GetBlockedAndRejectedFollowers(w http.ResponseWriter, r *http.Request) { - rejections, err := persistence.GetBlockedAndRejectedFollowers() + federationRepository := federationrepository.Get() + rejections, err := federationRepository.GetBlockedAndRejectedFollowers() if err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return diff --git a/webserver/handlers/adminApiHardwareStats.go b/webserver/handlers/adminApiHardwareStats.go index c6a806c46..41ca07e1c 100644 --- a/webserver/handlers/adminApiHardwareStats.go +++ b/webserver/handlers/adminApiHardwareStats.go @@ -10,7 +10,7 @@ import ( // GetHardwareStats will return hardware utilization over time. func (h *Handlers) GetHardwareStats(w http.ResponseWriter, r *http.Request) { - m := metrics.GetMetrics() + m := metrics.Get() w.Header().Set("Content-Type", "application/json") err := json.NewEncoder(w).Encode(m) diff --git a/webserver/handlers/adminApiServerConfig.go b/webserver/handlers/adminApiServerConfig.go index b1283acd4..4d4889ce4 100644 --- a/webserver/handlers/adminApiServerConfig.go +++ b/webserver/handlers/adminApiServerConfig.go @@ -17,7 +17,7 @@ func (h *Handlers) GetServerConfig(w http.ResponseWriter, r *http.Request) { ffmpeg := utils.ValidatedFfmpegPath(configRepository.GetFfMpegPath()) usernameBlocklist := configRepository.GetForbiddenUsernameList() usernameSuggestions := configRepository.GetSuggestedUsernamesList() - c := config.GetConfig() + c := config.Get() videoQualityVariants := make([]models.StreamOutputVariant, 0) for _, variant := range configRepository.GetStreamOutputVariants() { diff --git a/webserver/handlers/adminApiSoftwareUpdate.go b/webserver/handlers/adminApiSoftwareUpdate.go index d39c25d6d..ac54c6920 100644 --- a/webserver/handlers/adminApiSoftwareUpdate.go +++ b/webserver/handlers/adminApiSoftwareUpdate.go @@ -44,7 +44,7 @@ func (h *Handlers) AutoUpdateOptions(w http.ResponseWriter, r *http.Request) { CanRestart: false, } - c := config.GetConfig() + c := config.Get() // Nothing is supported when running under "dev" or the feature is // explicitly disabled. diff --git a/webserver/handlers/adminApiSystemStatus.go b/webserver/handlers/adminApiSystemStatus.go index ea2e7798e..a4cb53ce5 100644 --- a/webserver/handlers/adminApiSystemStatus.go +++ b/webserver/handlers/adminApiSystemStatus.go @@ -4,28 +4,30 @@ import ( "encoding/json" "net/http" - "github.com/owncast/owncast/core" "github.com/owncast/owncast/models" "github.com/owncast/owncast/services/metrics" + "github.com/owncast/owncast/services/status" "github.com/owncast/owncast/webserver/middleware" log "github.com/sirupsen/logrus" ) // Status gets the details of the inbound broadcaster. func (h *Handlers) GetAdminStatus(w http.ResponseWriter, r *http.Request) { - broadcaster := core.GetBroadcaster() - status := core.GetStatus() - currentBroadcast := core.GetCurrentBroadcast() - health := metrics.GetStreamHealthOverview() + s := status.Get() + m := metrics.Get() + + broadcaster := s.GetBroadcaster() + currentBroadcast := s.GetCurrentBroadcast() + health := m.GetStreamHealthOverview() response := adminStatusResponse{ Broadcaster: broadcaster, CurrentBroadcast: currentBroadcast, - Online: status.Online, + Online: s.Online, Health: health, - ViewerCount: status.ViewerCount, - OverallPeakViewerCount: status.OverallMaxViewerCount, - SessionPeakViewerCount: status.SessionMaxViewerCount, - VersionNumber: status.VersionNumber, + ViewerCount: s.ViewerCount, + OverallPeakViewerCount: s.OverallMaxViewerCount, + SessionPeakViewerCount: s.SessionMaxViewerCount, + VersionNumber: s.VersionNumber, StreamTitle: configRepository.GetStreamTitle(), } diff --git a/webserver/handlers/adminApiVideoConfig.go b/webserver/handlers/adminApiVideoConfig.go index 3b39d67ff..5d2fd1107 100644 --- a/webserver/handlers/adminApiVideoConfig.go +++ b/webserver/handlers/adminApiVideoConfig.go @@ -4,38 +4,41 @@ import ( "encoding/json" "net/http" - "github.com/owncast/owncast/core" + "github.com/owncast/owncast/models" "github.com/owncast/owncast/services/metrics" + "github.com/owncast/owncast/services/status" log "github.com/sirupsen/logrus" ) // GetVideoPlaybackMetrics returns video playback metrics. func (h *Handlers) GetVideoPlaybackMetrics(w http.ResponseWriter, r *http.Request) { type response struct { - Errors []metrics.TimestampedValue `json:"errors"` - QualityVariantChanges []metrics.TimestampedValue `json:"qualityVariantChanges"` + Errors []models.TimestampedValue `json:"errors"` + QualityVariantChanges []models.TimestampedValue `json:"qualityVariantChanges"` - HighestLatency []metrics.TimestampedValue `json:"highestLatency"` - MedianLatency []metrics.TimestampedValue `json:"medianLatency"` - LowestLatency []metrics.TimestampedValue `json:"lowestLatency"` + HighestLatency []models.TimestampedValue `json:"highestLatency"` + MedianLatency []models.TimestampedValue `json:"medianLatency"` + LowestLatency []models.TimestampedValue `json:"lowestLatency"` - MedianDownloadDuration []metrics.TimestampedValue `json:"medianSegmentDownloadDuration"` - MaximumDownloadDuration []metrics.TimestampedValue `json:"maximumSegmentDownloadDuration"` - MinimumDownloadDuration []metrics.TimestampedValue `json:"minimumSegmentDownloadDuration"` + MedianDownloadDuration []models.TimestampedValue `json:"medianSegmentDownloadDuration"` + MaximumDownloadDuration []models.TimestampedValue `json:"maximumSegmentDownloadDuration"` + MinimumDownloadDuration []models.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"` + SlowestDownloadRate []models.TimestampedValue `json:"minPlayerBitrate"` + MedianDownloadRate []models.TimestampedValue `json:"medianPlayerBitrate"` + HighestDownloadRater []models.TimestampedValue `json:"maxPlayerBitrate"` + AvailableBitrates []int `json:"availableBitrates"` + SegmentLength int `json:"segmentLength"` + Representation int `json:"representation"` } + s := status.Get() + availableBitrates := []int{} var segmentLength int - if core.GetCurrentBroadcast() != nil { - segmentLength = core.GetCurrentBroadcast().LatencyLevel.SecondsPerSegment - for _, variants := range core.GetCurrentBroadcast().OutputSettings { + if s.GetCurrentBroadcast() != nil { + segmentLength = s.GetCurrentBroadcast().LatencyLevel.SecondsPerSegment + for _, variants := range s.GetCurrentBroadcast().OutputSettings { availableBitrates = append(availableBitrates, variants.VideoBitrate) } } else { @@ -45,21 +48,23 @@ func (h *Handlers) GetVideoPlaybackMetrics(w http.ResponseWriter, r *http.Reques } } - errors := metrics.GetPlaybackErrorCountOverTime() - medianLatency := metrics.GetMedianLatencyOverTime() - minimumLatency := metrics.GetMinimumLatencyOverTime() - maximumLatency := metrics.GetMaximumLatencyOverTime() + m := metrics.Get() - medianDurations := metrics.GetMedianDownloadDurationsOverTime() - maximumDurations := metrics.GetMaximumDownloadDurationsOverTime() - minimumDurations := metrics.GetMinimumDownloadDurationsOverTime() + errors := m.GetPlaybackErrorCountOverTime() + medianLatency := m.GetMedianLatencyOverTime() + minimumLatency := m.GetMinimumLatencyOverTime() + maximumLatency := m.GetMaximumLatencyOverTime() - minPlayerBitrate := metrics.GetSlowestDownloadRateOverTime() - medianPlayerBitrate := metrics.GetMedianDownloadRateOverTime() - maxPlayerBitrate := metrics.GetMaxDownloadRateOverTime() - qualityVariantChanges := metrics.GetQualityVariantChangesOverTime() + medianDurations := m.GetMedianDownloadDurationsOverTime() + maximumDurations := m.GetMaximumDownloadDurationsOverTime() + minimumDurations := m.GetMinimumDownloadDurationsOverTime() - representation := metrics.GetPlaybackMetricsRepresentation() + minPlayerBitrate := m.GetSlowestDownloadRateOverTime() + medianPlayerBitrate := m.GetMedianDownloadRateOverTime() + maxPlayerBitrate := m.GetMaxDownloadRateOverTime() + qualityVariantChanges := m.GetQualityVariantChangesOverTime() + + representation := m.GetPlaybackMetricsRepresentation() resp := response{ AvailableBitrates: availableBitrates, diff --git a/webserver/handlers/adminApiViewers.go b/webserver/handlers/adminApiViewers.go index cbf737d2c..48dd9becf 100644 --- a/webserver/handlers/adminApiViewers.go +++ b/webserver/handlers/adminApiViewers.go @@ -25,7 +25,9 @@ func (h *Handlers) GetViewersOverTime(w http.ResponseWriter, r *http.Request) { windowStartAt := time.Unix(int64(windowStartAtUnix), 0) windowEnd := time.Now() - viewersOverTime := metrics.GetViewersOverTime(windowStartAt, windowEnd) + m := metrics.Get() + + viewersOverTime := m.GetViewersOverTime(windowStartAt, windowEnd) w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(viewersOverTime) if err != nil { diff --git a/webserver/handlers/auth/fediverse/fediverse.go b/webserver/handlers/auth/fediverse/fediverse.go index 0d96dcce9..e863a3b06 100644 --- a/webserver/handlers/auth/fediverse/fediverse.go +++ b/webserver/handlers/auth/fediverse/fediverse.go @@ -5,23 +5,36 @@ import ( "fmt" "net/http" - "github.com/owncast/owncast/activitypub" - "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/apfederation" fediverseauth "github.com/owncast/owncast/services/auth/fediverse" + "github.com/owncast/owncast/services/chat" + "github.com/owncast/owncast/storage/chatrepository" "github.com/owncast/owncast/storage/configrepository" "github.com/owncast/owncast/storage/userrepository" "github.com/owncast/owncast/webserver/responses" log "github.com/sirupsen/logrus" ) -var ( - userRepository = userrepository.Get() - configRepository = configrepository.Get() -) +type FediAuthHandlers struct { + configRepository *configrepository.SqlConfigRepository + chatService *chat.Chat + chatRepository *chatrepository.ChatRepository + userRepository *userrepository.SqlUserRepository +} + +// New creates a new instances of web server handlers. +func New() *FediAuthHandlers { + return &FediAuthHandlers{ + configRepository: configrepository.Get(), + chatService: chat.Get(), + chatRepository: chatrepository.Get(), + userRepository: userrepository.Get(), + } +} // RegisterFediverseOTPRequest registers a new OTP request for the given access token. -func RegisterFediverseOTPRequest(u models.User, w http.ResponseWriter, r *http.Request) { +func (h *FediAuthHandlers) RegisterFediverseOTPRequest(u models.User, w http.ResponseWriter, r *http.Request) { type request struct { FediverseAccount string `json:"account"` } @@ -45,7 +58,9 @@ func RegisterFediverseOTPRequest(u models.User, w http.ResponseWriter, r *http.R return } - msg := fmt.Sprintf("

This is an automated message from %s. If you did not request this message please ignore or block. Your requested one-time code is:

%s

", configRepository.GetServerName(), reg.Code) + activitypub := apfederation.Get() + + msg := fmt.Sprintf("

This is an automated message from %s. If you did not request this message please ignore or block. Your requested one-time code is:

%s

", h.configRepository.GetServerName(), reg.Code) if err := activitypub.SendDirectFederatedMessage(msg, reg.Account); err != nil { responses.WriteSimpleResponse(w, false, "Could not send code to fediverse: "+err.Error()) return @@ -55,7 +70,7 @@ func RegisterFediverseOTPRequest(u models.User, w http.ResponseWriter, r *http.R } // VerifyFediverseOTPRequest verifies the given OTP code for the given access token. -func VerifyFediverseOTPRequest(w http.ResponseWriter, r *http.Request) { +func (h *FediAuthHandlers) VerifyFediverseOTPRequest(w http.ResponseWriter, r *http.Request) { type request struct { Code string `json:"code"` } @@ -76,20 +91,20 @@ func VerifyFediverseOTPRequest(w http.ResponseWriter, r *http.Request) { } // Check if a user with this auth already exists, if so, log them in. - if u := userRepository.GetUserByAuth(authRegistration.Account, models.Fediverse); u != nil { + if u := h.userRepository.GetUserByAuth(authRegistration.Account, models.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 := userRepository.SetAccessTokenToOwner(accessToken, userID); err != nil { + if err := h.userRepository.SetAccessTokenToOwner(accessToken, userID); err != nil { responses.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 { + if err := h.chatService.SendSystemAction(loginMessage, true); err != nil { log.Errorln(err) } } @@ -101,14 +116,14 @@ func VerifyFediverseOTPRequest(w http.ResponseWriter, r *http.Request) { // 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 := userRepository.AddAuth(authRegistration.UserID, authRegistration.Account, models.Fediverse); err != nil { + if err := h.userRepository.AddAuth(authRegistration.UserID, authRegistration.Account, models.Fediverse); err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return } // Update the current user's authenticated flag so we can show it in // the chat UI. - if err := userRepository.SetUserAsAuthenticated(authRegistration.UserID); err != nil { + if err := h.userRepository.SetUserAsAuthenticated(authRegistration.UserID); err != nil { log.Errorln(err) } diff --git a/webserver/handlers/auth/indieauth/client.go b/webserver/handlers/auth/indieauth/client.go index d4199f274..5fa3238cf 100644 --- a/webserver/handlers/auth/indieauth/client.go +++ b/webserver/handlers/auth/indieauth/client.go @@ -6,22 +6,35 @@ import ( "io" "net/http" - "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/models" ia "github.com/owncast/owncast/services/auth/indieauth" + "github.com/owncast/owncast/services/chat" + "github.com/owncast/owncast/storage/chatrepository" "github.com/owncast/owncast/storage/configrepository" "github.com/owncast/owncast/storage/userrepository" "github.com/owncast/owncast/webserver/responses" log "github.com/sirupsen/logrus" ) -var ( - userRepository = userrepository.Get() - configRepository = configrepository.Get() -) +type IndieAuthHandlers struct { + configRepository *configrepository.SqlConfigRepository + chatService *chat.Chat + chatRepository *chatrepository.ChatRepository + userRepository *userrepository.SqlUserRepository +} + +// New creates a new instances of web server handlers. +func New() *IndieAuthHandlers { + return &IndieAuthHandlers{ + configRepository: configrepository.Get(), + chatService: chat.Get(), + chatRepository: chatrepository.Get(), + userRepository: userrepository.Get(), + } +} // StartAuthFlow will begin the IndieAuth flow for the current user. -func StartAuthFlow(u models.User, w http.ResponseWriter, r *http.Request) { +func (h *IndieAuthHandlers) StartAuthFlow(u models.User, w http.ResponseWriter, r *http.Request) { type request struct { AuthHost string `json:"authHost"` } @@ -59,7 +72,7 @@ func StartAuthFlow(u models.User, w http.ResponseWriter, r *http.Request) { // HandleRedirect will handle the redirect from an IndieAuth server to // continue the auth flow. -func HandleRedirect(w http.ResponseWriter, r *http.Request) { +func (h *IndieAuthHandlers) HandleRedirect(w http.ResponseWriter, r *http.Request) { indieAuthClient := ia.GetIndieAuthClient() state := r.URL.Query().Get("state") code := r.URL.Query().Get("code") @@ -72,21 +85,21 @@ func HandleRedirect(w http.ResponseWriter, r *http.Request) { } // Check if a user with this auth already exists, if so, log them in. - if u := userRepository.GetUserByAuth(response.Me, models.IndieAuth); u != nil { + if u := h.userRepository.GetUserByAuth(response.Me, models.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 := userRepository.SetAccessTokenToOwner(accessToken, userID); err != nil { + if err := h.userRepository.SetAccessTokenToOwner(accessToken, userID); err != nil { responses.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 { + if err := h.chatService.SendSystemAction(loginMessage, true); err != nil { log.Errorln(err) } } @@ -98,14 +111,14 @@ func HandleRedirect(w http.ResponseWriter, r *http.Request) { // 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 := userRepository.AddAuth(request.UserID, response.Me, models.IndieAuth); err != nil { + if err := h.userRepository.AddAuth(request.UserID, response.Me, models.IndieAuth); err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return } // Update the current user's authenticated flag so we can show it in // the chat UI. - if err := userRepository.SetUserAsAuthenticated(request.UserID); err != nil { + if err := h.userRepository.SetUserAsAuthenticated(request.UserID); err != nil { log.Errorln(err) } diff --git a/webserver/handlers/auth/indieauth/server.go b/webserver/handlers/auth/indieauth/server.go index 9396bea5a..c361e1e98 100644 --- a/webserver/handlers/auth/indieauth/server.go +++ b/webserver/handlers/auth/indieauth/server.go @@ -10,21 +10,21 @@ import ( ) // HandleAuthEndpoint will handle the IndieAuth auth endpoint. -func HandleAuthEndpoint(w http.ResponseWriter, r *http.Request) { +func (h *IndieAuthHandlers) HandleAuthEndpoint(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { // Require the GET request for IndieAuth to be behind admin login. - f := middleware.RequireAdminAuth(handleAuthEndpointGet) + f := middleware.RequireAdminAuth(h.handleAuthEndpointGet) f(w, r) return } else if r.Method == http.MethodPost { - handleAuthEndpointPost(w, r) + h.handleAuthEndpointPost(w, r) } else { w.WriteHeader(http.StatusMethodNotAllowed) return } } -func handleAuthEndpointGet(w http.ResponseWriter, r *http.Request) { +func (h *IndieAuthHandlers) handleAuthEndpointGet(w http.ResponseWriter, r *http.Request) { clientID := r.URL.Query().Get("client_id") redirectURI := r.URL.Query().Get("redirect_uri") codeChallenge := r.URL.Query().Get("code_challenge") @@ -58,7 +58,7 @@ func handleAuthEndpointGet(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect) } -func handleAuthEndpointPost(w http.ResponseWriter, r *http.Request) { +func (h *IndieAuthHandlers) handleAuthEndpointPost(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return diff --git a/webserver/handlers/chat.go b/webserver/handlers/chat.go index 45daebae8..7a7ce64dc 100644 --- a/webserver/handlers/chat.go +++ b/webserver/handlers/chat.go @@ -5,7 +5,6 @@ import ( "errors" "net/http" - "github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/models" "github.com/owncast/owncast/services/config" "github.com/owncast/owncast/storage/configrepository" @@ -18,21 +17,21 @@ import ( // ExternalGetChatMessages gets all of the chat messages. func (h *Handlers) ExternalGetChatMessages(integration models.ExternalAPIUser, w http.ResponseWriter, r *http.Request) { middleware.EnableCors(w) - getChatMessages(w, r) + h.getChatMessages(w, r) } // GetChatMessages gets all of the chat messages. func (h *Handlers) GetChatMessages(u models.User, w http.ResponseWriter, r *http.Request) { middleware.EnableCors(w) - getChatMessages(w, r) + h.getChatMessages(w, r) } -func getChatMessages(w http.ResponseWriter, r *http.Request) { +func (h *Handlers) getChatMessages(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.Method { case http.MethodGet: - messages := chat.GetChatHistory() + messages := h.chatRepository.GetChatHistory() if err := json.NewEncoder(w).Encode(messages); err != nil { log.Debugln(err) diff --git a/webserver/handlers/config.go b/webserver/handlers/config.go index 79dc0519e..f52727c20 100644 --- a/webserver/handlers/config.go +++ b/webserver/handlers/config.go @@ -6,9 +6,9 @@ import ( "net/http" "net/url" - "github.com/owncast/owncast/activitypub" "github.com/owncast/owncast/models" "github.com/owncast/owncast/services/config" + "github.com/owncast/owncast/storage/federationrepository" "github.com/owncast/owncast/utils" "github.com/owncast/owncast/webserver/middleware" "github.com/owncast/owncast/webserver/responses" @@ -87,7 +87,9 @@ func getConfigResponse() webConfigResponse { var federationResponse federationConfigResponse federationEnabled := configRepository.GetFederationEnabled() - followerCount, _ := activitypub.GetFollowerCount() + federationRepository := federationrepository.Get() + + followerCount, _ := federationRepository.GetFollowerCount() if federationEnabled { serverURLString := configRepository.GetServerURL() serverURL, _ := url.Parse(serverURLString) @@ -117,7 +119,7 @@ func getConfigResponse() webConfigResponse { IndieAuthEnabled: configRepository.GetServerURL() != "", } - c := config.GetConfig() + c := config.Get() return webConfigResponse{ Name: configRepository.GetServerName(), diff --git a/webserver/handlers/disconnect.go b/webserver/handlers/disconnect.go index 898c7fcd7..6c277d55a 100644 --- a/webserver/handlers/disconnect.go +++ b/webserver/handlers/disconnect.go @@ -3,7 +3,7 @@ package handlers import ( "net/http" - "github.com/owncast/owncast/core" + "github.com/owncast/owncast/services/status" "github.com/owncast/owncast/webserver/responses" "github.com/owncast/owncast/video/rtmp" @@ -11,7 +11,9 @@ import ( // DisconnectInboundConnection will force-disconnect an inbound stream. func (h *Handlers) DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) { - if !core.GetStatus().Online { + s := status.Get() + + if !s.Online { responses.WriteSimpleResponse(w, false, "no inbound stream connected") return } diff --git a/webserver/handlers/emoji.go b/webserver/handlers/emoji.go index 4a6a4f90b..0cf9f7b1e 100644 --- a/webserver/handlers/emoji.go +++ b/webserver/handlers/emoji.go @@ -7,8 +7,8 @@ import ( "strings" "github.com/owncast/owncast/core/data" - "github.com/owncast/owncast/router/middleware" "github.com/owncast/owncast/services/config" + "github.com/owncast/owncast/webserver/middleware" "github.com/owncast/owncast/webserver/responses" ) @@ -26,8 +26,7 @@ func (h *Handlers) GetCustomEmojiList(w http.ResponseWriter, r *http.Request) { func (h *Handlers) GetCustomEmojiImage(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/img/emoji/") r.URL.Path = path - - c := config.GetConfig() + c := config.Get() emojiFS := os.DirFS(c.CustomEmojiPath) middleware.SetCachingHeaders(w, r) diff --git a/activitypub/controllers/actors.go b/webserver/handlers/federation/actors.go similarity index 81% rename from activitypub/controllers/actors.go rename to webserver/handlers/federation/actors.go index 8a7109e90..6a245bf0e 100644 --- a/activitypub/controllers/actors.go +++ b/webserver/handlers/federation/actors.go @@ -1,4 +1,4 @@ -package controllers +package federation import ( "net/http" @@ -6,16 +6,17 @@ import ( log "github.com/sirupsen/logrus" - "github.com/owncast/owncast/activitypub/apmodels" - "github.com/owncast/owncast/activitypub/crypto" - "github.com/owncast/owncast/activitypub/requests" + "github.com/owncast/owncast/services/apfederation/apmodels" + "github.com/owncast/owncast/services/apfederation/crypto" + "github.com/owncast/owncast/services/apfederation/requests" "github.com/owncast/owncast/storage/configrepository" ) -var configRepository = configrepository.Get() - // ActorHandler handles requests for a single actor. func ActorHandler(w http.ResponseWriter, r *http.Request) { + configRepository := configrepository.Get() + req := requests.Get() + if !configRepository.GetFederationEnabled() { w.WriteHeader(http.StatusMethodNotAllowed) return @@ -52,7 +53,7 @@ func ActorHandler(w http.ResponseWriter, r *http.Request) { publicKey := crypto.GetPublicKey(actorIRI) person := apmodels.MakeServiceForAccount(accountName) - if err := requests.WriteStreamResponse(person, w, publicKey); err != nil { + if err := req.WriteStreamResponse(person, w, publicKey); err != nil { log.Errorln("unable to write stream response for actor handler", err) w.WriteHeader(http.StatusInternalServerError) return diff --git a/activitypub/controllers/followers.go b/webserver/handlers/federation/followers.go similarity index 83% rename from activitypub/controllers/followers.go rename to webserver/handlers/federation/followers.go index 0a7a982be..34fb9b7d9 100644 --- a/activitypub/controllers/followers.go +++ b/webserver/handlers/federation/followers.go @@ -1,4 +1,4 @@ -package controllers +package federation import ( "fmt" @@ -7,15 +7,16 @@ import ( "strconv" "strings" + "github.com/owncast/owncast/services/apfederation/apmodels" + "github.com/owncast/owncast/services/apfederation/crypto" + "github.com/owncast/owncast/services/apfederation/requests" + "github.com/owncast/owncast/storage/configrepository" + "github.com/owncast/owncast/storage/federationrepository" "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" - "github.com/owncast/owncast/activitypub/apmodels" - "github.com/owncast/owncast/activitypub/crypto" - "github.com/owncast/owncast/activitypub/persistence" - "github.com/owncast/owncast/activitypub/requests" ) const ( @@ -53,13 +54,16 @@ func FollowersHandler(w http.ResponseWriter, r *http.Request) { actorIRI := apmodels.MakeLocalIRIForAccount(accountName) publicKey := crypto.GetPublicKey(actorIRI) - if err := requests.WriteStreamResponse(response.(vocab.Type), w, publicKey); err != nil { + req := requests.Get() + if err := req.WriteStreamResponse(response.(vocab.Type), w, publicKey); err != nil { log.Errorln("unable to write stream response for followers handler", err) } } func getInitialFollowersRequest(r *http.Request) (vocab.ActivityStreamsOrderedCollection, error) { - followerCount, _ := persistence.GetFollowerCount() + federationRespository := federationrepository.Get() + + followerCount, _ := federationRespository.GetFollowerCount() collection := streams.NewActivityStreamsOrderedCollection() idProperty := streams.NewJSONLDIdProperty() id, err := createPageURL(r, nil) @@ -92,12 +96,14 @@ func getFollowersPage(page string, r *http.Request) (vocab.ActivityStreamsOrdere return nil, errors.Wrap(err, "unable to parse page number") } - followerCount, err := persistence.GetFollowerCount() + federationRespository := federationrepository.Get() + + followerCount, err := federationRespository.GetFollowerCount() if err != nil { return nil, errors.Wrap(err, "unable to get follower count") } - followers, _, err := persistence.GetFederationFollowers(followersPageSize, (pageInt-1)*followersPageSize) + followers, _, err := federationRespository.GetFederationFollowers(followersPageSize, (pageInt-1)*followersPageSize) if err != nil { return nil, errors.Wrap(err, "unable to get federation followers") } @@ -144,6 +150,8 @@ func getFollowersPage(page string, r *http.Request) (vocab.ActivityStreamsOrdere } func createPageURL(r *http.Request, page *string) (*url.URL, error) { + configRepository := configrepository.Get() + domain := configRepository.GetServerURL() if domain == "" { return nil, errors.New("unable to get server URL") diff --git a/activitypub/controllers/inbox.go b/webserver/handlers/federation/inbox.go similarity index 81% rename from activitypub/controllers/inbox.go rename to webserver/handlers/federation/inbox.go index 651c9cd2a..356a3da71 100644 --- a/activitypub/controllers/inbox.go +++ b/webserver/handlers/federation/inbox.go @@ -1,12 +1,13 @@ -package controllers +package federation import ( "io" "net/http" "strings" - "github.com/owncast/owncast/activitypub/apmodels" - "github.com/owncast/owncast/activitypub/inbox" + "github.com/owncast/owncast/services/apfederation/apmodels" + "github.com/owncast/owncast/services/apfederation/inbox" + "github.com/owncast/owncast/storage/configrepository" log "github.com/sirupsen/logrus" ) @@ -21,6 +22,8 @@ func InboxHandler(w http.ResponseWriter, r *http.Request) { } func acceptInboxRequest(w http.ResponseWriter, r *http.Request) { + configRepository := configrepository.Get() + if !configRepository.GetFederationEnabled() { w.WriteHeader(http.StatusMethodNotAllowed) return @@ -49,7 +52,8 @@ func acceptInboxRequest(w http.ResponseWriter, r *http.Request) { return } + ib := inbox.Get() inboxRequest := apmodels.InboxRequest{Request: r, ForLocalAccount: forLocalAccount, Body: data} - inbox.AddToQueue(inboxRequest) + ib.AddToQueue(inboxRequest) w.WriteHeader(http.StatusAccepted) } diff --git a/activitypub/controllers/nodeinfo.go b/webserver/handlers/federation/nodeinfo.go similarity index 87% rename from activitypub/controllers/nodeinfo.go rename to webserver/handlers/federation/nodeinfo.go index 0953e79f9..da68f18a7 100644 --- a/activitypub/controllers/nodeinfo.go +++ b/webserver/handlers/federation/nodeinfo.go @@ -1,15 +1,16 @@ -package controllers +package federation import ( "fmt" "net/http" "net/url" - "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/services/apfederation/apmodels" + "github.com/owncast/owncast/services/apfederation/crypto" + "github.com/owncast/owncast/services/apfederation/requests" "github.com/owncast/owncast/services/config" + "github.com/owncast/owncast/storage/configrepository" + "github.com/owncast/owncast/storage/federationrepository" log "github.com/sirupsen/logrus" ) @@ -24,6 +25,8 @@ func NodeInfoController(w http.ResponseWriter, r *http.Request) { Links []links `json:"links"` } + configRepository := configrepository.Get() + if !configRepository.GetFederationEnabled() { w.WriteHeader(http.StatusMethodNotAllowed) return @@ -88,13 +91,16 @@ func NodeInfoV2Controller(w http.ResponseWriter, r *http.Request) { Metadata metadata `json:"metadata"` } + configRepository := configrepository.Get() + if !configRepository.GetFederationEnabled() { w.WriteHeader(http.StatusMethodNotAllowed) return } - localPostCount, _ := persistence.GetLocalPostCount() - c := config.GetConfig() + federationRespository := federationrepository.Get() + localPostCount, _ := federationRespository.GetLocalPostCount() + c := config.Get() res := response{ Version: "2.0", @@ -163,6 +169,8 @@ func XNodeInfo2Controller(w http.ResponseWriter, r *http.Request) { OpenRegistrations bool `json:"openRegistrations"` } + configRepository := configrepository.Get() + if !configRepository.GetFederationEnabled() { w.WriteHeader(http.StatusMethodNotAllowed) return @@ -174,8 +182,9 @@ func XNodeInfo2Controller(w http.ResponseWriter, r *http.Request) { return } - localPostCount, _ := persistence.GetLocalPostCount() - c := config.GetConfig() + federationRespository := federationrepository.Get() + localPostCount, _ := federationRespository.GetLocalPostCount() + c := config.Get() res := &response{ Organization: Organization{ @@ -233,6 +242,8 @@ func InstanceV1Controller(w http.ResponseWriter, r *http.Request) { InvitesEnabled bool `json:"invites_enabled"` } + configRepository := configrepository.Get() + if !configRepository.GetFederationEnabled() { w.WriteHeader(http.StatusMethodNotAllowed) return @@ -251,8 +262,9 @@ func InstanceV1Controller(w http.ResponseWriter, r *http.Request) { } thumbnail.Path = "/logo/external" - localPostCount, _ := persistence.GetLocalPostCount() - c := config.GetConfig() + federationRespository := federationrepository.Get() + localPostCount, _ := federationRespository.GetLocalPostCount() + c := config.Get() res := response{ URI: serverURL, @@ -277,15 +289,20 @@ func InstanceV1Controller(w http.ResponseWriter, r *http.Request) { } func writeResponse(payload interface{}, w http.ResponseWriter) error { + configRepository := configrepository.Get() + accountName := configRepository.GetDefaultFederationUsername() actorIRI := apmodels.MakeLocalIRIForAccount(accountName) publicKey := crypto.GetPublicKey(actorIRI) - return requests.WritePayloadResponse(payload, w, publicKey) + req := requests.Get() + return req.WritePayloadResponse(payload, w, publicKey) } // HostMetaController points to webfinger. func HostMetaController(w http.ResponseWriter, r *http.Request) { + configRepository := configrepository.Get() + serverURL := configRepository.GetServerURL() if serverURL == "" { w.WriteHeader(http.StatusNotFound) diff --git a/activitypub/controllers/object.go b/webserver/handlers/federation/object.go similarity index 59% rename from activitypub/controllers/object.go rename to webserver/handlers/federation/object.go index 82a963e8c..e9ee81e96 100644 --- a/activitypub/controllers/object.go +++ b/webserver/handlers/federation/object.go @@ -1,18 +1,21 @@ -package controllers +package federation 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/services/apfederation/apmodels" + "github.com/owncast/owncast/services/apfederation/crypto" + "github.com/owncast/owncast/services/apfederation/requests" + "github.com/owncast/owncast/storage/configrepository" + "github.com/owncast/owncast/storage/federationrepository" log "github.com/sirupsen/logrus" ) // ObjectHandler handles requests for a single federated ActivityPub object. func ObjectHandler(w http.ResponseWriter, r *http.Request) { + configRepository := configrepository.Get() + if !configRepository.GetFederationEnabled() { w.WriteHeader(http.StatusMethodNotAllowed) return @@ -24,8 +27,9 @@ func ObjectHandler(w http.ResponseWriter, r *http.Request) { return } + federationRespository := federationrepository.Get() iri := strings.Join([]string{strings.TrimSuffix(configRepository.GetServerURL(), "/"), r.URL.Path}, "") - object, _, _, err := persistence.GetObjectByIRI(iri) + object, _, _, err := federationRespository.GetObjectByIRI(iri) if err != nil { w.WriteHeader(http.StatusNotFound) return @@ -35,7 +39,8 @@ func ObjectHandler(w http.ResponseWriter, r *http.Request) { actorIRI := apmodels.MakeLocalIRIForAccount(accountName) publicKey := crypto.GetPublicKey(actorIRI) - if err := requests.WriteResponse([]byte(object), w, publicKey); err != nil { + req := requests.Get() + if err := req.WriteResponse([]byte(object), w, publicKey); err != nil { log.Errorln(err) } } diff --git a/activitypub/controllers/outbox.go b/webserver/handlers/federation/outbox.go similarity index 82% rename from activitypub/controllers/outbox.go rename to webserver/handlers/federation/outbox.go index 0dbcb5a03..1eee5da0a 100644 --- a/activitypub/controllers/outbox.go +++ b/webserver/handlers/federation/outbox.go @@ -1,4 +1,4 @@ -package controllers +package federation import ( "fmt" @@ -8,10 +8,11 @@ import ( "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" - "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/services/apfederation/apmodels" + "github.com/owncast/owncast/services/apfederation/crypto" + "github.com/owncast/owncast/services/apfederation/requests" + "github.com/owncast/owncast/storage/federationrepository" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) @@ -51,14 +52,17 @@ func OutboxHandler(w http.ResponseWriter, r *http.Request) { actorIRI := apmodels.MakeLocalIRIForAccount(accountName) publicKey := crypto.GetPublicKey(actorIRI) - if err := requests.WriteStreamResponse(response.(vocab.Type), w, publicKey); err != nil { + req := requests.Get() + if err := req.WriteStreamResponse(response.(vocab.Type), w, publicKey); err != nil { log.Errorln("unable to write stream response for outbox handler", err) } } // ActorObjectHandler will handle the request for a single ActivityPub object. func ActorObjectHandler(w http.ResponseWriter, r *http.Request) { - object, _, _, err := persistence.GetObjectByIRI(r.URL.Path) + federationRespository := federationrepository.Get() + + object, _, _, err := federationRespository.GetObjectByIRI(r.URL.Path) if err != nil { w.WriteHeader(http.StatusNotFound) return @@ -81,7 +85,9 @@ func getInitialOutboxHandler(r *http.Request) (vocab.ActivityStreamsOrderedColle idProperty.SetIRI(id) collection.SetJSONLDId(idProperty) - totalPosts, err := persistence.GetOutboxPostCount() + federationRespository := federationrepository.Get() + + totalPosts, err := federationRespository.GetOutboxPostCount() if err != nil { return nil, errors.Wrap(err, "unable to get outbox post count") } @@ -108,7 +114,9 @@ func getOutboxPage(page string, r *http.Request) (vocab.ActivityStreamsOrderedCo return nil, errors.Wrap(err, "unable to parse page number") } - postCount, err := persistence.GetOutboxPostCount() + federationRespository := federationrepository.Get() + + postCount, err := federationRespository.GetOutboxPostCount() if err != nil { return nil, errors.Wrap(err, "unable to get outbox post count") } @@ -124,7 +132,7 @@ func getOutboxPage(page string, r *http.Request) (vocab.ActivityStreamsOrderedCo orderedItems := streams.NewActivityStreamsOrderedItemsProperty() - outboxItems, err := persistence.GetOutbox(outboxPageSize, (pageInt-1)*outboxPageSize) + outboxItems, err := federationRespository.GetOutbox(outboxPageSize, (pageInt-1)*outboxPageSize) if err != nil { return nil, errors.Wrap(err, "unable to get federation followers") } diff --git a/activitypub/controllers/webfinger.go b/webserver/handlers/federation/webfinger.go similarity index 92% rename from activitypub/controllers/webfinger.go rename to webserver/handlers/federation/webfinger.go index 11b347ebe..740eeb037 100644 --- a/activitypub/controllers/webfinger.go +++ b/webserver/handlers/federation/webfinger.go @@ -1,17 +1,20 @@ -package controllers +package federation import ( "encoding/json" "net/http" "strings" - "github.com/owncast/owncast/activitypub/apmodels" + "github.com/owncast/owncast/services/apfederation/apmodels" + "github.com/owncast/owncast/storage/configrepository" "github.com/owncast/owncast/utils" log "github.com/sirupsen/logrus" ) // WebfingerHandler will handle webfinger lookup requests. func WebfingerHandler(w http.ResponseWriter, r *http.Request) { + configRepository := configrepository.Get() + if !configRepository.GetFederationEnabled() { w.WriteHeader(http.StatusMethodNotAllowed) log.Debugln("webfinger request rejected! Federation is not enabled") diff --git a/webserver/handlers/followers.go b/webserver/handlers/followers.go index 61b502676..e64a1582c 100644 --- a/webserver/handlers/followers.go +++ b/webserver/handlers/followers.go @@ -3,13 +3,14 @@ package handlers import ( "net/http" - "github.com/owncast/owncast/activitypub/persistence" + "github.com/owncast/owncast/storage/federationrepository" "github.com/owncast/owncast/webserver/responses" ) // GetFollowers will handle an API request to fetch the list of followers (non-activitypub response). func (h *Handlers) GetFollowers(offset int, limit int, w http.ResponseWriter, r *http.Request) { - followers, total, err := persistence.GetFederationFollowers(limit, offset) + federationRepository := federationrepository.Get() + followers, total, err := federationRepository.GetFederationFollowers(limit, offset) if err != nil { responses.WriteSimpleResponse(w, false, "unable to fetch followers") return diff --git a/webserver/handlers/handlers.go b/webserver/handlers/handlers.go index 0d23597a3..4e2db1062 100644 --- a/webserver/handlers/handlers.go +++ b/webserver/handlers/handlers.go @@ -1,8 +1,22 @@ package handlers -type Handlers struct{} +import ( + "github.com/owncast/owncast/services/chat" + "github.com/owncast/owncast/storage/chatrepository" + "github.com/owncast/owncast/storage/configrepository" +) + +type Handlers struct { + configRepository *configrepository.SqlConfigRepository + chatService *chat.Chat + chatRepository *chatrepository.ChatRepository +} // New creates a new instances of web server handlers. func New() *Handlers { - return &Handlers{} + return &Handlers{ + configRepository: configrepository.Get(), + chatService: chat.Get(), + chatRepository: chatrepository.Get(), + } } diff --git a/webserver/handlers/hls.go b/webserver/handlers/hls.go index 4744d9045..0bbaa58a5 100644 --- a/webserver/handlers/hls.go +++ b/webserver/handlers/hls.go @@ -22,7 +22,7 @@ func (h *Handlers) HandleHLSRequest(w http.ResponseWriter, r *http.Request) { return } - c := config.GetConfig() + c := config.Get() requestedPath := r.URL.Path relativePath := strings.Replace(requestedPath, "/hls/", "", 1) diff --git a/webserver/handlers/images.go b/webserver/handlers/images.go index 8d2171fb3..5e7b765b7 100644 --- a/webserver/handlers/images.go +++ b/webserver/handlers/images.go @@ -16,7 +16,7 @@ const ( // GetThumbnail will return the thumbnail image as a response. func (h *Handlers) GetThumbnail(w http.ResponseWriter, r *http.Request) { - c := config.GetConfig() + c := config.Get() imageFilename := "thumbnail.jpg" imagePath := filepath.Join(c.TempDir, imageFilename) @@ -41,7 +41,7 @@ func (h *Handlers) GetThumbnail(w http.ResponseWriter, r *http.Request) { // GetPreview will return the preview gif as a response. func (h *Handlers) GetPreview(w http.ResponseWriter, r *http.Request) { - c := config.GetConfig() + c := config.Get() imageFilename := "preview.gif" imagePath := filepath.Join(c.TempDir, imageFilename) diff --git a/webserver/handlers/index.go b/webserver/handlers/index.go index 2aa5b5ab4..e5f16bf3e 100644 --- a/webserver/handlers/index.go +++ b/webserver/handlers/index.go @@ -8,10 +8,10 @@ import ( "path/filepath" "strings" - "github.com/owncast/owncast/config" "github.com/owncast/owncast/core" "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/config" "github.com/owncast/owncast/static" "github.com/owncast/owncast/utils" "github.com/owncast/owncast/webserver/middleware" diff --git a/webserver/handlers/moderation.go b/webserver/handlers/moderation.go index 6d1419002..e5ad63f0a 100644 --- a/webserver/handlers/moderation.go +++ b/webserver/handlers/moderation.go @@ -6,9 +6,8 @@ import ( "strings" "time" - "github.com/owncast/owncast/core/chat" - "github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/chat" "github.com/owncast/owncast/webserver/responses" log "github.com/sirupsen/logrus" ) @@ -24,9 +23,9 @@ func (h *Handlers) GetUserDetails(w http.ResponseWriter, r *http.Request) { } type response struct { - User *models.User `json:"user"` - ConnectedClients []connectedClient `json:"connectedClients"` - Messages []events.UserMessageEvent `json:"messages"` + User *models.User `json:"user"` + ConnectedClients []connectedClient `json:"connectedClients"` + Messages []chat.UserMessageEvent `json:"messages"` } pathComponents := strings.Split(r.URL.Path, "/") @@ -39,7 +38,7 @@ func (h *Handlers) GetUserDetails(w http.ResponseWriter, r *http.Request) { return } - c, _ := chat.GetClientsForUser(uid) + c, _ := h.chatService.GetClientsForUser(uid) clients := make([]connectedClient, len(c)) for i, c := range c { client := connectedClient{ @@ -55,7 +54,7 @@ func (h *Handlers) GetUserDetails(w http.ResponseWriter, r *http.Request) { clients[i] = client } - messages, err := chat.GetMessagesFromUser(uid) + messages, err := h.chatRepository.GetMessagesFromUser(uid) if err != nil { log.Errorln(err) } diff --git a/webserver/handlers/notifications.go b/webserver/handlers/notifications.go index ebeb8136b..042d6fa49 100644 --- a/webserver/handlers/notifications.go +++ b/webserver/handlers/notifications.go @@ -6,6 +6,7 @@ import ( "github.com/owncast/owncast/models" "github.com/owncast/owncast/services/notifications" + "github.com/owncast/owncast/storage/notificationsrepository" "github.com/owncast/owncast/webserver/responses" "github.com/owncast/owncast/utils" @@ -44,7 +45,9 @@ func (h *Handlers) RegisterForLiveNotifications(u models.User, w http.ResponseWr return } - if err := notifications.AddNotification(req.Channel, req.Destination); err != nil { + n := notificationsrepository.Get() + + if err := n.AddNotification(req.Channel, req.Destination); err != nil { log.Errorln(err) responses.WriteSimpleResponse(w, false, "unable to save notification") return diff --git a/webserver/handlers/playbackMetrics.go b/webserver/handlers/playbackMetrics.go index 4bbfff45e..7ae1de07d 100644 --- a/webserver/handlers/playbackMetrics.go +++ b/webserver/handlers/playbackMetrics.go @@ -35,19 +35,20 @@ func (h *Handlers) ReportPlaybackMetrics(w http.ResponseWriter, r *http.Request) } clientID := utils.GenerateClientIDFromRequest(r) + m := metrics.Get() - metrics.RegisterPlaybackErrorCount(clientID, request.Errors) + m.RegisterPlaybackErrorCount(clientID, request.Errors) if request.Bandwidth != 0.0 { - metrics.RegisterPlayerBandwidth(clientID, request.Bandwidth) + m.RegisterPlayerBandwidth(clientID, request.Bandwidth) } if request.Latency != 0.0 { - metrics.RegisterPlayerLatency(clientID, request.Latency) + m.RegisterPlayerLatency(clientID, request.Latency) } if request.DownloadDuration != 0.0 { - metrics.RegisterPlayerSegmentDownloadDuration(clientID, request.DownloadDuration) + m.RegisterPlayerSegmentDownloadDuration(clientID, request.DownloadDuration) } - metrics.RegisterQualityVariantChangesCount(clientID, request.QualityVariantChanges) + m.RegisterQualityVariantChangesCount(clientID, request.QualityVariantChanges) } diff --git a/webserver/handlers/remoteFollow.go b/webserver/handlers/remoteFollow.go index 12a70d630..3ec021934 100644 --- a/webserver/handlers/remoteFollow.go +++ b/webserver/handlers/remoteFollow.go @@ -7,7 +7,7 @@ import ( "net/url" "strings" - "github.com/owncast/owncast/activitypub/webfinger" + "github.com/owncast/owncast/services/apfederation/webfinger" "github.com/owncast/owncast/webserver/responses" ) @@ -33,10 +33,12 @@ func (h *Handlers) RemoteFollow(w http.ResponseWriter, r *http.Request) { return } + wf := webfinger.Get() + localActorPath, _ := url.Parse(configRepository.GetServerURL()) localActorPath.Path = fmt.Sprintf("/federation/user/%s", configRepository.GetDefaultFederationUsername()) var template string - links, err := webfinger.GetWebfingerLinks(request.Account) + links, err := wf.GetWebfingerLinks(request.Account) if err != nil { responses.WriteSimpleResponse(w, false, err.Error()) return diff --git a/webserver/handlers/status.go b/webserver/handlers/status.go index c6e778b6f..2e7701059 100644 --- a/webserver/handlers/status.go +++ b/webserver/handlers/status.go @@ -5,7 +5,7 @@ import ( "net/http" "time" - "github.com/owncast/owncast/core" + "github.com/owncast/owncast/services/status" "github.com/owncast/owncast/utils" "github.com/owncast/owncast/webserver/middleware" "github.com/owncast/owncast/webserver/responses" @@ -24,17 +24,17 @@ func (h *Handlers) GetStatus(w http.ResponseWriter, r *http.Request) { } func getStatusResponse() webStatusResponse { - status := core.GetStatus() + s := status.Get() response := webStatusResponse{ - Online: status.Online, + Online: s.Online, ServerTime: time.Now(), - LastConnectTime: status.LastConnectTime, - LastDisconnectTime: status.LastDisconnectTime, - VersionNumber: status.VersionNumber, - StreamTitle: status.StreamTitle, + LastConnectTime: s.Status.LastConnectTime, + LastDisconnectTime: s.Status.LastDisconnectTime, + VersionNumber: s.VersionNumber, + StreamTitle: s.StreamTitle, } if !configRepository.GetHideViewerCount() { - response.ViewerCount = status.ViewerCount + response.ViewerCount = s.ViewerCount } return response } diff --git a/webserver/handlers/ypApi.go b/webserver/handlers/ypApi.go index 910b5039d..cb6c76de5 100644 --- a/webserver/handlers/ypApi.go +++ b/webserver/handlers/ypApi.go @@ -4,8 +4,8 @@ import ( "encoding/json" "net/http" - "github.com/owncast/owncast/core" "github.com/owncast/owncast/models" + "github.com/owncast/owncast/services/status" "github.com/owncast/owncast/utils" log "github.com/sirupsen/logrus" ) @@ -33,7 +33,7 @@ func (h *Handlers) GetYPResponse(w http.ResponseWriter, r *http.Request) { return } - status := core.GetStatus() + s := status.Get() streamTitle := configRepository.GetStreamTitle() @@ -44,11 +44,11 @@ func (h *Handlers) GetYPResponse(w http.ResponseWriter, r *http.Request) { Logo: "/logo", NSFW: configRepository.GetNSFW(), Tags: configRepository.GetServerMetadataTags(), - Online: status.Online, - ViewerCount: status.ViewerCount, - OverallMaxViewerCount: status.OverallMaxViewerCount, - SessionMaxViewerCount: status.SessionMaxViewerCount, - LastConnectTime: status.LastConnectTime, + Online: s.Online, + ViewerCount: s.ViewerCount, + OverallMaxViewerCount: s.OverallMaxViewerCount, + SessionMaxViewerCount: s.SessionMaxViewerCount, + LastConnectTime: s.Status.LastConnectTime, Social: configRepository.GetSocialHandles(), } diff --git a/webserver/middleware/auth.go b/webserver/middleware/auth.go index 50319c4dc..2a12c49e3 100644 --- a/webserver/middleware/auth.go +++ b/webserver/middleware/auth.go @@ -106,7 +106,7 @@ func RequireExternalAPIAccessToken(scope string, handler ExternalAccessTokenHand // Not to be used for validating 3rd party access. func RequireUserAccessToken(handler UserAccessTokenHandlerFunc) http.HandlerFunc { userRepository := userrepository.Get() - chatRepository := chatrepository.GetChatRepository() + chatRepository := chatrepository.Get() return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { accessToken := r.URL.Query().Get("accessToken") diff --git a/webserver/router.go b/webserver/router.go index 37e788075..f93528a57 100644 --- a/webserver/router.go +++ b/webserver/router.go @@ -3,13 +3,10 @@ package webserver import ( "net/http" - "github.com/owncast/owncast/activitypub" - "github.com/owncast/owncast/core/chat" + "github.com/owncast/owncast/models" "github.com/owncast/owncast/services/config" - "github.com/owncast/owncast/storage" "github.com/owncast/owncast/utils" - fediverseauth "github.com/owncast/owncast/webserver/handlers/auth/fediverse" - "github.com/owncast/owncast/webserver/handlers/auth/indieauth" + "github.com/owncast/owncast/webserver/handlers/federation" "github.com/owncast/owncast/webserver/middleware" "github.com/prometheus/client_golang/prometheus/promhttp" ) @@ -20,12 +17,13 @@ func (s *webServer) setupRoutes() { s.setupAdminAPIRoutes() s.setupExternalThirdPartyAPIRoutes() s.setupModerationAPIRoutes() + s.setupActivityPubFederationRoutes() s.router.HandleFunc("/hls/", s.handlers.HandleHLSRequest) // websocket s.router.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { - chat.HandleClientConnection(w, r) + s.chatService.HandleClientConnection(w, r) }) s.router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { @@ -42,13 +40,10 @@ func (s *webServer) setupRoutes() { // s.ServeHTTP(w, r) } }) - - // ActivityPub has its own router - activitypub.Start(data.GetDatastore(), s.router) } func (s *webServer) setupWebAssetRoutes() { - c := config.GetConfig() + c := config.Get() // The admin web app. s.router.HandleFunc("/admin/", middleware.RequireAdminAuth(s.handlers.IndexHandler)) @@ -119,12 +114,12 @@ func (s *webServer) setupInternalAPIRoutes() { s.router.HandleFunc("/api/notifications/register", middleware.RequireUserAccessToken(s.handlers.RegisterForLiveNotifications)) // Start auth flow - s.router.HandleFunc("/api/auth/indieauth", middleware.RequireUserAccessToken(indieauth.StartAuthFlow)) - s.router.HandleFunc("/api/auth/indieauth/callback", indieauth.HandleRedirect) - s.router.HandleFunc("/api/auth/provider/indieauth", indieauth.HandleAuthEndpoint) + s.router.HandleFunc("/api/auth/indieauth", middleware.RequireUserAccessToken(s.indieAuthHandlers.StartAuthFlow)) + s.router.HandleFunc("/api/auth/indieauth/callback", s.indieAuthHandlers.HandleRedirect) + s.router.HandleFunc("/api/auth/provider/indieauth", s.indieAuthHandlers.HandleAuthEndpoint) - s.router.HandleFunc("/api/auth/fediverse", middleware.RequireUserAccessToken(fediverseauth.RegisterFediverseOTPRequest)) - s.router.HandleFunc("/api/auth/fediverse/verify", fediverseauth.VerifyFediverseOTPRequest) + s.router.HandleFunc("/api/auth/fediverse", middleware.RequireUserAccessToken(s.fediAuthHandlers.RegisterFediverseOTPRequest)) + s.router.HandleFunc("/api/auth/fediverse/verify", s.fediAuthHandlers.VerifyFediverseOTPRequest) } func (s *webServer) setupAdminAPIRoutes() { @@ -376,31 +371,31 @@ func (s *webServer) setupAdminAPIRoutes() { func (s *webServer) setupExternalThirdPartyAPIRoutes() { // Send a system message to chat - s.router.HandleFunc("/api/integrations/chat/system", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, s.handlers.SendSystemMessage)) + s.router.HandleFunc("/api/integrations/chat/system", middleware.RequireExternalAPIAccessToken(models.ScopeCanSendSystemMessages, s.handlers.SendSystemMessage)) // Send a system message to a single client - s.router.HandleFunc(utils.RestEndpoint("/api/integrations/chat/system/client/{clientId}", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, s.handlers.SendSystemMessageToConnectedClient))) + s.router.HandleFunc(utils.RestEndpoint("/api/integrations/chat/system/client/{clientId}", middleware.RequireExternalAPIAccessToken(models.ScopeCanSendSystemMessages, s.handlers.SendSystemMessageToConnectedClient))) // Send a user message to chat *NO LONGER SUPPORTED - s.router.HandleFunc("/api/integrations/chat/user", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendChatMessages, s.handlers.SendUserMessage)) + s.router.HandleFunc("/api/integrations/chat/user", middleware.RequireExternalAPIAccessToken(models.ScopeCanSendChatMessages, s.handlers.SendUserMessage)) // Send a message to chat as a specific 3rd party bot/integration based on its access token - s.router.HandleFunc("/api/integrations/chat/send", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendChatMessages, s.handlers.SendIntegrationChatMessage)) + s.router.HandleFunc("/api/integrations/chat/send", middleware.RequireExternalAPIAccessToken(models.ScopeCanSendChatMessages, s.handlers.SendIntegrationChatMessage)) // Send a user action to chat - s.router.HandleFunc("/api/integrations/chat/action", middleware.RequireExternalAPIAccessToken(user.ScopeCanSendSystemMessages, s.handlers.SendChatAction)) + s.router.HandleFunc("/api/integrations/chat/action", middleware.RequireExternalAPIAccessToken(models.ScopeCanSendSystemMessages, s.handlers.SendChatAction)) // Hide chat message - s.router.HandleFunc("/api/integrations/chat/messagevisibility", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, s.handlers.ExternalUpdateMessageVisibility)) + s.router.HandleFunc("/api/integrations/chat/messagevisibility", middleware.RequireExternalAPIAccessToken(models.ScopeHasAdminAccess, s.handlers.ExternalUpdateMessageVisibility)) // Stream title - s.router.HandleFunc("/api/integrations/streamtitle", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, s.handlers.ExternalSetStreamTitle)) + s.router.HandleFunc("/api/integrations/streamtitle", middleware.RequireExternalAPIAccessToken(models.ScopeHasAdminAccess, s.handlers.ExternalSetStreamTitle)) // Get chat history - s.router.HandleFunc("/api/integrations/chat", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, s.handlers.ExternalGetChatMessages)) + s.router.HandleFunc("/api/integrations/chat", middleware.RequireExternalAPIAccessToken(models.ScopeHasAdminAccess, s.handlers.ExternalGetChatMessages)) // Connected clients - s.router.HandleFunc("/api/integrations/clients", middleware.RequireExternalAPIAccessToken(user.ScopeHasAdminAccess, s.handlers.ExternalGetConnectedChatClients)) + s.router.HandleFunc("/api/integrations/clients", middleware.RequireExternalAPIAccessToken(models.ScopeHasAdminAccess, s.handlers.ExternalGetConnectedChatClients)) } func (s *webServer) setupModerationAPIRoutes() { @@ -413,3 +408,30 @@ func (s *webServer) setupModerationAPIRoutes() { // Get a user's details s.router.HandleFunc("/api/moderation/chat/user/", middleware.RequireUserModerationScopeAccesstoken(s.handlers.GetUserDetails)) } + +// StartRouter will start the federation specific http router. +func (s *webServer) setupActivityPubFederationRoutes() { + // WebFinger + s.router.HandleFunc("/.well-known/webfinger", federation.WebfingerHandler) + + // Host Metadata + s.router.HandleFunc("/.well-known/host-meta", federation.HostMetaController) + + // Nodeinfo v1 + s.router.HandleFunc("/.well-known/nodeinfo", federation.NodeInfoController) + + // x-nodeinfo v2 + s.router.HandleFunc("/.well-known/x-nodeinfo2", federation.XNodeInfo2Controller) + + // Nodeinfo v2 + s.router.HandleFunc("/nodeinfo/2.0", federation.NodeInfoV2Controller) + + // Instance details + s.router.HandleFunc("/api/v1/instance", federation.InstanceV1Controller) + + // Single ActivityPub Actor + s.router.HandleFunc("/federation/user/", middleware.RequireActivityPubOrRedirect(federation.ActorHandler)) + + // Single AP object + s.router.HandleFunc("/federation/", middleware.RequireActivityPubOrRedirect(federation.ObjectHandler)) +} diff --git a/webserver/webserver.go b/webserver/webserver.go index d9f433344..526a9a89e 100644 --- a/webserver/webserver.go +++ b/webserver/webserver.go @@ -6,21 +6,32 @@ import ( "time" "github.com/CAFxX/httpcompression" + "github.com/owncast/owncast/services/chat" "github.com/owncast/owncast/webserver/handlers" + "github.com/owncast/owncast/webserver/handlers/auth/fediverse" + "github.com/owncast/owncast/webserver/handlers/auth/indieauth" + log "github.com/sirupsen/logrus" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) type webServer struct { - router *http.ServeMux - handlers *handlers.Handlers - server *http.Server + router *http.ServeMux + handlers *handlers.Handlers + fediAuthHandlers *fediverse.FediAuthHandlers + indieAuthHandlers *indieauth.IndieAuthHandlers + chatService *chat.Chat + + server *http.Server } func New() *webServer { s := &webServer{ - router: http.NewServeMux(), + router: http.NewServeMux(), + handlers: handlers.New(), + fediAuthHandlers: fediverse.New(), + indieAuthHandlers: indieauth.New(), } s.setupRoutes() diff --git a/webserver/webserver_test.go b/webserver/webserver_test.go index 92bd1bc5d..4386757a4 100644 --- a/webserver/webserver_test.go +++ b/webserver/webserver_test.go @@ -3,19 +3,18 @@ package webserver import ( "net/http" "net/http/httptest" - "os" "testing" + + "github.com/owncast/owncast/storage/data" ) var srv *webServer func TestMain(m *testing.M) { - dbFile, err := os.CreateTemp(os.TempDir(), "owncast-test-db.db") + _, err := data.NewStore(":memory:") if err != nil { panic(err) } - - data.SetupPersistence(dbFile.Name()) srv = New() m.Run()