~bayindirh/nudge

0d4cc3eccf860068a1aa7c0ef447a4bbff0b799e — Hakan Bayindir 3 months ago c6cd0e8 v0.4.0
proj: Release v0.4.0

- proj: Release version 0.4.0
- proj: Bump version to 0.4.0.
- refactor: Pass source code through gofmt.
- fix: Finish implementing retry and expire fields for priority 2 messages.
- feat: Expose retry and expire fields to command line arguments.
- docs: Reformat all markdown files for compactness.
- docs: Update README.md, CHANGELOG.md and RELEASE_NOTES.md files.
3 files changed, 59 insertions(+), 61 deletions(-)

M CHANGELOG.md
M README.md
M src/nudge.go
M CHANGELOG.md => CHANGELOG.md +13 -36
@@ 2,8 2,20 @@

**Note:** Please add newest entries on top. Use ISO date format YYYY-MM-DD and markdown formatting.

## 2023-10-19
## 2023-10-24
- proj: Release version `0.4.0`
- proj: Bump version to `0.4.0`.
- refactor: Pass source code through `gofmt`.
- fix: Finish implementing `retry` and `expire` fields for priority 2 messages.
- feat: Expose `retry` and `expire` fields to command line arguments.
- docs: Reformat all markdown files for compactness.
- docs: Update `README.md`, `CHANGELOG.md` and `RELEASE_NOTES.md` files.

## 2023-10-20
- proj: Bump the version to `0.4.0a3`.
- fix: Implement `retry` and `expire` properties required for priority 2 notifications.

## 2023-10-19
- 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.


@@ 18,7 30,6 @@
- refactor: Update a log entry formatting in `applyDefaultConfiguration`, about hostname.

## 2023-10-16

- proj: Release version 0.3.0.
- proj: Bumped version to 0.3.0.
- fix: Change a mistyped `Debug` to `Debugf`.


@@ 30,13 41,11 @@
- refactor: Move logger initialization to the beginning of the process.

## 2023-10-15

- proj: Bump version to `0.3.0a1` to mark development start.
- feat: Add a small `debug` log line in `printState` function to show how many log targets are defined.
- refactor: Pass through `gofmt`.

## 2023-07-31

- proj: Version is changed to 0.2.0
- proj: Version 0.2.0 is released.
- feat: Log levels can be configured with command line now.


@@ 46,55 55,45 @@
- refactor: Rewrite and refine some comments to make them more meaningful.

## 2023-07-30

- refactor: Remove logging directives from `initializeValidConfigurationOptions()`, because it needs to run before logger initialization.
- refactor: Remove logging directives from `applyDefaultConfiguration()`, because it needs to run before logger initialization.
- refactor: Promote "No message body specified, exiting." message to `warning` level.

## 2023-07-29

- refactor: Demote "No message body specified, exiting." message to `debug` level.

## 2023-07-26

- fix: Correct a small debug log line to print prettier.
- refactor: Correct `applyFlags` function's signature.
- fix: Correct `fmt` strings to prevent panics and bad output.

## 2023-07-25

- refactor: Rewrite `applyFlags` function to set only provided flags.
- fix: Use `flag.Visit()` to get a list of set flags.

## 2023-07-15

- refactor: Remove verbosity settings related plumbing from code. Will use logging level only.
- fix: Store settings obtained via `initFlags` in the correct data structure (`FlagStorage` instead of `NotificationToSend`).
- refactor: Use `flag.Lookup()` instead of manually checking everything, which was not very reliable.
- feat: `applyFlags` function is implemented completely.

## 2023-07-08

- feat: Implement configuration file override flag, and make it operational.
- feat: Implement application of flags on top of parsed config file (was broken due to addition of configuration file override).

## 2023-07-04

- research: Make preparations to implement config file override option in `readAndApplyConfiguration` function.

## 2023-07-03

- refactor: Store passed flags in `FlagStorage` structure to enable more flexible program startup.
- refactor: Remove `versionRequested` variable from `RuntimeConfiguration` structure since `FlagStorage` handles this.
- refactor: Add `configFilePath` variable to `FlagStorage` since we parse the flags into it now.

## 2023-06-16

- refactor: Tidy some comments in `RuntimeConfiguration` data structure.
- feat: Start implementing `FlagStorage` struct, which will store flag values temporarily, to allow a more flexible initalizaion procedure (e.g.: configuration file path overrides) without changing initialization order.

## 2023-06-09

- refactor: Change logging level field name in configuration file.
- refactor: Update `nudge.conf.example` file to reflect the changes.
- feat: Implement logging level support in configuration file.


@@ 103,29 102,24 @@
- proj: Add `*.kate-swp` files to `.gitignore` file.

## 2023-05-15

- doc: `README.md` is updated.
- fix: A date in `CHANGELOG.md` is corrected.
- fix: Remove incomplete code preventing nudge to compile and run correctly.

## 2023-05-13

- refactor: Redesign calling convention for Nudge. New format is `nudge [OPTIONS] message`.
- feat: Added application name sanitization for error messages.

## 2023-04-22

- fix: Fix dates in `README.md` file.

## 2023-04-20

- refactor: Rewrite the description of `-dryrun` flag.
- refactor: Move the logic about printing help to `main()`. Config warnings will be shown only if the user tries to send a message.
- refactor: Increase error levels for missing keys to fatal, because they are mandatory.
- refactor: Move `printState()` function's output to logging subsystem.

## 2023-04-09

- refactor: `$CURRENT_WORKING_DIRECTORY/conf` folder added to configuration search path to support local, isolated installations.
- refactor: Configuration sanity checking is collected under `checkConfigurationSanity()` function. So, all warnings are issued from a single place.
- feat: Pass `html` field to Pushover API.


@@ 135,7 129,6 @@
- refactor: Disable `-config_path` flag temporarily.

## 2023-04-08

- refactor: Remove `setUnhandledFeatures` because it's not used anymore.
- refactor: Application defaults are stored in `applyDefaultConfiguration` function, and applied from there during startup.
- fix: `readAndApplyConfiguration` function shows errors correctly.


@@ 143,17 136,14 @@
- refactor: `readAndApplyConfiguration` function handles missing options more gracefully.

## 2023-04-06

- refactor: Convert panic statements to logged panics in `readAndApplyConfiguration()` function.
- refactor: Change `logging_path` to `logging_paths` directive in `conf/nudge.example.conf`, since we'll get slices and directly pass it to Zap logger.

## 2023-04-05

- refactor: Start completely overhaul startup process.
- refactor: Start refactoring `readAndApplyConfiguration()` function.

## 2023-04-03

- refactor: Moving to [Zap](https://github.com/uber-go/zap) logging library throughout the code.
- refactor: Remove the self-developed placeholder logging subsystem.
- refactor: Remove configuration file default from flags.


@@ 163,20 153,17 @@
- feat: Continue to implement Zap based logging inside the codebase.

## 2023-04-02

- refactor: Remove `getKeys()` method, since it's not replaced with Viper subsystem.
- feat: Implement [Viper](https://www.github.com/spf13/viper) based, TOML configuration file management.
- feat: Add default locations for configuration file.
- doc: Update README.md file.

## 2023-03-25

- proj: Starting work on `v0.1.0`.
- refactor: Move to [Viper](https://www.github.com/spf13/viper) package for configuration handling.
- proj: Add module file, migrate to module based dependency management.

## 2022-03-23

- Initial reveal version.
- docs: `README.md` is updated.
- refactor: Rename `recipient.conf.example` to `userkey.conf.example`. Makes more sense.


@@ 185,7 172,6 @@
- style: Code is formatted for readability.

## 2023-03-22

- proj: Add actual `.conf` files to `.gitignore`, so no secrets leak during development.
- feat: Add a function which prints current state of the program to terminal.
- feat: Check for errors during hostname gathering.


@@ 200,44 186,35 @@
- refactor: Do some code formatting.

## 2023-03-14

- feat: Start implementing a simple file reader for API key. Currently incomplete.

## 2023-03-10

- refactor: Move `initLoggers()` to `Loggers` struct as a method.
- refactor: Move `logger`s and `buffer`s to an array to make coding easier.
- refactor: Add a space after log level specifier, for easier reading and filtering.
- feat: Add `log(logLevel, message)` method to struct to enable logging to console.

## 2023-03-08

- doc: Add project logo.

## 2023-03-07

- feat: Started implementing logging subsystem.

## 2023-03-01

- refactor: Change and simplify usage/help messages.

## 2023-02-28

- feat: Finish implementing first version of command line parsing.

## 2023-02-26

- proj: Add `RuntimeConfiguration` struct.
- proj: Continue adding more flags and mapping to data structures in the code.

## 2023-02-19

- proj: Create devel branch.
- proj: Create `.gitignore` file.

## 2023-02-18

- proj: Fix conventional commits writing style in `CHANGELOG.md`.
- devel: Implement message structure.
- devel: Implement flag parsing.

M README.md => README.md +7 -12
@@ 1,29 1,27 @@
<img src="https://git.sr.ht/~bayindirh/nudge/blob/master/doc/assets/nudge_logo.png" alt="nudge logo" height="115">

# nudge - A Pushover CLI
**Current state:** *Beta*. Can be used reliably to send messages daily, but things may still change, esp. command line arguments. Most of the Pushover 4.0 features are supported.

**Current state:** *Almost beta*. Can be used reliably to send messages daily, but things may still change, esp. command line arguments.

**Current version:** 0.3.0
**Current version:** 0.4.0

nudge provides a small command line tool to send push notifications over 
[Pushover](https://pushover.net). It aims to provide a simple and composable
tool to send push notifications.

## What's New

- Since Zap doesn't allow log sink reconfiguration during runtime, all logging related configuration options are removed from the configuration file. Please use redirections to save your logs to files. Normal logs go to `stdout`, errors go to `stderr`.
- Log levels passed via command line are respected, but not applied immediately. If you're debugging for development, please change default options inside the code (see `applyDefaultConfiguration` function).
- You can override the location of your configuration file via command line. See `-help`. Doing this disables auto detection. Nudge will exit with an error if the file cannot be found or read.
- New API call generator function.
- Implements expiring messages feature.
- Fixed some bugs preventing sending priority 2 messages.
- Better configuration sanity checking and self-healing.
- Exposing new settings `retry_interval` and `expire` for priority 2 messages.

For older changes, please refer to [changelog](https://git.sr.ht/~bayindirh/nudge/tree/master/item/RELEASE_NOTES.md).

## Building

After cloning the repository, going to `src/` folder and running `go build nudge.go` should do.

## Configuration

You need two keys for Nudge to function. An API key and a User key.

- API keys are obtained by [registering your application](https://pushover.net/apps/build).


@@ 44,7 42,6 @@ The first found file takes precedence over the rest, even if they're present. Un
You can use `src/conf/nudge.conf.example` file as a template. File format is [TOML](https://toml.io).

## Running

Running nudge is simple. Considering your files are in the correct places (see `nudge -help` for defaults), it's simply:

```


@@ 79,11 76,9 @@ Current options are as follows:
```

## Known Issues

- Not all features provided by Pushover is implemented (image attachments, HTML formatting, and possibly others)

## Other Details

nudge is written in Go and licensed with GNU/GPLv3 or later. The project
uses [Semantic Versioning](https://semver.org) and
[Conventional Commits](https://www.conventionalcommits.org).

M src/nudge.go => src/nudge.go +39 -13
@@ 40,8 40,10 @@ type Notification struct {
	messageBody     string
	imageAttachment string // Will be sent empty for now.
	device          string // Contains a comma delimited list of devices which will receive the message.
	expireDuration  int    // Defines the duration when to give up in seconds when notification is sent with priority 2.
	html            int    // Set to 1 for enabling HTML parsing, will be 0 for now.
	priority        int    // Can be set from -2 to 2. 0 is default. Higher numbers are more urgent. Negative ones are informational levels.
	retryInterval   int    // Sets time between notification retries in seconds when the notification is sent with priority 2.
	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.


@@ 66,13 68,15 @@ 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.
	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.
	device         string // Contains a comma delimited list of devices which will receive the message.
	expireDuration int    // Defines the duration when to give up in seconds when notification is sent with priority 2.
	priority       int    // Can be set from -2 to 2. 0 is default. Higher numbers are more urgent. Negative ones are informational levels.
	retryInterval  int    // Sets time between notification retries in seconds when the notification is sent with priority 2.
	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.


@@ 86,9 90,11 @@ 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
	possiblePriorityLevelRange []int
	minimumTimeToLive          int
	possibleLogLevels           []string // Stores a list of valid log levels which we validate against.
	possiblePriorityLevelRange  []int    // Contains two integers. Minimum and maximum value. All values in that range is valid.
	possibleExpireDurationRange []int    // Contains the minimum and maximum duration for message expiration when the message is sent with priority 2.
	minimumRetryInterval        int      // This is a minimum interval for retries. We can't go below this number but there's no ceiling.
	minimumTimeToLive           int      // This is the minimum allowed time to live for messages.
}

// This function initializes the ValidConfigurationOptions data structure, which will be


@@ 97,7 103,9 @@ 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.
	ValidConfigurationOptions.possibleExpireDurationRange = []int{0, 10800} // This 3 hour limit is imposed by the API.
	ValidConfigurationOptions.minimumRetryInterval = 30                     // Again, this 30 second lower bound is imposed by the API.
	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.


@@ 120,11 128,15 @@ func applyDefaultConfiguration(runtimeConfiguration *RuntimeConfiguration, notif
	notificationToSend.sound = ""
	notificationToSend.title = notificationTitle

	// These are divided to this separate block, because they only apply to priority 2 notifications.
	notificationToSend.retryInterval = 30    // This is the minimum value allowed by the API.
	notificationToSend.expireDuration = 1800 // Half an hour is a sane default, in my opinion.

	// 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.0a2"
	runtimeConfiguration.version = "0.4.0"
	runtimeConfiguration.dryrun = false
	runtimeConfiguration.logLevel = "warn"
}


@@ 203,7 215,9 @@ func initFlags(notificationToSend *Notification, runtimeConfiguration *RuntimeCo
	// In this initial version, we only handle some of the features. Rest will be handled later.
	// Values are sent to flagStorage, because we'll apply them after parsing the config file.
	flag.StringVar(&flagStorage.device, "devices", notificationToSend.device, "List of devices to be notified. Separate multiple devices with ','.")
	flag.IntVar(&flagStorage.expireDuration, "expire", notificationToSend.expireDuration, "Time before giving-up re-sending notifications (for priority 2 only).")
	flag.IntVar(&flagStorage.priority, "priority", notificationToSend.priority, "Adjust notification priority. Between -2 and 2. (default "+strconv.Itoa(notificationToSend.priority)+")")
	flag.IntVar(&flagStorage.retryInterval, "retry_interval", notificationToSend.retryInterval, "Set the time between notification re-sends (for priortity 2 only).")
	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.")


@@ 329,7 343,7 @@ func checkConfigurationSanity(notificationToSend *Notification, runtimeConfigura
	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.")


@@ 373,6 387,13 @@ func printState(runtimeConfiguration *RuntimeConfiguration, notification *Notifi
	logger.Infof("Recipients: %s", notification.device)
	logger.Infof("Priority: %d", notification.priority)

	// Write the following only when notification priority is 2, because it's ignored
	// otherwise.
	if notification.priority == 2 {
		logger.Infof("Notification retry interval: %d seconds.", notification.retryInterval)
		logger.Infof("Notification expiry: %d second(s).", notification.expireDuration)
	}

	// If we don't override the sound, let it be known.
	if len(notification.sound) == 0 {
		logger.Infof("Sound: Recipient's default tone")


@@ 415,6 436,11 @@ func sendNotification(notificationToSend *Notification, runtimeConfiguration *Ru
	valuesToSend.Add("title", notificationToSend.title)
	valuesToSend.Add("html", fmt.Sprint(notificationToSend.html))

	// These are mandatory if priority is 2.
	if notificationToSend.priority == 2 {
		valuesToSend.Add("retry", fmt.Sprint(notificationToSend.retryInterval))
		valuesToSend.Add("expire", fmt.Sprint(notificationToSend.expireDuration))
	}
	// 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)