From f0904daa19fe7ee500a532c3ac77c210af2e082e Mon Sep 17 00:00:00 2001 From: Hakan Bayindir Date: Tue, 20 Feb 2024 20:32:04 +0300 Subject: [PATCH] feat: Add image attachment support. - proj: Update copyright year. - feat: Finish implementing image attachement support. - feat: Implemented file type detection support. - refactor: Remove warning about image attachments' state. - refactor: Go through gofmt. - docs: Update README.md for the new version. - docs: Update RELEASE_NOTES.md for the new version. - proj: Bump version to v0.6.0. - proj: Release version v0.6.0. --- CHANGELOG.md | 11 ++++++++ README.md | 38 +++++++++++++------------ RELEASE_NOTES.md | 17 +++++++++++- src/go.mod | 6 ++-- src/go.sum | 12 +++++--- src/nudge.go | 72 ++++++++++++++++++++++++++++++------------------ 6 files changed, 104 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68fa310..8bd9c5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ **Note:** Please add newest entries on top. Use ISO date format YYYY-MM-DD and markdown formatting. +## 2024-02-20 +- proj: Update copyright year. +- feat: Finish implementing image attachement support. +- feat: Implemented file type detection support. +- refactor: Remove warning about image attachments' state. +- refactor: Go through `gofmt`. +- docs: Update `README.md` for the new version. +- docs: Update `RELEASE_NOTES.md` for the new version. +- proj: Bump version to `v0.6.0`. +- proj: Release version `v0.6.0`. + ## 2024-02-07 - feat: Read file to memory as a byte array. diff --git a/README.md b/README.md index b5a9911..9f493dc 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ nudge logo # 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:** **Beta**. Can be used reliably to send messages daily, most things are stable now. Most of the Pushover 4.0 features are supported. -**Current version:** v0.5.0 +**Current version:** v0.6.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 -- Unix pipes support. Now you can pipe to `nudge`, and your output will be sent as a notification. e.g.: `echo "Hello, world!" | nudge` will work. +- Image attachment support. Can attach JPEG, PNG and WEBP images up to 2.5MBs. - Internal improvements and refactoring. For older changes, please refer to [release notes](https://git.sr.ht/~bayindirh/nudge/tree/master/item/RELEASE_NOTES.md) or [changelog](https://git.sr.ht/~bayindirh/nudge/tree/master/item/CHANGELOG.md) for even more detailed history. @@ -55,41 +55,43 @@ Current options are as follows: ``` Usage: nudge [OPTIONS] message + -attach_image string + Attach an image to your notification. -config_path string - Define or override configuration file path. + Define or override configuration file path. -devices string - List of devices to be notified. Separate multiple devices with ','. (default "all") + List of devices to be notified. Separate multiple devices with ','. (default "all") -dryrun - Simulate sending a notification. + Simulate sending a notification. -expire int - Seconds before giving-up re-sending notifications (for priority 2 only). (default 1800) + Seconds before giving-up re-sending notifications (for priority 2 only). (default 1800) -html - Enable HTML formatting in notifications. + Enable HTML formatting in notifications. -log_level string - Change the logging level. (default "warn") + Change the logging level. (default "warn") -priority int - Adjust notification priority. Between -2 and 2. (default 0) + Adjust notification priority. Between -2 and 2. (default 0) -retry_interval int - Seconds between notification re-sends (for priortity 2 only). (default 30) + Seconds between notification re-sends (for priortity 2 only). (default 30) -sound string - Set notification sound. + Set notification sound. -title string - Notification title. Hostname is used if omitted. (default "temple") + Notification title. Hostname is used if omitted. (default "temple") -ttl int - Seconds before the notification removed from devices automatically (ignored if priority is 2). + Seconds before the notification removed from devices automatically (ignored if priority is 2). -url string - An optional URL to attach to the notification. + An optional URL to attach to the notification. -url_title string - An optional title for URL to send. + An optional title for URL to send. -version - Print version and exit. + Print version and exit. ``` ## Known Issues - Not all features provided by Pushover is implemented (image attachments, priority 2 message status tracking, and possibly others) ## Roadmap -- **`0.6.0`:** Image attachments support. +- **`0.7.0`:** Ability to query Pushover for available recipients. ## Other Details nudge is written in Go and licensed with GNU/GPLv3 or later. The project diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 00bc71f..dcc4ab3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,21 @@ # Nudge Release Notes +## v0.6.0 +**Release Date:** 20240220 + +### At a Glance +This version adds image attachment support, allowing users to send images up to 2.5MB in size alongside notifications. + +### What's New +- Image attachment support. + +### Known Issues +- Not all features provided by Pushover is supported yet. + +### Next Plans +- Ability to query Pushover for recipient list. + ## v0.5.0 **Release Date:** 20231229 @@ -67,4 +82,4 @@ This version brings Nudge to almost beta level. The tool can be used to send not ### Known Issues - Logs can't be redirected to files via configuration file or command line, yet. These options are not handled in the code. - Configuration sanity checking doesn't check everything for acceptable values. -- Not all features provided by Pushover is implemented (image attachments, HTML formatting, and possibly others) \ No newline at end of file +- Not all features provided by Pushover is implemented (image attachments, HTML formatting, and possibly others) diff --git a/src/go.mod b/src/go.mod index 61bfd1f..9c61575 100644 --- a/src/go.mod +++ b/src/go.mod @@ -3,6 +3,7 @@ module git.sr.ht/~bayindirh/nudge go 1.19 require ( + github.com/gabriel-vasile/mimetype v1.4.3 github.com/spf13/viper v1.15.0 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 ) @@ -10,6 +11,7 @@ require ( require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.8.0 // indirect + golang.org/x/net v0.17.0 // indirect ) require ( @@ -24,8 +26,8 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect go.uber.org/zap v1.24.0 - golang.org/x/sys v0.3.0 // indirect - golang.org/x/text v0.5.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/src/go.sum b/src/go.sum index b4dd2d8..2caabdb 100644 --- a/src/go.sum +++ b/src/go.sum @@ -59,6 +59,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -261,6 +263,8 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -315,8 +319,8 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -324,8 +328,8 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/src/nudge.go b/src/nudge.go index 454ce30..a77a5c7 100644 --- a/src/nudge.go +++ b/src/nudge.go @@ -1,6 +1,6 @@ /* * nudge - A CLI agent for Pushover, written in Go. - * Copyright (C) 2022 Hakan Bayindir + * Copyright (C) 2024 Hakan Bayindir * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -18,11 +18,10 @@ package main import ( -// "encoding/base64" + "encoding/base64" "encoding/json" "flag" "fmt" -// "image" "io" "net/http" "net/url" @@ -32,22 +31,17 @@ import ( "golang.org/x/exp/slices" - "github.com/spf13/viper" - "go.uber.org/zap" - - // Packages image/{jpeg, png, gif} are not used explicitly in the code below, - // but is imported for its initialization side-effect, which allows - // image.Decode to understand JPEG/PNG/GIF formatted images. - _ "image/gif" - _ "image/png" - _ "image/jpeg" + "github.com/gabriel-vasile/mimetype" // We need this for image type detection. + "github.com/spf13/viper" // Handles our configuration files. + "go.uber.org/zap" // Handles our logging. ) // This struct contains all the fields required for sending a notification. // The reference can be found at: https://pushover.net/api type Notification struct { messageBody string - imageAttachment string // Will be sent empty for now. + imageAttachment string // This is the BASE64 encoded version of our image file, if we have an image attachment. Will be empty otherwise. + imageAttachmentType string // Contains the MIME type of the image we're sending. This is automatically detected. imageAttachmentPath string // Contains the path for the image to be attached. 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. @@ -107,7 +101,8 @@ type ValidConfigurationOptions struct { 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. - maximumFileSizeInBytes int64 // This is the maximum image size allowed by Pushover API. + maximumFileSizeInBytes int64 // This is the maximum image size allowed by Pushover API. + attachableFileTypes []string // Stores the file types which can be attached to a notification. } // This function determines our input method (direct call or pipe). @@ -140,10 +135,11 @@ func isInputFromPipe(logger *zap.SugaredLogger) bool { func initializeValidConfigurationOptions(ValidConfigurationOptions *ValidConfigurationOptions) { ValidConfigurationOptions.possibleLogLevels = []string{"debug", "info", "warn", "error", "fatal", "panic"} ValidConfigurationOptions.possiblePriorityLevelRange = []int{-2, 2} - 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. - ValidConfigurationOptions.maximumFileSizeInBytes = 2621440 // This is bytes, equal to 2MB. Limit is imposed by Pushover API. + 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. + ValidConfigurationOptions.maximumFileSizeInBytes = 2621440 // This is bytes, equal to 2MB. Limit is imposed by Pushover API. + ValidConfigurationOptions.attachableFileTypes = []string{"image/png", "image/jpeg", "image/webp"} // Currently we allow JPEG, PNG and WEBP images. } // This function stores and applies the defaults of the application. @@ -174,7 +170,7 @@ func applyDefaultConfiguration(runtimeConfiguration *RuntimeConfiguration, notif // 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.6.0a20240126" + runtimeConfiguration.version = "0.6.0" runtimeConfiguration.dryrun = false runtimeConfiguration.logLevel = "warn" } @@ -472,9 +468,6 @@ func checkConfigurationSanity(notificationToSend *Notification, runtimeConfigura // This function handles the attachment of the image from end to end. // Attachment path comes in, BASE64 encoded image is copied to notificationToSend. func attachImageToNotification(notificationToSend *Notification, validConfigurationOptions *ValidConfigurationOptions, logger *zap.SugaredLogger) { - //TODO: Remove this directive when the function is complete. - logger.Warnf("Image attachment feature is under development, and may not work correctly.") - // First step is to make sure that the file is there. fileInformation, err := os.Stat(notificationToSend.imageAttachmentPath) @@ -490,23 +483,42 @@ func attachImageToNotification(notificationToSend *Notification, validConfigurat logger.Infof("File's size is %d byte(s)", fileInformation.Size()) logger.Infof("Permissions are: %s", fileInformation.Mode().Perm().String()) logger.Infof("Last modification time: %s", fileInformation.ModTime()) - + // Let's check the file size. if fileInformation.Size() > validConfigurationOptions.maximumFileSizeInBytes { logger.Fatalf("File %s is bigger than %d bytes, which is the upper limit imposed by Pushover API. Try with a smaller image file.", fileInformation.Name(), validConfigurationOptions.maximumFileSizeInBytes) } - + // Let's read the file into a byte array, so we can convert it to BASE64. fileContents := make([]byte, fileInformation.Size()) fileContents, err = os.ReadFile(notificationToSend.imageAttachmentPath) - + // If we can't open the file, we catch the problem here. if err != nil { logger.Fatalf("Reading file returned an error (error is %s).", err) } - + logger.Debugf("The file %s has been read successfully.", fileInformation.Name()) + + // Let's detect the file type, and see what whether it's one of the ones we allow. + // Following section is derived from: https://pkg.go.dev/github.com/gabriel-vasile/mimetype#example-package-Whitelist + mimeType := mimetype.Detect(fileContents) + + logger.Debugf("The read file's type is %s.", mimeType) + + // Let's check whether we can send this file along the notification. + if mimetype.EqualsAny(mimeType.String(), validConfigurationOptions.attachableFileTypes...) { + logger.Debugf("Attached file is %s, and can be attached.", mimeType) + notificationToSend.imageAttachmentType = mimeType.String() + } else { + logger.Fatalf("Attachment type %s cannot be attached, exiting.", mimeType) + } + + // Next, we need to convert this image data to BASE64 encoding. + // The Inspiration is at https://pkg.go.dev/encoding/base64#example-Encoding.EncodeToString + notificationToSend.imageAttachment = base64.StdEncoding.EncodeToString(fileContents) + logger.Debugf("The BASE64 encoded image is %d byte(s) long.", len(notificationToSend.imageAttachment)) } // This function pretty prints application state. @@ -606,6 +618,12 @@ func sendNotification(notificationToSend *Notification, runtimeConfiguration *Ru valuesToSend.Add("url_title", notificationToSend.urlTitle) } + // Let's attach our image if there's any. + if len(notificationToSend.imageAttachment) > 0 { + valuesToSend.Add("attachment_base64", notificationToSend.imageAttachment) + valuesToSend.Add("attachment_type", notificationToSend.imageAttachmentType) + } + response, err := http.PostForm("https://api.pushover.net/1/messages.json", valuesToSend) if err != nil { @@ -728,7 +746,7 @@ func main() { flag.PrintDefaults() os.Exit(1) } - + // We should attach our image file here, before finalizing the notification object. if notificationToSend.imageAttachmentPath != "" { attachImageToNotification(¬ificationToSend, &validConfigurationOptions, sugaredLogger) -- 2.45.2