Outbound live stream notifications (#1663)
* First pass at browser, discord, twilio notifications * Commit updated Javascript packages * Remove twilio notification support * Email notifications/smtp support * Fix Firefox notification support, remove chrome checks * WIP more email work * Add support for twitter notifications * Add stream title to discord and twitter notifications * Update notification registration modal * Fix hide/show email section * Commit updated API documentation * Commit updated Javascript packages * Fix post-rebase missing var * Remove unused var * Handle unsubscribe errors for browser push * Standardize email config prop names * Allow overriding go live email template * Some notifications cleanup * Commit updated Javascript packages * Remove email/smtp/mailjet support * Remove more references to email notifications Co-authored-by: Owncast <owncast@owncast.online>
This commit is contained in:
10
core/core.go
10
core/core.go
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/owncast/owncast/core/user"
|
||||
"github.com/owncast/owncast/core/webhooks"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/notifications"
|
||||
"github.com/owncast/owncast/utils"
|
||||
"github.com/owncast/owncast/yp"
|
||||
)
|
||||
@@ -25,11 +26,8 @@ var (
|
||||
_transcoder *transcoder.Transcoder
|
||||
_yp *yp.YP
|
||||
_broadcaster *models.Broadcaster
|
||||
)
|
||||
|
||||
var (
|
||||
handler transcoder.HLSHandler
|
||||
fileWriter = transcoder.FileWriterReceiverService{}
|
||||
handler transcoder.HLSHandler
|
||||
fileWriter = transcoder.FileWriterReceiverService{}
|
||||
)
|
||||
|
||||
// Start starts up the core processing.
|
||||
@@ -80,6 +78,8 @@ func Start() error {
|
||||
|
||||
webhooks.InitWorkerPool()
|
||||
|
||||
notifications.Setup(data.GetStore())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -14,49 +14,55 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
extraContentKey = "extra_page_content"
|
||||
streamTitleKey = "stream_title"
|
||||
streamKeyKey = "stream_key"
|
||||
logoPathKey = "logo_path"
|
||||
logoUniquenessKey = "logo_uniqueness"
|
||||
serverSummaryKey = "server_summary"
|
||||
serverWelcomeMessageKey = "server_welcome_message"
|
||||
serverNameKey = "server_name"
|
||||
serverURLKey = "server_url"
|
||||
httpPortNumberKey = "http_port_number"
|
||||
httpListenAddressKey = "http_listen_address"
|
||||
websocketHostOverrideKey = "websocket_host_override"
|
||||
rtmpPortNumberKey = "rtmp_port_number"
|
||||
serverMetadataTagsKey = "server_metadata_tags"
|
||||
directoryEnabledKey = "directory_enabled"
|
||||
directoryRegistrationKeyKey = "directory_registration_key"
|
||||
socialHandlesKey = "social_handles"
|
||||
peakViewersSessionKey = "peak_viewers_session"
|
||||
peakViewersOverallKey = "peak_viewers_overall"
|
||||
lastDisconnectTimeKey = "last_disconnect_time"
|
||||
ffmpegPathKey = "ffmpeg_path"
|
||||
nsfwKey = "nsfw"
|
||||
s3StorageEnabledKey = "s3_storage_enabled"
|
||||
s3StorageConfigKey = "s3_storage_config"
|
||||
videoLatencyLevel = "video_latency_level"
|
||||
videoStreamOutputVariantsKey = "video_stream_output_variants"
|
||||
chatDisabledKey = "chat_disabled"
|
||||
externalActionsKey = "external_actions"
|
||||
customStylesKey = "custom_styles"
|
||||
videoCodecKey = "video_codec"
|
||||
blockedUsernamesKey = "blocked_usernames"
|
||||
publicKeyKey = "public_key"
|
||||
privateKeyKey = "private_key"
|
||||
serverInitDateKey = "server_init_date"
|
||||
federationEnabledKey = "federation_enabled"
|
||||
federationUsernameKey = "federation_username"
|
||||
federationPrivateKey = "federation_private"
|
||||
federationGoLiveMessageKey = "federation_go_live_message"
|
||||
federationShowEngagementKey = "federation_show_engagement"
|
||||
federationBlockedDomainsKey = "federation_blocked_domains"
|
||||
suggestedUsernamesKey = "suggested_usernames"
|
||||
chatJoinMessagesEnabledKey = "chat_join_messages_enabled"
|
||||
chatEstablishedUsersOnlyModeKey = "chat_established_users_only_mode"
|
||||
extraContentKey = "extra_page_content"
|
||||
streamTitleKey = "stream_title"
|
||||
streamKeyKey = "stream_key"
|
||||
logoPathKey = "logo_path"
|
||||
logoUniquenessKey = "logo_uniqueness"
|
||||
serverSummaryKey = "server_summary"
|
||||
serverWelcomeMessageKey = "server_welcome_message"
|
||||
serverNameKey = "server_name"
|
||||
serverURLKey = "server_url"
|
||||
httpPortNumberKey = "http_port_number"
|
||||
httpListenAddressKey = "http_listen_address"
|
||||
websocketHostOverrideKey = "websocket_host_override"
|
||||
rtmpPortNumberKey = "rtmp_port_number"
|
||||
serverMetadataTagsKey = "server_metadata_tags"
|
||||
directoryEnabledKey = "directory_enabled"
|
||||
directoryRegistrationKeyKey = "directory_registration_key"
|
||||
socialHandlesKey = "social_handles"
|
||||
peakViewersSessionKey = "peak_viewers_session"
|
||||
peakViewersOverallKey = "peak_viewers_overall"
|
||||
lastDisconnectTimeKey = "last_disconnect_time"
|
||||
ffmpegPathKey = "ffmpeg_path"
|
||||
nsfwKey = "nsfw"
|
||||
s3StorageConfigKey = "s3_storage_config"
|
||||
videoLatencyLevel = "video_latency_level"
|
||||
videoStreamOutputVariantsKey = "video_stream_output_variants"
|
||||
chatDisabledKey = "chat_disabled"
|
||||
externalActionsKey = "external_actions"
|
||||
customStylesKey = "custom_styles"
|
||||
videoCodecKey = "video_codec"
|
||||
blockedUsernamesKey = "blocked_usernames"
|
||||
publicKeyKey = "public_key"
|
||||
privateKeyKey = "private_key"
|
||||
serverInitDateKey = "server_init_date"
|
||||
federationEnabledKey = "federation_enabled"
|
||||
federationUsernameKey = "federation_username"
|
||||
federationPrivateKey = "federation_private"
|
||||
federationGoLiveMessageKey = "federation_go_live_message"
|
||||
federationShowEngagementKey = "federation_show_engagement"
|
||||
federationBlockedDomainsKey = "federation_blocked_domains"
|
||||
suggestedUsernamesKey = "suggested_usernames"
|
||||
chatJoinMessagesEnabledKey = "chat_join_messages_enabled"
|
||||
chatEstablishedUsersOnlyModeKey = "chat_established_users_only_mode"
|
||||
notificationsEnabledKey = "notifications_enabled"
|
||||
discordConfigurationKey = "discord_configuration"
|
||||
browserPushConfigurationKey = "browser_push_configuration"
|
||||
browserPushPublicKeyKey = "browser_push_public_key"
|
||||
browserPushPrivateKeyKey = "browser_push_private_key"
|
||||
twitterConfigurationKey = "twitter_configuration"
|
||||
hasConfiguredInitialNotificationsKey = "has_configured_initial_notifications"
|
||||
)
|
||||
|
||||
// GetExtraPageBodyContent will return the user-supplied body content.
|
||||
@@ -446,22 +452,6 @@ func SetS3Config(config models.S3) error {
|
||||
return _datastore.Save(configEntry)
|
||||
}
|
||||
|
||||
// GetS3StorageEnabled will return if external storage is enabled.
|
||||
func GetS3StorageEnabled() bool {
|
||||
enabled, err := _datastore.GetBool(s3StorageEnabledKey)
|
||||
if err != nil {
|
||||
log.Traceln(err)
|
||||
return false
|
||||
}
|
||||
|
||||
return enabled
|
||||
}
|
||||
|
||||
// SetS3StorageEnabled will enable or disable external storage.
|
||||
func SetS3StorageEnabled(enabled bool) error {
|
||||
return _datastore.SetBool(s3StorageEnabledKey, enabled)
|
||||
}
|
||||
|
||||
// GetStreamLatencyLevel will return the stream latency level.
|
||||
func GetStreamLatencyLevel() models.LatencyLevel {
|
||||
level, err := _datastore.GetNumber(videoLatencyLevel)
|
||||
@@ -816,3 +806,108 @@ func GetChatJoinMessagesEnabled() bool {
|
||||
|
||||
return enabled
|
||||
}
|
||||
|
||||
// SetNotificationsEnabled will save the enabled state of notifications.
|
||||
func SetNotificationsEnabled(enabled bool) error {
|
||||
return _datastore.SetBool(notificationsEnabledKey, enabled)
|
||||
}
|
||||
|
||||
// GetNotificationsEnabled will return the enabled state of notifications.
|
||||
func GetNotificationsEnabled() bool {
|
||||
enabled, _ := _datastore.GetBool(notificationsEnabledKey)
|
||||
return enabled
|
||||
}
|
||||
|
||||
// GetDiscordConfig will return the Discord configuration.
|
||||
func GetDiscordConfig() models.DiscordConfiguration {
|
||||
configEntry, err := _datastore.Get(discordConfigurationKey)
|
||||
if err != nil {
|
||||
return models.DiscordConfiguration{Enabled: false}
|
||||
}
|
||||
|
||||
var config models.DiscordConfiguration
|
||||
if err := configEntry.getObject(&config); err != nil {
|
||||
return models.DiscordConfiguration{Enabled: false}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// SetDiscordConfig will set the Discord configuration.
|
||||
func SetDiscordConfig(config models.DiscordConfiguration) error {
|
||||
configEntry := ConfigEntry{Key: discordConfigurationKey, Value: config}
|
||||
return _datastore.Save(configEntry)
|
||||
}
|
||||
|
||||
// GetBrowserPushConfig will return the browser push configuration.
|
||||
func GetBrowserPushConfig() models.BrowserNotificationConfiguration {
|
||||
configEntry, err := _datastore.Get(browserPushConfigurationKey)
|
||||
if err != nil {
|
||||
return models.BrowserNotificationConfiguration{Enabled: false}
|
||||
}
|
||||
|
||||
var config models.BrowserNotificationConfiguration
|
||||
if err := configEntry.getObject(&config); err != nil {
|
||||
return models.BrowserNotificationConfiguration{Enabled: false}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// SetBrowserPushConfig will set the browser push configuration.
|
||||
func SetBrowserPushConfig(config models.BrowserNotificationConfiguration) error {
|
||||
configEntry := ConfigEntry{Key: browserPushConfigurationKey, Value: config}
|
||||
return _datastore.Save(configEntry)
|
||||
}
|
||||
|
||||
// SetBrowserPushPublicKey will set the public key for browser pushes.
|
||||
func SetBrowserPushPublicKey(key string) error {
|
||||
return _datastore.SetString(browserPushPublicKeyKey, key)
|
||||
}
|
||||
|
||||
// GetBrowserPushPublicKey will return the public key for browser pushes.
|
||||
func GetBrowserPushPublicKey() (string, error) {
|
||||
return _datastore.GetString(browserPushPublicKeyKey)
|
||||
}
|
||||
|
||||
// SetBrowserPushPrivateKey will set the private key for browser pushes.
|
||||
func SetBrowserPushPrivateKey(key string) error {
|
||||
return _datastore.SetString(browserPushPrivateKeyKey, key)
|
||||
}
|
||||
|
||||
// GetBrowserPushPrivateKey will return the private key for browser pushes.
|
||||
func GetBrowserPushPrivateKey() (string, error) {
|
||||
return _datastore.GetString(browserPushPrivateKeyKey)
|
||||
}
|
||||
|
||||
// SetTwitterConfiguration will set the Twitter configuration.
|
||||
func SetTwitterConfiguration(config models.TwitterConfiguration) error {
|
||||
configEntry := ConfigEntry{Key: twitterConfigurationKey, Value: config}
|
||||
return _datastore.Save(configEntry)
|
||||
}
|
||||
|
||||
// GetTwitterConfiguration will return the Twitter configuration.
|
||||
func GetTwitterConfiguration() models.TwitterConfiguration {
|
||||
configEntry, err := _datastore.Get(twitterConfigurationKey)
|
||||
if err != nil {
|
||||
return models.TwitterConfiguration{Enabled: false}
|
||||
}
|
||||
|
||||
var config models.TwitterConfiguration
|
||||
if err := configEntry.getObject(&config); err != nil {
|
||||
return models.TwitterConfiguration{Enabled: false}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// SetHasPerformedInitialNotificationsConfig sets when performed initial setup.
|
||||
func SetHasPerformedInitialNotificationsConfig(hasConfigured bool) error {
|
||||
return _datastore.SetBool(hasConfiguredInitialNotificationsKey, true)
|
||||
}
|
||||
|
||||
// GetHasPerformedInitialNotificationsConfig gets when performed initial setup.
|
||||
func GetHasPerformedInitialNotificationsConfig() bool {
|
||||
configured, _ := _datastore.GetBool(hasConfiguredInitialNotificationsKey)
|
||||
return configured
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/owncast/owncast/core/transcoder"
|
||||
"github.com/owncast/owncast/core/webhooks"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/notifications"
|
||||
"github.com/owncast/owncast/utils"
|
||||
)
|
||||
|
||||
@@ -28,6 +29,8 @@ 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}
|
||||
@@ -71,10 +74,8 @@ func setStreamAsConnected(rtmpOut *io.PipeReader) {
|
||||
_ = chat.SendSystemAction("Stay tuned, the stream is **starting**!", true)
|
||||
chat.SendAllWelcomeMessage()
|
||||
|
||||
// Send a delayed live Federated message.
|
||||
if data.GetFederationEnabled() {
|
||||
_onlineTimerCancelFunc = startFederatedLiveStreamMessageTimer()
|
||||
}
|
||||
// Send delayed notification messages.
|
||||
_onlineTimerCancelFunc = startLiveStreamNotificationsTimer()
|
||||
}
|
||||
|
||||
// SetStreamAsDisconnected sets the stream as disconnected.
|
||||
@@ -161,19 +162,37 @@ func stopOnlineCleanupTimer() {
|
||||
}
|
||||
}
|
||||
|
||||
func startFederatedLiveStreamMessageTimer() context.CancelFunc {
|
||||
// Send a delayed live Federated message.
|
||||
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):
|
||||
log.Traceln("Sending Federated Go Live message.")
|
||||
if err := activitypub.SendLive(); err != nil {
|
||||
log.Errorln(err)
|
||||
if _lastNotified != nil && time.Since(*_lastNotified) < 10*time.Minute {
|
||||
return
|
||||
}
|
||||
|
||||
// Send Fediverse message.
|
||||
if data.GetFederationEnabled() {
|
||||
log.Traceln("Sending Federated Go Live message.")
|
||||
if err := activitypub.SendLive(); err != nil {
|
||||
log.Errorln(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Send notification to those who have registered for them.
|
||||
if notifier, err := notifications.New(data.GetDatastore()); err != nil {
|
||||
log.Errorln(err)
|
||||
} else {
|
||||
notifier.Notify()
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
_lastNotified = &now
|
||||
case <-c.Done():
|
||||
}
|
||||
}(c)
|
||||
|
||||
return cancelFunc
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user