소스 검색

Add webhook event for saving entry

Frédéric Guillot 2 년 전
부모
커밋
9990afb722
4개의 변경된 파일121개의 추가작업 그리고 19개의 파일을 삭제
  1. 10 1
      internal/integration/integration.go
  2. 107 13
      internal/integration/webhook/webhook.go
  3. 2 1
      internal/storage/integration.go
  4. 2 4
      internal/ui/entry_save.go

+ 10 - 1
internal/integration/integration.go

@@ -167,6 +167,15 @@ func SendEntry(entry *model.Entry, integration *model.Integration) {
 			logger.Error("[Integration] Unable to send entry #%d to Shaarli for user #%d: %v", entry.ID, integration.UserID, err)
 		}
 	}
+
+	if integration.WebhookEnabled {
+		logger.Debug("[Integration] Sending entry #%d %q for user #%d to Webhook URL: %s", entry.ID, entry.URL, integration.UserID, integration.WebhookURL)
+
+		webhookClient := webhook.NewClient(integration.WebhookURL, integration.WebhookSecret)
+		if err := webhookClient.SendSaveEntryWebhookEvent(entry); err != nil {
+			logger.Error("[Integration] UserID #%d: %v", integration.UserID, err)
+		}
+	}
 }
 
 // PushEntries pushes a list of entries to activated third-party providers during feed refreshes.
@@ -184,7 +193,7 @@ func PushEntries(feed *model.Feed, entries model.Entries, userIntegrations *mode
 		logger.Debug("[Integration] Sending %d entries for user #%d to Webhook URL: %s", len(entries), userIntegrations.UserID, userIntegrations.WebhookURL)
 
 		webhookClient := webhook.NewClient(userIntegrations.WebhookURL, userIntegrations.WebhookSecret)
-		if err := webhookClient.SendWebhook(feed, entries); err != nil {
+		if err := webhookClient.SendNewEntriesWebhookEvent(feed, entries); err != nil {
 			logger.Error("[Integration] sending entries to webhook failed: %v", err)
 		}
 	}

+ 107 - 13
internal/integration/webhook/webhook.go

@@ -15,7 +15,12 @@ import (
 	"miniflux.app/v2/internal/version"
 )
 
-const defaultClientTimeout = 10 * time.Second
+const (
+	defaultClientTimeout = 10 * time.Second
+
+	NewEntriesEventType = "new_entries"
+	SaveEntryEventType  = "save_entry"
+)
 
 type Client struct {
 	webhookURL    string
@@ -26,17 +31,71 @@ func NewClient(webhookURL, webhookSecret string) *Client {
 	return &Client{webhookURL, webhookSecret}
 }
 
-func (c *Client) SendWebhook(feed *model.Feed, entries model.Entries) error {
-	if c.webhookURL == "" {
-		return fmt.Errorf(`webhook: missing webhook URL`)
-	}
+func (c *Client) SendSaveEntryWebhookEvent(entry *model.Entry) error {
+	return c.makeRequest(SaveEntryEventType, &WebhookSaveEntryEvent{
+		EventType: SaveEntryEventType,
+		Entry: &WebhookEntry{
+			ID:          entry.ID,
+			UserID:      entry.UserID,
+			FeedID:      entry.FeedID,
+			Status:      entry.Status,
+			Hash:        entry.Hash,
+			Title:       entry.Title,
+			URL:         entry.URL,
+			CommentsURL: entry.CommentsURL,
+			Date:        entry.Date,
+			CreatedAt:   entry.CreatedAt,
+			ChangedAt:   entry.ChangedAt,
+			Content:     entry.Content,
+			Author:      entry.Author,
+			ShareCode:   entry.ShareCode,
+			Starred:     entry.Starred,
+			ReadingTime: entry.ReadingTime,
+			Enclosures:  entry.Enclosures,
+			Tags:        entry.Tags,
+			Feed: &WebhookFeed{
+				ID:        entry.Feed.ID,
+				UserID:    entry.Feed.UserID,
+				FeedURL:   entry.Feed.FeedURL,
+				SiteURL:   entry.Feed.SiteURL,
+				Title:     entry.Feed.Title,
+				CheckedAt: entry.Feed.CheckedAt,
+			},
+		},
+	})
+}
 
+func (c *Client) SendNewEntriesWebhookEvent(feed *model.Feed, entries model.Entries) error {
 	if len(entries) == 0 {
 		return nil
 	}
 
-	webhookEvent := &WebhookEvent{
-		// Send only a subset of the fields to avoid leaking sensitive data.
+	var webhookEntries []*WebhookEntry
+	for _, entry := range entries {
+		webhookEntries = append(webhookEntries, &WebhookEntry{
+			ID:          entry.ID,
+			UserID:      entry.UserID,
+			FeedID:      entry.FeedID,
+			Status:      entry.Status,
+			Hash:        entry.Hash,
+			Title:       entry.Title,
+			URL:         entry.URL,
+			CommentsURL: entry.CommentsURL,
+			Date:        entry.Date,
+			CreatedAt:   entry.CreatedAt,
+			ChangedAt:   entry.ChangedAt,
+			Content:     entry.Content,
+			Author:      entry.Author,
+			ShareCode:   entry.ShareCode,
+			Starred:     entry.Starred,
+			ReadingTime: entry.ReadingTime,
+			Enclosures:  entry.Enclosures,
+			Tags:        entry.Tags,
+		})
+	}
+
+	return c.makeRequest(NewEntriesEventType, &WebhookNewEntriesEvent{
+		EventType: NewEntriesEventType,
 		Feed: &WebhookFeed{
 			ID:        feed.ID,
 			UserID:    feed.UserID,
@@ -45,10 +104,16 @@ func (c *Client) SendWebhook(feed *model.Feed, entries model.Entries) error {
 			Title:     feed.Title,
 			CheckedAt: feed.CheckedAt,
 		},
-		Entries: entries,
+		Entries: webhookEntries,
+	})
+}
+
+func (c *Client) makeRequest(eventType string, payload any) error {
+	if c.webhookURL == "" {
+		return fmt.Errorf(`webhook: missing webhook URL`)
 	}
 
-	requestBody, err := json.Marshal(webhookEvent)
+	requestBody, err := json.Marshal(payload)
 	if err != nil {
 		return fmt.Errorf("webhook: unable to encode request body: %v", err)
 	}
@@ -61,6 +126,7 @@ func (c *Client) SendWebhook(feed *model.Feed, entries model.Entries) error {
 	request.Header.Set("Content-Type", "application/json")
 	request.Header.Set("User-Agent", "Miniflux/"+version.Version)
 	request.Header.Set("X-Miniflux-Signature", crypto.GenerateSHA256Hmac(c.webhookSecret, requestBody))
+	request.Header.Set("X-Miniflux-Event-Type", eventType)
 
 	httpClient := &http.Client{Timeout: defaultClientTimeout}
 	response, err := httpClient.Do(request)
@@ -70,7 +136,7 @@ func (c *Client) SendWebhook(feed *model.Feed, entries model.Entries) error {
 	defer response.Body.Close()
 
 	if response.StatusCode >= 400 {
-		return fmt.Errorf("webhook: incorrect response status code: url=%s status=%d", c.webhookURL, response.StatusCode)
+		return fmt.Errorf("webhook: incorrect response status code %d for url %s", response.StatusCode, c.webhookURL)
 	}
 
 	return nil
@@ -85,7 +151,35 @@ type WebhookFeed struct {
 	CheckedAt time.Time `json:"checked_at"`
 }
 
-type WebhookEvent struct {
-	Feed    *WebhookFeed  `json:"feed"`
-	Entries model.Entries `json:"entries"`
+type WebhookEntry struct {
+	ID          int64               `json:"id"`
+	UserID      int64               `json:"user_id"`
+	FeedID      int64               `json:"feed_id"`
+	Status      string              `json:"status"`
+	Hash        string              `json:"hash"`
+	Title       string              `json:"title"`
+	URL         string              `json:"url"`
+	CommentsURL string              `json:"comments_url"`
+	Date        time.Time           `json:"published_at"`
+	CreatedAt   time.Time           `json:"created_at"`
+	ChangedAt   time.Time           `json:"changed_at"`
+	Content     string              `json:"content"`
+	Author      string              `json:"author"`
+	ShareCode   string              `json:"share_code"`
+	Starred     bool                `json:"starred"`
+	ReadingTime int                 `json:"reading_time"`
+	Enclosures  model.EnclosureList `json:"enclosures"`
+	Tags        []string            `json:"tags"`
+	Feed        *WebhookFeed        `json:"feed,omitempty"`
+}
+
+type WebhookNewEntriesEvent struct {
+	EventType string          `json:"event_type"`
+	Feed      *WebhookFeed    `json:"feed"`
+	Entries   []*WebhookEntry `json:"entries"`
+}
+
+type WebhookSaveEntryEvent struct {
+	EventType string        `json:"event_type"`
+	Entry     *WebhookEntry `json:"entry"`
 }

+ 2 - 1
internal/storage/integration.go

@@ -428,7 +428,8 @@ func (s *Storage) HasSaveEntry(userID int64) (result bool) {
 				linkding_enabled='t' OR
 				apprise_enabled='t' OR
 				shiori_enabled='t' OR
-				shaarli_enabled='t'
+				shaarli_enabled='t' OR
+				webhook_enabled='t'
 			)
 	`
 	if err := s.db.QueryRow(query, userID).Scan(&result); err != nil {

+ 2 - 4
internal/ui/entry_save.go

@@ -29,15 +29,13 @@ func (h *handler) saveEntry(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	settings, err := h.store.Integration(request.UserID(r))
+	userIntegrations, err := h.store.Integration(request.UserID(r))
 	if err != nil {
 		json.ServerError(w, r, err)
 		return
 	}
 
-	go func() {
-		integration.SendEntry(entry, settings)
-	}()
+	go integration.SendEntry(entry, userIntegrations)
 
 	json.Created(w, r, map[string]string{"message": "saved"})
 }