mirror of
https://github.com/owncast/owncast.git
synced 2024-11-24 05:38:58 +03:00
WIP
This commit is contained in:
parent
32b1dbeaf3
commit
b80ccc4966
170 changed files with 2573 additions and 2026 deletions
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package inbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"github.com/owncast/owncast/activitypub/persistence"
|
||||
"github.com/owncast/owncast/activitypub/resolvers"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func handleUpdateRequest(c context.Context, activity vocab.ActivityStreamsUpdate) error {
|
||||
// We only care about update events to followers.
|
||||
if !activity.GetActivityStreamsObject().At(0).IsActivityStreamsPerson() {
|
||||
return nil
|
||||
}
|
||||
|
||||
actor, err := resolvers.GetResolvedActorFromActorProperty(activity.GetActivityStreamsActor())
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return persistence.UpdateFollower(actor.ActorIri.String(), actor.Inbox.String(), actor.Name, actor.FullUsername, actor.Image.String())
|
||||
}
|
|
@ -1,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))
|
||||
}
|
48
cmd/application.go
Normal file
48
cmd/application.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/owncast/owncast/services/config"
|
||||
"github.com/owncast/owncast/services/metrics"
|
||||
"github.com/owncast/owncast/storage/configrepository"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
configservice *config.Config
|
||||
metricsservice *metrics.Metrics
|
||||
configRepository *configrepository.SqlConfigRepository
|
||||
|
||||
maximumConcurrentConnectionLimit int64
|
||||
}
|
||||
|
||||
/*
|
||||
The order of this setup matters.
|
||||
- Parse flags
|
||||
- Set the session runtime values
|
||||
- Use the session values to configure data persistence
|
||||
*/
|
||||
func (app *Application) Start() {
|
||||
app.configservice = config.Get()
|
||||
|
||||
app.parseFlags()
|
||||
app.configureLogging(*enableDebugOptions, *enableVerboseLogging, app.configservice.LogDirectory)
|
||||
app.showStartupMessage()
|
||||
|
||||
app.setSessionConfig()
|
||||
app.createDirectories()
|
||||
|
||||
app.maximumConcurrentConnectionLimit = getMaximumConcurrentConnectionLimit()
|
||||
setSystemConcurrentConnectionLimit(app.maximumConcurrentConnectionLimit)
|
||||
|
||||
// If we're restoring a backup, do that and exit.
|
||||
if *restoreDatabaseFile != "" {
|
||||
app.handleRestoreBackup(restoreDatabaseFile)
|
||||
log.Exit(0)
|
||||
}
|
||||
|
||||
if *backupDirectory != "" {
|
||||
app.configservice.BackupDirectory = *backupDirectory
|
||||
}
|
||||
|
||||
app.startServices()
|
||||
}
|
20
cmd/backuprestore.go
Normal file
20
cmd/backuprestore.go
Normal file
|
@ -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.")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
//go:build freebsd
|
||||
// +build freebsd
|
||||
|
||||
package chat
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"syscall"
|
|
@ -1,6 +1,6 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package chat
|
||||
package cmd
|
||||
|
||||
func setSystemConcurrentConnectionLimit(limit int64) {}
|
71
cmd/config.go
Normal file
71
cmd/config.go
Normal file
|
@ -0,0 +1,71 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/owncast/owncast/storage/configrepository"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (app *Application) setSessionConfig() {
|
||||
// Stream key
|
||||
if *newStreamKey != "" {
|
||||
log.Println("Temporary stream key is set for this session.")
|
||||
app.configservice.TemporaryStreamKey = *newStreamKey
|
||||
}
|
||||
|
||||
app.configservice.EnableDebugFeatures = *enableDebugOptions
|
||||
|
||||
if *dbFile != "" {
|
||||
app.configservice.DatabaseFilePath = *dbFile
|
||||
}
|
||||
|
||||
if *logDirectory != "" {
|
||||
app.configservice.LogDirectory = *logDirectory
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) saveUpdatedConfig() {
|
||||
configRepository := configrepository.Get()
|
||||
|
||||
if *newAdminPassword != "" {
|
||||
if err := configRepository.SetAdminPassword(*newAdminPassword); err != nil {
|
||||
log.Errorln("Error setting your admin password.", err)
|
||||
log.Exit(1)
|
||||
} else {
|
||||
log.Infoln("Admin password changed")
|
||||
}
|
||||
}
|
||||
|
||||
// Set the web server port
|
||||
if *webServerPortOverride != "" {
|
||||
portNumber, err := strconv.Atoi(*webServerPortOverride)
|
||||
if err != nil {
|
||||
log.Warnln(err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Saving new web server port number to", portNumber)
|
||||
if err := configRepository.SetHTTPPortNumber(float64(portNumber)); err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
}
|
||||
app.configservice.WebServerPort = configRepository.GetHTTPPortNumber()
|
||||
|
||||
// Set the web server ip
|
||||
if *webServerIPOverride != "" {
|
||||
log.Println("Saving new web server listen IP address to", *webServerIPOverride)
|
||||
if err := configRepository.SetHTTPListenAddress(*webServerIPOverride); err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
}
|
||||
app.configservice.WebServerIP = configRepository.GetHTTPListenAddress()
|
||||
|
||||
// Set the rtmp server port
|
||||
if *rtmpPortOverride > 0 {
|
||||
log.Println("Saving new RTMP server port number to", *rtmpPortOverride)
|
||||
if err := configRepository.SetRTMPPortNumber(float64(*rtmpPortOverride)); err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
}
|
||||
}
|
7
cmd/console.go
Normal file
7
cmd/console.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package cmd
|
||||
|
||||
import log "github.com/sirupsen/logrus"
|
||||
|
||||
func (app *Application) showStartupMessage() {
|
||||
log.Infoln(app.configservice.GetReleaseString())
|
||||
}
|
4
cmd/data.go
Normal file
4
cmd/data.go
Normal file
|
@ -0,0 +1,4 @@
|
|||
package cmd
|
||||
|
||||
func initializeData() {
|
||||
}
|
23
cmd/flags.go
Normal file
23
cmd/flags.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"flag"
|
||||
)
|
||||
|
||||
var (
|
||||
dbFile = flag.String("database", "", "Path to the database file.")
|
||||
logDirectory = flag.String("logdir", "", "Directory where logs will be written to")
|
||||
backupDirectory = flag.String("backupdir", "", "Directory where backups will be written to")
|
||||
enableDebugOptions = flag.Bool("enableDebugFeatures", false, "Enable additional debugging options.")
|
||||
enableVerboseLogging = flag.Bool("enableVerboseLogging", false, "Enable additional logging.")
|
||||
restoreDatabaseFile = flag.String("restoreDatabase", "", "Restore an Owncast database backup")
|
||||
newAdminPassword = flag.String("adminpassword", "", "Set your admin password")
|
||||
newStreamKey = flag.String("streamkey", "", "Set a temporary stream key for this session")
|
||||
webServerPortOverride = flag.String("webserverport", "", "Force the web server to listen on a specific port")
|
||||
webServerIPOverride = flag.String("webserverip", "", "Force web server to listen on this IP address")
|
||||
rtmpPortOverride = flag.Int("rtmpport", 0, "Set listen port for the RTMP server")
|
||||
)
|
||||
|
||||
func (app *Application) parseFlags() {
|
||||
flag.Parse()
|
||||
}
|
4
cmd/services.go
Normal file
4
cmd/services.go
Normal file
|
@ -0,0 +1,4 @@
|
|||
package cmd
|
||||
|
||||
func (app *Application) startServices() {
|
||||
}
|
162
cmd/setup.go
Normal file
162
cmd/setup.go
Normal file
|
@ -0,0 +1,162 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/owncast/owncast/logging"
|
||||
"github.com/owncast/owncast/services/config"
|
||||
"github.com/owncast/owncast/static"
|
||||
"github.com/owncast/owncast/utils"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (app *Application) createDirectories() {
|
||||
// Create the data directory if needed
|
||||
if !utils.DoesFileExists("data") {
|
||||
if err := os.Mkdir("./data", 0o700); err != nil {
|
||||
log.Fatalln("Cannot create data directory", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Recreate the temp dir
|
||||
if utils.DoesFileExists(app.configservice.TempDir) {
|
||||
err := os.RemoveAll(app.configservice.TempDir)
|
||||
if err != nil {
|
||||
log.Fatalln("Unable to remove temp dir! Check permissions.", app.configservice.TempDir, err)
|
||||
}
|
||||
}
|
||||
if err := os.Mkdir(app.configservice.TempDir, 0o700); err != nil {
|
||||
log.Fatalln("Unable to create temp dir!", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) configureLogging(enableDebugFeatures bool, enableVerboseLogging bool, logDirectory string) {
|
||||
logging.Setup(enableDebugFeatures, enableVerboseLogging, logDirectory)
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
})
|
||||
}
|
||||
|
||||
// setupEmojiDirectory sets up the custom emoji directory by copying all built-in
|
||||
// emojis if the directory does not yet exist.
|
||||
func (app *Application) setupEmojiDirectory() (err error) {
|
||||
type emojiDirectory struct {
|
||||
path string
|
||||
isDir bool
|
||||
}
|
||||
|
||||
// Migrate old (pre 0.1.0) emoji to new location if they exist.
|
||||
app.migrateCustomEmojiLocations()
|
||||
|
||||
if utils.DoesFileExists(app.configservice.CustomEmojiPath) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = os.MkdirAll(app.configservice.CustomEmojiPath, 0o750); err != nil {
|
||||
return fmt.Errorf("unable to create custom emoji directory: %w", err)
|
||||
}
|
||||
|
||||
staticFS := static.GetEmoji()
|
||||
files := []emojiDirectory{}
|
||||
|
||||
walkFunction := func(path string, d os.DirEntry, err error) error {
|
||||
if path == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
if d.Name() == "LICENSE.md" {
|
||||
return nil
|
||||
}
|
||||
|
||||
files = append(files, emojiDirectory{path: path, isDir: d.IsDir()})
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := fs.WalkDir(staticFS, ".", walkFunction); err != nil {
|
||||
log.Errorln("unable to fetch emojis: " + err.Error())
|
||||
return errors.Wrap(err, "unable to fetch embedded emoji files")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read built-in emoji files: %w", err)
|
||||
}
|
||||
|
||||
// Now copy all built-in emojis to the custom emoji directory
|
||||
for _, path := range files {
|
||||
emojiPath := filepath.Join(app.configservice.CustomEmojiPath, path.path)
|
||||
|
||||
if path.isDir {
|
||||
if err := os.Mkdir(emojiPath, 0o700); err != nil {
|
||||
return errors.Wrap(err, "unable to create emoji directory, check permissions?: "+path.path)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
memFile, staticOpenErr := staticFS.Open(path.path)
|
||||
if staticOpenErr != nil {
|
||||
return errors.Wrap(staticOpenErr, "unable to open emoji file from embedded filesystem")
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
diskFile, err := os.Create(emojiPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create custom emoji file on disk: %w", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
_ = diskFile.Close()
|
||||
return fmt.Errorf("unable to open built-in emoji file: %w", err)
|
||||
}
|
||||
|
||||
if _, err = io.Copy(diskFile, memFile); err != nil {
|
||||
_ = diskFile.Close()
|
||||
_ = os.Remove(emojiPath)
|
||||
return fmt.Errorf("unable to copy built-in emoji file to disk: %w", err)
|
||||
}
|
||||
|
||||
if err = diskFile.Close(); err != nil {
|
||||
_ = os.Remove(emojiPath)
|
||||
return fmt.Errorf("unable to close custom emoji file on disk: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MigrateCustomEmojiLocations migrates custom emoji from the old location to the new location.
|
||||
func (app *Application) migrateCustomEmojiLocations() {
|
||||
oldLocation := path.Join("webroot", "img", "emoji")
|
||||
newLocation := path.Join("data", "emoji")
|
||||
|
||||
if !utils.DoesFileExists(oldLocation) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Moving custom emoji directory from", oldLocation, "to", newLocation)
|
||||
|
||||
if err := utils.Move(oldLocation, newLocation); err != nil {
|
||||
log.Errorln("error moving custom emoji directory", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) resetDirectories() {
|
||||
log.Trace("Resetting file directories to a clean slate.")
|
||||
|
||||
// Wipe hls data directory
|
||||
utils.CleanupDirectory(app.configservice.HLSStoragePath)
|
||||
|
||||
// Remove the previous thumbnail
|
||||
logo := app.configRepository.GetLogoPath()
|
||||
if utils.DoesFileExists(logo) {
|
||||
err := utils.Copy(path.Join("data", logo), filepath.Join(config.DataDirectory, "thumbnail.jpg"))
|
||||
if err != nil {
|
||||
log.Warnln(err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package events
|
||||
|
||||
// UserJoinedEvent is the event fired when a user joins chat.
|
||||
type UserJoinedEvent struct {
|
||||
Event
|
||||
UserEvent
|
||||
}
|
||||
|
||||
// GetBroadcastPayload will return the object to send to all chat users.
|
||||
func (e *UserJoinedEvent) GetBroadcastPayload() EventPayload {
|
||||
return EventPayload{
|
||||
"type": UserJoined,
|
||||
"id": e.ID,
|
||||
"timestamp": e.Timestamp,
|
||||
"user": e.User,
|
||||
}
|
||||
}
|
|
@ -1,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
|
||||
}
|
|
@ -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
|
||||
}
|
75
core/core.go
75
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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),
|
||||
)
|
||||
|
|
|
@ -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")
|
||||
}
|
192
main.go
192
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() {
|
||||
// }
|
||||
|
|
12
models/chatAccessScopes.go
Normal file
12
models/chatAccessScopes.go
Normal file
|
@ -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"
|
||||
)
|
|
@ -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.
|
|
@ -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"`
|
||||
}
|
20
models/event.go
Normal file
20
models/event.go
Normal file
|
@ -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()
|
||||
}
|
4
models/eventPayload.go
Normal file
4
models/eventPayload.go
Normal file
|
@ -0,0 +1,4 @@
|
|||
package models
|
||||
|
||||
// EventPayload is a generic key/value map for sending out to chat clients.
|
||||
type EventPayload map[string]interface{}
|
|
@ -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"
|
||||
)
|
|
@ -1,4 +1,4 @@
|
|||
package events
|
||||
package models
|
||||
|
||||
// NameChangeEvent is received when a user changes their chat display name.
|
||||
type NameChangeEvent struct {
|
|
@ -1,4 +1,4 @@
|
|||
package events
|
||||
package models
|
||||
|
||||
// SetMessageVisibilityEvent is the event fired when one or more message
|
||||
// visibilities are changed.
|
|
@ -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"`
|
||||
|
|
|
@ -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 {
|
10
models/userEvent.go
Normal file
10
models/userEvent.go
Normal file
|
@ -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"`
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
82
services/apfederation/activitypub.go
Normal file
82
services/apfederation/activitypub.go
Normal file
|
@ -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()
|
||||
}
|
|
@ -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"
|
||||
)
|
|
@ -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)
|
|
@ -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)
|
||||
}
|
59
services/apfederation/inbox/chat.go
Normal file
59
services/apfederation/inbox/chat.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
37
services/apfederation/inbox/inbox.go
Normal file
37
services/apfederation/inbox/inbox.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
23
services/apfederation/inbox/update.go
Normal file
23
services/apfederation/inbox/update.go
Normal file
|
@ -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())
|
||||
}
|
|
@ -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")
|
|
@ -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")
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
|
@ -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("<p>%s</p>", title)
|
||||
}
|
||||
<<<<<<< HEAD:activitypub/outbox/outbox.go
|
||||
textContent = fmt.Sprintf("<p>%s</p>%s<p>%s</p><p><a href=\"%s\">%s</a></p>", textContent, streamTitle, tagsString, data.GetServerURL(), data.GetServerURL())
|
||||
=======
|
||||
textContent = fmt.Sprintf("<p>%s</p>%s<p>%s</p><a href=\"%s\">%s</a>", 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("<a class=\"hashtag\" href=\"https://directory.owncast.online/tags/%s\">#%s</a>", 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)
|
||||
}
|
|
@ -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")
|
20
services/apfederation/requests/requests.go
Normal file
20
services/apfederation/requests/requests.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
24
services/apfederation/resolvers/resolvers.go
Normal file
24
services/apfederation/resolvers/resolvers.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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)
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
227
services/chat/chat.go
Normal file
227
services/chat/chat.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
}
|
131
services/chat/emoji.go
Normal file
131
services/chat/emoji.go
Normal file
|
@ -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: `<img src="{{ .URL }}" class="emoji" alt=":{{ .Name }}:" title=":{{ .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 = `<img src="{{ .URL }}" class="emoji" alt=":{{ .Name }}:" title=":{{ .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
|
||||
}
|
|
@ -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++
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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 = `<img src="{{ .URL }}" class="emoji" alt=":{{ .Name }}:" title=":{{ .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(
|
|
@ -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 := `<img src="/img/emoji/beerparrot.gif"><ul><li>**test thing**</li></ul>`
|
||||
expected := "<p><img src=\"/img/emoji/beerparrot.gif\"><ul><li><strong>test thing</strong></li></ul></p>\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)
|
|
@ -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())
|
||||
}
|
||||
|
|
@ -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()
|
28
services/chat/systemMessageEvent.go
Normal file
28
services/chat/systemMessageEvent.go
Normal file
|
@ -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
|
||||
}
|
39
services/chat/userMessageEvent.go
Normal file
39
services/chat/userMessageEvent.go
Normal file
|
@ -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()
|
||||
}
|
|
@ -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,
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
61
services/status/status.go
Normal file
61
services/status/status.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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{}{
|
||||
|
|
|
@ -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",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue