webhook.go 5.5 KB

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