~bayindirh/nudge

f955616cc4a0f0819ed78dba0ed79a825e476f81 — Hakan Bayindir 1 year, 2 months ago 03cee26 + 4779def
Merge remote-tracking branch 'origin/devel'

# Conflicts:
#	CHANGELOG.md
7 files changed, 429 insertions(+), 140 deletions(-)

M .gitignore
M CHANGELOG.md
M README.md
M go.mod
M go.sum
M src/conf/nudge.conf.example
M src/nudge.go
M .gitignore => .gitignore +1 -0
@@ 1,3 1,4 @@
.DS_Store
*.kate-swp
/src/conf/nudge.conf
/src/conf/userkey.conf

M CHANGELOG.md => CHANGELOG.md +99 -6
@@ 2,7 2,96 @@

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

## 2023-04-09
## 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.
- refactor: Change `warning` to `warn` since it's how Zap uses it.
- refactor: Remove `getZapAtomicLevel()` function since Zap provides a better one already.
- refactor: Change place of `checkConfigurationSanity()` function to correct place. After applying everything, yet before doing anything.
- 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.
- feat: Add logging level to default configuration.
- feat: Check whether provided logging level is correct.
- 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.


@@ 25,7 114,7 @@
- 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
## 2023-04-05

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


@@ 47,7 136,7 @@
- feat: Add default locations for configuration file.
- doc: Update README.md file.

## 2023-03-25
## 2023-03-25

- proj: Starting work on `v0.1.0`.
- refactor: Move to [Viper](https://www.github.com/spf13/viper) package for configuration handling.


@@ 88,15 177,19 @@
- 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-07
## 2023-03-08

- doc: Add project logo.

## 2023-03-07

- feat: Started implementing logging subsystem.

## 2023-03-01
## 2023-03-01

- refactor: Change and simplify usage/help messages.

## 2023-02-28
## 2023-02-28

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


M README.md => README.md +18 -20
@@ 36,10 36,10 @@ You can use `src/conf/nudge.conf.example` file as a template. File format is [TO

## Running

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

```
nudge -message "Hello, world!"
nudge "Hello, world!"
```

Message titles are set to sender's `hostname` by default, but can be changed. Message will be sent to all devices registered to that user, with default sound and priority. These can be changed, too.


@@ 47,24 47,22 @@ Message titles are set to sender's `hostname` by default, but can be changed. Me
Current options are as follows:

```
  -devices string
        List of devices to be notified. Separate multiple devices with ','. (default "all")
  -dryrun
        Do not send the message, but pretend to do, for testing.
  -message string
        Message to send in the notification.
  -priority int
        Adjust notification priority. Between -2 and 2. (default 0)
  -sound string
        Set notification sound. (default "pushover")
  -title string
        Notification title. Hostname is used if omitted. (default "temple")
  -url string
        An optional URL to attach to the notification.
  -url_title string
        An optional title for URL to send.
  -version
        Print version and exit.
-devices string
  	List of devices to be notified. Separate multiple devices with ','. (default "all")
-dryrun
  	Simulate sending a notification.
-priority int
  	Adjust notification priority. Between -2 and 2. (default 0)
-sound string
  	Set notification sound. (default "pushover")
-title string
  	Notification title. Hostname is used if omitted. (default "Hakans-MacBook-Pro.local")
-url string
  	An optional URL to attach to the notification.
-url_title string
  	An optional title for URL to send.
-version
  	Print version and exit.
```

## Known Issues

M go.mod => go.mod +5 -2
@@ 1,8 1,11 @@
module git.sr.ht/~bayindirh/nudge

go 1.20
go 1.19

require github.com/spf13/viper v1.15.0
require (
	github.com/spf13/viper v1.15.0
	golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
)

require (
	go.uber.org/atomic v1.9.0 // indirect

M go.sum => go.sum +2 -0
@@ 205,6 205,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=

M src/conf/nudge.conf.example => src/conf/nudge.conf.example +3 -2
@@ 8,7 8,8 @@ api_key  = '!-CHANGE-WITH-API-KEY-CAFEBABE'
user_key = '!CHANGE-WITH-USER-KEY-CAFED00D'

[logging]
# You can set your default logging level and logfile path there.
default_level = 'INFO'
# For more details using logging_paths, see https://pkg.go.dev/go.uber.org/zap#example-package-BasicConfiguration
logging_paths  = ['stdout', '/var/log/nudge.log']
# A default, fairly verbose log level to see what the code is doing.
# NOTE: Log levels are case sensitive.
log_level = 'info'

M src/nudge.go => src/nudge.go +301 -110
@@ 26,6 26,9 @@ import (
	"net/url"
	"os"
	"strconv"
	"strings"

	"golang.org/x/exp/slices"

	"github.com/spf13/viper"
	"go.uber.org/zap"


@@ 36,37 39,72 @@ import (
type Notification struct {
	messageBody     string
	imageAttachment string // Will be sent empty for now.
	device          string
	device          string // Contains a comma delimited list of devices which will receive the message.
	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.
	sound           string // See the API docs for valid names.
	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.
	title           string
	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.
}

// This struct contains the configuration information we are going to use during execution.
type RuntimeConfiguration struct {
	version          string   // Stores the version of the application.
	versionRequested bool     // Set to true if version is requested.
	dryrun           bool     // Set to true if we're simulating, but not sending messages.
	configFilePath   string   // This variable stores our configuration file's path.
	apiKey           string   // This is the secret key we use for sending messages.
	userKey          string   // This is the secret key of the user, used as a recipient address.
	logfilePaths     []string // This variable contains the absolute path for the logfile.
	verbosity        int      // This variable stores the requested log verbosity. Loggers will be setup according to this.
	applicationName string   // Stores the sanitized version of the application name.
	version         string   // Stores the version of the application.
	dryrun          bool     // Setting to true will make code switch to "simulation" mode.
	configFilePath  string   // This variable stores our configuration file's path.
	apiKey          string   // Secret key we use for sending messages.
	userKey         string   // Secret key of the user, used as a recipient address.
	logfilePaths    []string // Contains the absolute path for the logfile.
	logLevel        string   // Contains the logging level the application starts with.
}

// Following struct is for temporarily storing flag values passed during program invocation.
// Storing these here allows us neat tricks like configuration file location overrides
// 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.

	// Configuration file related flags are stored below:
	configFilePath string // Store the configuration file override here, if present.

	// Next block contains options about program execution and behavior.
	versionRequested bool   // Setting to true will make nudge to write version and exit.
	dryrun           bool   // Setting to true will make code switch to "simulation" mode.
	logLevel         string // Contains the logging level the application starts with.
}

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

// This function initializes the ValidConfigurationOptions data structure, which will be
// used by the configuration sanity checker down the road, to make sure that everything
// is nice and shiny.
func initializeValidConfigurationOptions(ValidConfigurationOptions *ValidConfigurationOptions) {
	ValidConfigurationOptions.possibleLogLevels = []string{"debug", "info", "warn", "error", "fatal", "panic"}
}

// This function stores and applies the defaults of the application.
// It's called first to initialize the defaults, then config file and the lastly the flags
// Can change these defaults.
func applyDefaultConfiguration(runtimeConfiguration *RuntimeConfiguration, notificationToSend *Notification, logger *zap.SugaredLogger) {
func applyDefaultConfiguration(runtimeConfiguration *RuntimeConfiguration, notificationToSend *Notification) {
	// Get the notificationTitle first. We will use it for message titles, as default
	notificationTitle, err := os.Hostname()

	if err != nil {
		logger.Warnf("Encountered an error while obtaining hostname, will use \"Nudge\" instead.")
		// Since logger is not up when we call this function, we'll use Println here.
		fmt.Println("Encountered an error while obtaining hostname, will use \"Nudge\" instead.")
		notificationTitle = "Nudge"
	}



@@ 78,43 116,53 @@ func applyDefaultConfiguration(runtimeConfiguration *RuntimeConfiguration, notif
	notificationToSend.title = notificationTitle

	// Let's set defaults for the application itself, again where it makes sense.
	runtimeConfiguration.version = "0.1.0"
	applicationName := strings.Split(os.Args[0], "/")
	runtimeConfiguration.applicationName = applicationName[len(applicationName)-1]
	runtimeConfiguration.version = "0.2.0"
	runtimeConfiguration.dryrun = false
	runtimeConfiguration.logfilePaths = []string{"stdout"}
	runtimeConfiguration.verbosity = 0
	runtimeConfiguration.logLevel = "warn"
}

// This function reads the configuration file and applies it to relevant data structures.
// It's not designed as a method, since it might touch multiple data structures at once.
func readAndApplyConfiguration(configurationFilePath *string, runtimeConfiguration *RuntimeConfiguration, notificationToSend *Notification, logger *zap.SugaredLogger) {
	// Let's define the nature of our configuration file.
	viper.SetConfigName("nudge.conf")
	viper.SetConfigType("toml")
func readAndApplyConfiguration(configurationFilePath *string, runtimeConfiguration *RuntimeConfiguration, notificationToSend *Notification, flagStorage *FlagStorage, logger *zap.SugaredLogger) {
	if flagStorage.configFilePath != "" {
		logger.Debugf("Configuration file is set to %s, via command line.", flagStorage.configFilePath)
		viper.SetConfigFile(flagStorage.configFilePath)
		viper.SetConfigType("toml") // Because we use ".conf" extension, not ".toml".

	// If we have overriden the config path, add it. If not, add the defaults.
	// We need user's home directory to add some neat magic.
	userConfigDirectory, err := os.UserConfigDir()
	} else {
		// If no configuration file override is given:
		// Let's define the nature of our configuration file.
		viper.SetConfigName("nudge.conf")
		viper.SetConfigType("toml")

	if err != nil {
		logger.Panicf("Error getting user's config directory, exiting (error is %s).", err.Error())
	}
		// We need user's home directory to add some neat magic.
		userConfigDirectory, err := os.UserConfigDir()

	workingDirectory, err := os.Getwd()
	logger.Debugf("Current directory is %s", workingDirectory)
		if err != nil {
			logger.Panicf("Error getting user's config directory, exiting (error is %s).", err.Error())
		}

	if err != nil {
		logger.Panicf("Error getting working directory, exiting (error is %s).", err.Error())
	}
		workingDirectory, err := os.Getwd()
		logger.Debugf("Current directory is %s", workingDirectory)

		if err != nil {
			logger.Panicf("Error getting working directory, exiting (error is %s).", err.Error())
		}

	// The logic is to allow user to override local config with a test config.
	// And to override system-wide config with a local config.
	viper.AddConfigPath(".")                        // User might create a test-config at the current directory.
	viper.AddConfigPath(userConfigDirectory)        // User might create a user-wide config.
	viper.AddConfigPath("/etc")                     // A system-wide config can be present.
	viper.AddConfigPath(workingDirectory + "/conf") // The user may have installed the application in a portable way.
	viper.AddConfigPath(workingDirectory)           // This is a last resort and test directory.
		// The logic is to allow user to override local config with a test config.
		// And to override system-wide config with a local config.
		viper.AddConfigPath(".")                        // User might create a test-config at the current directory.
		viper.AddConfigPath(userConfigDirectory)        // User might create a user-wide config.
		viper.AddConfigPath("/etc")                     // A system-wide config can be present.
		viper.AddConfigPath(workingDirectory + "/conf") // The user may have installed the application in a portable way.
		viper.AddConfigPath(workingDirectory)           // This is a last resort and test directory.
	}

	err = viper.ReadInConfig() // Find and read the config file
	// Since this is common to both methods, this can live here.
	err := viper.ReadInConfig() // Find and read the config file

	// Handle errors reading the config file.
	// Do not create warnings here, all config warnings are handled by checkConfigurationSanity() function.


@@ 125,60 173,143 @@ func readAndApplyConfiguration(configurationFilePath *string, runtimeConfigurati
		// If we've reached here, we can take note of the file path now.
		runtimeConfiguration.configFilePath = viper.ConfigFileUsed()

		// Let the user know where the config file is.
		logger.Debugf("Configuration file is found at %s.", runtimeConfiguration.configFilePath)

		// Check options one by one whether they're set and apply present options.
		// We do not panic if anything mandatory is missing, but we'll warn the user politely.
		// Below is the pushover section.
		if viper.IsSet("pushover.api_key") {
			runtimeConfiguration.apiKey = viper.GetString("pushover.api_key")
			logger.Debugf("Pushover API key found & set to %s.", runtimeConfiguration.apiKey)
		}

		if viper.IsSet("pushover.user_key") {
			runtimeConfiguration.userKey = viper.GetString("pushover.user_key")
			logger.Debugf("Pushover User key is found & set to %s.", runtimeConfiguration.userKey)
		}

		// Options related to logging section handled next.
		// Logging related configuration follows:
		// Logging paths:
		if viper.IsSet("logging.logging_paths") {
			runtimeConfiguration.logfilePaths = viper.GetStringSlice("logging.logging_paths")
			logger.Debugf("Logging paths are found & set to %s.", runtimeConfiguration.logfilePaths)
		}

		// Logging related configuration follows:
		// TODO: Get logging level and process the data.
		// Logging level:
		if viper.IsSet("logging.log_level") {
			runtimeConfiguration.logLevel = viper.GetString("logging.log_level")
			logger.Debugf("Logging level is found and set to %s.", runtimeConfiguration.logLevel)
		}
	}
}

// This functions defines the flags, binds them and parses them.
// Note: Code defaults are also baked into this function as a start, they will be moved out later.
// Cue Lord of the Rings from Blind Guardian.
func initFlags(notificationToSend *Notification, runtimeConfiguration *RuntimeConfiguration) {
func initFlags(notificationToSend *Notification, flagStorage *FlagStorage, logger *zap.SugaredLogger) {
	// In this initial version, we only handle some of the features. Rest will be handled later.
	flag.StringVar(&notificationToSend.messageBody, "message", notificationToSend.messageBody, "Message to send in the notification.")
	flag.StringVar(&notificationToSend.device, "devices", notificationToSend.device, "List of devices to be notified. Separate multiple devices with ','.")
	flag.IntVar(&notificationToSend.priority, "priority", notificationToSend.priority, "Adjust notification priority. Between -2 and 2. (default "+strconv.Itoa(notificationToSend.priority)+")")
	flag.StringVar(&notificationToSend.sound, "sound", notificationToSend.sound, "Set notification sound.")
	flag.StringVar(&notificationToSend.title, "title", notificationToSend.title, "Notification title. Hostname is used if omitted.")
	flag.StringVar(&notificationToSend.url, "url", notificationToSend.url, "An optional URL to attach to the notification.")
	flag.StringVar(&notificationToSend.urlTitle, "url_title", notificationToSend.urlTitle, "An optional title for URL to send.")
	flag.BoolVar(&runtimeConfiguration.dryrun, "dryrun", runtimeConfiguration.dryrun, "Do not send the message, but pretend to do, for testing.")

	// Following flags are for controlling the output & verbosity.
	// TODO: Will be implemented in version 0.2
	// flag.IntVar(&runtimeConfiguration.verbosity, "verbosity", 1, "Set verbosity level. Between -1 and 5.")
	// flag.StringVar(&runtimeConfiguration.logfilePath, "logfile_path", "", "Define the path of the log file.")
	// 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.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.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.")

	// Below this point we handle the runtime configuration of Nudge.
	flag.BoolVar(&flagStorage.dryrun, "dryrun", flagStorage.dryrun, "Simulate sending a notification.")

	// Get logging level and save it.
	flag.StringVar(&flagStorage.logLevel, "log_level", flagStorage.logLevel, "Change the logging level.")

	// Following flag is for controlling the place of the configuration file.
	// FIXME: This feature is not working at the moment, so it's disabled.
	// flag.StringVar(&runtimeConfiguration.configFilePath, "config_path", runtimeConfiguration.configFilePath, "Define or override configuration file path.")
	flag.StringVar(&flagStorage.configFilePath, "config_path", flagStorage.configFilePath, "Define or override configuration file path.")

	// Always implement a version string.
	flag.BoolVar(&runtimeConfiguration.versionRequested, "version", false, "Print version and exit.")
	flag.BoolVar(&flagStorage.versionRequested, "version", false, "Print version and exit.")

	flag.Parse()

	// We accept a single argument as the message. If we have the message, we set it.
	// Otherwise, we leave the message empty. The check in the min() will
	// handle the error condition.
	if flag.NArg() == 1 {
		notificationToSend.messageBody = flag.Arg(0)
	}

	// Print how many arguments we got, as a debug message. flag.Narg() returns the number
	// of remaining arguments after parsing everything.
	logger.Debugf("Received %d arguments, after parsing flags.", flag.NArg())
}

// This function simply copies relevant flags to runtime configuration and notification to send.
func applyFlags(setFlags *[]string, flagStorage *FlagStorage, runtimeConfiguration *RuntimeConfiguration, notificationToSend *Notification, logger *zap.SugaredLogger) {
	// TODO: Revise this function and add proper comments.
	logger.Debugf("Starting to apply flags to runtime configuration.")
	// This is left as a check, to see whether we can approach flag module globally.
	logger.Debugf("Flag parse status is %t.", flag.Parsed())

	logger.Debugf("The set flags are %s.", *setFlags)

	for _, flagValue := range *setFlags {
		logger.Debugf("Working on flag %s.", flagValue)

		switch flagValue {
		case "devices":
			notificationToSend.device = flagStorage.device
			logger.Debugf("Recipient devices are set to %s.", notificationToSend.device)
		case "priority":
			notificationToSend.priority = flagStorage.priority
			logger.Debugf("Notification priority is changed to %d.", notificationToSend.priority)
		case "sound":
			notificationToSend.sound = flagStorage.sound
			logger.Debugf("Notification sound is changed to %s.", notificationToSend.sound)
		case "title":
			notificationToSend.title = flagStorage.title
			logger.Debugf("Notification title is changed to %s.", notificationToSend.title)
		case "url":
			notificationToSend.url = flagStorage.url
			logger.Debugf("Notification attached URL is set to %s.", notificationToSend.url)
		case "url_title":
			notificationToSend.urlTitle = flagStorage.urlTitle
			logger.Debugf("Notification attached URL will be shown as %s.", notificationToSend.urlTitle)
		case "dryrun":
			runtimeConfiguration.dryrun = flagStorage.dryrun
			logger.Debugf("Dry run mode is set to %t.", runtimeConfiguration.dryrun)
		case "log_level":
			runtimeConfiguration.logLevel = flagStorage.logLevel
			logger.Debugf("Logging level is set to %s.", runtimeConfiguration.logLevel)
		case "config_path":
			logger.Debugf("Ignoring flag %s. Handled elsewhere.", flagValue)
		case "version":
			logger.Debugf("Ignoring flag %s. Handled elsewhere.", flagValue)
		default:
			logger.Warnf("Unhandled flag %s. Ignoring.", flagValue)
		}
	}
	// Skipped configPath, because it's handled in readAndApplyConfiguration.
	// Skipped versionRequested, because it's handled in main() directly.

	logger.Debugf("Application of flags is completed, returning.")
}

// This function checks configuration sanity when called, and creates warnings or errors depending on the situation.
// This function also called just before sending the notification, hence it handles showing help, creating warnings and other
// related user interfacing notifications.
func checkConfigurationSanity(notificationToSend *Notification, runtimeConfiguration *RuntimeConfiguration, logger *zap.SugaredLogger) {
func checkConfigurationSanity(notificationToSend *Notification, runtimeConfiguration *RuntimeConfiguration, validConfigurationOptions *ValidConfigurationOptions, logger *zap.SugaredLogger) {
	// Check whether supplied logging level is acceptable.
	// Pointer for the method is taken from https://stackoverflow.com/a/38654444
	// Also see https://go.dev/blog/slices-intro & https://pkg.go.dev/golang.org/x/exp/slices
	// Since slices.Index() returns -1 if the value is not found, comparing it with -1 and negating the result gives us the correct boolean value.
	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.Debugf("Provided log level (%s) is valid.", runtimeConfiguration.logLevel)

	// Check whether we have the API & User keys.
	// Let's do some magic:
	apiKeyPresent := len(runtimeConfiguration.apiKey) != 0   // This evaluates to bool, heh. :)


@@ 186,18 317,10 @@ func checkConfigurationSanity(notificationToSend *Notification, runtimeConfigura

	// Print the warnings if required:
	if !apiKeyPresent {
		logger.Warnf("API key is not found or defined. You can't send notifications until you set it.")
		logger.Fatalf("API key is not found or defined. You can't send notifications until you set it.")
	}
	if !userKeyPresent {
		logger.Warnf("User key is not found or defined. You can't send notifications until you set it.")
	}



	// If no message is given, let's print help and exit.
	if notificationToSend.messageBody == "" {
		flag.PrintDefaults()
		os.Exit(1)
		logger.Fatalf("User key is not found or defined. You can't send notifications until you set it.")
	}

	// If we arrive to this point, it means we can send a message, but we need to check


@@ 209,28 332,31 @@ func checkConfigurationSanity(notificationToSend *Notification, runtimeConfigura

// This function pretty prints application state.
// The output is limited to 80 chars to fit legacy terminals.
func printState(runtimeConfiguration *RuntimeConfiguration, notification *Notification) {
	fmt.Println("Current configuration")
	fmt.Println("---------------------")
	fmt.Printf("Configuration file path: %s\n", runtimeConfiguration.configFilePath)
	fmt.Printf("Pushover API key: %s\n", runtimeConfiguration.apiKey)
	fmt.Printf("Pushover user key: %s\n", runtimeConfiguration.userKey)
	fmt.Printf("Log file path: %s\n", runtimeConfiguration.logfilePaths)
	fmt.Printf("Log verbosity: %d\n\n", runtimeConfiguration.verbosity)
func printState(runtimeConfiguration *RuntimeConfiguration, notification *Notification, logger *zap.SugaredLogger) {
	logger.Infof("Current configuration")
	logger.Infof("---------------------")
	logger.Infof("Configuration file path: %s", runtimeConfiguration.configFilePath)
	logger.Infof("Pushover API key: %s", runtimeConfiguration.apiKey)
	logger.Infof("Pushover user key: %s", runtimeConfiguration.userKey)
	logger.Infof("Log file path: %s", runtimeConfiguration.logfilePaths)
	logger.Infof("Current logging level is %s.", runtimeConfiguration.logLevel)
	logger.Infof("") // Leave an empty line.

	// Dump notification in a different block.
	fmt.Println("Notification details")
	fmt.Println("--------------------")
	fmt.Printf("Recipients: %s\n", notification.device)
	fmt.Printf("Priority: %d\n", notification.priority)
	fmt.Printf("Sound: %s\n", notification.sound)
	fmt.Printf("Timestamp: %d\n", notification.timestamp)
	fmt.Printf("HTML message: %d\n", notification.html)
	fmt.Printf("URL title: %s\n", notification.urlTitle)
	fmt.Printf("URL: %s\n", notification.url)
	fmt.Printf("Image attachment: %s\n", notification.imageAttachment)
	fmt.Printf("Message title: %s\n", notification.title)
	fmt.Printf("Message: %s\n", notification.messageBody)
	logger.Infof("Notification details")
	logger.Infof("--------------------")
	logger.Infof("Recipients: %s", notification.device)
	logger.Infof("Priority: %d", notification.priority)
	logger.Infof("Sound: %s", notification.sound)
	logger.Infof("Timestamp: %d", notification.timestamp)
	logger.Infof("HTML message: %d", notification.html)
	logger.Infof("URL title: %s", notification.urlTitle)
	logger.Infof("URL: %s", notification.url)
	logger.Infof("Image attachment: %s", notification.imageAttachment)
	logger.Infof("Message title: %s", notification.title)
	logger.Infof("Message: %s", notification.messageBody)

	logger.Infof("") // Leave an empty line.
}

// This function sends the actual notification via PUSH request, via Pushover.


@@ 261,13 387,33 @@ func sendNotification(notificationToSend *Notification, runtimeConfiguration *Ru
func main() {
	// Create the data structures we need.
	var runtimeConfiguration RuntimeConfiguration
	var flagStorage FlagStorage
	var notificationToSend Notification
	var validConfigurationOptions ValidConfigurationOptions

	// Since there's no easy way to check whether a flag is set, I need a list of set flags.
	var setFlags []string

	// Start by applying default configuration before building things up.
	// Start with initializing valid configuration space.
	initializeValidConfigurationOptions(&validConfigurationOptions)

	// Next, apply the defaults:
	applyDefaultConfiguration(&runtimeConfiguration, &notificationToSend)

	// TODO: Prettify logger with some stylistic configuration here.
	// Relevant documentation is here: https://pkg.go.dev/go.uber.org/zap#hdr-Configuring_Zap

	rawJSON := []byte(`{
	  "level": "warn",
	/*
	 * To be able to ship 0.2 as quickly as possible, Zap will be configured as follows:
	 * 1- Create a good enough configuration with JSON based on system defaults.
	 * 2- Build Zap with that configuration.
	 * 3- Reconfigure Zap after parsing all the options and creating the final runtime
	 *    configuration.
	 *
	 * XXX: This JSON config part will be replaced with a programmatic block later.
	 */
	zapDefaultConfigJSON := []byte(`{
	  "level": "debug",
	  "encoding": "console",
	  "outputPaths": ["stdout"],
	  "errorOutputPaths": ["stderr"],


@@ 280,11 426,24 @@ func main() {
	  }
	}`)

	var cfg zap.Config
	if err := json.Unmarshal(rawJSON, &cfg); err != nil {
	var zapRuntimeConfig zap.Config
	if err := json.Unmarshal(zapDefaultConfigJSON, &zapRuntimeConfig); err != nil {
		panic(err)
	}
	logger := zap.Must(cfg.Build())
	
	/*
	 * We build the logger with the default configuration before everything else.
	 * We know that the config is sane, because it's hardcoded. We're using built-in
	 * defaults at that point.
	 */
	zapDefaultAtomicLevel, err := zap.ParseAtomicLevel(runtimeConfiguration.logLevel)
	
	if err != nil {
		panic (err)
	}
	
	zapRuntimeConfig.Level = zapDefaultAtomicLevel
	logger := zap.Must(zapRuntimeConfig.Build())

	defer logger.Sync() // Make sure that we sync when we exit.



@@ 292,33 451,65 @@ func main() {
	sugaredLogger := logger.Sugar()
	sugaredLogger.Debugf("Logger is up.")

	// First, apply the defaults:
	applyDefaultConfiguration(&runtimeConfiguration, &notificationToSend, sugaredLogger)

	// First, start with reading the config, if present.
	// If file is not present, fallback to defaults.
	readAndApplyConfiguration(&runtimeConfiguration.configFilePath, &runtimeConfiguration, &notificationToSend, sugaredLogger)

	// Then let's see what we have at hand (options, parameters, flags).
	initFlags(&notificationToSend, &runtimeConfiguration)
	// Flags are parsed first, stored in a secondary config area.
	// This allows us to override config file with flags more gracefully.
	initFlags(&notificationToSend, &flagStorage, sugaredLogger)

	// Start with reading the config, if present.
	// If file is not present, fallback to defaults.
	readAndApplyConfiguration(&runtimeConfiguration.configFilePath, &runtimeConfiguration, &notificationToSend, &flagStorage, sugaredLogger)

	// To be able to satisfy Defaults -> Configuration -> Flags chain,
	// I need to apply the options got from flags to Runtime Configuration at this point.
	// However I need to get the list of set flags from Flag. Since only visit() allows me
	// to do it, I need to use this anonymous function to make this work.
	flag.Visit(func(setFlag *flag.Flag) {
		setFlags = append(setFlags, setFlag.Name)
	})
	
	// Then I can apply the set flags.
	applyFlags(&setFlags, &flagStorage, &runtimeConfiguration, &notificationToSend, sugaredLogger)
	
	// Run all the configuration checks and issue relevant warnings or errors.
	// TODO: Is here the best place to call this? Take a look before releasing.
	checkConfigurationSanity(&notificationToSend, &runtimeConfiguration, &validConfigurationOptions, sugaredLogger)

	// This is the demarcation line between config reading and starting doing things.
	
	// Let's try to change the logging level of our current logger.
	newLogLevelJSON := []byte(runtimeConfiguration.logLevel)
	
	err = zapRuntimeConfig.Level.UnmarshalText(newLogLevelJSON)
	
	if err != nil {
		sugaredLogger.Panicf("Supplied log level %s is invalid, exiting.", runtimeConfiguration.logLevel)
	}
	
	sugaredLogger.Debug("Logging level is changed to %s.", runtimeConfiguration.logLevel)
	
	// Check whether our version is asked or not.
	// Version info shall always return clean.
	if runtimeConfiguration.versionRequested {
		fmt.Printf("%s version %s\n", os.Args[0], runtimeConfiguration.version)
	if flagStorage.versionRequested {
		fmt.Printf("%s version %s\n", runtimeConfiguration.applicationName, runtimeConfiguration.version)
		os.Exit(0) // Exit gracefully.
	}

	// Run all the configuration checks and issue relevant warnings or errors.
	checkConfigurationSanity(&notificationToSend, &runtimeConfiguration, sugaredLogger)
	// If no message is given, let's print help and exit.
	if notificationToSend.messageBody == "" {
		sugaredLogger.Debugf("No message body specified, exiting.")
		fmt.Printf("Usage: %s [OPTIONS] message\n", runtimeConfiguration.applicationName)
		flag.PrintDefaults()
		os.Exit(1)
	}

	// Show the current state of the code.
	printState(&runtimeConfiguration, &notificationToSend)
	printState(&runtimeConfiguration, &notificationToSend, sugaredLogger)

	// Let's send the notification:
	if runtimeConfiguration.dryrun == false {
		sendNotification(&notificationToSend, &runtimeConfiguration)
	} else {
		sugaredLogger.Infow("Not sending the notification since in dry run mode.")
		sugaredLogger.Warnf("Not sending the notification since in dry run mode.")
	}
}