types.go 18 KB

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