types.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. package server
  2. import (
  3. "net/http"
  4. "net/netip"
  5. "time"
  6. "heckel.io/ntfy/v2/log"
  7. "heckel.io/ntfy/v2/user"
  8. "heckel.io/ntfy/v2/util"
  9. )
  10. // List of possible events
  11. const (
  12. openEvent = "open"
  13. keepaliveEvent = "keepalive"
  14. messageEvent = "message"
  15. messageDeleteEvent = "message_delete"
  16. messageClearEvent = "message_clear"
  17. pollRequestEvent = "poll_request"
  18. )
  19. const (
  20. messageIDLength = 12
  21. )
  22. // message represents a message published to a topic
  23. type message struct {
  24. ID string `json:"id"` // Random message ID
  25. SequenceID string `json:"sequence_id,omitempty"` // Message sequence ID for updating message contents (omitted if same as ID)
  26. Time int64 `json:"time"` // Unix time in seconds
  27. Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
  28. Event string `json:"event"` // One of the above
  29. Topic string `json:"topic"`
  30. Title string `json:"title,omitempty"`
  31. Message string `json:"message,omitempty"`
  32. Priority int `json:"priority,omitempty"`
  33. Tags []string `json:"tags,omitempty"`
  34. Click string `json:"click,omitempty"`
  35. Icon string `json:"icon,omitempty"`
  36. Actions []*action `json:"actions,omitempty"`
  37. Attachment *attachment `json:"attachment,omitempty"`
  38. PollID string `json:"poll_id,omitempty"`
  39. ContentType string `json:"content_type,omitempty"` // text/plain by default (if empty), or text/markdown
  40. Encoding string `json:"encoding,omitempty"` // Empty for raw UTF-8, or "base64" for encoded bytes
  41. Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting
  42. User string `json:"-"` // UserID of the uploader, used to associated attachments
  43. }
  44. func (m *message) Context() log.Context {
  45. fields := map[string]any{
  46. "topic": m.Topic,
  47. "message_id": m.ID,
  48. "message_sequence_id": m.SequenceID,
  49. "message_time": m.Time,
  50. "message_event": m.Event,
  51. "message_body_size": len(m.Message),
  52. }
  53. if m.Sender.IsValid() {
  54. fields["message_sender"] = m.Sender.String()
  55. }
  56. if m.User != "" {
  57. fields["message_user"] = m.User
  58. }
  59. return fields
  60. }
  61. // forJSON returns a copy of the message suitable for JSON output.
  62. // It clears the SequenceID if it equals the ID to reduce redundancy.
  63. func (m *message) forJSON() *message {
  64. if m.SequenceID == m.ID {
  65. clone := *m
  66. clone.SequenceID = ""
  67. return &clone
  68. }
  69. return m
  70. }
  71. type attachment struct {
  72. Name string `json:"name"`
  73. Type string `json:"type,omitempty"`
  74. Size int64 `json:"size,omitempty"`
  75. Expires int64 `json:"expires,omitempty"`
  76. URL string `json:"url"`
  77. }
  78. type action struct {
  79. ID string `json:"id"`
  80. Action string `json:"action"` // "view", "broadcast", or "http"
  81. Label string `json:"label"` // action button label
  82. Clear bool `json:"clear"` // clear notification after successful execution
  83. URL string `json:"url,omitempty"` // used in "view" and "http" actions
  84. Method string `json:"method,omitempty"` // used in "http" action, default is POST (!)
  85. Headers map[string]string `json:"headers,omitempty"` // used in "http" action
  86. Body string `json:"body,omitempty"` // used in "http" action
  87. Intent string `json:"intent,omitempty"` // used in "broadcast" action
  88. Extras map[string]string `json:"extras,omitempty"` // used in "broadcast" action
  89. }
  90. func newAction() *action {
  91. return &action{
  92. Headers: make(map[string]string),
  93. Extras: make(map[string]string),
  94. }
  95. }
  96. // publishMessage is used as input when publishing as JSON
  97. type publishMessage struct {
  98. Topic string `json:"topic"`
  99. SequenceID string `json:"sequence_id"`
  100. Title string `json:"title"`
  101. Message string `json:"message"`
  102. Priority int `json:"priority"`
  103. Tags []string `json:"tags"`
  104. Click string `json:"click"`
  105. Icon string `json:"icon"`
  106. Actions []action `json:"actions"`
  107. Attach string `json:"attach"`
  108. Markdown bool `json:"markdown"`
  109. Filename string `json:"filename"`
  110. Email string `json:"email"`
  111. Call string `json:"call"`
  112. Cache string `json:"cache"` // use string as it defaults to true (or use &bool instead)
  113. Firebase string `json:"firebase"` // use string as it defaults to true (or use &bool instead)
  114. Delay string `json:"delay"`
  115. }
  116. // messageEncoder is a function that knows how to encode a message
  117. type messageEncoder func(msg *message) (string, error)
  118. // newMessage creates a new message with the current timestamp
  119. func newMessage(event, topic, msg string) *message {
  120. return &message{
  121. ID: util.RandomString(messageIDLength),
  122. Time: time.Now().Unix(),
  123. Event: event,
  124. Topic: topic,
  125. Message: msg,
  126. }
  127. }
  128. // newOpenMessage is a convenience method to create an open message
  129. func newOpenMessage(topic string) *message {
  130. return newMessage(openEvent, topic, "")
  131. }
  132. // newKeepaliveMessage is a convenience method to create a keepalive message
  133. func newKeepaliveMessage(topic string) *message {
  134. return newMessage(keepaliveEvent, topic, "")
  135. }
  136. // newDefaultMessage is a convenience method to create a notification message
  137. func newDefaultMessage(topic, msg string) *message {
  138. return newMessage(messageEvent, topic, msg)
  139. }
  140. // newPollRequestMessage is a convenience method to create a poll request message
  141. func newPollRequestMessage(topic, pollID string) *message {
  142. m := newMessage(pollRequestEvent, topic, newMessageBody)
  143. m.PollID = pollID
  144. return m
  145. }
  146. // newActionMessage creates a new action message (message_delete or message_clear)
  147. func newActionMessage(event, topic, sequenceID string) *message {
  148. m := newMessage(event, topic, "")
  149. m.SequenceID = sequenceID
  150. return m
  151. }
  152. func validMessageID(s string) bool {
  153. return util.ValidRandomString(s, messageIDLength)
  154. }
  155. type sinceMarker struct {
  156. time time.Time
  157. id string
  158. }
  159. func newSinceTime(timestamp int64) sinceMarker {
  160. return sinceMarker{time.Unix(timestamp, 0), ""}
  161. }
  162. func newSinceID(id string) sinceMarker {
  163. return sinceMarker{time.Unix(0, 0), id}
  164. }
  165. func (t sinceMarker) IsAll() bool {
  166. return t == sinceAllMessages
  167. }
  168. func (t sinceMarker) IsNone() bool {
  169. return t == sinceNoMessages
  170. }
  171. func (t sinceMarker) IsLatest() bool {
  172. return t == sinceLatestMessage
  173. }
  174. func (t sinceMarker) IsID() bool {
  175. return t.id != "" && t.id != "latest"
  176. }
  177. func (t sinceMarker) Time() time.Time {
  178. return t.time
  179. }
  180. func (t sinceMarker) ID() string {
  181. return t.id
  182. }
  183. var (
  184. sinceAllMessages = sinceMarker{time.Unix(0, 0), ""}
  185. sinceNoMessages = sinceMarker{time.Unix(1, 0), ""}
  186. sinceLatestMessage = sinceMarker{time.Unix(0, 0), "latest"}
  187. )
  188. type queryFilter struct {
  189. ID string
  190. Message string
  191. Title string
  192. Tags []string
  193. Priority []int
  194. }
  195. func parseQueryFilters(r *http.Request) (*queryFilter, error) {
  196. idFilter := readParam(r, "x-id", "id")
  197. messageFilter := readParam(r, "x-message", "message", "m")
  198. titleFilter := readParam(r, "x-title", "title", "t")
  199. tagsFilter := util.SplitNoEmpty(readParam(r, "x-tags", "tags", "tag", "ta"), ",")
  200. priorityFilter := make([]int, 0)
  201. for _, p := range util.SplitNoEmpty(readParam(r, "x-priority", "priority", "prio", "p"), ",") {
  202. priority, err := util.ParsePriority(p)
  203. if err != nil {
  204. return nil, errHTTPBadRequestPriorityInvalid
  205. }
  206. priorityFilter = append(priorityFilter, priority)
  207. }
  208. return &queryFilter{
  209. ID: idFilter,
  210. Message: messageFilter,
  211. Title: titleFilter,
  212. Tags: tagsFilter,
  213. Priority: priorityFilter,
  214. }, nil
  215. }
  216. func (q *queryFilter) Pass(msg *message) bool {
  217. if msg.Event != messageEvent && msg.Event != messageDeleteEvent && msg.Event != messageClearEvent {
  218. return true // filters only apply to messages
  219. } else if q.ID != "" && msg.ID != q.ID {
  220. return false
  221. } else if q.Message != "" && msg.Message != q.Message {
  222. return false
  223. } else if q.Title != "" && msg.Title != q.Title {
  224. return false
  225. }
  226. messagePriority := msg.Priority
  227. if messagePriority == 0 {
  228. messagePriority = 3 // For query filters, default priority (3) is the same as "not set" (0)
  229. }
  230. if len(q.Priority) > 0 && !util.Contains(q.Priority, messagePriority) {
  231. return false
  232. }
  233. if len(q.Tags) > 0 && !util.ContainsAll(msg.Tags, q.Tags) {
  234. return false
  235. }
  236. return true
  237. }
  238. // templateMode represents the mode in which templates are used
  239. //
  240. // It can be
  241. // - empty: templating is disabled
  242. // - a boolean string (yes/1/true/no/0/false): inline-templating mode
  243. // - a filename (e.g. grafana): template mode with a file
  244. type templateMode string
  245. // Enabled returns true if templating is enabled
  246. func (t templateMode) Enabled() bool {
  247. return t != ""
  248. }
  249. // InlineMode returns true if inline-templating mode is enabled
  250. func (t templateMode) InlineMode() bool {
  251. return t.Enabled() && isBoolValue(string(t))
  252. }
  253. // FileMode returns true if file-templating mode is enabled
  254. func (t templateMode) FileMode() bool {
  255. return t.Enabled() && !isBoolValue(string(t))
  256. }
  257. // FileName returns the filename if file-templating mode is enabled, or an empty string otherwise
  258. func (t templateMode) FileName() string {
  259. if t.FileMode() {
  260. return string(t)
  261. }
  262. return ""
  263. }
  264. // templateFile represents a template file with title, message, and priority
  265. // It is used for file-based templates, e.g. grafana, influxdb, etc.
  266. //
  267. // Example YAML:
  268. //
  269. // title: "Alert: {{ .Title }}"
  270. // message: |
  271. // This is a {{ .Type }} alert.
  272. // It can be multiline.
  273. // priority: '{{ if eq .status "Error" }}5{{ else }}3{{ end }}'
  274. type templateFile struct {
  275. Title *string `yaml:"title"`
  276. Message *string `yaml:"message"`
  277. Priority *string `yaml:"priority"`
  278. }
  279. type apiHealthResponse struct {
  280. Healthy bool `json:"healthy"`
  281. }
  282. type apiStatsResponse struct {
  283. Messages int64 `json:"messages"`
  284. MessagesRate float64 `json:"messages_rate"` // Average number of messages per second
  285. }
  286. type apiUserAddOrUpdateRequest struct {
  287. Username string `json:"username"`
  288. Password string `json:"password"`
  289. Hash string `json:"hash"`
  290. Tier string `json:"tier"`
  291. // Do not add 'role' here. We don't want to add admins via the API.
  292. }
  293. type apiUserResponse struct {
  294. Username string `json:"username"`
  295. Role string `json:"role"`
  296. Tier string `json:"tier,omitempty"`
  297. Grants []*apiUserGrantResponse `json:"grants,omitempty"`
  298. }
  299. type apiUserGrantResponse struct {
  300. Topic string `json:"topic"` // This may be a pattern
  301. Permission string `json:"permission"`
  302. }
  303. type apiUserDeleteRequest struct {
  304. Username string `json:"username"`
  305. }
  306. type apiAccessAllowRequest struct {
  307. Username string `json:"username"`
  308. Topic string `json:"topic"` // This may be a pattern
  309. Permission string `json:"permission"`
  310. }
  311. type apiAccessResetRequest struct {
  312. Username string `json:"username"`
  313. Topic string `json:"topic"`
  314. }
  315. type apiAccountCreateRequest struct {
  316. Username string `json:"username"`
  317. Password string `json:"password"`
  318. }
  319. type apiAccountPasswordChangeRequest struct {
  320. Password string `json:"password"`
  321. NewPassword string `json:"new_password"`
  322. }
  323. type apiAccountDeleteRequest struct {
  324. Password string `json:"password"`
  325. }
  326. type apiAccountTokenIssueRequest struct {
  327. Label *string `json:"label"`
  328. Expires *int64 `json:"expires"` // Unix timestamp
  329. }
  330. type apiAccountTokenUpdateRequest struct {
  331. Token string `json:"token"`
  332. Label *string `json:"label"`
  333. Expires *int64 `json:"expires"` // Unix timestamp
  334. }
  335. type apiAccountTokenResponse struct {
  336. Token string `json:"token"`
  337. Label string `json:"label,omitempty"`
  338. LastAccess int64 `json:"last_access,omitempty"`
  339. LastOrigin string `json:"last_origin,omitempty"`
  340. Expires int64 `json:"expires,omitempty"` // Unix timestamp
  341. Provisioned bool `json:"provisioned,omitempty"` // True if this token was provisioned by the server config
  342. }
  343. type apiAccountPhoneNumberVerifyRequest struct {
  344. Number string `json:"number"`
  345. Channel string `json:"channel"`
  346. }
  347. type apiAccountPhoneNumberAddRequest struct {
  348. Number string `json:"number"`
  349. Code string `json:"code"` // Only set when adding a phone number
  350. }
  351. type apiAccountTier struct {
  352. Code string `json:"code"`
  353. Name string `json:"name"`
  354. }
  355. type apiAccountLimits struct {
  356. Basis string `json:"basis,omitempty"` // "ip" or "tier"
  357. Messages int64 `json:"messages"`
  358. MessagesExpiryDuration int64 `json:"messages_expiry_duration"`
  359. Emails int64 `json:"emails"`
  360. Calls int64 `json:"calls"`
  361. Reservations int64 `json:"reservations"`
  362. AttachmentTotalSize int64 `json:"attachment_total_size"`
  363. AttachmentFileSize int64 `json:"attachment_file_size"`
  364. AttachmentExpiryDuration int64 `json:"attachment_expiry_duration"`
  365. AttachmentBandwidth int64 `json:"attachment_bandwidth"`
  366. }
  367. type apiAccountStats struct {
  368. Messages int64 `json:"messages"`
  369. MessagesRemaining int64 `json:"messages_remaining"`
  370. Emails int64 `json:"emails"`
  371. EmailsRemaining int64 `json:"emails_remaining"`
  372. Calls int64 `json:"calls"`
  373. CallsRemaining int64 `json:"calls_remaining"`
  374. Reservations int64 `json:"reservations"`
  375. ReservationsRemaining int64 `json:"reservations_remaining"`
  376. AttachmentTotalSize int64 `json:"attachment_total_size"`
  377. AttachmentTotalSizeRemaining int64 `json:"attachment_total_size_remaining"`
  378. }
  379. type apiAccountReservation struct {
  380. Topic string `json:"topic"`
  381. Everyone string `json:"everyone"`
  382. }
  383. type apiAccountBilling struct {
  384. Customer bool `json:"customer"`
  385. Subscription bool `json:"subscription"`
  386. Status string `json:"status,omitempty"`
  387. Interval string `json:"interval,omitempty"`
  388. PaidUntil int64 `json:"paid_until,omitempty"`
  389. CancelAt int64 `json:"cancel_at,omitempty"`
  390. }
  391. type apiAccountResponse struct {
  392. Username string `json:"username"`
  393. Role string `json:"role,omitempty"`
  394. SyncTopic string `json:"sync_topic,omitempty"`
  395. Provisioned bool `json:"provisioned,omitempty"`
  396. Language string `json:"language,omitempty"`
  397. Notification *user.NotificationPrefs `json:"notification,omitempty"`
  398. Subscriptions []*user.Subscription `json:"subscriptions,omitempty"`
  399. Reservations []*apiAccountReservation `json:"reservations,omitempty"`
  400. Tokens []*apiAccountTokenResponse `json:"tokens,omitempty"`
  401. PhoneNumbers []string `json:"phone_numbers,omitempty"`
  402. Tier *apiAccountTier `json:"tier,omitempty"`
  403. Limits *apiAccountLimits `json:"limits,omitempty"`
  404. Stats *apiAccountStats `json:"stats,omitempty"`
  405. Billing *apiAccountBilling `json:"billing,omitempty"`
  406. }
  407. type apiAccountReservationRequest struct {
  408. Topic string `json:"topic"`
  409. Everyone string `json:"everyone"`
  410. }
  411. type apiConfigResponse struct {
  412. BaseURL string `json:"base_url"`
  413. AppRoot string `json:"app_root"`
  414. EnableLogin bool `json:"enable_login"`
  415. RequireLogin bool `json:"require_login"`
  416. EnableSignup bool `json:"enable_signup"`
  417. EnablePayments bool `json:"enable_payments"`
  418. EnableCalls bool `json:"enable_calls"`
  419. EnableEmails bool `json:"enable_emails"`
  420. EnableReservations bool `json:"enable_reservations"`
  421. EnableWebPush bool `json:"enable_web_push"`
  422. BillingContact string `json:"billing_contact"`
  423. WebPushPublicKey string `json:"web_push_public_key"`
  424. DisallowedTopics []string `json:"disallowed_topics"`
  425. ConfigHash string `json:"config_hash"`
  426. }
  427. type apiAccountBillingPrices struct {
  428. Month int64 `json:"month"`
  429. Year int64 `json:"year"`
  430. }
  431. type apiAccountBillingTier struct {
  432. Code string `json:"code,omitempty"`
  433. Name string `json:"name,omitempty"`
  434. Prices *apiAccountBillingPrices `json:"prices,omitempty"`
  435. Limits *apiAccountLimits `json:"limits"`
  436. }
  437. type apiAccountBillingSubscriptionCreateResponse struct {
  438. RedirectURL string `json:"redirect_url"`
  439. }
  440. type apiAccountBillingSubscriptionChangeRequest struct {
  441. Tier string `json:"tier"`
  442. Interval string `json:"interval"`
  443. }
  444. type apiAccountBillingPortalRedirectResponse struct {
  445. RedirectURL string `json:"redirect_url"`
  446. }
  447. type apiAccountSyncTopicResponse struct {
  448. Event string `json:"event"`
  449. }
  450. type apiSuccessResponse struct {
  451. Success bool `json:"success"`
  452. }
  453. func newSuccessResponse() *apiSuccessResponse {
  454. return &apiSuccessResponse{
  455. Success: true,
  456. }
  457. }
  458. type apiStripeSubscriptionUpdatedEvent struct {
  459. ID string `json:"id"`
  460. Customer string `json:"customer"`
  461. Status string `json:"status"`
  462. CurrentPeriodEnd int64 `json:"current_period_end"`
  463. CancelAt int64 `json:"cancel_at"`
  464. Items *struct {
  465. Data []*struct {
  466. Price *struct {
  467. ID string `json:"id"`
  468. Recurring *struct {
  469. Interval string `json:"interval"`
  470. } `json:"recurring"`
  471. } `json:"price"`
  472. } `json:"data"`
  473. } `json:"items"`
  474. }
  475. type apiStripeSubscriptionDeletedEvent struct {
  476. ID string `json:"id"`
  477. Customer string `json:"customer"`
  478. }
  479. type apiWebPushUpdateSubscriptionRequest struct {
  480. Endpoint string `json:"endpoint"`
  481. Auth string `json:"auth"`
  482. P256dh string `json:"p256dh"`
  483. Topics []string `json:"topics"`
  484. }
  485. // List of possible Web Push events (see sw.js)
  486. const (
  487. webPushMessageEvent = "message"
  488. webPushExpiringEvent = "subscription_expiring"
  489. )
  490. type webPushPayload struct {
  491. Event string `json:"event"`
  492. SubscriptionID string `json:"subscription_id"`
  493. Message *message `json:"message"`
  494. }
  495. func newWebPushPayload(subscriptionID string, message *message) *webPushPayload {
  496. return &webPushPayload{
  497. Event: webPushMessageEvent,
  498. SubscriptionID: subscriptionID,
  499. Message: message,
  500. }
  501. }
  502. type webPushControlMessagePayload struct {
  503. Event string `json:"event"`
  504. }
  505. func newWebPushSubscriptionExpiringPayload() *webPushControlMessagePayload {
  506. return &webPushControlMessagePayload{
  507. Event: webPushExpiringEvent,
  508. }
  509. }
  510. type webPushSubscription struct {
  511. ID string
  512. Endpoint string
  513. Auth string
  514. P256dh string
  515. UserID string
  516. }
  517. func (w *webPushSubscription) Context() log.Context {
  518. return map[string]any{
  519. "web_push_subscription_id": w.ID,
  520. "web_push_subscription_user_id": w.UserID,
  521. "web_push_subscription_endpoint": w.Endpoint,
  522. }
  523. }
  524. // https://developer.mozilla.org/en-US/docs/Web/Manifest
  525. type webManifestResponse struct {
  526. Name string `json:"name"`
  527. Description string `json:"description"`
  528. ShortName string `json:"short_name"`
  529. Scope string `json:"scope"`
  530. StartURL string `json:"start_url"`
  531. Display string `json:"display"`
  532. BackgroundColor string `json:"background_color"`
  533. ThemeColor string `json:"theme_color"`
  534. Icons []*webManifestIcon `json:"icons"`
  535. }
  536. type webManifestIcon struct {
  537. SRC string `json:"src"`
  538. Sizes string `json:"sizes"`
  539. Type string `json:"type"`
  540. }