Skip to content

Commit

Permalink
feat(integration): add Pocket support
Browse files Browse the repository at this point in the history
- Add account integration package
- Add account linking API
- Add Pocket account linking
- Add Pocket outgoing webhook
  • Loading branch information
ncarlier committed Mar 3, 2021
1 parent b37ef4b commit 7c77489
Show file tree
Hide file tree
Showing 25 changed files with 556 additions and 67 deletions.
45 changes: 45 additions & 0 deletions pkg/api/linking.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package api

import (
"errors"
"net/http"
"path"
"strings"

"github.com/ncarlier/readflow/pkg/config"
"github.com/ncarlier/readflow/pkg/integration/account"

// import all account providers
_ "github.com/ncarlier/readflow/pkg/integration/account/all"
)

// linking is the handler used for account linking.
func linking(conf *config.Config) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
stub := strings.TrimPrefix(r.URL.Path, "/linking/")
if stub == "" {
http.Error(w, "not found", http.StatusNotFound)
return
}

providerName := path.Dir(stub)
provider, err := account.NewAccountProvider(providerName, conf)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}

action := path.Base(stub)
if action == "request" {
err = provider.RequestHandler(w, r)
} else if action == "authorize" {
err = provider.AuthorizeHandler(w, r)
} else {
err = errors.New("action non supported")
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
}
2 changes: 1 addition & 1 deletion pkg/api/qrcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func qrcodeHandler(conf *config.Config) http.Handler {

// Build UI outgoing webhook configuration URL
payload := strings.Replace(conf.PublicURL, "api.", "", 1)
payload = fmt.Sprintf("%s/settings/integrations/outgoing-webhooks/add?endpoint=%s", payload, url.QueryEscape(u.String()))
payload = fmt.Sprintf("%s/settings/integrations/outgoing-webhooks/add?provider=generic&endpoint=%s", payload, url.QueryEscape(u.String()))

// Build QR code
png, err := qrcode.Encode(payload, qrcode.Medium, 256)
Expand Down
7 changes: 7 additions & 0 deletions pkg/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ func routes(conf *config.Config) Routes {
middleware.Methods("GET", "POST"),
middleware.Cors(origin),
),
route(
"/linking/",
linking(conf),
authnMiddleware,
middleware.Methods("GET"),
middleware.Cors(origin),
),
route(
"/admin",
adminHandler(),
Expand Down
1 change: 1 addition & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ type Config struct {
ImageProxy string `flag:"image-proxy" desc:"Image proxy service (passthrough if empty)"`
UserPlans string `flag:"user-plans" desc:"User plans definition file (deactivated if empty)"`
WebScraping string `flag:"web-scraping" desc:"Web Scraping service (internal if empty)"`
PocketConsumerKey string `flag:"pocket-consumer-key" desc:"Pocket consumer key"`
}
11 changes: 11 additions & 0 deletions pkg/integration/account/account-provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package account

import (
"net/http"
)

// Provider used for account linking
type Provider interface {
RequestHandler(w http.ResponseWriter, r *http.Request) error
AuthorizeHandler(w http.ResponseWriter, r *http.Request) error
}
6 changes: 6 additions & 0 deletions pkg/integration/account/all/all.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package all

import (
// activate pocket account provider
_ "github.com/ncarlier/readflow/pkg/integration/account/pocket"
)
126 changes: 126 additions & 0 deletions pkg/integration/account/pocket/pocket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package pocket

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"

"github.com/ncarlier/readflow/pkg/config"
"github.com/ncarlier/readflow/pkg/constant"
"github.com/ncarlier/readflow/pkg/integration/account"
)

type pocketProvider struct {
key string
}

type pocketAuthorizeRequest struct {
Key string `json:"consumer_key"`
Code string `json:"code"`
}

type pocketRequestResponse struct {
Redirect string `json:"redirect"`
Code string `json:"code"`
}

func newPocketProvider(conf *config.Config) (account.Provider, error) {
if conf.PocketConsumerKey == "" {
return nil, errors.New("Pocket consumer key not set")
}
provider := &pocketProvider{
key: conf.PocketConsumerKey,
}
return provider, nil
}

// RequestHandler used for linking account request
func (p *pocketProvider) RequestHandler(w http.ResponseWriter, r *http.Request) error {
redirect, ok := r.URL.Query()["redirect_uri"]
if !ok || len(redirect[0]) < 1 {
return errors.New("missing redirect_uri parameter")
}
params := url.Values{}
params.Set("consumer_key", p.key)
params.Set("redirect_uri", redirect[0])
payload := strings.NewReader(params.Encode())
resp, err := http.Post("https://getpocket.com/v3/oauth/request", constant.ContentTypeForm, payload)
if err != nil {
return err
}
defer resp.Body.Close()

if resp.StatusCode >= 300 {
return fmt.Errorf("bad status code: %d", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
values, err := url.ParseQuery(string(body))
if err != nil {
return err
}
code := values.Get("code")
if code == "" {
return errors.New("code is empty")
}
params.Del("consumer_key")
params.Set("request_token", code)
data := pocketRequestResponse{
Redirect: "https://getpocket.com/auth/authorize?" + params.Encode(),
Code: code,
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(data)
return nil
}

// AuthorizeHandler used for linking account authorization request
func (p *pocketProvider) AuthorizeHandler(w http.ResponseWriter, r *http.Request) error {
code, ok := r.URL.Query()["code"]
if !ok || len(code[0]) < 1 {
return errors.New("missing code parameter")
}
params := pocketAuthorizeRequest{
Key: p.key,
Code: code[0],
}
b := new(bytes.Buffer)
json.NewEncoder(b).Encode(params)

req, err := http.NewRequest("POST", "https://getpocket.com/v3/oauth/authorize", b)
if err != nil {
return err
}
req.Header.Set("Content-Type", constant.ContentTypeJSON)
req.Header.Set("X-Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

// Forward response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)

return nil
}

func init() {
account.Register("pocket", &account.Def{
Name: "Pocket",
Desc: "Put knowledge in your Pocket.",
Create: newPocketProvider,
})
}
34 changes: 34 additions & 0 deletions pkg/integration/account/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package account

import (
"fmt"

"github.com/ncarlier/readflow/pkg/config"
)

// Creator function for create an account provider
type Creator func(conf *config.Config) (Provider, error)

// Def is a webhook provider definition
type Def struct {
Name string
Desc string
Create Creator
}

// Registry of all account provider
var Registry = map[string]*Def{}

// Register add account provider definition to the registry
func Register(name string, def *Def) {
Registry[name] = def
}

// NewAccountProvider create new account provider
func NewAccountProvider(name string, conf *config.Config) (Provider, error) {
def, ok := Registry[name]
if !ok {
return nil, fmt.Errorf("unknown account provider: %s", name)
}
return def.Create(conf)
}
2 changes: 2 additions & 0 deletions pkg/integration/webhook/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
_ "github.com/ncarlier/readflow/pkg/integration/webhook/generic"
// activate keeper outgoing webhook
_ "github.com/ncarlier/readflow/pkg/integration/webhook/keeper"
// activate pocket outgoing webhook
_ "github.com/ncarlier/readflow/pkg/integration/webhook/pocket"
// activate wallabag outgoing webhook
_ "github.com/ncarlier/readflow/pkg/integration/webhook/wallabag"
)
5 changes: 3 additions & 2 deletions pkg/integration/webhook/generic/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"text/template"
"time"

"github.com/ncarlier/readflow/pkg/config"
"github.com/ncarlier/readflow/pkg/constant"
"github.com/ncarlier/readflow/pkg/integration/webhook"
"github.com/ncarlier/readflow/pkg/model"
Expand Down Expand Up @@ -51,7 +52,7 @@ type Provider struct {
tpl *template.Template
}

func newWebhookProvider(srv model.OutgoingWebhook) (webhook.Provider, error) {
func newWebhookProvider(srv model.OutgoingWebhook, conf config.Config) (webhook.Provider, error) {
config := ProviderConfig{}
if err := json.Unmarshal([]byte(srv.Config), &config); err != nil {
return nil, err
Expand Down Expand Up @@ -86,7 +87,7 @@ func newWebhookProvider(srv model.OutgoingWebhook) (webhook.Provider, error) {
return provider, nil
}

// Archive article to Webhook endpoint.
// Send article to Webhook endpoint.
func (whp *Provider) Send(ctx context.Context, article model.Article) error {
art := webhookArticle{
Title: article.Title,
Expand Down
6 changes: 4 additions & 2 deletions pkg/integration/webhook/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"net/http"
"net/url"

"github.com/ncarlier/readflow/pkg/config"
"github.com/ncarlier/readflow/pkg/constant"
"github.com/ncarlier/readflow/pkg/integration/webhook"
"github.com/ncarlier/readflow/pkg/model"
)
Expand All @@ -31,7 +33,7 @@ type Provider struct {
config ProviderConfig
}

func newKeeperProvider(srv model.OutgoingWebhook) (webhook.Provider, error) {
func newKeeperProvider(srv model.OutgoingWebhook, conf config.Config) (webhook.Provider, error) {
config := ProviderConfig{}
if err := json.Unmarshal([]byte(srv.Config), &config); err != nil {
return nil, err
Expand Down Expand Up @@ -66,7 +68,7 @@ func (kp *Provider) Send(ctx context.Context, article model.Article) error {
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Content-Type", constant.ContentTypeJSON)
req.SetBasicAuth("api", kp.config.APIKey)
client := &http.Client{}
resp, err := client.Do(req)
Expand Down
Loading

0 comments on commit 7c77489

Please sign in to comment.