Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

integration/raindrop: initial draft implementation #2623

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions internal/database/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -888,4 +888,14 @@ var migrations = []func(tx *sql.Tx) error{
_, err = tx.Exec(`DROP INDEX entries_feed_url_idx`)
return err
},
func(tx *sql.Tx) (err error) {
sql := `
ALTER TABLE integrations ADD COLUMN raindrop_enabled bool default 'f';
ALTER TABLE integrations ADD COLUMN raindrop_token text default '';
ALTER TABLE integrations ADD COLUMN raindrop_collection_id text default '';
ALTER TABLE integrations ADD COLUMN raindrop_tags text default '';
`
_, err = tx.Exec(sql)
return err
},
}
20 changes: 20 additions & 0 deletions internal/integration/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"miniflux.app/v2/internal/integration/omnivore"
"miniflux.app/v2/internal/integration/pinboard"
"miniflux.app/v2/internal/integration/pocket"
"miniflux.app/v2/internal/integration/raindrop"
"miniflux.app/v2/internal/integration/readeck"
"miniflux.app/v2/internal/integration/readwise"
"miniflux.app/v2/internal/integration/shaarli"
Expand Down Expand Up @@ -359,6 +360,7 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
)
}
}

if userIntegrations.OmnivoreEnabled {
slog.Debug("Sending entry to Omnivore",
slog.Int64("user_id", userIntegrations.UserID),
Expand All @@ -376,6 +378,24 @@ func SendEntry(entry *model.Entry, userIntegrations *model.Integration) {
)
}
}

if userIntegrations.RaindropEnabled {
slog.Debug("Sending entry to Raindrop",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int64("entry_id", entry.ID),
slog.String("entry_url", entry.URL),
)

client := raindrop.NewClient(userIntegrations.RaindropToken, userIntegrations.RaindropCollectionID, userIntegrations.RaindropTags)
if err := client.CreateRaindrop(entry.URL, entry.Title); err != nil {
slog.Error("Unable to send entry to Raindrop",
slog.Int64("user_id", userIntegrations.UserID),
slog.Int64("entry_id", entry.ID),
slog.String("entry_url", entry.URL),
slog.Any("error", err),
)
}
}
}

// PushEntries pushes a list of entries to activated third-party providers during feed refreshes.
Expand Down
78 changes: 78 additions & 0 deletions internal/integration/raindrop/raindrop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package raindrop // import "miniflux.app/v2/internal/integration/raindrop"

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"

"miniflux.app/v2/internal/version"
)

const defaultClientTimeout = 10 * time.Second

type Client struct {
token string
collectionID string
tags []string
}

func NewClient(token, collectionID, tags string) *Client {
return &Client{token: token, collectionID: collectionID, tags: strings.Split(tags, ",")}
}

// https://developer.raindrop.io/v1/raindrops/single#create-raindrop
func (c *Client) CreateRaindrop(entryURL, entryTitle string) error {
if c.token == "" {
return fmt.Errorf("raindrop: missing token")
}

var request *http.Request
requestBodyJson, err := json.Marshal(&raindrop{
Link: entryURL,
Title: entryTitle,
Collection: collection{Id: c.collectionID},
Tags: c.tags,
})
if err != nil {
return fmt.Errorf("raindrop: unable to encode request body: %v", err)
}

request, err = http.NewRequest(http.MethodPost, "https://api.raindrop.io/rest/v1/raindrop", bytes.NewReader(requestBodyJson))
if err != nil {
return fmt.Errorf("raindrop: unable to create request: %v", err)
}
request.Header.Set("Content-Type", "application/json")

request.Header.Set("User-Agent", "Miniflux/"+version.Version)
request.Header.Set("Authorization", "Bearer "+c.token)

httpClient := &http.Client{Timeout: defaultClientTimeout}
response, err := httpClient.Do(request)
if err != nil {
return fmt.Errorf("raindrop: unable to send request: %v", err)
}
defer response.Body.Close()

if response.StatusCode >= 400 {
return fmt.Errorf("raindrop: unable to create bookmark: status=%d", response.StatusCode)
}

return nil
}

type raindrop struct {
Link string `json:"link"`
Title string `json:"title"`
Collection collection `json:"collection,omitempty"`
Tags []string `json:"tags"`
}

type collection struct {
Id string `json:"$id"`
}
4 changes: 4 additions & 0 deletions internal/locale/translations/en_US.json
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,10 @@
"form.integration.webhook_secret": "Webhook Secret",
"form.integration.rssbridge_activate": "Check RSS-Bridge when adding subscriptions",
"form.integration.rssbridge_url": "RSS-Bridge server URL",
"form.integration.raindrop_activate": "Save entries to Raindrop",
"form.integration.raindrop_token": "(Test) Token",
"form.integration.raindrop_collection_id": "Collection ID",
"form.integration.raindrop_tags": "Tags (comma-separated)",
"form.api_key.label.description": "API Key Label",
"form.submit.loading": "Loading…",
"form.submit.saving": "Saving…",
Expand Down
4 changes: 4 additions & 0 deletions internal/model/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,8 @@ type Integration struct {
OmnivoreEnabled bool
OmnivoreAPIKey string
OmnivoreURL string
RaindropEnabled bool
RaindropToken string
RaindropCollectionID string
RaindropTags string
}
25 changes: 21 additions & 4 deletions internal/storage/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,11 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
rssbridge_url,
omnivore_enabled,
omnivore_api_key,
omnivore_url
omnivore_url,
raindrop_enabled,
raindrop_token,
raindrop_collection_id,
raindrop_tags
FROM
integrations
WHERE
Expand Down Expand Up @@ -286,6 +290,10 @@ func (s *Storage) Integration(userID int64) (*model.Integration, error) {
&integration.OmnivoreEnabled,
&integration.OmnivoreAPIKey,
&integration.OmnivoreURL,
&integration.RaindropEnabled,
&integration.RaindropToken,
&integration.RaindropCollectionID,
&integration.RaindropTags,
)
switch {
case err == sql.ErrNoRows:
Expand Down Expand Up @@ -386,9 +394,13 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
omnivore_url=$81,
linkwarden_enabled=$82,
linkwarden_url=$83,
linkwarden_api_key=$84
linkwarden_api_key=$84,
raindrop_enabled=$85,
raindrop_token=$86,
raindrop_collection_id=$87,
raindrop_tags=$88
WHERE
user_id=$85
user_id=$89
`
_, err := s.db.Exec(
query,
Expand Down Expand Up @@ -476,6 +488,10 @@ func (s *Storage) UpdateIntegration(integration *model.Integration) error {
integration.LinkwardenEnabled,
integration.LinkwardenURL,
integration.LinkwardenAPIKey,
integration.RaindropEnabled,
integration.RaindropToken,
integration.RaindropCollectionID,
integration.RaindropTags,
integration.UserID,
)

Expand Down Expand Up @@ -513,7 +529,8 @@ func (s *Storage) HasSaveEntry(userID int64) (result bool) {
readeck_enabled='t' OR
shaarli_enabled='t' OR
webhook_enabled='t' OR
omnivore_enabled='t'
omnivore_enabled='t' OR
raindrop_enabled='t'
)
`
if err := s.db.QueryRow(query, userID).Scan(&result); err != nil {
Expand Down
22 changes: 22 additions & 0 deletions internal/template/templates/views/integrations.html
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,28 @@ <h1 id="page-header-title">{{ t "page.integrations.title" }}</h1>
</div>
</details>

<details {{ if .form.RaindropEnabled }}open{{ end }}>
<summary>Raindrop</summary>
<div class="form-section">
<label>
<input type="checkbox" name="raindrop_enabled" value="1" {{ if .form.RaindropEnabled }}checked{{ end }}> {{ t "form.integration.raindrop_activate" }}
</label>

<label for="form-raindrop-token">{{ t "form.integration.raindrop_token" }}</label>
<input type="text" name="raindrop_token" id="form-raindrop-token" value="{{ .form.RaindropToken }}" spellcheck="false">

<label for="form-raindrop-collection-id">{{ t "form.integration.raindrop_collection_id" }}</label>
<input type="text" name="raindrop_collection_id" id="form-raindrop-collection-id" value="{{ .form.RaindropCollectionID }}" spellcheck="false">

<label for="form-raindrop-tags">{{ t "form.integration.raindrop_tags" }}</label>
<input type="text" name="raindrop_tags" id="form-raindrop-tags" value="{{ .form.RaindropTags }}" spellcheck="false">

<div class="buttons">
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.update" }}</button>
</div>
</div>
</details>

<details {{ if .form.ReadeckEnabled }}open{{ end }}>
<summary>Readeck</summary>
<div class="form-section">
Expand Down
12 changes: 12 additions & 0 deletions internal/ui/form/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ type IntegrationForm struct {
OmnivoreEnabled bool
OmnivoreAPIKey string
OmnivoreURL string
RaindropEnabled bool
RaindropToken string
RaindropCollectionID string
RaindropTags string
}

// Merge copy form values to the model.
Expand Down Expand Up @@ -181,6 +185,10 @@ func (i IntegrationForm) Merge(integration *model.Integration) {
integration.OmnivoreEnabled = i.OmnivoreEnabled
integration.OmnivoreAPIKey = i.OmnivoreAPIKey
integration.OmnivoreURL = i.OmnivoreURL
integration.RaindropEnabled = i.RaindropEnabled
integration.RaindropToken = i.RaindropToken
integration.RaindropCollectionID = i.RaindropCollectionID
integration.RaindropTags = i.RaindropTags
}

// NewIntegrationForm returns a new IntegrationForm.
Expand Down Expand Up @@ -269,6 +277,10 @@ func NewIntegrationForm(r *http.Request) *IntegrationForm {
OmnivoreEnabled: r.FormValue("omnivore_enabled") == "1",
OmnivoreAPIKey: r.FormValue("omnivore_api_key"),
OmnivoreURL: r.FormValue("omnivore_url"),
RaindropEnabled: r.FormValue("raindrop_enabled") == "1",
RaindropToken: r.FormValue("raindrop_token"),
RaindropCollectionID: r.FormValue("raindrop_collection_id"),
RaindropTags: r.FormValue("raindrop_tags"),
}
}

Expand Down
4 changes: 4 additions & 0 deletions internal/ui/integration_show.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ func (h *handler) showIntegrationPage(w http.ResponseWriter, r *http.Request) {
OmnivoreEnabled: integration.OmnivoreEnabled,
OmnivoreAPIKey: integration.OmnivoreAPIKey,
OmnivoreURL: integration.OmnivoreURL,
RaindropEnabled: integration.RaindropEnabled,
RaindropToken: integration.RaindropToken,
RaindropCollectionID: integration.RaindropCollectionID,
RaindropTags: integration.RaindropTags,
}

sess := session.New(h.store, request.SessionID(r))
Expand Down