2021-12-17 02:33:01 +01:00
package cmd
import (
"errors"
2021-12-20 03:01:49 +01:00
"fmt"
2021-12-17 02:33:01 +01:00
"github.com/urfave/cli/v2"
"heckel.io/ntfy/client"
2022-06-20 16:56:45 +02:00
"heckel.io/ntfy/log"
2022-02-02 05:39:57 +01:00
"heckel.io/ntfy/util"
2022-01-13 03:24:48 +01:00
"io"
"os"
2022-06-21 03:57:54 +02:00
"os/exec"
2022-01-13 03:24:48 +01:00
"path/filepath"
2021-12-17 02:33:01 +01:00
"strings"
2022-06-20 16:56:45 +02:00
"time"
2021-12-17 02:33:01 +01:00
)
2022-05-09 17:03:40 +02:00
func init ( ) {
2022-06-21 05:03:16 +02:00
commands = append ( commands , cmdPublish )
2022-05-09 17:03:40 +02:00
}
2022-05-30 04:14:14 +02:00
var flagsPublish = append (
2023-02-06 05:34:27 +01:00
append ( [ ] cli . Flag { } , flagsDefault ... ) ,
2022-05-30 04:14:14 +02:00
& 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" } ,
2022-06-21 03:57:54 +02:00
& cli . StringFlag { Name : "message" , Aliases : [ ] string { "m" } , EnvVars : [ ] string { "NTFY_MESSAGE" } , Usage : "message body" } ,
2022-05-30 04:14:14 +02:00
& 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 : "delay" , Aliases : [ ] string { "at" , "in" , "D" } , EnvVars : [ ] string { "NTFY_DELAY" } , Usage : "delay/schedule message" } ,
& cli . StringFlag { Name : "click" , Aliases : [ ] string { "U" } , EnvVars : [ ] string { "NTFY_CLICK" } , Usage : "URL to open when notification is clicked" } ,
2022-07-16 21:31:03 +02:00
& cli . StringFlag { Name : "icon" , Aliases : [ ] string { "i" } , EnvVars : [ ] string { "NTFY_ICON" } , Usage : "URL to use as notification icon" } ,
2022-05-30 04:14:14 +02:00
& cli . StringFlag { Name : "actions" , Aliases : [ ] string { "A" } , EnvVars : [ ] string { "NTFY_ACTIONS" } , Usage : "actions JSON array or simple definition" } ,
& cli . StringFlag { Name : "attach" , Aliases : [ ] string { "a" } , EnvVars : [ ] string { "NTFY_ATTACH" } , Usage : "URL to send as an external attachment" } ,
& cli . StringFlag { Name : "filename" , Aliases : [ ] string { "name" , "n" } , EnvVars : [ ] string { "NTFY_FILENAME" } , Usage : "filename for the 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 : "user" , Aliases : [ ] string { "u" } , EnvVars : [ ] string { "NTFY_USER" } , Usage : "username[:password] used to auth against the server" } ,
2023-02-14 03:35:58 +01:00
& cli . StringFlag { Name : "token" , Aliases : [ ] string { "k" } , EnvVars : [ ] string { "NTFY_TOKEN" } , Usage : "access token used to auth against the server" } ,
2022-06-25 01:13:10 +02:00
& cli . IntFlag { Name : "wait-pid" , Aliases : [ ] string { "wait_pid" , "pid" } , EnvVars : [ ] string { "NTFY_WAIT_PID" } , Usage : "wait until PID exits before publishing" } ,
& cli . BoolFlag { Name : "wait-cmd" , Aliases : [ ] string { "wait_cmd" , "cmd" , "done" } , EnvVars : [ ] string { "NTFY_WAIT_CMD" } , Usage : "run command and wait until it finishes before publishing" } ,
& cli . BoolFlag { Name : "no-cache" , Aliases : [ ] string { "no_cache" , "C" } , EnvVars : [ ] string { "NTFY_NO_CACHE" } , Usage : "do not cache message server-side" } ,
& cli . BoolFlag { Name : "no-firebase" , Aliases : [ ] string { "no_firebase" , "F" } , EnvVars : [ ] string { "NTFY_NO_FIREBASE" } , Usage : "do not forward message to Firebase" } ,
2022-05-30 04:14:14 +02:00
& cli . BoolFlag { Name : "quiet" , Aliases : [ ] string { "q" } , EnvVars : [ ] string { "NTFY_QUIET" } , Usage : "do not print message" } ,
)
2021-12-17 02:33:01 +01:00
var cmdPublish = & cli . Command {
2022-06-21 05:03:16 +02:00
Name : "publish" ,
Aliases : [ ] string { "pub" , "send" , "trigger" } ,
Usage : "Send message via a ntfy server" ,
UsageText : ` ntfy publish [ OPTIONS . . ] TOPIC [ MESSAGE ... ]
2022-06-21 17:18:35 +02:00
ntfy publish [ OPTIONS . . ] -- wait - cmd COMMAND ...
2022-11-28 17:06:47 +01:00
NTFY_TOPIC = . . ntfy publish [ OPTIONS . . ] [ MESSAGE ... ] ` ,
2022-06-21 05:03:16 +02:00
Action : execPublish ,
Category : categoryClient ,
Flags : flagsPublish ,
Before : initLogFunc ,
2021-12-17 02:33:01 +01:00
Description : ` Publish a message to a ntfy server .
Examples :
ntfy publish mytopic This is my message # Send simple message
ntfy send myserver . com / mytopic "This is my message" # Send message to different default host
ntfy pub - p high backups "Backups failed" # Send high priority message
ntfy pub -- tags = warning , skull backups "Backups failed" # Add tags / emojis to message
ntfy pub -- delay = 10 s delayed_topic Laterzz # Delay message by 10 s
ntfy pub -- at = 8 : 30 am delayed_topic Laterzz # Send message at 8 : 30 am
2021-12-24 15:01:29 +01:00
ntfy pub - e phil @ example . com alerts ' App is down ! ' # Also send email to phil @ example . com
2022-01-05 00:11:36 +01:00
ntfy pub -- click = "https://reddit.com" redd ' New msg ' # Opens Reddit when notification is clicked
2022-07-16 21:31:03 +02:00
ntfy pub -- icon = "http://some.tld/icon.png" ' Icon ! ' # Send notification with custom icon
2022-01-13 03:24:48 +01:00
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
2022-02-02 05:39:57 +01:00
ntfy pub - u phil : mypass secret Psst # Publish with username / password
2022-06-21 05:03:16 +02:00
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
2022-02-02 05:39:57 +01:00
NTFY_USER = phil : mypass ntfy pub secret Psst # Use env variables to set username / password
2022-11-28 17:06:47 +01:00
NTFY_TOPIC = mytopic ntfy pub "some message" # Use NTFY_TOPIC variable as topic
2022-01-13 03:24:48 +01:00
cat flower . jpg | ntfy pub -- file = - flowers ' Nice ! ' # Same as above , send image . jpg as attachment
2021-12-18 04:38:29 +01:00
ntfy trigger mywebhook # Sending without message , useful for webhooks
2022-02-02 05:39:57 +01:00
2021-12-17 02:33:01 +01:00
Please also check out the docs on publishing messages . Especially for the -- tags and -- delay options ,
2021-12-20 03:01:49 +01:00
it has incredibly useful information : https : //ntfy.sh/docs/publish/.
2022-05-10 03:25:00 +02:00
` + clientCommandDescriptionSuffix ,
2021-12-17 02:33:01 +01:00
}
func execPublish ( c * cli . Context ) error {
2021-12-20 03:01:49 +01:00
conf , err := loadConfig ( c )
if err != nil {
return err
2021-12-17 02:33:01 +01:00
}
title := c . String ( "title" )
priority := c . String ( "priority" )
tags := c . String ( "tags" )
delay := c . String ( "delay" )
2022-01-05 00:11:36 +01:00
click := c . String ( "click" )
2022-07-16 21:31:03 +02:00
icon := c . String ( "icon" )
2022-04-20 22:31:25 +02:00
actions := c . String ( "actions" )
2022-01-13 03:24:48 +01:00
attach := c . String ( "attach" )
filename := c . String ( "filename" )
file := c . String ( "file" )
2021-12-24 15:01:29 +01:00
email := c . String ( "email" )
2022-02-02 05:39:57 +01:00
user := c . String ( "user" )
2023-02-14 03:35:58 +01:00
token := c . String ( "token" )
2021-12-17 02:33:01 +01:00
noCache := c . Bool ( "no-cache" )
noFirebase := c . Bool ( "no-firebase" )
2021-12-20 03:01:49 +01:00
quiet := c . Bool ( "quiet" )
2022-06-21 05:03:16 +02:00
pid := c . Int ( "wait-pid" )
2023-02-14 03:35:58 +01:00
// Checks
if user != "" && token != "" {
return errors . New ( "cannot set both --user and --token" )
}
// Do the things
2022-06-21 05:03:16 +02:00
topic , message , command , err := parseTopicMessageCommand ( c )
2022-06-21 03:57:54 +02:00
if err != nil {
return err
2021-12-18 04:38:29 +01:00
}
2021-12-17 02:33:01 +01:00
var options [ ] client . PublishOption
if title != "" {
options = append ( options , client . WithTitle ( title ) )
}
if priority != "" {
options = append ( options , client . WithPriority ( priority ) )
}
if tags != "" {
2021-12-19 20:27:26 +01:00
options = append ( options , client . WithTagsList ( tags ) )
2021-12-17 02:33:01 +01:00
}
if delay != "" {
options = append ( options , client . WithDelay ( delay ) )
}
2022-01-05 00:11:36 +01:00
if click != "" {
2022-01-13 03:24:48 +01:00
options = append ( options , client . WithClick ( click ) )
}
2022-07-16 21:31:03 +02:00
if icon != "" {
options = append ( options , client . WithIcon ( icon ) )
}
2022-04-20 22:31:25 +02:00
if actions != "" {
options = append ( options , client . WithActions ( strings . ReplaceAll ( actions , "\n" , " " ) ) )
}
2022-01-13 03:24:48 +01:00
if attach != "" {
options = append ( options , client . WithAttach ( attach ) )
}
if filename != "" {
options = append ( options , client . WithFilename ( filename ) )
2022-01-05 00:11:36 +01:00
}
2021-12-24 15:01:29 +01:00
if email != "" {
options = append ( options , client . WithEmail ( email ) )
}
2021-12-17 02:33:01 +01:00
if noCache {
options = append ( options , client . WithNoCache ( ) )
}
if noFirebase {
options = append ( options , client . WithNoFirebase ( ) )
}
2023-02-14 03:35:58 +01:00
if token != "" {
options = append ( options , client . WithBearerAuth ( token ) )
2023-03-07 05:12:46 +01:00
} else if user != "" {
var pass string
parts := strings . SplitN ( user , ":" , 2 )
if len ( parts ) == 2 {
user = parts [ 0 ]
pass = parts [ 1 ]
} else {
fmt . Fprint ( c . App . ErrWriter , "Enter Password: " )
p , err := util . ReadPassword ( c . App . Reader )
if err != nil {
return err
2022-02-02 05:39:57 +01:00
}
2023-03-07 05:12:46 +01:00
pass = string ( p )
fmt . Fprintf ( c . App . ErrWriter , "\r%s\r" , strings . Repeat ( " " , 20 ) )
2022-02-02 05:39:57 +01:00
}
2023-03-07 05:12:46 +01:00
options = append ( options , client . WithBasicAuth ( user , pass ) )
} else if conf . DefaultToken != "" {
options = append ( options , client . WithBearerAuth ( conf . DefaultToken ) )
} else if conf . DefaultUser != "" && conf . DefaultPassword != nil {
options = append ( options , client . WithBasicAuth ( conf . DefaultUser , * conf . DefaultPassword ) )
2022-02-02 05:39:57 +01:00
}
2022-06-21 03:57:54 +02:00
if pid > 0 {
2022-06-21 17:18:35 +02:00
newMessage , err := waitForProcess ( pid )
if err != nil {
2022-06-21 03:57:54 +02:00
return err
2022-06-21 17:18:35 +02:00
} else if message == "" {
message = newMessage
2022-06-21 05:03:16 +02:00
}
2022-06-21 03:57:54 +02:00
} else if len ( command ) > 0 {
2022-06-21 17:18:35 +02:00
newMessage , err := runAndWaitForCommand ( command )
2022-06-21 03:57:54 +02:00
if err != nil {
return err
} else if message == "" {
2022-06-21 17:18:35 +02:00
message = newMessage
2022-06-21 03:57:54 +02:00
}
}
2022-01-13 03:24:48 +01:00
var body io . Reader
if file == "" {
body = strings . NewReader ( message )
} else {
if message != "" {
options = append ( options , client . WithMessage ( message ) )
}
if file == "-" {
if filename == "" {
options = append ( options , client . WithFilename ( "stdin" ) )
}
body = c . App . Reader
} else {
if filename == "" {
options = append ( options , client . WithFilename ( filepath . Base ( file ) ) )
}
body , err = os . Open ( file )
if err != nil {
return err
}
}
}
2021-12-20 03:01:49 +01:00
cl := client . New ( conf )
2022-01-13 03:24:48 +01:00
m , err := cl . PublishReader ( topic , body , options ... )
2021-12-18 20:43:27 +01:00
if err != nil {
return err
}
2021-12-20 03:01:49 +01:00
if ! quiet {
fmt . Fprintln ( c . App . Writer , strings . TrimSpace ( m . Raw ) )
}
return nil
2021-12-17 02:33:01 +01:00
}
2022-06-20 16:56:45 +02:00
2022-06-21 17:18:35 +02:00
// parseTopicMessageCommand reads the topic and the remaining arguments from the context.
// There are a few cases to consider:
2022-09-27 18:37:02 +02:00
//
// ntfy publish <topic> [<message>]
// ntfy publish --wait-cmd <topic> <command>
// NTFY_TOPIC=.. ntfy publish [<message>]
// NTFY_TOPIC=.. ntfy publish --wait-cmd <command>
2022-06-21 05:03:16 +02:00
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 ) {
2022-11-28 17:06:47 +01:00
envTopic := os . Getenv ( "NTFY_TOPIC" )
if envTopic != "" {
topic = envTopic
2022-06-21 05:03:16 +02:00
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 { }
}
2022-06-21 17:18:35 +02:00
func waitForProcess ( pid int ) ( message string , err error ) {
2022-06-20 16:56:45 +02:00
if ! processExists ( pid ) {
2022-06-21 17:18:35 +02:00
return "" , fmt . Errorf ( "process with PID %d not running" , pid )
2022-06-20 16:56:45 +02:00
}
2022-06-21 17:18:35 +02:00
start := time . Now ( )
2022-06-20 16:56:45 +02:00
log . Debug ( "Waiting for process with PID %d to exit" , pid )
for processExists ( pid ) {
time . Sleep ( 500 * time . Millisecond )
}
2022-06-21 17:18:35 +02:00
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
2022-06-20 16:56:45 +02:00
}
2022-06-21 03:57:54 +02:00
func runAndWaitForCommand ( command [ ] string ) ( message string , err error ) {
2022-06-21 05:03:16 +02:00
prettyCmd := util . QuoteCommand ( command )
2022-06-21 03:57:54 +02:00
log . Debug ( "Running command: %s" , prettyCmd )
2022-06-21 17:18:35 +02:00
start := time . Now ( )
2022-06-21 03:57:54 +02:00
cmd := exec . Command ( command [ 0 ] , command [ 1 : ] ... )
if log . IsTrace ( ) {
cmd . Stdout = os . Stdout
cmd . Stderr = os . Stderr
}
2022-06-21 17:18:35 +02:00
err = cmd . Run ( )
runtime := time . Since ( start ) . Round ( time . Millisecond )
if err != nil {
2022-06-21 03:57:54 +02:00
if exitError , ok := err . ( * exec . ExitError ) ; ok {
2022-06-21 17:18:35 +02:00
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
2022-06-21 03:57:54 +02:00
}
2022-06-21 17:18:35 +02:00
// Hard fail when command does not exist or could not be properly launched
return "" , fmt . Errorf ( "command failed: %s, error: %s" , prettyCmd , err . Error ( ) )
2022-06-21 03:57:54 +02:00
}
2022-06-21 17:18:35 +02:00
log . Debug ( "Command succeeded after %s: %s" , runtime , prettyCmd )
return fmt . Sprintf ( "Command succeeded after %s: %s" , runtime , prettyCmd ) , nil
2022-06-21 03:57:54 +02:00
}