Skip to content

Commit

Permalink
Fix:interation:Pinboard Do not override existing bookmarks
Browse files Browse the repository at this point in the history
The issue:
When saving an entry that is already bookmarked on pinboard,
miniflux was basically overriding all existing data on piboard
with this action. This removed any `extended` content or worse
changed the `private` settings to no private making any previously
private bookmark publicly available.

The fix:
Now, upon saving an entry as bookmark I first fetch it. If it
already exists, I do the proper modifications (adding tag and any state)
miniflux would have normally done, then do the add again. This way, no
data is lost in the process. Pinboard has a pretty stable API, I don't think
any new fields will be added soon.

I manually tested the integration by hitting the save button in the following situations:
 - Entry url does not exist on pinboard
  - Bookmark is properly added on pinboard with tags and `to read` acroding to miniflux settings
 - Entry url already exist on piboard
  - Existing data stays unchanged
  - Tags in miniflux settings are properly added to the bookmark
  - Set `to read` is properly set to `yes` when the option is checked on miniflux. Nothing is changed otherwise
  • Loading branch information
ztec committed Jun 25, 2024
1 parent a0106c9 commit 177bb53
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 9 deletions.
71 changes: 62 additions & 9 deletions internal/integration/pinboard/pinboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
package pinboard // import "miniflux.app/v2/internal/integration/pinboard"

import (
"encoding/xml"
"errors"
"fmt"
"miniflux.app/v2/internal/version"
"net/http"
"net/url"
"time"

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

var errPostNotFound = fmt.Errorf("pinboard: post not found")
var errMissingCredentials = fmt.Errorf("pinboard: missing auth token")

const defaultClientTimeout = 10 * time.Second

type Client struct {
Expand All @@ -24,20 +28,27 @@ func NewClient(authToken string) *Client {

func (c *Client) CreateBookmark(entryURL, entryTitle, pinboardTags string, markAsUnread bool) error {
if c.authToken == "" {
return fmt.Errorf("pinboard: missing auth token")
return errMissingCredentials
}

// We check if the url is already bookmarked to avoid overriding existing data.
post, err := c.getBookmark(entryURL)

if err != nil && errors.Is(err, errPostNotFound) {
post = NewPost(entryURL, entryTitle)
} else if err != nil {
// In case of any other error, we return immediately to avoid overriding existing data.
return err
}

toRead := "no"
post.addTag(pinboardTags)
if markAsUnread {
toRead = "yes"
post.SetToread()
}

values := url.Values{}
values.Add("auth_token", c.authToken)
values.Add("url", entryURL)
values.Add("description", entryTitle)
values.Add("tags", pinboardTags)
values.Add("toread", toRead)
post.AddValues(values)

apiEndpoint := "https://api.pinboard.in/v1/posts/add?" + values.Encode()
request, err := http.NewRequest(http.MethodGet, apiEndpoint, nil)
Expand All @@ -61,3 +72,45 @@ func (c *Client) CreateBookmark(entryURL, entryTitle, pinboardTags string, markA

return nil
}

// getBookmark fetches a bookmark from Pinboard. https://www.pinboard.in/api/#posts_get
func (c *Client) getBookmark(entryURL string) (*Post, error) {
if c.authToken == "" {
return nil, errMissingCredentials
}

values := url.Values{}
values.Add("auth_token", c.authToken)
values.Add("url", entryURL)

apiEndpoint := "https://api.pinboard.in/v1/posts/get?" + values.Encode()
request, err := http.NewRequest(http.MethodGet, apiEndpoint, nil)
if err != nil {
return nil, fmt.Errorf("pinboard: unable to create request: %v", err)
}

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

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

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

var results posts
err = xml.NewDecoder(response.Body).Decode(&results)
if err != nil {
return nil, fmt.Errorf("pinboard: unable to decode XML: %v", err)
}

if len(results.Posts) == 0 {
return nil, errPostNotFound
}

return &results.Posts[0], nil
}
59 changes: 59 additions & 0 deletions internal/integration/pinboard/post.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package pinboard

import (
"encoding/xml"
"net/url"
"strings"
"time"
)

// Post a Pinboard bookmark. "inspiration" from https://github.com/drags/pinboard/blob/master/posts.go#L32-L42
type Post struct {
XMLName xml.Name `xml:"post"`
Url string `xml:"href,attr"`
Description string `xml:"description,attr"`
Tags string `xml:"tag,attr"`
Extended string `xml:"extended,attr"`
Date time.Time `xml:"time,attr"`
Shared string `xml:"shared,attr"`
Toread string `xml:"toread,attr"`
}

// Posts A result of a Pinboard API call
type posts struct {
XMLName xml.Name `xml:"posts"`
Posts []Post `xml:"post"`
}

func NewPost(url string, description string) *Post {
return &Post{
Url: url,
Description: description,
Date: time.Now(),
Toread: "no",
}
}

func (p *Post) addTag(tag string) {
if !strings.Contains(p.Tags, tag) {
p.Tags += " " + tag
}
}

func (p *Post) SetToread() {
p.Toread = "yes"
}

func (p *Post) AddValues(values url.Values) {
values.Add("url", p.Url)
values.Add("description", p.Description)
values.Add("tags", p.Tags)
if p.Toread != "" {
values.Add("toread", p.Toread)
}
if p.Shared != "" {
values.Add("shared", p.Shared)
}
values.Add("dt", p.Date.Format(time.RFC3339))
values.Add("extended", p.Extended)
}

0 comments on commit 177bb53

Please sign in to comment.