types.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. package server
  2. import (
  3. "heckel.io/ntfy/user"
  4. "net/http"
  5. "net/netip"
  6. "time"
  7. "heckel.io/ntfy/util"
  8. )
  9. // List of possible events
  10. const (
  11. openEvent = "open"
  12. keepaliveEvent = "keepalive"
  13. messageEvent = "message"
  14. pollRequestEvent = "poll_request"
  15. )
  16. const (
  17. messageIDLength = 12
  18. )
  19. // message represents a message published to a topic
  20. type message struct {
  21. ID string `json:"id"` // Random message ID
  22. Time int64 `json:"time"` // Unix time in seconds
  23. Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
  24. Event string `json:"event"` // One of the above
  25. Topic string `json:"topic"`
  26. Title string `json:"title,omitempty"`
  27. Message string `json:"message,omitempty"`
  28. Priority int `json:"priority,omitempty"`
  29. Tags []string `json:"tags,omitempty"`
  30. Click string `json:"click,omitempty"`
  31. Icon string `json:"icon,omitempty"`
  32. Actions []*action `json:"actions,omitempty"`
  33. Attachment *attachment `json:"attachment,omitempty"`
  34. PollID string `json:"poll_id,omitempty"`
  35. Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes
  36. Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting
  37. User string `json:"-"` // Username of the uploader, used to associated attachments
  38. }
  39. func (m *message) Context() map[string]any {
  40. fields := map[string]any{
  41. "message_id": m.ID,
  42. "message_time": m.Time,
  43. "message_event": m.Event,
  44. "message_topic": m.Topic,
  45. "message_body_size": len(m.Message),
  46. }
  47. if m.Sender != netip.IPv4Unspecified() {
  48. fields["message_sender"] = m.Sender.String()
  49. }
  50. if m.User != "" {
  51. fields["message_user"] = m.User
  52. }
  53. return fields
  54. }
  55. type attachment struct {
  56. Name string `json:"name"`
  57. Type string `json:"type,omitempty"`
  58. Size int64 `json:"size,omitempty"`
  59. Expires int64 `json:"expires,omitempty"`
  60. URL string `json:"url"`
  61. }
  62. type action struct {
  63. ID string `json:"id"`
  64. Action string `json:"action"` // "view", "broadcast", or "http"
  65. Label string `json:"label"` // action button label
  66. Clear bool `json:"clear"` // clear notification after successful execution
  67. URL string `json:"url,omitempty"` // used in "view" and "http" actions
  68. Method string `json:"method,omitempty"` // used in "http" action, default is POST (!)
  69. Headers map[string]string `json:"headers,omitempty"` // used in "http" action
  70. Body string `json:"body,omitempty"` // used in "http" action
  71. Intent string `json:"intent,omitempty"` // used in "broadcast" action
  72. Extras map[string]string `json:"extras,omitempty"` // used in "broadcast" action
  73. }
  74. func newAction() *action {
  75. return &action{
  76. Headers: make(map[string]string),
  77. Extras: make(map[string]string),
  78. }
  79. }
  80. // publishMessage is used as input when publishing as JSON
  81. type publishMessage struct {
  82. Topic string `json:"topic"`
  83. Title string `json:"title"`
  84. Message string `json:"message"`
  85. Priority int `json:"priority"`
  86. Tags []string `json:"tags"`
  87. Click string `json:"click"`
  88. Icon string `json:"icon"`
  89. Actions []action `json:"actions"`
  90. Attach string `json:"attach"`
  91. Filename string `json:"filename"`
  92. Email string `json:"email"`
  93. Delay string `json:"delay"`
  94. }
  95. // messageEncoder is a function that knows how to encode a message
  96. type messageEncoder func(msg *message) (string, error)
  97. // newMessage creates a new message with the current timestamp
  98. func newMessage(event, topic, msg string) *message {
  99. return &message{
  100. ID: util.RandomString(messageIDLength),
  101. Time: time.Now().Unix(),
  102. Event: event,
  103. Topic: topic,
  104. Message: msg,
  105. }
  106. }
  107. // newOpenMessage is a convenience method to create an open message
  108. func newOpenMessage(topic string) *message {
  109. return newMessage(openEvent, topic, "")
  110. }
  111. // newKeepaliveMessage is a convenience method to create a keepalive message
  112. func newKeepaliveMessage(topic string) *message {
  113. return newMessage(keepaliveEvent, topic, "")
  114. }
  115. // newDefaultMessage is a convenience method to create a notification message
  116. func newDefaultMessage(topic, msg string) *message {
  117. return newMessage(messageEvent, topic, msg)
  118. }
  119. // newPollRequestMessage is a convenience method to create a poll request message
  120. func newPollRequestMessage(topic, pollID string) *message {
  121. m := newMessage(pollRequestEvent, topic, newMessageBody)
  122. m.PollID = pollID
  123. return m
  124. }
  125. func validMessageID(s string) bool {
  126. return util.ValidRandomString(s, messageIDLength)
  127. }
  128. type sinceMarker struct {
  129. time time.Time
  130. id string
  131. }
  132. func newSinceTime(timestamp int64) sinceMarker {
  133. return sinceMarker{time.Unix(timestamp, 0), ""}
  134. }
  135. func newSinceID(id string) sinceMarker {
  136. return sinceMarker{time.Unix(0, 0), id}
  137. }
  138. func (t sinceMarker) IsAll() bool {
  139. return t == sinceAllMessages
  140. }
  141. func (t sinceMarker) IsNone() bool {
  142. return t == sinceNoMessages
  143. }
  144. func (t sinceMarker) IsID() bool {
  145. return t.id != ""
  146. }
  147. func (t sinceMarker) Time() time.Time {
  148. return t.time
  149. }
  150. func (t sinceMarker) ID() string {
  151. return t.id
  152. }
  153. var (
  154. sinceAllMessages = sinceMarker{time.Unix(0, 0), ""}
  155. sinceNoMessages = sinceMarker{time.Unix(1, 0), ""}
  156. )
  157. type queryFilter struct {
  158. ID string
  159. Message string
  160. Title string
  161. Tags []string
  162. Priority []int
  163. }
  164. func parseQueryFilters(r *http.Request) (*queryFilter, error) {
  165. idFilter := readParam(r, "x-id", "id")
  166. messageFilter := readParam(r, "x-message", "message", "m")
  167. titleFilter := readParam(r, "x-title", "title", "t")
  168. tagsFilter := util.SplitNoEmpty(readParam(r, "x-tags", "tags", "tag", "ta"), ",")
  169. priorityFilter := make([]int, 0)
  170. for _, p := range util.SplitNoEmpty(readParam(r, "x-priority", "priority", "prio", "p"), ",") {
  171. priority, err := util.ParsePriority(p)
  172. if err != nil {
  173. return nil, errHTTPBadRequestPriorityInvalid
  174. }
  175. priorityFilter = append(priorityFilter, priority)
  176. }
  177. return &queryFilter{
  178. ID: idFilter,
  179. Message: messageFilter,
  180. Title: titleFilter,
  181. Tags: tagsFilter,
  182. Priority: priorityFilter,
  183. }, nil
  184. }
  185. func (q *queryFilter) Pass(msg *message) bool {
  186. if msg.Event != messageEvent {
  187. return true // filters only apply to messages
  188. } else if q.ID != "" && msg.ID != q.ID {
  189. return false
  190. } else if q.Message != "" && msg.Message != q.Message {
  191. return false
  192. } else if q.Title != "" && msg.Title != q.Title {
  193. return false
  194. }
  195. messagePriority := msg.Priority
  196. if messagePriority == 0 {
  197. messagePriority = 3 // For query filters, default priority (3) is the same as "not set" (0)
  198. }
  199. if len(q.Priority) > 0 && !util.Contains(q.Priority, messagePriority) {
  200. return false
  201. }
  202. if len(q.Tags) > 0 && !util.ContainsAll(msg.Tags, q.Tags) {
  203. return false
  204. }
  205. return true
  206. }
  207. type apiHealthResponse struct {
  208. Healthy bool `json:"healthy"`
  209. }
  210. type apiAccountCreateRequest struct {
  211. Username string `json:"username"`
  212. Password string `json:"password"`
  213. }
  214. type apiAccountPasswordChangeRequest struct {
  215. Password string `json:"password"`
  216. NewPassword string `json:"new_password"`
  217. }
  218. type apiAccountDeleteRequest struct {
  219. Password string `json:"password"`
  220. }
  221. type apiAccountTokenIssueRequest struct {
  222. Label *string `json:"label"`
  223. Expires *int64 `json:"expires"` // Unix timestamp
  224. }
  225. type apiAccountTokenUpdateRequest struct {
  226. Token string `json:"token"`
  227. Label *string `json:"label"`
  228. Expires *int64 `json:"expires"` // Unix timestamp
  229. }
  230. type apiAccountTokenResponse struct {
  231. Token string `json:"token"`
  232. Label string `json:"label,omitempty"`
  233. LastAccess int64 `json:"last_access,omitempty"`
  234. LastOrigin string `json:"last_origin,omitempty"`
  235. Expires int64 `json:"expires,omitempty"` // Unix timestamp
  236. }
  237. type apiAccountTier struct {
  238. Code string `json:"code"`
  239. Name string `json:"name"`
  240. }
  241. type apiAccountLimits struct {
  242. Basis string `json:"basis,omitempty"` // "ip" or "tier"
  243. Messages int64 `json:"messages"`
  244. MessagesExpiryDuration int64 `json:"messages_expiry_duration"`
  245. Emails int64 `json:"emails"`
  246. Reservations int64 `json:"reservations"`
  247. AttachmentTotalSize int64 `json:"attachment_total_size"`
  248. AttachmentFileSize int64 `json:"attachment_file_size"`
  249. AttachmentExpiryDuration int64 `json:"attachment_expiry_duration"`
  250. AttachmentBandwidth int64 `json:"attachment_bandwidth"`
  251. }
  252. type apiAccountStats struct {
  253. Messages int64 `json:"messages"`
  254. MessagesRemaining int64 `json:"messages_remaining"`
  255. Emails int64 `json:"emails"`
  256. EmailsRemaining int64 `json:"emails_remaining"`
  257. Reservations int64 `json:"reservations"`
  258. ReservationsRemaining int64 `json:"reservations_remaining"`
  259. AttachmentTotalSize int64 `json:"attachment_total_size"`
  260. AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"`
  261. }
  262. type apiAccountReservation struct {
  263. Topic string `json:"topic"`
  264. Everyone string `json:"everyone"`
  265. }
  266. type apiAccountBilling struct {
  267. Customer bool `json:"customer"`
  268. Subscription bool `json:"subscription"`
  269. Status string `json:"status,omitempty"`
  270. PaidUntil int64 `json:"paid_until,omitempty"`
  271. CancelAt int64 `json:"cancel_at,omitempty"`
  272. }
  273. type apiAccountResponse struct {
  274. Username string `json:"username"`
  275. Role string `json:"role,omitempty"`
  276. SyncTopic string `json:"sync_topic,omitempty"`
  277. Language string `json:"language,omitempty"`
  278. Notification *user.NotificationPrefs `json:"notification,omitempty"`
  279. Subscriptions []*user.Subscription `json:"subscriptions,omitempty"`
  280. Reservations []*apiAccountReservation `json:"reservations,omitempty"`
  281. Tokens []*apiAccountTokenResponse `json:"tokens,omitempty"`
  282. Tier *apiAccountTier `json:"tier,omitempty"`
  283. Limits *apiAccountLimits `json:"limits,omitempty"`
  284. Stats *apiAccountStats `json:"stats,omitempty"`
  285. Billing *apiAccountBilling `json:"billing,omitempty"`
  286. }
  287. type apiAccountReservationRequest struct {
  288. Topic string `json:"topic"`
  289. Everyone string `json:"everyone"`
  290. }
  291. type apiConfigResponse struct {
  292. BaseURL string `json:"base_url"`
  293. AppRoot string `json:"app_root"`
  294. EnableLogin bool `json:"enable_login"`
  295. EnableSignup bool `json:"enable_signup"`
  296. EnablePayments bool `json:"enable_payments"`
  297. EnableReservations bool `json:"enable_reservations"`
  298. DisallowedTopics []string `json:"disallowed_topics"`
  299. }
  300. type apiAccountBillingTier struct {
  301. Code string `json:"code,omitempty"`
  302. Name string `json:"name,omitempty"`
  303. Price string `json:"price,omitempty"`
  304. Limits *apiAccountLimits `json:"limits"`
  305. }
  306. type apiAccountBillingSubscriptionCreateResponse struct {
  307. RedirectURL string `json:"redirect_url"`
  308. }
  309. type apiAccountBillingSubscriptionChangeRequest struct {
  310. Tier string `json:"tier"`
  311. }
  312. type apiAccountBillingPortalRedirectResponse struct {
  313. RedirectURL string `json:"redirect_url"`
  314. }
  315. type apiAccountSyncTopicResponse struct {
  316. Event string `json:"event"`
  317. }
  318. type apiSuccessResponse struct {
  319. Success bool `json:"success"`
  320. }
  321. func newSuccessResponse() *apiSuccessResponse {
  322. return &apiSuccessResponse{
  323. Success: true,
  324. }
  325. }
  326. type apiStripeSubscriptionUpdatedEvent struct {
  327. ID string `json:"id"`
  328. Customer string `json:"customer"`
  329. Status string `json:"status"`
  330. CurrentPeriodEnd int64 `json:"current_period_end"`
  331. CancelAt int64 `json:"cancel_at"`
  332. Items *struct {
  333. Data []*struct {
  334. Price *struct {
  335. ID string `json:"id"`
  336. } `json:"price"`
  337. } `json:"data"`
  338. } `json:"items"`
  339. }
  340. type apiStripeSubscriptionDeletedEvent struct {
  341. ID string `json:"id"`
  342. Customer string `json:"customer"`
  343. }