webhook.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package webhook // import "miniflux.app/v2/internal/integration/webhook"
  4. import (
  5. "bytes"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "net/http"
  10. "time"
  11. "miniflux.app/v2/internal/config"
  12. "miniflux.app/v2/internal/crypto"
  13. "miniflux.app/v2/internal/http/client"
  14. "miniflux.app/v2/internal/model"
  15. "miniflux.app/v2/internal/version"
  16. )
  17. const (
  18. defaultClientTimeout = 10 * time.Second
  19. NewEntriesEventType = "new_entries"
  20. SaveEntryEventType = "save_entry"
  21. )
  22. type Client struct {
  23. webhookURL string
  24. webhookSecret string
  25. }
  26. func NewClient(webhookURL, webhookSecret string) *Client {
  27. return &Client{webhookURL, webhookSecret}
  28. }
  29. func (c *Client) SendSaveEntryWebhookEvent(entry *model.Entry) error {
  30. return c.makeRequest(SaveEntryEventType, &WebhookSaveEntryEvent{
  31. EventType: SaveEntryEventType,
  32. Entry: &WebhookEntry{
  33. ID: entry.ID,
  34. UserID: entry.UserID,
  35. FeedID: entry.FeedID,
  36. Status: entry.Status,
  37. Hash: entry.Hash,
  38. Title: entry.Title,
  39. URL: entry.URL,
  40. CommentsURL: entry.CommentsURL,
  41. Date: entry.Date,
  42. CreatedAt: entry.CreatedAt,
  43. ChangedAt: entry.ChangedAt,
  44. Content: entry.Content,
  45. Author: entry.Author,
  46. ShareCode: entry.ShareCode,
  47. Starred: entry.Starred,
  48. ReadingTime: entry.ReadingTime,
  49. Enclosures: entry.Enclosures,
  50. Tags: entry.Tags,
  51. Feed: &WebhookFeed{
  52. ID: entry.Feed.ID,
  53. UserID: entry.Feed.UserID,
  54. CategoryID: entry.Feed.Category.ID,
  55. Category: &WebhookCategory{ID: entry.Feed.Category.ID, Title: entry.Feed.Category.Title},
  56. FeedURL: entry.Feed.FeedURL,
  57. SiteURL: entry.Feed.SiteURL,
  58. Title: entry.Feed.Title,
  59. CheckedAt: entry.Feed.CheckedAt,
  60. },
  61. },
  62. })
  63. }
  64. func (c *Client) SendNewEntriesWebhookEvent(feed *model.Feed, entries model.Entries) error {
  65. if len(entries) == 0 {
  66. return nil
  67. }
  68. webhookEntries := make([]*WebhookEntry, 0, len(entries))
  69. for _, entry := range entries {
  70. webhookEntries = append(webhookEntries, &WebhookEntry{
  71. ID: entry.ID,
  72. UserID: entry.UserID,
  73. FeedID: entry.FeedID,
  74. Status: entry.Status,
  75. Hash: entry.Hash,
  76. Title: entry.Title,
  77. URL: entry.URL,
  78. CommentsURL: entry.CommentsURL,
  79. Date: entry.Date,
  80. CreatedAt: entry.CreatedAt,
  81. ChangedAt: entry.ChangedAt,
  82. Content: entry.Content,
  83. Author: entry.Author,
  84. ShareCode: entry.ShareCode,
  85. Starred: entry.Starred,
  86. ReadingTime: entry.ReadingTime,
  87. Enclosures: entry.Enclosures,
  88. Tags: entry.Tags,
  89. })
  90. }
  91. return c.makeRequest(NewEntriesEventType, &WebhookNewEntriesEvent{
  92. EventType: NewEntriesEventType,
  93. Feed: &WebhookFeed{
  94. ID: feed.ID,
  95. UserID: feed.UserID,
  96. CategoryID: feed.Category.ID,
  97. Category: &WebhookCategory{ID: feed.Category.ID, Title: feed.Category.Title},
  98. FeedURL: feed.FeedURL,
  99. SiteURL: feed.SiteURL,
  100. Title: feed.Title,
  101. CheckedAt: feed.CheckedAt,
  102. },
  103. Entries: webhookEntries,
  104. })
  105. }
  106. func (c *Client) makeRequest(eventType string, payload any) error {
  107. if c.webhookURL == "" {
  108. return errors.New(`webhook: missing webhook URL`)
  109. }
  110. requestBody, err := json.Marshal(payload)
  111. if err != nil {
  112. return fmt.Errorf("webhook: unable to encode request body: %v", err)
  113. }
  114. request, err := http.NewRequest(http.MethodPost, c.webhookURL, bytes.NewReader(requestBody))
  115. if err != nil {
  116. return fmt.Errorf("webhook: unable to create request: %v", err)
  117. }
  118. request.Header.Set("Content-Type", "application/json")
  119. request.Header.Set("User-Agent", "Miniflux/"+version.Version)
  120. request.Header.Set("X-Miniflux-Signature", crypto.GenerateSHA256Hmac(c.webhookSecret, requestBody))
  121. request.Header.Set("X-Miniflux-Event-Type", eventType)
  122. httpClient := client.NewClientWithOptions(client.Options{Timeout: defaultClientTimeout, BlockPrivateNetworks: !config.Opts.IntegrationAllowPrivateNetworks()})
  123. response, err := httpClient.Do(request)
  124. if err != nil {
  125. return fmt.Errorf("webhook: unable to send request: %v", err)
  126. }
  127. defer response.Body.Close()
  128. if response.StatusCode >= 400 {
  129. return fmt.Errorf("webhook: incorrect response status code %d for url %s", response.StatusCode, c.webhookURL)
  130. }
  131. return nil
  132. }
  133. type WebhookFeed struct {
  134. ID int64 `json:"id"`
  135. UserID int64 `json:"user_id"`
  136. CategoryID int64 `json:"category_id"`
  137. Category *WebhookCategory `json:"category,omitempty"`
  138. FeedURL string `json:"feed_url"`
  139. SiteURL string `json:"site_url"`
  140. Title string `json:"title"`
  141. CheckedAt time.Time `json:"checked_at"`
  142. }
  143. type WebhookCategory struct {
  144. ID int64 `json:"id"`
  145. Title string `json:"title"`
  146. }
  147. type WebhookEntry struct {
  148. ID int64 `json:"id"`
  149. UserID int64 `json:"user_id"`
  150. FeedID int64 `json:"feed_id"`
  151. Status string `json:"status"`
  152. Hash string `json:"hash"`
  153. Title string `json:"title"`
  154. URL string `json:"url"`
  155. CommentsURL string `json:"comments_url"`
  156. Date time.Time `json:"published_at"`
  157. CreatedAt time.Time `json:"created_at"`
  158. ChangedAt time.Time `json:"changed_at"`
  159. Content string `json:"content"`
  160. Author string `json:"author"`
  161. ShareCode string `json:"share_code"`
  162. Starred bool `json:"starred"`
  163. ReadingTime int `json:"reading_time"`
  164. Enclosures model.EnclosureList `json:"enclosures"`
  165. Tags []string `json:"tags"`
  166. Feed *WebhookFeed `json:"feed,omitempty"`
  167. }
  168. type WebhookNewEntriesEvent struct {
  169. EventType string `json:"event_type"`
  170. Feed *WebhookFeed `json:"feed"`
  171. Entries []*WebhookEntry `json:"entries"`
  172. }
  173. type WebhookSaveEntryEvent struct {
  174. EventType string `json:"event_type"`
  175. Entry *WebhookEntry `json:"entry"`
  176. }