aboutsummaryrefslogtreecommitdiff
path: root/internal/integration/readeck/readeck.go
blob: a56ea01f6c3981075c0cc1842228fb293db4e4db (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

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

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

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

const defaultClientTimeout = 10 * time.Second

type Client struct {
	baseURL string
	apiKey  string
	labels  string
	onlyURL bool
}

func NewClient(baseURL, apiKey, labels string, onlyURL bool) *Client {
	return &Client{baseURL: baseURL, apiKey: apiKey, labels: labels, onlyURL: onlyURL}
}

func (c *Client) CreateBookmark(entryURL, entryTitle string, entryContent string) error {
	if c.baseURL == "" || c.apiKey == "" {
		return fmt.Errorf("readeck: missing base URL or API key")
	}

	apiEndpoint, err := urllib.JoinBaseURLAndPath(c.baseURL, "/api/bookmarks/")
	if err != nil {
		return fmt.Errorf(`readeck: invalid API endpoint: %v`, err)
	}

	labelsSplitFn := func(c rune) bool {
		return c == ',' || c == ' '
	}
	labelsSplit := strings.FieldsFunc(c.labels, labelsSplitFn)

	var request *http.Request
	if c.onlyURL {
		requestBodyJson, err := json.Marshal(&readeckBookmark{
			Url:    entryURL,
			Title:  entryTitle,
			Labels: labelsSplit,
		})
		if err != nil {
			return fmt.Errorf("readeck: unable to encode request body: %v", err)
		}
		request, err = http.NewRequest(http.MethodPost, apiEndpoint, bytes.NewReader(requestBodyJson))
		if err != nil {
			return fmt.Errorf("readeck: unable to create request: %v", err)
		}
		request.Header.Set("Content-Type", "application/json")
	} else {
		requestBody := new(bytes.Buffer)
		multipartWriter := multipart.NewWriter(requestBody)

		urlPart, err := multipartWriter.CreateFormField("url")
		if err != nil {
			return fmt.Errorf("readeck: unable to encode request body (entry url): %v", err)
		}
		urlPart.Write([]byte(entryURL))

		titlePart, err := multipartWriter.CreateFormField("title")
		if err != nil {
			return fmt.Errorf("readeck: unable to encode request body (entry title): %v", err)
		}
		titlePart.Write([]byte(entryTitle))

		featurePart, err := multipartWriter.CreateFormField("feature_find_main")
		if err != nil {
			return fmt.Errorf("readeck: unable to encode request body (feature_find_main flag): %v", err)
		}
		featurePart.Write([]byte("false")) // false to disable readability

		for _, label := range labelsSplit {
			labelPart, err := multipartWriter.CreateFormField("labels")
			if err != nil {
				return fmt.Errorf("readeck: unable to encode request body (entry labels): %v", err)
			}
			labelPart.Write([]byte(label))
		}

		contentBodyHeader, err := json.Marshal(&partContentHeader{
			Url:           entryURL,
			ContentHeader: contentHeader{ContentType: "text/html; charset=utf-8"},
		})
		if err != nil {
			return fmt.Errorf("readeck: unable to encode request body (entry content header): %v", err)
		}

		contentPart, err := multipartWriter.CreateFormFile("resource", "blob")
		if err != nil {
			return fmt.Errorf("readeck: unable to encode request body (entry content): %v", err)
		}
		contentPart.Write(contentBodyHeader)
		contentPart.Write([]byte("\n"))
		contentPart.Write([]byte(entryContent))

		err = multipartWriter.Close()
		if err != nil {
			return fmt.Errorf("readeck: unable to encode request body: %v", err)
		}
		request, err = http.NewRequest(http.MethodPost, apiEndpoint, requestBody)
		if err != nil {
			return fmt.Errorf("readeck: unable to create request: %v", err)
		}
		request.Header.Set("Content-Type", multipartWriter.FormDataContentType())
	}

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

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

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

	return nil
}

type readeckBookmark struct {
	Url    string   `json:"url"`
	Title  string   `json:"title"`
	Labels []string `json:"labels,omitempty"`
}

type contentHeader struct {
	ContentType string `json:"content-type"`
}

type partContentHeader struct {
	Url           string        `json:"url"`
	ContentHeader contentHeader `json:"headers"`
}