gotify.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package gotify // import "miniflux.app/v2/internal/integration/gotify"
  4. import (
  5. "errors"
  6. "fmt"
  7. "log/slog"
  8. "net/http"
  9. "strings"
  10. "miniflux.app/v2/internal/http/client"
  11. "miniflux.app/v2/internal/model"
  12. "miniflux.app/v2/internal/urllib"
  13. )
  14. type Client struct {
  15. baseURL string
  16. token string
  17. priority int
  18. }
  19. func NewClient(baseURL, token string, priority int) *Client {
  20. if priority < 0 {
  21. priority = 0
  22. }
  23. return &Client{
  24. baseURL: baseURL,
  25. token: token,
  26. priority: priority,
  27. }
  28. }
  29. // Payload represents a Gotify push notification message payload.
  30. type Payload struct {
  31. Title string `json:"title,omitempty"`
  32. Message string `json:"message"`
  33. Priority int `json:"priority,omitempty"`
  34. Extras map[string]any `json:"extras,omitempty"`
  35. }
  36. // Validate validates the Gotify message payload according to the Gotify schema.
  37. func (p *Payload) Validate() error {
  38. if p.Message == "" {
  39. return errors.New("gotify: missing message")
  40. }
  41. if p.Priority < 0 {
  42. return errors.New("gotify: priority must be non-negative")
  43. }
  44. return nil
  45. }
  46. func (c *Client) SendMessage(title, message string, priority int, extras map[string]any) error {
  47. if c.baseURL == "" || c.token == "" {
  48. return errors.New("gotify: missing base URL or token")
  49. }
  50. msg := &Payload{
  51. Title: title,
  52. Message: message,
  53. Priority: priority,
  54. Extras: extras,
  55. }
  56. if err := msg.Validate(); err != nil {
  57. return err
  58. }
  59. apiEndpoint := strings.TrimRight(c.baseURL, "/")
  60. if !strings.HasSuffix(apiEndpoint, "/message") {
  61. var err error
  62. apiEndpoint, err = urllib.JoinBaseURLAndPath(c.baseURL, "/message")
  63. if err != nil {
  64. return fmt.Errorf("gotify: invalid API endpoint: %w", err)
  65. }
  66. }
  67. response, err := client.NewRequestBuilder(apiEndpoint).
  68. WithMethod(http.MethodPost).
  69. WithHeader("X-Gotify-Key", c.token).
  70. WithJSON(msg).
  71. Do()
  72. if err != nil {
  73. return fmt.Errorf("gotify: %w", err)
  74. }
  75. defer response.Body.Close()
  76. if response.StatusCode >= 400 {
  77. return fmt.Errorf("gotify: unable to send message: url=%s status=%d", apiEndpoint, response.StatusCode)
  78. }
  79. return nil
  80. }
  81. func (c *Client) SendMessages(feed *model.Feed, entries model.Entries) error {
  82. if c.baseURL == "" || c.token == "" {
  83. return errors.New("gotify: missing base URL or token")
  84. }
  85. var title string
  86. if feed != nil {
  87. title = feed.Title
  88. }
  89. for _, entry := range entries {
  90. if entry == nil {
  91. continue
  92. }
  93. message := entry.Title
  94. if message == "" {
  95. message = entry.URL
  96. }
  97. if message == "" {
  98. continue
  99. }
  100. slog.Debug("Sending Gotify notification",
  101. slog.String("url", c.baseURL),
  102. slog.String("title", title),
  103. slog.String("entry_title", entry.Title),
  104. slog.String("entry_url", entry.URL),
  105. )
  106. extras := make(map[string]any)
  107. if entry.URL != "" {
  108. extras["client::notification"] = map[string]any{
  109. "click": map[string]any{
  110. "url": entry.URL,
  111. },
  112. }
  113. }
  114. if err := c.SendMessage(title, message, c.priority, extras); err != nil {
  115. return err
  116. }
  117. }
  118. return nil
  119. }