subscribe.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. package cmd
  2. import (
  3. "errors"
  4. "fmt"
  5. "github.com/urfave/cli/v2"
  6. "heckel.io/ntfy/client"
  7. "heckel.io/ntfy/util"
  8. "log"
  9. "os"
  10. "os/exec"
  11. "strings"
  12. )
  13. var cmdSubscribe = &cli.Command{
  14. Name: "subscribe",
  15. Aliases: []string{"sub"},
  16. Usage: "Subscribe to one or more topics on a ntfy server",
  17. UsageText: "ntfy subscribe [OPTIONS..] TOPIC",
  18. Action: execSubscribe,
  19. Flags: []cli.Flag{
  20. &cli.StringFlag{Name: "exec", Aliases: []string{"e"}, Usage: "execute command for each message event"},
  21. &cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since (Unix timestamp, or all)"},
  22. &cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"},
  23. &cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"},
  24. },
  25. Description: `(THIS COMMAND IS INCUBATING. IT MAY CHANGE WITHOUT NOTICE.)
  26. Subscribe to one or more topics on a ntfy server, and either print
  27. or execute commands for every arriving message.
  28. By default, the subscribe command just prints the JSON representation of a message.
  29. When --exec is passed, each incoming message will execute a command. The message fields
  30. are passed to the command as environment variables:
  31. Variable Aliases Description
  32. --------------- --------------- -----------------------------------
  33. $NTFY_MESSAGE $message, $m Message body
  34. $NTFY_TITLE $title, $t Message title
  35. $NTFY_PRIORITY $priority, $p Message priority (1=min, 5=max)
  36. $NTFY_TAGS $tags, $ta Message tags (comma separated list)
  37. $NTFY_ID $id Unique message ID
  38. $NTFY_TIME $time Unix timestamp of the message delivery
  39. $NTFY_TOPIC $topic Topic name
  40. $NTFY_EVENT $event, $ev Event identifier (always "message")
  41. Examples:
  42. ntfy subscribe mytopic # Prints JSON for incoming messages to stdout
  43. ntfy sub home.lan/backups alerts # Subscribe to two different topics
  44. ntfy sub --exec='notify-send "$m"' mytopic # Execute command for incoming messages
  45. ntfy sub --exec=/my/script topic1 topic2 # Subscribe to two topics and execute command for each message
  46. `,
  47. }
  48. func execSubscribe(c *cli.Context) error {
  49. if c.NArg() < 1 {
  50. return errors.New("topic missing")
  51. }
  52. fmt.Fprintln(c.App.ErrWriter, "\x1b[1;33mThis command is incubating. The interface may change without notice.\x1b[0m")
  53. cl := client.DefaultClient
  54. command := c.String("exec")
  55. since := c.String("since")
  56. poll := c.Bool("poll")
  57. scheduled := c.Bool("scheduled")
  58. topics := c.Args().Slice()
  59. var options []client.SubscribeOption
  60. if since != "" {
  61. options = append(options, client.WithSince(since))
  62. }
  63. if poll {
  64. options = append(options, client.WithPoll())
  65. }
  66. if scheduled {
  67. options = append(options, client.WithScheduled())
  68. }
  69. if poll {
  70. for _, topic := range topics {
  71. messages, err := cl.Poll(expandTopicURL(topic), options...)
  72. if err != nil {
  73. return err
  74. }
  75. for _, m := range messages {
  76. _ = dispatchMessage(c, command, m)
  77. }
  78. }
  79. } else {
  80. for _, topic := range topics {
  81. cl.Subscribe(expandTopicURL(topic), options...)
  82. }
  83. for m := range cl.Messages {
  84. _ = dispatchMessage(c, command, m)
  85. }
  86. }
  87. return nil
  88. }
  89. func dispatchMessage(c *cli.Context, command string, m *client.Message) error {
  90. if command != "" {
  91. return execCommand(c, command, m)
  92. }
  93. fmt.Println(m.Raw)
  94. return nil
  95. }
  96. func execCommand(c *cli.Context, command string, m *client.Message) error {
  97. if m.Event == client.OpenEvent {
  98. log.Printf("[%s] Connection opened, subscribed to topic", collapseTopicURL(m.TopicURL))
  99. } else if m.Event == client.MessageEvent {
  100. if err := runCommandInternal(c, command, m); err != nil {
  101. log.Printf("[%s] Command failed: %s", collapseTopicURL(m.TopicURL), err.Error())
  102. }
  103. }
  104. return nil
  105. }
  106. func runCommandInternal(c *cli.Context, command string, m *client.Message) error {
  107. scriptFile, err := createTmpScript(command)
  108. if err != nil {
  109. return err
  110. }
  111. defer os.Remove(scriptFile)
  112. log.Printf("[%s] Executing: %s (for message: %s)", collapseTopicURL(m.TopicURL), command, m.Raw)
  113. cmd := exec.Command("sh", "-c", scriptFile)
  114. cmd.Stdin = c.App.Reader
  115. cmd.Stdout = c.App.Writer
  116. cmd.Stderr = c.App.ErrWriter
  117. cmd.Env = envVars(m)
  118. return cmd.Run()
  119. }
  120. func createTmpScript(command string) (string, error) {
  121. scriptFile := fmt.Sprintf("%s/ntfy-subscribe-%s.sh.tmp", os.TempDir(), util.RandomString(10))
  122. script := fmt.Sprintf("#!/bin/sh\n%s", command)
  123. if err := os.WriteFile(scriptFile, []byte(script), 0700); err != nil {
  124. return "", err
  125. }
  126. return scriptFile, nil
  127. }
  128. func envVars(m *client.Message) []string {
  129. env := os.Environ()
  130. env = append(env, envVar(m.ID, "NTFY_ID", "id")...)
  131. env = append(env, envVar(m.Event, "NTFY_EVENT", "event", "ev")...)
  132. env = append(env, envVar(m.Topic, "NTFY_TOPIC", "topic")...)
  133. env = append(env, envVar(fmt.Sprintf("%d", m.Time), "NTFY_TIME", "time")...)
  134. env = append(env, envVar(m.Message, "NTFY_MESSAGE", "message", "m")...)
  135. env = append(env, envVar(m.Title, "NTFY_TITLE", "title", "t")...)
  136. env = append(env, envVar(fmt.Sprintf("%d", m.Priority), "NTFY_PRIORITY", "priority", "prio", "p")...)
  137. env = append(env, envVar(strings.Join(m.Tags, ","), "NTFY_TAGS", "tags", "ta")...)
  138. return env
  139. }
  140. func envVar(value string, vars ...string) []string {
  141. env := make([]string, 0)
  142. for _, v := range vars {
  143. env = append(env, fmt.Sprintf("%s=%s", v, value))
  144. }
  145. return env
  146. }