types.go 19 KB

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