types.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. package server
  2. import (
  3. "net/http"
  4. "net/netip"
  5. "time"
  6. "heckel.io/ntfy/util"
  7. )
  8. // List of possible events
  9. const (
  10. openEvent = "open"
  11. keepaliveEvent = "keepalive"
  12. messageEvent = "message"
  13. pollRequestEvent = "poll_request"
  14. )
  15. const (
  16. messageIDLength = 12
  17. )
  18. // message represents a message published to a topic
  19. type message struct {
  20. ID string `json:"id"` // Random message ID
  21. Time int64 `json:"time"` // Unix time in seconds
  22. Event string `json:"event"` // One of the above
  23. Topic string `json:"topic"`
  24. Title string `json:"title,omitempty"`
  25. Message string `json:"message,omitempty"`
  26. Priority int `json:"priority,omitempty"`
  27. Tags []string `json:"tags,omitempty"`
  28. Click string `json:"click,omitempty"`
  29. Icon string `json:"icon,omitempty"`
  30. Actions []*action `json:"actions,omitempty"`
  31. Attachment *attachment `json:"attachment,omitempty"`
  32. PollID string `json:"poll_id,omitempty"`
  33. Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting
  34. Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes
  35. }
  36. type attachment struct {
  37. Name string `json:"name"`
  38. Type string `json:"type,omitempty"`
  39. Size int64 `json:"size,omitempty"`
  40. Expires int64 `json:"expires,omitempty"`
  41. URL string `json:"url"`
  42. }
  43. type action struct {
  44. ID string `json:"id"`
  45. Action string `json:"action"` // "view", "broadcast", or "http"
  46. Label string `json:"label"` // action button label
  47. Clear bool `json:"clear"` // clear notification after successful execution
  48. URL string `json:"url,omitempty"` // used in "view" and "http" actions
  49. Method string `json:"method,omitempty"` // used in "http" action, default is POST (!)
  50. Headers map[string]string `json:"headers,omitempty"` // used in "http" action
  51. Body string `json:"body,omitempty"` // used in "http" action
  52. Intent string `json:"intent,omitempty"` // used in "broadcast" action
  53. Extras map[string]string `json:"extras,omitempty"` // used in "broadcast" action
  54. }
  55. func newAction() *action {
  56. return &action{
  57. Headers: make(map[string]string),
  58. Extras: make(map[string]string),
  59. }
  60. }
  61. // publishMessage is used as input when publishing as JSON
  62. type publishMessage struct {
  63. Topic string `json:"topic"`
  64. Title string `json:"title"`
  65. Message string `json:"message"`
  66. Priority int `json:"priority"`
  67. Tags []string `json:"tags"`
  68. Click string `json:"click"`
  69. Icon string `json:"icon"`
  70. Actions []action `json:"actions"`
  71. Attach string `json:"attach"`
  72. Filename string `json:"filename"`
  73. Email string `json:"email"`
  74. Delay string `json:"delay"`
  75. }
  76. // messageEncoder is a function that knows how to encode a message
  77. type messageEncoder func(msg *message) (string, error)
  78. // newMessage creates a new message with the current timestamp
  79. func newMessage(event, topic, msg string) *message {
  80. return &message{
  81. ID: util.RandomString(messageIDLength),
  82. Time: time.Now().Unix(),
  83. Event: event,
  84. Topic: topic,
  85. Message: msg,
  86. }
  87. }
  88. // newOpenMessage is a convenience method to create an open message
  89. func newOpenMessage(topic string) *message {
  90. return newMessage(openEvent, topic, "")
  91. }
  92. // newKeepaliveMessage is a convenience method to create a keepalive message
  93. func newKeepaliveMessage(topic string) *message {
  94. return newMessage(keepaliveEvent, topic, "")
  95. }
  96. // newDefaultMessage is a convenience method to create a notification message
  97. func newDefaultMessage(topic, msg string) *message {
  98. return newMessage(messageEvent, topic, msg)
  99. }
  100. // newPollRequestMessage is a convenience method to create a poll request message
  101. func newPollRequestMessage(topic, pollID string) *message {
  102. m := newMessage(pollRequestEvent, topic, newMessageBody)
  103. m.PollID = pollID
  104. return m
  105. }
  106. func validMessageID(s string) bool {
  107. return util.ValidRandomString(s, messageIDLength)
  108. }
  109. type sinceMarker struct {
  110. time time.Time
  111. id string
  112. }
  113. func newSinceTime(timestamp int64) sinceMarker {
  114. return sinceMarker{time.Unix(timestamp, 0), ""}
  115. }
  116. func newSinceID(id string) sinceMarker {
  117. return sinceMarker{time.Unix(0, 0), id}
  118. }
  119. func (t sinceMarker) IsAll() bool {
  120. return t == sinceAllMessages
  121. }
  122. func (t sinceMarker) IsNone() bool {
  123. return t == sinceNoMessages
  124. }
  125. func (t sinceMarker) IsID() bool {
  126. return t.id != ""
  127. }
  128. func (t sinceMarker) Time() time.Time {
  129. return t.time
  130. }
  131. func (t sinceMarker) ID() string {
  132. return t.id
  133. }
  134. var (
  135. sinceAllMessages = sinceMarker{time.Unix(0, 0), ""}
  136. sinceNoMessages = sinceMarker{time.Unix(1, 0), ""}
  137. )
  138. type queryFilter struct {
  139. ID string
  140. Message string
  141. Title string
  142. Tags []string
  143. Priority []int
  144. }
  145. func parseQueryFilters(r *http.Request) (*queryFilter, error) {
  146. idFilter := readParam(r, "x-id", "id")
  147. messageFilter := readParam(r, "x-message", "message", "m")
  148. titleFilter := readParam(r, "x-title", "title", "t")
  149. tagsFilter := util.SplitNoEmpty(readParam(r, "x-tags", "tags", "tag", "ta"), ",")
  150. priorityFilter := make([]int, 0)
  151. for _, p := range util.SplitNoEmpty(readParam(r, "x-priority", "priority", "prio", "p"), ",") {
  152. priority, err := util.ParsePriority(p)
  153. if err != nil {
  154. return nil, errHTTPBadRequestPriorityInvalid
  155. }
  156. priorityFilter = append(priorityFilter, priority)
  157. }
  158. return &queryFilter{
  159. ID: idFilter,
  160. Message: messageFilter,
  161. Title: titleFilter,
  162. Tags: tagsFilter,
  163. Priority: priorityFilter,
  164. }, nil
  165. }
  166. func (q *queryFilter) Pass(msg *message) bool {
  167. if msg.Event != messageEvent {
  168. return true // filters only apply to messages
  169. } else if q.ID != "" && msg.ID != q.ID {
  170. return false
  171. } else if q.Message != "" && msg.Message != q.Message {
  172. return false
  173. } else if q.Title != "" && msg.Title != q.Title {
  174. return false
  175. }
  176. messagePriority := msg.Priority
  177. if messagePriority == 0 {
  178. messagePriority = 3 // For query filters, default priority (3) is the same as "not set" (0)
  179. }
  180. if len(q.Priority) > 0 && !util.Contains(q.Priority, messagePriority) {
  181. return false
  182. }
  183. if len(q.Tags) > 0 && !util.ContainsAll(msg.Tags, q.Tags) {
  184. return false
  185. }
  186. return true
  187. }
  188. type apiHealthResponse struct {
  189. Healthy bool `json:"healthy"`
  190. }