commit
4e29216b5f
136
cmd/publish.go
136
cmd/publish.go
|
@ -5,11 +5,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
"heckel.io/ntfy/client"
|
"heckel.io/ntfy/client"
|
||||||
|
"heckel.io/ntfy/log"
|
||||||
"heckel.io/ntfy/util"
|
"heckel.io/ntfy/util"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -20,6 +23,7 @@ var flagsPublish = append(
|
||||||
flagsDefault,
|
flagsDefault,
|
||||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG"}, Usage: "client config file"},
|
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG"}, Usage: "client config file"},
|
||||||
&cli.StringFlag{Name: "title", Aliases: []string{"t"}, EnvVars: []string{"NTFY_TITLE"}, Usage: "message title"},
|
&cli.StringFlag{Name: "title", Aliases: []string{"t"}, EnvVars: []string{"NTFY_TITLE"}, Usage: "message title"},
|
||||||
|
&cli.StringFlag{Name: "message", Aliases: []string{"m"}, EnvVars: []string{"NTFY_MESSAGE"}, Usage: "message body"},
|
||||||
&cli.StringFlag{Name: "priority", Aliases: []string{"p"}, EnvVars: []string{"NTFY_PRIORITY"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"},
|
&cli.StringFlag{Name: "priority", Aliases: []string{"p"}, EnvVars: []string{"NTFY_PRIORITY"}, Usage: "priority of the message (1=min, 2=low, 3=default, 4=high, 5=max)"},
|
||||||
&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, EnvVars: []string{"NTFY_TAGS"}, Usage: "comma separated list of tags and emojis"},
|
&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, EnvVars: []string{"NTFY_TAGS"}, Usage: "comma separated list of tags and emojis"},
|
||||||
&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, EnvVars: []string{"NTFY_DELAY"}, Usage: "delay/schedule message"},
|
&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, EnvVars: []string{"NTFY_DELAY"}, Usage: "delay/schedule message"},
|
||||||
|
@ -30,6 +34,8 @@ var flagsPublish = append(
|
||||||
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"},
|
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"},
|
||||||
&cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"},
|
&cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"},
|
||||||
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
|
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
|
||||||
|
&cli.IntFlag{Name: "wait-pid", Aliases: []string{"pid"}, EnvVars: []string{"NTFY_WAIT_PID"}, Usage: "wait until PID exits before publishing"},
|
||||||
|
&cli.BoolFlag{Name: "wait-cmd", Aliases: []string{"cmd", "done"}, EnvVars: []string{"NTFY_WAIT_CMD"}, Usage: "run command and wait until it finishes before publishing"},
|
||||||
&cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"},
|
&cli.BoolFlag{Name: "no-cache", Aliases: []string{"C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"},
|
||||||
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"},
|
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"},
|
||||||
&cli.BoolFlag{Name: "env-topic", Aliases: []string{"P"}, EnvVars: []string{"NTFY_ENV_TOPIC"}, Usage: "use topic from NTFY_TOPIC env variable"},
|
&cli.BoolFlag{Name: "env-topic", Aliases: []string{"P"}, EnvVars: []string{"NTFY_ENV_TOPIC"}, Usage: "use topic from NTFY_TOPIC env variable"},
|
||||||
|
@ -40,7 +46,9 @@ var cmdPublish = &cli.Command{
|
||||||
Name: "publish",
|
Name: "publish",
|
||||||
Aliases: []string{"pub", "send", "trigger"},
|
Aliases: []string{"pub", "send", "trigger"},
|
||||||
Usage: "Send message via a ntfy server",
|
Usage: "Send message via a ntfy server",
|
||||||
UsageText: "ntfy publish [OPTIONS..] TOPIC [MESSAGE]\nNTFY_TOPIC=.. ntfy publish [OPTIONS..] -P [MESSAGE]",
|
UsageText: `ntfy publish [OPTIONS..] TOPIC [MESSAGE...]
|
||||||
|
ntfy publish [OPTIONS..] --wait-cmd COMMAND...
|
||||||
|
NTFY_TOPIC=.. ntfy publish [OPTIONS..] -P [MESSAGE...]`,
|
||||||
Action: execPublish,
|
Action: execPublish,
|
||||||
Category: categoryClient,
|
Category: categoryClient,
|
||||||
Flags: flagsPublish,
|
Flags: flagsPublish,
|
||||||
|
@ -59,8 +67,10 @@ Examples:
|
||||||
ntfy pub --attach="http://some.tld/file.zip" files # Send ZIP archive from URL as attachment
|
ntfy pub --attach="http://some.tld/file.zip" files # Send ZIP archive from URL as attachment
|
||||||
ntfy pub --file=flower.jpg flowers 'Nice!' # Send image.jpg as attachment
|
ntfy pub --file=flower.jpg flowers 'Nice!' # Send image.jpg as attachment
|
||||||
ntfy pub -u phil:mypass secret Psst # Publish with username/password
|
ntfy pub -u phil:mypass secret Psst # Publish with username/password
|
||||||
|
ntfy pub --wait-pid 1234 mytopic # Wait for process 1234 to exit before publishing
|
||||||
|
ntfy pub --wait-cmd mytopic rsync -av ./ /tmp/a # Run command and publish after it completes
|
||||||
NTFY_USER=phil:mypass ntfy pub secret Psst # Use env variables to set username/password
|
NTFY_USER=phil:mypass ntfy pub secret Psst # Use env variables to set username/password
|
||||||
NTFY_TOPIC=mytopic ntfy pub -P "some message"" # Use NTFY_TOPIC variable as topic
|
NTFY_TOPIC=mytopic ntfy pub -P "some message" # Use NTFY_TOPIC variable as topic
|
||||||
cat flower.jpg | ntfy pub --file=- flowers 'Nice!' # Same as above, send image.jpg as attachment
|
cat flower.jpg | ntfy pub --file=- flowers 'Nice!' # Same as above, send image.jpg as attachment
|
||||||
ntfy trigger mywebhook # Sending without message, useful for webhooks
|
ntfy trigger mywebhook # Sending without message, useful for webhooks
|
||||||
|
|
||||||
|
@ -88,22 +98,11 @@ func execPublish(c *cli.Context) error {
|
||||||
user := c.String("user")
|
user := c.String("user")
|
||||||
noCache := c.Bool("no-cache")
|
noCache := c.Bool("no-cache")
|
||||||
noFirebase := c.Bool("no-firebase")
|
noFirebase := c.Bool("no-firebase")
|
||||||
envTopic := c.Bool("env-topic")
|
|
||||||
quiet := c.Bool("quiet")
|
quiet := c.Bool("quiet")
|
||||||
var topic, message string
|
pid := c.Int("wait-pid")
|
||||||
if envTopic {
|
topic, message, command, err := parseTopicMessageCommand(c)
|
||||||
topic = os.Getenv("NTFY_TOPIC")
|
if err != nil {
|
||||||
if c.NArg() > 0 {
|
return err
|
||||||
message = strings.Join(c.Args().Slice(), " ")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if c.NArg() < 1 {
|
|
||||||
return errors.New("must specify topic, type 'ntfy publish --help' for help")
|
|
||||||
}
|
|
||||||
topic = c.Args().Get(0)
|
|
||||||
if c.NArg() > 1 {
|
|
||||||
message = strings.Join(c.Args().Slice()[1:], " ")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var options []client.PublishOption
|
var options []client.PublishOption
|
||||||
if title != "" {
|
if title != "" {
|
||||||
|
@ -156,6 +155,21 @@ func execPublish(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
options = append(options, client.WithBasicAuth(user, pass))
|
options = append(options, client.WithBasicAuth(user, pass))
|
||||||
}
|
}
|
||||||
|
if pid > 0 {
|
||||||
|
newMessage, err := waitForProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if message == "" {
|
||||||
|
message = newMessage
|
||||||
|
}
|
||||||
|
} else if len(command) > 0 {
|
||||||
|
newMessage, err := runAndWaitForCommand(command)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if message == "" {
|
||||||
|
message = newMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
var body io.Reader
|
var body io.Reader
|
||||||
if file == "" {
|
if file == "" {
|
||||||
body = strings.NewReader(message)
|
body = strings.NewReader(message)
|
||||||
|
@ -188,3 +202,91 @@ func execPublish(c *cli.Context) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseTopicMessageCommand reads the topic and the remaining arguments from the context.
|
||||||
|
|
||||||
|
// There are a few cases to consider:
|
||||||
|
// ntfy publish <topic> [<message>]
|
||||||
|
// ntfy publish --wait-cmd <topic> <command>
|
||||||
|
// NTFY_TOPIC=.. ntfy publish [<message>]
|
||||||
|
// NTFY_TOPIC=.. ntfy publish --wait-cmd <command>
|
||||||
|
func parseTopicMessageCommand(c *cli.Context) (topic string, message string, command []string, err error) {
|
||||||
|
var args []string
|
||||||
|
topic, args, err = parseTopicAndArgs(c)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.Bool("wait-cmd") {
|
||||||
|
if len(args) == 0 {
|
||||||
|
err = errors.New("must specify command when --wait-cmd is passed, type 'ntfy publish --help' for help")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
command = args
|
||||||
|
} else {
|
||||||
|
message = strings.Join(args, " ")
|
||||||
|
}
|
||||||
|
if c.String("message") != "" {
|
||||||
|
message = c.String("message")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTopicAndArgs(c *cli.Context) (topic string, args []string, err error) {
|
||||||
|
envTopic := c.Bool("env-topic")
|
||||||
|
if envTopic {
|
||||||
|
fmt.Fprintln(c.App.ErrWriter, "\x1b[1;33mDeprecation notice: The --env-topic/-P flag will be removed in July 2022, see https://ntfy.sh/docs/deprecations/ for details.\x1b[0m")
|
||||||
|
topic = os.Getenv("NTFY_TOPIC")
|
||||||
|
if topic == "" {
|
||||||
|
return "", nil, errors.New("when --env-topic is passed, must define NTFY_TOPIC environment variable")
|
||||||
|
}
|
||||||
|
return topic, remainingArgs(c, 0), nil
|
||||||
|
}
|
||||||
|
if c.NArg() < 1 {
|
||||||
|
return "", nil, errors.New("must specify topic, type 'ntfy publish --help' for help")
|
||||||
|
}
|
||||||
|
return c.Args().Get(0), remainingArgs(c, 1), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func remainingArgs(c *cli.Context, fromIndex int) []string {
|
||||||
|
if c.NArg() > fromIndex {
|
||||||
|
return c.Args().Slice()[fromIndex:]
|
||||||
|
}
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForProcess(pid int) (message string, err error) {
|
||||||
|
if !processExists(pid) {
|
||||||
|
return "", fmt.Errorf("process with PID %d not running", pid)
|
||||||
|
}
|
||||||
|
start := time.Now()
|
||||||
|
log.Debug("Waiting for process with PID %d to exit", pid)
|
||||||
|
for processExists(pid) {
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
runtime := time.Since(start).Round(time.Millisecond)
|
||||||
|
log.Debug("Process with PID %d exited after %s", pid, runtime)
|
||||||
|
return fmt.Sprintf("Process with PID %d exited after %s", pid, runtime), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAndWaitForCommand(command []string) (message string, err error) {
|
||||||
|
prettyCmd := util.QuoteCommand(command)
|
||||||
|
log.Debug("Running command: %s", prettyCmd)
|
||||||
|
start := time.Now()
|
||||||
|
cmd := exec.Command(command[0], command[1:]...)
|
||||||
|
if log.IsTrace() {
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
}
|
||||||
|
err = cmd.Run()
|
||||||
|
runtime := time.Since(start).Round(time.Millisecond)
|
||||||
|
if err != nil {
|
||||||
|
if exitError, ok := err.(*exec.ExitError); ok {
|
||||||
|
log.Debug("Command failed after %s (exit code %d): %s", runtime, exitError.ExitCode(), prettyCmd)
|
||||||
|
return fmt.Sprintf("Command failed after %s (exit code %d): %s", runtime, exitError.ExitCode(), prettyCmd), nil
|
||||||
|
}
|
||||||
|
// Hard fail when command does not exist or could not be properly launched
|
||||||
|
return "", fmt.Errorf("command failed: %s, error: %s", prettyCmd, err.Error())
|
||||||
|
}
|
||||||
|
log.Debug("Command succeeded after %s: %s", runtime, prettyCmd)
|
||||||
|
return fmt.Sprintf("Command succeeded after %s: %s", runtime, prettyCmd), nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
func processExists(pid int) bool {
|
||||||
|
err := syscall.Kill(pid, syscall.Signal(0))
|
||||||
|
return err == nil
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import "syscall"
|
||||||
|
|
||||||
|
func processExists(pid int) bool {
|
||||||
|
err := syscall.Kill(pid, syscall.Signal(0))
|
||||||
|
return err == nil
|
||||||
|
}
|
|
@ -5,7 +5,11 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"heckel.io/ntfy/test"
|
"heckel.io/ntfy/test"
|
||||||
"heckel.io/ntfy/util"
|
"heckel.io/ntfy/util"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
|
func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
|
||||||
|
@ -70,3 +74,66 @@ func TestCLI_Publish_All_The_Things(t *testing.T) {
|
||||||
require.Equal(t, int64(0), m.Attachment.Expires)
|
require.Equal(t, int64(0), m.Attachment.Expires)
|
||||||
require.Equal(t, "", m.Attachment.Type)
|
require.Equal(t, "", m.Attachment.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {
|
||||||
|
s, port := test.StartServer(t)
|
||||||
|
defer test.StopServer(t, s, port)
|
||||||
|
topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
|
||||||
|
|
||||||
|
// Test: sleep 0.5
|
||||||
|
sleep := exec.Command("sleep", "0.5")
|
||||||
|
require.Nil(t, sleep.Start())
|
||||||
|
go sleep.Wait() // Must be called to release resources
|
||||||
|
start := time.Now()
|
||||||
|
app, _, stdout, _ := newTestApp()
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-pid", strconv.Itoa(sleep.Process.Pid), topic}))
|
||||||
|
m := toMessage(t, stdout.String())
|
||||||
|
require.True(t, time.Since(start) >= 500*time.Millisecond)
|
||||||
|
require.Regexp(t, `Process with PID \d+ exited after `, m.Message)
|
||||||
|
|
||||||
|
// Test: PID does not exist
|
||||||
|
app, _, _, _ = newTestApp()
|
||||||
|
err := app.Run([]string{"ntfy", "publish", "--wait-pid", "1234567", topic})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, "process with PID 1234567 not running", err.Error())
|
||||||
|
|
||||||
|
// Test: Successful command (exit 0)
|
||||||
|
start = time.Now()
|
||||||
|
app, _, stdout, _ = newTestApp()
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-cmd", topic, "sleep", "0.5"}))
|
||||||
|
m = toMessage(t, stdout.String())
|
||||||
|
require.True(t, time.Since(start) >= 500*time.Millisecond)
|
||||||
|
require.Contains(t, m.Message, `Command succeeded after `)
|
||||||
|
require.Contains(t, m.Message, `: sleep 0.5`)
|
||||||
|
|
||||||
|
// Test: Failing command (exit 1)
|
||||||
|
app, _, stdout, _ = newTestApp()
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-cmd", topic, "/bin/false", "false doesn't care about its args"}))
|
||||||
|
m = toMessage(t, stdout.String())
|
||||||
|
require.Contains(t, m.Message, `Command failed after `)
|
||||||
|
require.Contains(t, m.Message, `(exit code 1): /bin/false "false doesn't care about its args"`, m.Message)
|
||||||
|
|
||||||
|
// Test: Non-existing command (hard fail!)
|
||||||
|
app, _, _, _ = newTestApp()
|
||||||
|
err = app.Run([]string{"ntfy", "publish", "--wait-cmd", topic, "does-not-exist-no-really", "really though"})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, `command failed: does-not-exist-no-really "really though", error: exec: "does-not-exist-no-really": executable file not found in $PATH`, err.Error())
|
||||||
|
|
||||||
|
// Tests with NTFY_TOPIC set ////
|
||||||
|
require.Nil(t, os.Setenv("NTFY_TOPIC", topic))
|
||||||
|
|
||||||
|
// Test: Successful command with NTFY_TOPIC
|
||||||
|
app, _, stdout, _ = newTestApp()
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "--env-topic", "--cmd", "echo", "hi there"}))
|
||||||
|
m = toMessage(t, stdout.String())
|
||||||
|
require.Equal(t, "mytopic", m.Topic)
|
||||||
|
|
||||||
|
// Test: Successful --wait-pid with NTFY_TOPIC
|
||||||
|
sleep = exec.Command("sleep", "0.2")
|
||||||
|
require.Nil(t, sleep.Start())
|
||||||
|
go sleep.Wait() // Must be called to release resources
|
||||||
|
app, _, stdout, _ = newTestApp()
|
||||||
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "--env-topic", "--wait-pid", strconv.Itoa(sleep.Process.Pid)}))
|
||||||
|
m = toMessage(t, stdout.String())
|
||||||
|
require.Regexp(t, `Process with PID \d+ exited after .+ms`, m.Message)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func processExists(pid int) bool {
|
||||||
|
_, err := os.FindProcess(pid)
|
||||||
|
return err == nil
|
||||||
|
}
|
|
@ -1,21 +1,35 @@
|
||||||
# Deprecation notices
|
# Deprecation notices
|
||||||
This page is used to list deprecation notices for ntfy. Deprecated commands and options will be
|
This page is used to list deprecation notices for ntfy. Deprecated commands and options will be
|
||||||
**removed after ~3 months** from the time they were deprecated.
|
**removed after 1-3 months** from the time they were deprecated. How long the feature is deprecated
|
||||||
|
before the behavior is changed depends on the severity of the change, and how prominent the feature is.
|
||||||
|
|
||||||
## Active deprecations
|
## Active deprecations
|
||||||
|
|
||||||
### Android app: WebSockets will become the default connection protocol
|
### ntfy CLI: `ntfy publish --env-topic` will be removed
|
||||||
> Active since 2022-03-13, behavior will change in **June 2022**
|
> Active since 2022-06-20, behavior will change end of **July 2022**
|
||||||
|
|
||||||
In future versions of the Android app, instant delivery connections and connections to self-hosted servers will
|
The `ntfy publish --env-topic` option will be removed. It'll still be possible to specify a topic via the
|
||||||
be using the WebSockets protocol. This potentially requires [configuration changes in your proxy](https://ntfy.sh/docs/config/#nginxapache2caddy).
|
`NTFY_TOPIC` environment variable, but it won't be necessary anymore to specify the `--env-topic` flag.
|
||||||
|
|
||||||
Due to [reports of varying battery consumption](https://github.com/binwiederhier/ntfy/issues/190) (which entirely
|
=== "Before"
|
||||||
seems to depend on the phone), JSON HTTP stream support will not be removed. Instead, I'll just flip the default to
|
```
|
||||||
WebSocket in June.
|
$ NTFY_TOPIC=mytopic ntfy publish --env-topic "this is the message"
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "After"
|
||||||
|
```
|
||||||
|
$ NTFY_TOPIC=mytopic ntfy publish "this is the message"
|
||||||
|
```
|
||||||
|
|
||||||
## Previous deprecations
|
## Previous deprecations
|
||||||
|
|
||||||
|
### <del>Android app: WebSockets will become the default connection protocol</del>
|
||||||
|
> Active since 2022-03-13, behavior will not change (deprecation removed 2022-06-20)
|
||||||
|
|
||||||
|
Instant delivery connections and connections to self-hosted servers in the Android app were going to switch
|
||||||
|
to use the WebSockets protocol by default. It was decided to keep JSON stream as the most compatible default
|
||||||
|
and add a notice banner in the Android app instead.
|
||||||
|
|
||||||
### Android app: Using `since=<timestamp>` instead of `since=<id>`
|
### Android app: Using `since=<timestamp>` instead of `since=<id>`
|
||||||
> Active since 2022-02-27, behavior changed with v1.14.0
|
> Active since 2022-02-27, behavior changed with v1.14.0
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|
||||||
|
* ntfy CLI can now [wait for a command or PID](https://ntfy.sh/docs/subscribe/cli/#wait-for-pidcommand) before publishing ([#263](https://github.com/binwiederhier/ntfy/issues/263), thanks to the [original ntfy](https://github.com/dschep/ntfy) for the idea)
|
||||||
* Trace: Log entire HTTP request to simplify debugging (no ticket)
|
* Trace: Log entire HTTP request to simplify debugging (no ticket)
|
||||||
* Allow setting user password via `NTFY_PASSWORD` env variable ([#327](https://github.com/binwiederhier/ntfy/pull/327), thanks to [@Kenix3](https://github.com/Kenix3))
|
* Allow setting user password via `NTFY_PASSWORD` env variable ([#327](https://github.com/binwiederhier/ntfy/pull/327), thanks to [@Kenix3](https://github.com/Kenix3))
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,71 @@ quick ones:
|
||||||
ntfy pub mywebhook
|
ntfy pub mywebhook
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Attaching a local file
|
||||||
|
You can easily upload and attach a local file to a notification:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ntfy pub --file README.md mytopic | jq .
|
||||||
|
{
|
||||||
|
"id": "meIlClVLABJQ",
|
||||||
|
"time": 1655825460,
|
||||||
|
"event": "message",
|
||||||
|
"topic": "mytopic",
|
||||||
|
"message": "You received a file: README.md",
|
||||||
|
"attachment": {
|
||||||
|
"name": "README.md",
|
||||||
|
"type": "text/plain; charset=utf-8",
|
||||||
|
"size": 2892,
|
||||||
|
"expires": 1655836260,
|
||||||
|
"url": "https://ntfy.sh/file/meIlClVLABJQ.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wait for PID/command
|
||||||
|
If you have a long-running command, you may wrap it directly with `ntfy publish --wait-cmd`,
|
||||||
|
or if you forgot to wrap it and it's already running, wait for the process to complete with
|
||||||
|
`ntfy publish --wait-pid`.
|
||||||
|
|
||||||
|
Run a command and wait for it to complete (here: `rsync ...`):
|
||||||
|
|
||||||
|
```
|
||||||
|
$ ntfy pub --wait-cmd mytopic rsync -av ./ root@example.com:/backups/ | jq .
|
||||||
|
{
|
||||||
|
"id": "Re0rWXZQM8WB",
|
||||||
|
"time": 1655825624,
|
||||||
|
"event": "message",
|
||||||
|
"topic": "mytopic",
|
||||||
|
"message": "Command succeeded after 56.553s: rsync -av ./ root@example.com:/backups/"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or, if you already started the long-running process and want to wait for it, you can do this:
|
||||||
|
|
||||||
|
=== "Using a PID directly"
|
||||||
|
```
|
||||||
|
$ ntfy pub --wait-pid 8458 mytopic | jq .
|
||||||
|
{
|
||||||
|
"id": "orM6hJKNYkWb",
|
||||||
|
"time": 1655825827,
|
||||||
|
"event": "message",
|
||||||
|
"topic": "mytopic",
|
||||||
|
"message": "Process with PID 8458 exited after 2.003s"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Using a `pidof`"
|
||||||
|
```
|
||||||
|
$ ntfy pub --wait-pid $(pidof rsync) mytopic | jq .
|
||||||
|
{
|
||||||
|
"id": "orM6hJKNYkWb",
|
||||||
|
"time": 1655825827,
|
||||||
|
"event": "message",
|
||||||
|
"topic": "mytopic",
|
||||||
|
"message": "Process with PID 8458 exited after 2.003s"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Subscribe to topics
|
## Subscribe to topics
|
||||||
You can subscribe to topics using `ntfy subscribe`. Depending on how it is called, this command
|
You can subscribe to topics using `ntfy subscribe`. Depending on how it is called, this command
|
||||||
will either print or execute a command for every arriving message. There are a few different ways
|
will either print or execute a command for every arriving message. There are a few different ways
|
||||||
|
|
52
util/util.go
52
util/util.go
|
@ -26,6 +26,7 @@ var (
|
||||||
randomMutex = sync.Mutex{}
|
randomMutex = sync.Mutex{}
|
||||||
sizeStrRegex = regexp.MustCompile(`(?i)^(\d+)([gmkb])?$`)
|
sizeStrRegex = regexp.MustCompile(`(?i)^(\d+)([gmkb])?$`)
|
||||||
errInvalidPriority = errors.New("invalid priority")
|
errInvalidPriority = errors.New("invalid priority")
|
||||||
|
noQuotesRegex = regexp.MustCompile(`^[-_./:@a-zA-Z0-9]+$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// FileExists checks if a file exists, and returns true if it does
|
// FileExists checks if a file exists, and returns true if it does
|
||||||
|
@ -120,38 +121,6 @@ func ValidRandomString(s string, length int) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// DurationToHuman converts a duration to a human-readable format
|
|
||||||
func DurationToHuman(d time.Duration) (str string) {
|
|
||||||
if d == 0 {
|
|
||||||
return "0"
|
|
||||||
}
|
|
||||||
|
|
||||||
d = d.Round(time.Second)
|
|
||||||
days := d / time.Hour / 24
|
|
||||||
if days > 0 {
|
|
||||||
str += fmt.Sprintf("%dd", days)
|
|
||||||
}
|
|
||||||
d -= days * time.Hour * 24
|
|
||||||
|
|
||||||
hours := d / time.Hour
|
|
||||||
if hours > 0 {
|
|
||||||
str += fmt.Sprintf("%dh", hours)
|
|
||||||
}
|
|
||||||
d -= hours * time.Hour
|
|
||||||
|
|
||||||
minutes := d / time.Minute
|
|
||||||
if minutes > 0 {
|
|
||||||
str += fmt.Sprintf("%dm", minutes)
|
|
||||||
}
|
|
||||||
d -= minutes * time.Minute
|
|
||||||
|
|
||||||
seconds := d / time.Second
|
|
||||||
if seconds > 0 {
|
|
||||||
str += fmt.Sprintf("%ds", seconds)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParsePriority parses a priority string into its equivalent integer value
|
// ParsePriority parses a priority string into its equivalent integer value
|
||||||
func ParsePriority(priority string) (int, error) {
|
func ParsePriority(priority string) (int, error) {
|
||||||
switch strings.TrimSpace(strings.ToLower(priority)) {
|
switch strings.TrimSpace(strings.ToLower(priority)) {
|
||||||
|
@ -286,3 +255,22 @@ func MaybeMarshalJSON(v interface{}) string {
|
||||||
}
|
}
|
||||||
return string(jsonBytes)
|
return string(jsonBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QuoteCommand combines a command array to a string, quoting arguments that need quoting.
|
||||||
|
// This function is naive, and sometimes wrong. It is only meant for lo pretty-printing a command.
|
||||||
|
//
|
||||||
|
// Warning: Never use this function with the intent to run the resulting command.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// []string{"ls", "-al", "Document Folder"} -> ls -al "Document Folder"
|
||||||
|
func QuoteCommand(command []string) string {
|
||||||
|
var quoted []string
|
||||||
|
for _, c := range command {
|
||||||
|
if noQuotesRegex.MatchString(c) {
|
||||||
|
quoted = append(quoted, c)
|
||||||
|
} else {
|
||||||
|
quoted = append(quoted, fmt.Sprintf(`"%s"`, c))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(quoted, " ")
|
||||||
|
}
|
||||||
|
|
|
@ -5,33 +5,8 @@ import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDurationToHuman_SevenDays(t *testing.T) {
|
|
||||||
d := 7 * 24 * time.Hour
|
|
||||||
require.Equal(t, "7d", DurationToHuman(d))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDurationToHuman_MoreThanOneDay(t *testing.T) {
|
|
||||||
d := 49 * time.Hour
|
|
||||||
require.Equal(t, "2d1h", DurationToHuman(d))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDurationToHuman_LessThanOneDay(t *testing.T) {
|
|
||||||
d := 17*time.Hour + 15*time.Minute
|
|
||||||
require.Equal(t, "17h15m", DurationToHuman(d))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDurationToHuman_TenOfThings(t *testing.T) {
|
|
||||||
d := 10*time.Hour + 10*time.Minute + 10*time.Second
|
|
||||||
require.Equal(t, "10h10m10s", DurationToHuman(d))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDurationToHuman_Zero(t *testing.T) {
|
|
||||||
require.Equal(t, "0", DurationToHuman(0))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRandomString(t *testing.T) {
|
func TestRandomString(t *testing.T) {
|
||||||
s1 := RandomString(10)
|
s1 := RandomString(10)
|
||||||
s2 := RandomString(10)
|
s2 := RandomString(10)
|
||||||
|
@ -162,3 +137,9 @@ func TestLastString(t *testing.T) {
|
||||||
require.Equal(t, "last", LastString([]string{"first", "second", "last"}, "default"))
|
require.Equal(t, "last", LastString([]string{"first", "second", "last"}, "default"))
|
||||||
require.Equal(t, "default", LastString([]string{}, "default"))
|
require.Equal(t, "default", LastString([]string{}, "default"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestQuoteCommand(t *testing.T) {
|
||||||
|
require.Equal(t, `ls -al "Document Folder"`, QuoteCommand([]string{"ls", "-al", "Document Folder"}))
|
||||||
|
require.Equal(t, `rsync -av /home/phil/ root@example.com:/home/phil/`, QuoteCommand([]string{"rsync", "-av", "/home/phil/", "root@example.com:/home/phil/"}))
|
||||||
|
require.Equal(t, `/home/sweet/home "Äöü this is a test" "\a\b"`, QuoteCommand([]string{"/home/sweet/home", "Äöü this is a test", "\\a\\b"}))
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue