From c6cd0e83e3fc1176a0a4c906864d54c93539b0ea Mon Sep 17 00:00:00 2001 From: Hakan Bayindir Date: Thu, 19 Oct 2023 22:35:22 +0300 Subject: [PATCH] feat: Add message TTL support and more. - feat: Do not send a sound parameter unless user overrides it, since it's required by the API. - proj: Bump project version to 0.4.0a2. - feat: Warn user if -url_title is used without -url flag. - feat: Do not send -url_title if no `-url` is specified. - refactor: Rewrite notification sending function to handle optional fields correctly. - feat: Add message time to live support. --- CHANGELOG.md | 8 +++- src/nudge.go | 116 +++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 96 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93642c4..9f5fcf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,13 @@ ## 2023-10-19 -- proj: Bump project version to 0.4.0a +- feat: Do not send a `sound` parameter unless user overrides it, since it's required by the API. +- proj: Bump project version to 0.4.0a2. +- feat: Warn user if `-url_title` is used without `-url` flag. +- feat: Do not send `-url_title` if no `-url` is specified. +- refactor: Rewrite notification sending function to handle optional fields correctly. +- feat: Add message time to live support. +- proj: Bump project version to 0.4.0a1 - feat: Add notification priority sanity checking support. - fix: Call defaults from the correct structure (`runTimeConfiguration`) in `initFlags` function. - fix: Store user supplied configuration file path in `runTimeConfiguration`. diff --git a/src/nudge.go b/src/nudge.go index e385fe1..ef99134 100644 --- a/src/nudge.go +++ b/src/nudge.go @@ -44,6 +44,7 @@ type Notification struct { priority int // Can be set from -2 to 2. 0 is default. Higher numbers are more urgent. Negative ones are informational levels. sound string // Notification's sound. See the API docs for valid names. timestamp int // This is a UNIX timestamp. + timeToLive int // Seconds before the notification is automatically deleted from recipient devices. title string // Notification's title. url string // You can attach a URL to a notification. urlTitle string // You can add a title to the URL, if left blank, the URL will be shown. @@ -65,12 +66,13 @@ type RuntimeConfiguration struct { // without altering the initialization order of the application. type FlagStorage struct { // Below block contains variables about the notification itself. - device string // Contains a comma delimited list of devices which will receive the message. - priority int // Can be set from -2 to 2. 0 is default. Higher numbers are more urgent. Negative ones are informational levels. - sound string // Notification's sound. See the API docs for valid names. - title string // Notification's title. - url string // You can attach a URL to a notification. - urlTitle string // You can add a title to the URL, if left blank, the URL will be shown. + device string // Contains a comma delimited list of devices which will receive the message. + priority int // Can be set from -2 to 2. 0 is default. Higher numbers are more urgent. Negative ones are informational levels. + sound string // Notification's sound. See the API docs for valid names. + timeToLive int // Seconds before the notification is automatically deleted from recipient devices. + title string // Notification's title. + url string // You can attach a URL to a notification. + urlTitle string // You can add a title to the URL, if left blank, the URL will be shown. // Configuration file related flags are stored below: configFilePath string // Store the configuration file override here, if present. @@ -84,8 +86,9 @@ type FlagStorage struct { // Following struct contains the set of acceptable configuration option values. // checkConfigurationSanity() function consulsts this structure while verifying the final configuration. type ValidConfigurationOptions struct { - possibleLogLevels []string + possibleLogLevels []string possiblePriorityLevelRange []int + minimumTimeToLive int } // This function initializes the ValidConfigurationOptions data structure, which will be @@ -94,6 +97,7 @@ type ValidConfigurationOptions struct { func initializeValidConfigurationOptions(ValidConfigurationOptions *ValidConfigurationOptions) { ValidConfigurationOptions.possibleLogLevels = []string{"debug", "info", "warn", "error", "fatal", "panic"} ValidConfigurationOptions.possiblePriorityLevelRange = []int{-2, 2} + ValidConfigurationOptions.minimumTimeToLive = 0 // We'll set this to 0, so timeToLive should be zero or positive. } // This function stores and applies the defaults of the application. @@ -113,14 +117,14 @@ func applyDefaultConfiguration(runtimeConfiguration *RuntimeConfiguration, notif notificationToSend.device = "all" notificationToSend.html = 0 notificationToSend.priority = 0 - notificationToSend.sound = "pushover" + notificationToSend.sound = "" notificationToSend.title = notificationTitle // Let's set defaults for the application itself, again where it makes sense. // Some applications do not sanitize the name like that, but I prefer to do. applicationName := strings.Split(os.Args[0], "/") runtimeConfiguration.applicationName = applicationName[len(applicationName)-1] - runtimeConfiguration.version = "0.4.0a1" + runtimeConfiguration.version = "0.4.0a2" runtimeConfiguration.dryrun = false runtimeConfiguration.logLevel = "warn" } @@ -201,6 +205,7 @@ func initFlags(notificationToSend *Notification, runtimeConfiguration *RuntimeCo flag.StringVar(&flagStorage.device, "devices", notificationToSend.device, "List of devices to be notified. Separate multiple devices with ','.") flag.IntVar(&flagStorage.priority, "priority", notificationToSend.priority, "Adjust notification priority. Between -2 and 2. (default "+strconv.Itoa(notificationToSend.priority)+")") flag.StringVar(&flagStorage.sound, "sound", notificationToSend.sound, "Set notification sound.") + flag.IntVar(&flagStorage.timeToLive, "ttl", notificationToSend.timeToLive, "Seconds before the notification removed from devices automatically (ignored if priority is 2).") flag.StringVar(&flagStorage.title, "title", notificationToSend.title, "Notification title. Hostname is used if omitted.") flag.StringVar(&flagStorage.url, "url", notificationToSend.url, "An optional URL to attach to the notification.") flag.StringVar(&flagStorage.urlTitle, "url_title", notificationToSend.urlTitle, "An optional title for URL to send.") @@ -244,7 +249,7 @@ func applyFlags(setFlags *[]string, flagStorage *FlagStorage, runtimeConfigurati // It's fast enough, so I'm fine with this. for _, flagValue := range *setFlags { logger.Debugf("Working on flag %s.", flagValue) - + // The for loop above visits the set flags one by one, then we handle it here by looking up what it is. // Go doesn't provide JMP tables for switches, but I don't think this slows down that much. // ref: https://github.com/golang/go/issues/5496 @@ -258,6 +263,9 @@ func applyFlags(setFlags *[]string, flagStorage *FlagStorage, runtimeConfigurati case "sound": notificationToSend.sound = flagStorage.sound logger.Debugf("Notification sound is changed to %s.", notificationToSend.sound) + case "ttl": + notificationToSend.timeToLive = flagStorage.timeToLive + logger.Debugf("Notification's time to live is set to %s second(s).", notificationToSend.timeToLive) case "title": notificationToSend.title = flagStorage.title logger.Debugf("Notification title is changed to %s.", notificationToSend.title) @@ -298,20 +306,36 @@ func checkConfigurationSanity(notificationToSend *Notification, runtimeConfigura logLevelValid := !(slices.Index(validConfigurationOptions.possibleLogLevels, runtimeConfiguration.logLevel) == -1) if !logLevelValid { - logger.Fatalf("Provided log level (%s) is not in the set of acceptable values. Please make sure you define or supply a valid log level.", runtimeConfiguration.logLevel) + logger.Warnf("Provided log level (%s) is not in the set of acceptable values. Please make sure you define or supply a valid log level.", runtimeConfiguration.logLevel) } logger.Debugf("Provided log level (%s) is valid.", runtimeConfiguration.logLevel) - + // Check message notification is at acceptable levels and clamp the value if necessary. if notificationToSend.priority < validConfigurationOptions.possiblePriorityLevelRange[0] { logger.Warnf("Supplied notification priority %d is too low. Clamping to %d.", notificationToSend.priority, validConfigurationOptions.possiblePriorityLevelRange[0]) notificationToSend.priority = validConfigurationOptions.possiblePriorityLevelRange[0] - }else if notificationToSend.priority > validConfigurationOptions.possiblePriorityLevelRange[1] { + } else if notificationToSend.priority > validConfigurationOptions.possiblePriorityLevelRange[1] { logger.Warnf("Supplied notification priority %d is too high. Clamping to %d.", notificationToSend.priority, validConfigurationOptions.possiblePriorityLevelRange[1]) notificationToSend.priority = validConfigurationOptions.possiblePriorityLevelRange[1] } + // Warn the user if both TTL is set and message priority is set to 2. + if notificationToSend.timeToLive > validConfigurationOptions.minimumTimeToLive && notificationToSend.priority == validConfigurationOptions.possiblePriorityLevelRange[1] { + logger.Warnf("Message is set to maximum priority (%d). Provided TTL of %d second(s) will be ignored by the API.", notificationToSend.priority, notificationToSend.timeToLive) + } + + // Sending URL titles without URL is not meaningful. Inform user. + if len(notificationToSend.url) == 0 && len(notificationToSend.urlTitle) > 0 { + logger.Warnf("URL Title (-url_title) provided without an URL. URL Title will be ignored and will not be send.") + } + + // TTL must be positive, and if it's negative, change it to 0, making message non-expiring. + if notificationToSend.timeToLive < 0 { + logger.Warnf("Message TTL cannot be negative. Removing TTL from the notification.") + notificationToSend.timeToLive = 0 + } + // Check whether we have the API & User keys. // Let's do some magic: apiKeyPresent := len(runtimeConfiguration.apiKey) != 0 // This evaluates to bool, heh. :) @@ -348,8 +372,22 @@ func printState(runtimeConfiguration *RuntimeConfiguration, notification *Notifi logger.Infof("--------------------") logger.Infof("Recipients: %s", notification.device) logger.Infof("Priority: %d", notification.priority) - logger.Infof("Sound: %s", notification.sound) + + // If we don't override the sound, let it be known. + if len(notification.sound) == 0 { + logger.Infof("Sound: Recipient's default tone") + } else { + logger.Infof("Sound: %s", notification.sound) + } logger.Infof("Timestamp: %d", notification.timestamp) + + // We default TTL to 0, and don't send a TTL to API if it's 0. + if notification.timeToLive == 0 { + logger.Infof("Message TTL: Infinite") + } else { + logger.Infof("Message TTL: %d second(s)", notification.timeToLive) + } + logger.Infof("HTML message: %d", notification.html) logger.Infof("URL title: %s", notification.urlTitle) logger.Infof("URL: %s", notification.url) @@ -363,18 +401,41 @@ func printState(runtimeConfiguration *RuntimeConfiguration, notification *Notifi // This function sends the actual notification via PUSH request, via Pushover. // TODO: Add logging directives into this function. func sendNotification(notificationToSend *Notification, runtimeConfiguration *RuntimeConfiguration, logger *zap.SugaredLogger) (body []byte) { - response, err := http.PostForm("https://api.pushover.net/1/messages.json", - url.Values{ - "token": {runtimeConfiguration.apiKey}, - "user": {runtimeConfiguration.userKey}, - "device": {notificationToSend.device}, - "sound": {notificationToSend.sound}, - "priority": {fmt.Sprint(notificationToSend.priority)}, - "url": {notificationToSend.url}, - "url_title": {notificationToSend.urlTitle}, - "title": {notificationToSend.title}, - "message": {notificationToSend.messageBody}, - "html": {fmt.Sprint(notificationToSend.html)}}) + + valuesToSend := url.Values{} // We will build our URL object and send it to the API. + + // First the mandatory ones. + valuesToSend.Add("token", runtimeConfiguration.apiKey) + valuesToSend.Add("user", runtimeConfiguration.userKey) + valuesToSend.Add("message", notificationToSend.messageBody) + + // These are declared by optipnal by API, but we always send them. + valuesToSend.Add("device", notificationToSend.device) + valuesToSend.Add("priority", fmt.Sprint(notificationToSend.priority)) + valuesToSend.Add("title", notificationToSend.title) + valuesToSend.Add("html", fmt.Sprint(notificationToSend.html)) + + // These are conditional variables, and we send them if they are set by the user. + if len(notificationToSend.sound) > 0 { + valuesToSend.Add("sound", notificationToSend.sound) + } + + // Do not send a time to live value if we don't need to. + if notificationToSend.timeToLive > 0 { + valuesToSend.Add("ttl", fmt.Sprint(notificationToSend.timeToLive)) + } + + // Do not send a URL if there's none specified. + if len(notificationToSend.url) > 0 { + valuesToSend.Add("url", notificationToSend.url) + } + + // Only send a URL title, if there's an URL to attach it. + if len(notificationToSend.url) > 0 && len(notificationToSend.urlTitle) > 0 { + valuesToSend.Add("url_title", notificationToSend.urlTitle) + } + + response, err := http.PostForm("https://api.pushover.net/1/messages.json", valuesToSend) if err != nil { logger.Panicf("Something went wrong, exiting (error is %s).", err) @@ -502,7 +563,8 @@ func main() { // Let's send the notification: if runtimeConfiguration.dryrun == false { - sendNotification(¬ificationToSend, &runtimeConfiguration, sugaredLogger) + result := sendNotification(¬ificationToSend, &runtimeConfiguration, sugaredLogger) + sugaredLogger.Debugf("Message sending result is %s.", result) } else { sugaredLogger.Warnf("Not sending the notification since in dry run mode.") } -- 2.45.2