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:
Gabe Kangas
2022-03-18 13:33:23 -07:00
committed by GitHub
parent 4e415f7257
commit 4a17f30da8
39 changed files with 2209 additions and 3313 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}