Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions internal/client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package client

import (
"fmt"
"io"
"net/http"
"time"

"github.com/cenkalti/backoff/v4"
)

type Response struct {
StatusCode int
Body string
Header http.Header
Err error
}

func Request(request *http.Request) (*Response, error) {
var response Response

opt := func() error {
return operation(request, &response)
}

if err := backoff.Retry(opt, backoff.NewExponentialBackOff()); err != nil {
return nil, err
}

return &response, nil
}

func operation(request *http.Request, response *Response) error {
client := &http.Client{
Timeout: time.Second * 10,
}
r, err := client.Do(request)
if err != nil {
response.Err = err
return fmt.Errorf("failed to send request: %w", err)
}
defer r.Body.Close()

if r.StatusCode == http.StatusUnauthorized {
err = fmt.Errorf("unauthorized")
response.Err = err
return backoff.Permanent(err)
}

bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
response.Err = err
return fmt.Errorf("failed to read response body: %w", err)
}
response.Body = string(bodyBytes)

response.StatusCode = r.StatusCode
response.Header = r.Header
response.Err = nil

return nil
}
File renamed without changes.
34 changes: 9 additions & 25 deletions linkding/linkding.go
Original file line number Diff line number Diff line change
@@ -1,32 +1,22 @@
package linkding

import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"

"github.com/cenkalti/backoff/v4"
"github.com/tidwall/sjson"

"github.com/juev/sync/internal/client"
)

type Linkding struct {
request http.Request
client *http.Client
}

var (
ErrLinkdingUnauthorized = errors.New("Linkding Unauthorized")
)

func New(apiURL, token string) (*Linkding, error) {
client := &http.Client{
Timeout: time.Second * 10,
}

u, err := url.Parse(apiURL)
if err != nil {
return nil, fmt.Errorf("failed parse linkding apiUrl: %w", err)
Expand All @@ -39,7 +29,6 @@ func New(apiURL, token string) (*Linkding, error) {

return &Linkding{
request: *request,
client: client,
}, nil
}

Expand All @@ -48,19 +37,14 @@ func (l *Linkding) Add(u string) error {
request := l.request
request.Body = io.NopCloser(strings.NewReader(body))

operation := func() error {
response, err := l.client.Do(&request)
if err != nil {
return fmt.Errorf("failed to send request to linkding: %w", err)
}
defer response.Body.Close()

if response.StatusCode == http.StatusUnauthorized {
return backoff.Permanent(ErrLinkdingUnauthorized)
}
response, err := client.Request(&request)
if err != nil {
return fmt.Errorf("failed to send request to linkding: %w", err)
}

return nil
if response.StatusCode == http.StatusUnauthorized {
return fmt.Errorf("got response %d; X-Error=[%s]", response.StatusCode, response.Header.Get("X-Error"))
}

return backoff.Retry(operation, backoff.NewExponentialBackOff())
return nil
}
14 changes: 7 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
"syscall"
"time"

"github.com/juev/sync/linkding"
"github.com/juev/sync/pocket"
"github.com/juev/sync/prettylog"
"github.com/juev/sync/internal/prettylog"
"github.com/juev/sync/pkg/linkding"
"github.com/juev/sync/pkg/pocket"
)

const (
Expand Down Expand Up @@ -94,14 +94,14 @@ func main() {
runProcess := func(since int64) int64 {
slog.Debug("Processing", "since", since)
links, newSince, err := pocketClient.Retrive(since)
if len(links) == 0 {
slog.Debug("No new data from Pocket")
return newSince
}
if err != nil {
slog.Error("Failed to retrieve Pocket data", "error", err)
return since
}
if len(links) == 0 {
slog.Debug("No new data from Pocket")
return newSince
}
for _, link := range links {
slog.Info("Processing", "resolved_url", link)
if err := linkdingClient.Add(link); err != nil {
Expand Down
55 changes: 55 additions & 0 deletions pkg/linkding/linkding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package linkding

import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"

"github.com/juev/sync/internal/client"
"github.com/tidwall/sjson"
)

type Linkding struct {
request http.Request
client *http.Client
}

var (
ErrLinkdingUnauthorized = errors.New("Linkding Unauthorized")
)

func New(apiURL, token string) (*Linkding, error) {
u, err := url.Parse(apiURL)
if err != nil {
return nil, fmt.Errorf("failed parse linkding apiUrl: %w", err)
}
u = u.ResolveReference(&url.URL{Path: "/api/bookmarks/"})

request, _ := http.NewRequest(http.MethodPost, u.String(), nil)
request.Header.Add("Authorization", "Token "+token)
request.Header.Add("Content-Type", "application/json")

return &Linkding{
request: *request,
}, nil
}

func (l *Linkding) Add(u string) error {
body, _ := sjson.Set("", "url", u)
request := l.request
request.Body = io.NopCloser(strings.NewReader(body))

response, err := client.Request(&request)
if err != nil {
return fmt.Errorf("failed to send request to linkding: %w", err)
}

if response.StatusCode == http.StatusUnauthorized {
return fmt.Errorf("got response %d; X-Error=[%s]", response.StatusCode, response.Header.Get("X-Error"))
}

return nil
}
128 changes: 128 additions & 0 deletions pkg/pocket/pocket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package pocket

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

"github.com/juev/sync/internal/client"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
)

type Pocket struct {
ConsumerKey string `json:"consumer_key"`
AccessToken string `json:"access_token"`
State string `json:"state"`
DetailType string `json:"detailType"`
Count int `json:"count"`
Offset int `json:"offset"`
Total int `json:"total"`
body string
client *http.Client
}

const (
endpoint = "https://getpocket.com/v3/get"
pocketCount = 30
pocketTotal = 1
pocketDefaultOffset = 0
pocketState = "unread"
pocketDetailType = "simple"
)

func New(consumerKey, accessToken string) (*Pocket, error) {
body, _ := json.Marshal(&Pocket{
ConsumerKey: consumerKey,
AccessToken: accessToken,
State: pocketState,
DetailType: pocketDetailType,
Count: pocketCount,
Offset: pocketDefaultOffset,
Total: pocketTotal,
})

client := &http.Client{
Timeout: time.Second * 10,
}

return &Pocket{
body: string(body),
client: client,
}, nil
}

func (p *Pocket) Retrive(since int64) ([]string, int64, error) {
offset := pocketDefaultOffset
var (
newSince int64
result []string
err error
)

count := pocketCount
for count > 0 {
var links []string
links, newSince, err = p.operation(since, offset)
if err != nil {
return nil, 0, err
}
count = len(links)
if count > 0 {
result = append(result, links...)
}
offset += pocketCount
}

return result, newSince, nil
}

func (p *Pocket) operation(since int64, offset int) ([]string, int64, error) {
request, _ := http.NewRequest(http.MethodPost, endpoint, nil)
request.Header.Add("Content-Type", "application/json")
request.Header.Add("X-Accept", "application/json")

body := p.body
body, _ = sjson.Set(body, "since", since)
body, _ = sjson.Set(body, "offset", offset)
request.Body = io.NopCloser(strings.NewReader(body))
response, err := client.Request(request)
if err != nil {
return nil, 0, err
}

if response.StatusCode != http.StatusOK {
return nil, 0, fmt.Errorf("got response %d; X-Error=[%s]", response.StatusCode, response.Header.Get("X-Error"))
}

bodyString := response.Body

if gjson.Get(bodyString, "error").String() != "" {
return nil, 0, fmt.Errorf("got response %d; X-Error=[%s]", response.StatusCode, response.Header.Get("X-Error"))
}

// Update since
newSince := gjson.Get(bodyString, "since").Int()

if gjson.Get(bodyString, "status").Int() == 2 {
return nil, newSince, nil
}

list := gjson.Get(bodyString, "list").Map()
var result []string
for k := range list {
value := list[k].String()
u := gjson.Get(value, "resolved_url")
if u.String() == "" {
u = gjson.Get(value, "given_url")
}
if u.Exists() {
result = append(result, u.String())
}
}

return result, newSince, nil
}
Loading
Loading