subscribe.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. package cmd
  2. import (
  3. "errors"
  4. "fmt"
  5. "github.com/urfave/cli/v2"
  6. "gopkg.in/yaml.v2"
  7. "heckel.io/ntfy/client"
  8. "heckel.io/ntfy/util"
  9. "log"
  10. "os"
  11. "os/exec"
  12. "os/user"
  13. "strings"
  14. )
  15. var cmdSubscribe = &cli.Command{
  16. Name: "subscribe",
  17. Aliases: []string{"sub"},
  18. Usage: "Subscribe to one or more topics on a ntfy server",
  19. UsageText: "ntfy subscribe [OPTIONS..] [TOPIC]",
  20. Action: execSubscribe,
  21. OnUsageError: func(context *cli.Context, err error, isSubcommand bool) error {
  22. println("ee")
  23. return nil
  24. },
  25. Flags: []cli.Flag{
  26. &cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "config file `FILE`"},
  27. &cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"},
  28. &cli.BoolFlag{Name: "from-config", Aliases: []string{"C"}, Usage: "read subscriptions from config file (service mode)"},
  29. &cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"},
  30. &cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"},
  31. &cli.BoolFlag{Name: "verbose", Aliases: []string{"v"}, Usage: "print verbose output"},
  32. },
  33. Description: `Subscribe to a topic from a ntfy server, and either print or execute a command for
  34. every arriving message. There are 3 modes in which the command can be run:
  35. ntfy subscribe TOPIC
  36. This prints the JSON representation of every incoming message. It is useful when you
  37. have a command that wants to stream-read incoming JSON messages. Unless --poll is passed,
  38. this command stays open forever.
  39. Examples:
  40. ntfy subscribe mytopic # Prints JSON for incoming messages for ntfy.sh/mytopic
  41. ntfy sub home.lan/backups # Subscribe to topic on different server
  42. ntfy sub --poll home.lan/backups # Just query for latest messages and exit
  43. ntfy subscribe TOPIC COMMAND
  44. This executes COMMAND for every incoming messages. The message fields are passed to the
  45. command as environment variables:
  46. Variable Aliases Description
  47. --------------- --------------- -----------------------------------
  48. $NTFY_ID $id Unique message ID
  49. $NTFY_TIME $time Unix timestamp of the message delivery
  50. $NTFY_TOPIC $topic Topic name
  51. $NTFY_MESSAGE $message, $m Message body
  52. $NTFY_TITLE $title, $t Message title
  53. $NTFY_PRIORITY $priority, $p Message priority (1=min, 5=max)
  54. $NTFY_TAGS $tags, $ta Message tags (comma separated list)
  55. Examples:
  56. ntfy sub mytopic 'notify-send "$m"' # Execute command for incoming messages
  57. ntfy sub topic1 /my/script.sh # Execute script for incoming messages
  58. ntfy subscribe --from-config
  59. Service mode (used in ntfy-client.service). This reads the config file (/etc/ntfy/client.yml
  60. or ~/.config/ntfy/client.yml) and sets up subscriptions for every topic in the "subscribe:"
  61. block (see config file).
  62. Examples:
  63. ntfy sub --from-config # Read topics from config file
  64. ntfy sub --config=/my/client.yml --from-config # Read topics from alternate config file
  65. `,
  66. }
  67. func execSubscribe(c *cli.Context) error {
  68. // Read config and options
  69. conf, err := loadConfig(c)
  70. if err != nil {
  71. return err
  72. }
  73. cl := client.New(conf)
  74. since := c.String("since")
  75. poll := c.Bool("poll")
  76. scheduled := c.Bool("scheduled")
  77. fromConfig := c.Bool("from-config")
  78. topic := c.Args().Get(0)
  79. command := c.Args().Get(1)
  80. if !fromConfig {
  81. conf.Subscribe = nil // wipe if --from-config not passed
  82. }
  83. var options []client.SubscribeOption
  84. if since != "" {
  85. options = append(options, client.WithSince(since))
  86. }
  87. if poll {
  88. options = append(options, client.WithPoll())
  89. }
  90. if scheduled {
  91. options = append(options, client.WithScheduled())
  92. }
  93. if topic == "" && len(conf.Subscribe) == 0 {
  94. return errors.New("must specify topic, type 'ntfy subscribe --help' for help")
  95. }
  96. // Execute poll or subscribe
  97. if poll {
  98. return execPoll(c, cl, conf, topic, command, options...)
  99. }
  100. return execSubscribeInternal(c, cl, conf, topic, command, options...)
  101. }
  102. func execPoll(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
  103. for _, s := range conf.Subscribe { // may be nil
  104. if err := execPollSingle(c, cl, s.Topic, s.Command, options...); err != nil {
  105. return err
  106. }
  107. }
  108. if topic != "" {
  109. if err := execPollSingle(c, cl, topic, command, options...); err != nil {
  110. return err
  111. }
  112. }
  113. return nil
  114. }
  115. func execPollSingle(c *cli.Context, cl *client.Client, topic, command string, options ...client.SubscribeOption) error {
  116. messages, err := cl.Poll(topic, options...)
  117. if err != nil {
  118. return err
  119. }
  120. for _, m := range messages {
  121. printMessageOrRunCommand(c, m, command)
  122. }
  123. return nil
  124. }
  125. func execSubscribeInternal(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
  126. commands := make(map[string]string)
  127. for _, s := range conf.Subscribe { // May be nil
  128. topicURL := cl.Subscribe(s.Topic, options...)
  129. commands[topicURL] = s.Command
  130. }
  131. if topic != "" {
  132. topicURL := cl.Subscribe(topic, options...)
  133. commands[topicURL] = command
  134. }
  135. for m := range cl.Messages {
  136. command, ok := commands[m.TopicURL]
  137. if !ok {
  138. continue
  139. }
  140. printMessageOrRunCommand(c, m, command)
  141. }
  142. return nil
  143. }
  144. func printMessageOrRunCommand(c *cli.Context, m *client.Message, command string) {
  145. if m.Event != client.MessageEvent {
  146. return
  147. }
  148. if command != "" {
  149. runCommand(c, command, m)
  150. } else {
  151. fmt.Fprintln(c.App.Writer, m.Raw)
  152. }
  153. }
  154. func runCommand(c *cli.Context, command string, m *client.Message) {
  155. if err := runCommandInternal(c, command, m); err != nil {
  156. fmt.Fprintf(c.App.ErrWriter, "Command failed: %s\n", err.Error())
  157. }
  158. }
  159. func runCommandInternal(c *cli.Context, command string, m *client.Message) error {
  160. scriptFile, err := createTmpScript(command)
  161. if err != nil {
  162. return err
  163. }
  164. defer os.Remove(scriptFile)
  165. verbose := c.Bool("verbose")
  166. if verbose {
  167. log.Printf("[%s] Executing: %s (for message: %s)", collapseTopicURL(m.TopicURL), command, m.Raw)
  168. }
  169. cmd := exec.Command("sh", "-c", scriptFile)
  170. cmd.Stdin = c.App.Reader
  171. cmd.Stdout = c.App.Writer
  172. cmd.Stderr = c.App.ErrWriter
  173. cmd.Env = envVars(m)
  174. return cmd.Run()
  175. }
  176. func createTmpScript(command string) (string, error) {
  177. scriptFile := fmt.Sprintf("%s/ntfy-subscribe-%s.sh.tmp", os.TempDir(), util.RandomString(10))
  178. script := fmt.Sprintf("#!/bin/sh\n%s", command)
  179. if err := os.WriteFile(scriptFile, []byte(script), 0700); err != nil {
  180. return "", err
  181. }
  182. return scriptFile, nil
  183. }
  184. func envVars(m *client.Message) []string {
  185. env := os.Environ()
  186. env = append(env, envVar(m.ID, "NTFY_ID", "id")...)
  187. env = append(env, envVar(m.Topic, "NTFY_TOPIC", "topic")...)
  188. env = append(env, envVar(fmt.Sprintf("%d", m.Time), "NTFY_TIME", "time")...)
  189. env = append(env, envVar(m.Message, "NTFY_MESSAGE", "message", "m")...)
  190. env = append(env, envVar(m.Title, "NTFY_TITLE", "title", "t")...)
  191. env = append(env, envVar(fmt.Sprintf("%d", m.Priority), "NTFY_PRIORITY", "priority", "prio", "p")...)
  192. env = append(env, envVar(strings.Join(m.Tags, ","), "NTFY_TAGS", "tags", "ta")...)
  193. return env
  194. }
  195. func envVar(value string, vars ...string) []string {
  196. env := make([]string, 0)
  197. for _, v := range vars {
  198. env = append(env, fmt.Sprintf("%s=%s", v, value))
  199. }
  200. return env
  201. }
  202. func loadConfig(c *cli.Context) (*client.Config, error) {
  203. filename := c.String("config")
  204. if filename != "" {
  205. return loadConfigFromFile(filename)
  206. }
  207. u, _ := user.Current()
  208. configFile := defaultClientRootConfigFile
  209. if u.Uid != "0" {
  210. configFile = util.ExpandHome(defaultClientUserConfigFile)
  211. }
  212. if s, _ := os.Stat(configFile); s != nil {
  213. return loadConfigFromFile(configFile)
  214. }
  215. return client.NewConfig(), nil
  216. }
  217. func loadConfigFromFile(filename string) (*client.Config, error) {
  218. b, err := os.ReadFile(filename)
  219. if err != nil {
  220. return nil, err
  221. }
  222. c := client.NewConfig()
  223. if err := yaml.Unmarshal(b, c); err != nil {
  224. return nil, err
  225. }
  226. return c, nil
  227. }