webhook.go 5.3 KB

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