Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Automatically split STDIN on null characters on push #70

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions command/initialize.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func inputConfigLocation() string {
for {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would I use this feature to simplify the journalctl call in #69?

I'm also not 100% sure about the actual use-case of feature, is there something not possible with the current cli feature set and xargs? I don't find the argument that we don't have to use xargs very strong for implementing this inside gotify/cli.

I think if someone doesn't have access to xargs then they probably don't use gotify/cli and instead use some "raw" http call.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would I use this feature to simplify the journalctl call in #69?

journalctl -f -u sshd | awk 'NR % 5 == 0 { print "\0"} { print }' | gotify push

is there something not possible with the current cli feature set and xargs

makes sense, maybe I will just cherry pick the formatting bugs to a different PR and do that

fmt.Println("Where to put the config file?")
for i, location := range locations {
fmt.Println(fmt.Sprintf("%d. %s", i+1, location))
fmt.Printf("%d. %s\n", i+1, location)
}
value := inputString("Enter a number: ")
hr()
Expand Down Expand Up @@ -215,9 +215,9 @@ func inputDefaultPriority() int {
erred("Priority needs to be a number between 0 and 10.")
continue
} else {
hr()
return defaultPriority
}
hr()
}
}

Expand Down Expand Up @@ -251,7 +251,7 @@ func inputServerURL() *url.URL {
})
if err == nil {
info := version.(models.VersionInfo)
fmt.Println(fmt.Sprintf("Gotify v%s@%s", info.Version, info.BuildDate))
fmt.Printf("Gotify v%s@%s\n", info.Version, info.BuildDate)
return parsedURL
}
hr()
Expand Down
86 changes: 40 additions & 46 deletions command/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"net/url"
"os"
"strings"

"github.com/gotify/cli/v2/config"
"github.com/gotify/cli/v2/utils"
Expand All @@ -31,6 +30,7 @@ func Push() cli.Command {
cli.StringFlag{Name: "contentType", Usage: "The content type of the message. See https://gotify.net/docs/msgextras#client-display"},
cli.StringFlag{Name: "clickUrl", Usage: "An URL to open upon clicking the notification. See https://gotify.net/docs/msgextras#client-notification"},
cli.BoolFlag{Name: "disable-unescape-backslash", Usage: "Disable evaluating \\n and \\t (if set, \\n and \\t will be seen as a string)"},
cli.BoolFlag{Name: "no-split", Usage: "Do not split the message on null character when reading from stdin"},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we invert this, so we don't changing existing behavior (reading stdin completely and pushing one message).

},
Action: doPush,
}
Expand All @@ -39,9 +39,12 @@ func Push() cli.Command {
func doPush(ctx *cli.Context) {
conf, confErr := config.ReadConfig(config.GetLocations())

msgText := readMessage(ctx)
if !ctx.Bool("disable-unescape-backslash") {
msgText = utils.Evaluate(msgText)
msgText := make(chan string)
null := '\x00'
if ctx.Bool("no-split") {
eternal-flame-AD marked this conversation as resolved.
Show resolved Hide resolved
go readMessage(ctx.Args(), os.Stdin, msgText, nil)
} else {
go readMessage(ctx.Args(), os.Stdin, msgText, &null)
}

priority := ctx.Int("priority")
Expand Down Expand Up @@ -72,36 +75,47 @@ func doPush(ctx *cli.Context) {
priority = conf.DefaultPriority
}

msg := models.MessageExternal{
Message: msgText,
Title: title,
Priority: priority,
parsedURL, err := url.Parse(stringURL)
if err != nil {
utils.Exit1With("invalid url", stringURL)
return
}

msg.Extras = map[string]interface{}{
}
var sent bool
for msgText := range msgText {
eternal-flame-AD marked this conversation as resolved.
Show resolved Hide resolved
if !ctx.Bool("disable-unescape-backslash") {
msgText = utils.Evaluate(msgText)
}

if contentType != "" {
msg.Extras["client::display"] = map[string]interface{}{
"contentType": contentType,
msg := models.MessageExternal{
Message: msgText,
Title: title,
Priority: priority,
}
}

if clickUrl != "" {
msg.Extras["client::notification"] = map[string]interface{}{
"click": map[string]string{
"url": clickUrl,
},
msg.Extras = map[string]interface{}{}

if contentType != "" {
msg.Extras["client::display"] = map[string]interface{}{
"contentType": contentType,
}
}
}

parsedURL, err := url.Parse(stringURL)
if err != nil {
utils.Exit1With("invalid url", stringURL)
return
}
if clickUrl != "" {
msg.Extras["client::notification"] = map[string]interface{}{
"click": map[string]string{
"url": clickUrl,
},
}
}
eternal-flame-AD marked this conversation as resolved.
Show resolved Hide resolved

pushMessage(parsedURL, token, msg, quiet)

pushMessage(parsedURL, token, msg, quiet)
sent = true
}
if !sent {
utils.Exit1With("no message sent! a message must be set, either as argument or via stdin")
}
}

func pushMessage(parsedURL *url.URL, token string, msg models.MessageExternal, quiet bool) {
Expand All @@ -119,23 +133,3 @@ func pushMessage(parsedURL *url.URL, token string, msg models.MessageExternal, q
utils.Exit1With(err)
}
}

func readMessage(ctx *cli.Context) string {
msgArgs := strings.Join(ctx.Args(), " ")

msgStdin := utils.ReadFrom(os.Stdin)

if msgArgs == "" && msgStdin == "" {
utils.Exit1With("a message must be set, either as argument or via stdin")
jmattheis marked this conversation as resolved.
Show resolved Hide resolved
}

if msgArgs != "" && msgStdin != "" {
utils.Exit1With("a message is set via stdin and arguments, use only one of them")
}

if msgArgs == "" {
return msgStdin
} else {
return msgArgs
}
}
61 changes: 61 additions & 0 deletions command/read.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package command

import (
"io"
"strings"

"github.com/gotify/cli/v2/utils"
)

func readMessage(args []string, r io.Reader, output chan<- string, split *rune) {
jmattheis marked this conversation as resolved.
Show resolved Hide resolved
msgArgs := strings.Join(args, " ")

if msgArgs != "" {
if utils.ProbeStdin(r) {
utils.Exit1With("message is set via arguments and stdin, use only one of them")
}

output <- msgArgs
close(output)
return
}

var buf strings.Builder
for {
var tmp [256]byte
n, err := r.Read(tmp[:])
if err != nil {
if err.Error() == "EOF" {
break
}
utils.Exit1With(err)
}
tmpStr := string(tmp[:n])
if split != nil {
// split the message on the null character
parts := strings.Split(tmpStr, string(*split))
eternal-flame-AD marked this conversation as resolved.
Show resolved Hide resolved
if len(parts) == 1 {
buf.WriteString(parts[0])
continue
}

previous := buf.String()
// fuse previous with parts[0], send parts[1] .. parts[n-2] and set parts[n-1] as new previous
firstMsg := previous + parts[0]
output <- firstMsg
for _, part := range parts[1 : len(parts)-1] {
output <- part
}
buf.Reset()
buf.WriteString(parts[len(parts)-1])
} else {
buf.WriteString(tmpStr)
}
}

if buf.Len() > 0 {
output <- buf.String()
}

close(output)
}
55 changes: 55 additions & 0 deletions command/read_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package command

import (
"strings"
"testing"
)

// Polyfill for slices.Equal for Go 1.20
func slicesEqual[T comparable](a, b []T) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}

func readChanAll[T any](c chan T) []T {
var res []T
for s := range c {
res = append(res, s)
}
return res
}

func TestReadMessage(t *testing.T) {
var split rune = '\x00'

// Test case 1: message set via arguments
output := make(chan string)
go readMessage([]string{"Hello", "World"}, nil, output, nil)

if res := readChanAll(output); !(slicesEqual(res, []string{"Hello World"})) {
t.Errorf("Expected %v, but got %v", []string{"Hello World"}, res)
}

// Test case 2: message set via arguments should not split on 'split' character
output = make(chan string)
go readMessage([]string{"Hello\x00World"}, nil, output, &split)

if res := readChanAll(output); !(slicesEqual(res, []string{"Hello\x00World"})) {
t.Errorf("Expected %v, but got %v", []string{"Hello\x00World"}, res)
}

// Test case 3: message set via stdin
output = make(chan string)
go readMessage([]string{}, strings.NewReader("Hello\x00World"), output, &split)

if res := readChanAll(output); !(slicesEqual(res, []string{"Hello", "World"})) {
t.Errorf("Expected %v, but got %v", []string{"Hello", "World"}, res)
}
}
8 changes: 4 additions & 4 deletions command/watch.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,18 +120,18 @@ func doWatch(ctx *cli.Context) {
case "long":
fmt.Fprintf(msgData, "command output for \"%s\" changed:\n\n", cmdStringNotation)
fmt.Fprintln(msgData, "== BEGIN OLD OUTPUT ==")
fmt.Fprint(msgData, lastOutput)
fmt.Fprintln(msgData, lastOutput)
fmt.Fprintln(msgData, "== END OLD OUTPUT ==")
fmt.Fprintln(msgData, "== BEGIN NEW OUTPUT ==")
fmt.Fprint(msgData, output)
fmt.Fprintln(msgData, output)
fmt.Fprintln(msgData, "== END NEW OUTPUT ==")
case "default":
fmt.Fprintf(msgData, "command output for \"%s\" changed:\n\n", cmdStringNotation)
fmt.Fprintln(msgData, "== BEGIN NEW OUTPUT ==")
fmt.Fprint(msgData, output)
fmt.Fprintln(msgData, output)
fmt.Fprintln(msgData, "== END NEW OUTPUT ==")
case "short":
fmt.Fprintf(msgData, output)
fmt.Fprintln(msgData, output)
eternal-flame-AD marked this conversation as resolved.
Show resolved Hide resolved
}

msgString := msgData.String()
Expand Down
25 changes: 13 additions & 12 deletions utils/readfromstdin.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
package utils

import (
"io"
"os"
"io/ioutil"
)

func ReadFrom(file *os.File) string {
fi, err := os.Stdin.Stat()
if err != nil {
return ""
func ProbeStdin(file io.Reader) bool {
if file == nil {
return false
}
if fi.Mode()&os.ModeNamedPipe == 0 && !fi.Mode().IsRegular() {
return ""
if file, ok := file.(*os.File); ok {
fi, err := file.Stat()
if err != nil {
return false
}
if fi.Mode()&os.ModeNamedPipe == 0 && !fi.Mode().IsRegular() {
return false
}
}

bytes, err := ioutil.ReadAll(file)
if err != nil {
return ""
}
return string(bytes)
return true
}
Loading