types.go 16 KB

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