2021-12-17 01:33:01 +00:00
package cmd
import (
"errors"
2021-12-20 02:01:49 +00:00
"fmt"
2021-12-17 01:33:01 +00:00
"github.com/urfave/cli/v2"
"heckel.io/ntfy/client"
2022-06-20 14:56:45 +00:00
"heckel.io/ntfy/log"
2022-02-02 04:39:57 +00:00
"heckel.io/ntfy/util"
2022-01-13 02:24:48 +00:00
"io"
"os"
2022-06-21 01:57:54 +00:00
"os/exec"
2022-01-13 02:24:48 +00:00
"path/filepath"
2022-06-21 01:57:54 +00:00
"regexp"
2021-12-17 01:33:01 +00:00
"strings"
2022-06-20 14:56:45 +00:00
"time"
2021-12-17 01:33:01 +00:00
)
2022-05-09 15:03:40 +00:00
func init ( ) {
2022-06-21 01:57:54 +00:00
commands = append ( commands , cmdPublish , cmdDone )
2022-05-09 15:03:40 +00:00
}
2022-05-30 02:14:14 +00:00
var flagsPublish = append (
flagsDefault ,
& 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 01:57:54 +00:00
& cli . StringFlag { Name : "message" , Aliases : [ ] string { "m" } , EnvVars : [ ] string { "NTFY_MESSAGE" } , Usage : "message body" } ,
2022-05-30 02:14:14 +00: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" } ,
& 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" } ,
2022-06-20 14:56:45 +00:00
& cli . IntFlag { Name : "pid" , Aliases : [ ] string { "done" , "w" } , EnvVars : [ ] string { "NTFY_PID" } , Usage : "monitor process with given PID and publish when it exists" } ,
2022-05-30 02:14:14 +00:00
& 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 : "env-topic" , Aliases : [ ] string { "P" } , EnvVars : [ ] string { "NTFY_ENV_TOPIC" } , Usage : "use topic from NTFY_TOPIC env variable" } ,
& cli . BoolFlag { Name : "quiet" , Aliases : [ ] string { "q" } , EnvVars : [ ] string { "NTFY_QUIET" } , Usage : "do not print message" } ,
)
2021-12-17 01:33:01 +00:00
var cmdPublish = & cli . Command {
Name : "publish" ,
2021-12-20 02:01:49 +00:00
Aliases : [ ] string { "pub" , "send" , "trigger" } ,
2021-12-17 01:33:01 +00:00
Usage : "Send message via a ntfy server" ,
2022-05-30 02:14:14 +00:00
UsageText : "ntfy publish [OPTIONS..] TOPIC [MESSAGE]\nNTFY_TOPIC=.. ntfy publish [OPTIONS..] -P [MESSAGE]" ,
2021-12-17 01:33:01 +00:00
Action : execPublish ,
2022-01-23 06:00:38 +00:00
Category : categoryClient ,
2022-05-30 02:14:14 +00:00
Flags : flagsPublish ,
2022-06-01 20:57:35 +00:00
Before : initLogFunc ,
2021-12-17 01:33:01 +00: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 14:01:29 +00:00
ntfy pub - e phil @ example . com alerts ' App is down ! ' # Also send email to phil @ example . com
2022-01-04 23:11:36 +00:00
ntfy pub -- click = "https://reddit.com" redd ' New msg ' # Opens Reddit when notification is clicked
2022-01-13 02:24:48 +00: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 04:39:57 +00:00
ntfy pub - u phil : mypass secret Psst # Publish with 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
2022-01-13 02:24:48 +00:00
cat flower . jpg | ntfy pub -- file = - flowers ' Nice ! ' # Same as above , send image . jpg as attachment
2021-12-18 03:38:29 +00:00
ntfy trigger mywebhook # Sending without message , useful for webhooks
2022-02-02 04:39:57 +00:00
2021-12-17 01:33:01 +00:00
Please also check out the docs on publishing messages . Especially for the -- tags and -- delay options ,
2021-12-20 02:01:49 +00:00
it has incredibly useful information : https : //ntfy.sh/docs/publish/.
2022-05-10 01:25:00 +00:00
` + clientCommandDescriptionSuffix ,
2021-12-17 01:33:01 +00:00
}
2022-06-21 01:57:54 +00:00
var cmdDone = & cli . Command {
Name : "done" ,
Usage : "xxx" ,
UsageText : "xxx" ,
Action : execDone ,
Category : categoryClient ,
Flags : flagsPublish ,
Before : initLogFunc ,
Description : ` xxx
` + clientCommandDescriptionSuffix ,
}
func execDone ( c * cli . Context ) error {
return execPublishInternal ( c , true )
}
2021-12-17 01:33:01 +00:00
func execPublish ( c * cli . Context ) error {
2022-06-21 01:57:54 +00:00
return execPublishInternal ( c , false )
}
func parseTopicMessageCommand ( c * cli . Context , isDoneCommand bool ) ( topic string , message string , command [ ] string , err error ) {
// 1. ntfy done <topic> <command>
// 2. ntfy done --pid <pid> <topic> [<message>]
// 3. NTFY_TOPIC=.. ntfy done <command>
// 4. NTFY_TOPIC=.. ntfy done --pid <pid> [<message>]
// 5. ntfy publish <topic> [<message>]
// 6. NTFY_TOPIC=.. ntfy publish [<message>]
var args [ ] string
topic , args , err = parseTopicAndArgs ( c )
if err != nil {
return
}
if isDoneCommand {
if c . Int ( "pid" ) > 0 {
message = strings . Join ( args , " " )
} else if len ( args ) > 0 {
command = args
} else {
err = errors . New ( "must either specify --pid or a command" )
}
} 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 {
topic = os . Getenv ( "NTFY_TOPIC" )
if topic == "" {
return "" , nil , errors . New ( "if --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" )
}
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 execPublishInternal ( c * cli . Context , doneCmd bool ) error {
2021-12-20 02:01:49 +00:00
conf , err := loadConfig ( c )
if err != nil {
return err
2021-12-17 01:33:01 +00:00
}
title := c . String ( "title" )
priority := c . String ( "priority" )
tags := c . String ( "tags" )
delay := c . String ( "delay" )
2022-01-04 23:11:36 +00:00
click := c . String ( "click" )
2022-04-20 20:31:25 +00:00
actions := c . String ( "actions" )
2022-01-13 02:24:48 +00:00
attach := c . String ( "attach" )
filename := c . String ( "filename" )
file := c . String ( "file" )
2021-12-24 14:01:29 +00:00
email := c . String ( "email" )
2022-02-02 04:39:57 +00:00
user := c . String ( "user" )
2021-12-17 01:33:01 +00:00
noCache := c . Bool ( "no-cache" )
noFirebase := c . Bool ( "no-firebase" )
2021-12-20 02:01:49 +00:00
quiet := c . Bool ( "quiet" )
2022-06-21 01:57:54 +00:00
pid := c . Int ( "pid" )
topic , message , command , err := parseTopicMessageCommand ( c , doneCmd )
if err != nil {
return err
2021-12-18 03:38:29 +00:00
}
2021-12-17 01:33:01 +00: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 19:27:26 +00:00
options = append ( options , client . WithTagsList ( tags ) )
2021-12-17 01:33:01 +00:00
}
if delay != "" {
options = append ( options , client . WithDelay ( delay ) )
}
2022-01-04 23:11:36 +00:00
if click != "" {
2022-01-13 02:24:48 +00:00
options = append ( options , client . WithClick ( click ) )
}
2022-04-20 20:31:25 +00:00
if actions != "" {
options = append ( options , client . WithActions ( strings . ReplaceAll ( actions , "\n" , " " ) ) )
}
2022-01-13 02:24:48 +00:00
if attach != "" {
options = append ( options , client . WithAttach ( attach ) )
}
if filename != "" {
options = append ( options , client . WithFilename ( filename ) )
2022-01-04 23:11:36 +00:00
}
2021-12-24 14:01:29 +00:00
if email != "" {
options = append ( options , client . WithEmail ( email ) )
}
2021-12-17 01:33:01 +00:00
if noCache {
options = append ( options , client . WithNoCache ( ) )
}
if noFirebase {
options = append ( options , client . WithNoFirebase ( ) )
}
2022-02-02 04:39:57 +00:00
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
}
pass = string ( p )
fmt . Fprintf ( c . App . ErrWriter , "\r%s\r" , strings . Repeat ( " " , 20 ) )
}
options = append ( options , client . WithBasicAuth ( user , pass ) )
}
2022-06-21 01:57:54 +00:00
if pid > 0 {
if err := waitForProcess ( pid ) ; err != nil {
return err
}
} else if len ( command ) > 0 {
cmdResultMessage , err := runAndWaitForCommand ( command )
if err != nil {
return err
} else if message == "" {
message = cmdResultMessage
}
}
2022-01-13 02:24:48 +00: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 02:01:49 +00:00
cl := client . New ( conf )
2022-01-13 02:24:48 +00:00
m , err := cl . PublishReader ( topic , body , options ... )
2021-12-18 19:43:27 +00:00
if err != nil {
return err
}
2021-12-20 02:01:49 +00:00
if ! quiet {
fmt . Fprintln ( c . App . Writer , strings . TrimSpace ( m . Raw ) )
}
return nil
2021-12-17 01:33:01 +00:00
}
2022-06-20 14:56:45 +00:00
func waitForProcess ( pid int ) error {
if ! processExists ( pid ) {
return fmt . Errorf ( "process with PID %d not running" , pid )
}
log . Debug ( "Waiting for process with PID %d to exit" , pid )
for processExists ( pid ) {
time . Sleep ( 500 * time . Millisecond )
}
log . Debug ( "Process with PID %d exited" , pid )
return nil
}
2022-06-21 01:57:54 +00:00
func runAndWaitForCommand ( command [ ] string ) ( message string , err error ) {
prettyCmd := formatCommand ( command )
log . Debug ( "Running command: %s" , prettyCmd )
cmd := exec . Command ( command [ 0 ] , command [ 1 : ] ... )
if log . IsTrace ( ) {
cmd . Stdout = os . Stdout
cmd . Stderr = os . Stderr
}
if err := cmd . Run ( ) ; err != nil {
if exitError , ok := err . ( * exec . ExitError ) ; ok {
message = fmt . Sprintf ( "Command failed (exit code %d): %s" , exitError . ExitCode ( ) , prettyCmd )
} else {
message = fmt . Sprintf ( "Command failed: %s, error: %s" , prettyCmd , err . Error ( ) )
}
} else {
message = fmt . Sprintf ( "Command done: %s" , prettyCmd )
}
log . Debug ( message )
return message , nil
}
func formatCommand ( command [ ] string ) string {
quoted := [ ] string { command [ 0 ] }
noQuotesRegex := regexp . MustCompile ( ` ^[-_./a-z0-9]+$ ` )
for _ , c := range command [ 1 : ] {
if noQuotesRegex . MatchString ( c ) {
quoted = append ( quoted , c )
} else {
quoted = append ( quoted , fmt . Sprintf ( ` "%s" ` , c ) )
}
}
return strings . Join ( quoted , " " )
}