commit 612738049ef9d092560a198a09ad8a6fac008d0e
Author: Paco Esteban <paco@e1e0.net>
Date: Thu, 30 Jul 2020 20:20:16 +0200
initial commit.
it authenticates and lists categories.
I'm now hitting a problem with the way ttrss returns JSON for this,
with a mix of ints and strings that makes json unmarshalling crash.
I've sent a patch for it but I can't do much more without adding a lot
of complexity to the project at this stage.
Diffstat:
A | cmd/ttrss-cli/main.go | | | 87 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | go.mod | | | 8 | ++++++++ |
A | go.sum | | | 15 | +++++++++++++++ |
A | pkg/ttrss/ttrss.go | | | 218 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
4 files changed, 328 insertions(+), 0 deletions(-)
diff --git a/cmd/ttrss-cli/main.go b/cmd/ttrss-cli/main.go
@@ -0,0 +1,87 @@
+// Copyright (c) 2020 Paco Esteban <paco@e1e0.net>
+
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+package main
+
+import (
+ "fmt"
+ "log"
+ "os"
+
+ "git.e1e0.net/go-ttrss/pkg/ttrss"
+ "github.com/urfave/cli/v2"
+)
+
+func main() {
+
+ ttrssCli, err := ttrss.New()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ app := &cli.App{
+ Name: "ttrss-cli",
+ Usage: "interact with your TTRSS instance from the command line",
+ Version: "v0.1",
+ Commands: []*cli.Command{
+ {
+ Name: "list",
+ Aliases: []string{"ls"},
+ Usage: "list feeds",
+ Action: func(c *cli.Context) error {
+ ttrssCli.List(42)
+ return nil
+ },
+ },
+ {
+ Name: "categories",
+ Aliases: []string{"cat"},
+ Usage: "list categories",
+ Action: func(c *cli.Context) error {
+ categories, err := ttrssCli.Categories()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ for _, cat := range categories {
+ fmt.Printf("%s \n", cat.Title)
+ }
+ return nil
+ },
+ },
+ {
+ Name: "subscribe",
+ Aliases: []string{"sub"},
+ Usage: "subscribe to feed",
+ Action: func(c *cli.Context) error {
+ ttrssCli.Subscribe()
+ return nil
+ },
+ },
+ {
+ Name: "unsubscribe",
+ Aliases: []string{"rm", "unsub"},
+ Usage: "unsubscribe from feed",
+ Action: func(c *cli.Context) error {
+ ttrssCli.Unsubscribe()
+ return nil
+ },
+ },
+ },
+ }
+
+ err = app.Run(os.Args)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/go.mod b/go.mod
@@ -0,0 +1,8 @@
+module git.e1e0.net/go-ttrss
+
+go 1.15
+
+require (
+ github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
+ github.com/urfave/cli/v2 v2.2.0
+)
diff --git a/go.sum b/go.sum
@@ -0,0 +1,15 @@
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA=
+github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4=
+github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/pkg/ttrss/ttrss.go b/pkg/ttrss/ttrss.go
@@ -0,0 +1,218 @@
+// partialy implemented TTRSS api wrapper
+// Copyright (c) 2020 Paco Esteban <paco@e1e0.net>
+
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+package ttrss
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+)
+
+// Return status values
+const (
+ API_STATUS_OK = 0
+ API_STATUS_ERR = 1
+)
+
+// Predefined category IDs
+const (
+ CATEGORY_UNCATEGORIZED = 0
+ CATEGORY_SPECIAL = -1
+ CATEGORY_LABELS = -2
+ CATEGORY_FEEDS_NOT_VIRTUAL = -3
+ CATEGORY_FEEDS_ALL = -4
+)
+
+type Client struct {
+ BaseUrl string
+ User string
+ password string
+ sessionID string
+ httpClient *http.Client
+}
+
+type Item struct {
+ Name string
+}
+
+type Category struct {
+ Id interface{} `json:"id"`
+ Title string `json:"title"`
+ Unread interface{} `json:"unread"`
+ OrderId int `json:"order_id"`
+}
+
+// JSON response returned by the TTRSS API.
+type Resp struct {
+ Seq int
+ Status int
+ Error error
+ Content json.RawMessage
+}
+
+type apiError struct {
+ Message string `json:"error"`
+}
+
+type apiLogin struct {
+ SessionId string `json:"session_id"`
+ ApiLevel int `json:"api_level"`
+}
+
+func New() (*Client, error) {
+ if len(os.Getenv("TTRSS_HOST")) == 0 || len(os.Getenv("TTRSS_USER")) == 0 || len(os.Getenv("TTRSS_PWD")) == 0 {
+ return &Client{}, errors.New("TTRSS_* vars not set")
+ }
+
+ return &Client{
+ BaseUrl: os.Getenv("TTRSS_HOST"),
+ User: os.Getenv("TTRSS_USER"),
+ password: os.Getenv("TTRSS_PWD"),
+ httpClient: &http.Client{
+ Timeout: time.Minute,
+ },
+ }, nil
+}
+
+// apiCall issues an API request.
+// body should always contain at least "op".
+func (c *Client) apiCall(body map[string]interface{}) (resp Resp, err error) {
+ if c.sessionID != "" {
+ body["sid"] = c.sessionID
+ }
+
+ jsonObject, err := json.Marshal(body)
+ if err != nil {
+ return
+ }
+
+ apiUrl := c.BaseUrl
+ if !strings.HasSuffix(apiUrl, "/") {
+ apiUrl += "/"
+ }
+ apiUrl += "api/"
+
+ httpResp, err := c.httpClient.Post(apiUrl, "application/json",
+ bytes.NewBuffer(jsonObject))
+ if err != nil {
+ err = fmt.Errorf("api connection error: %v", err)
+ return
+ }
+ defer httpResp.Body.Close()
+
+ bodyBytes, err := ioutil.ReadAll(httpResp.Body)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ if err = json.Unmarshal(bodyBytes, &resp); err != nil {
+ err = fmt.Errorf("api JSON response error: %v", err)
+ return
+ }
+
+ if resp.Status == API_STATUS_ERR {
+ var apiE apiError
+ if err = json.Unmarshal(resp.Content, &apiE); err != nil {
+ err = fmt.Errorf("api error JSON unmarshall error: %v", err)
+ return
+ }
+ err = fmt.Errorf(apiE.Message)
+ }
+
+ return
+}
+
+func (c *Client) login() error {
+ loginMap := map[string]interface{}{
+ "op": "login",
+ "user": c.User,
+ "password": c.password,
+ }
+ resp, err := c.apiCall(loginMap)
+ if err != nil {
+ return err
+ }
+
+ var apiL apiLogin
+ if err = json.Unmarshal(resp.Content, &apiL); err != nil {
+ err = fmt.Errorf("api error JSON unmarshall login: %v", err)
+ return err
+ }
+ if len(apiL.SessionId) < 1 {
+ return fmt.Errorf("login failed as %s", c.User)
+ }
+ c.sessionID = apiL.SessionId
+
+ return nil
+}
+
+// List categories
+func (c *Client) Categories() ([]Category, error) {
+ if err := c.login(); err != nil {
+ return nil, err
+ }
+
+ var cat []Category
+ catMap := map[string]interface{}{
+ "op": "getCategories",
+ "unread_only": "false",
+ "enble_nested": "false",
+ "include_empty": "true",
+ }
+ resp, err := c.apiCall(catMap)
+ if err != nil {
+ return cat, err
+ }
+
+ if err = json.Unmarshal(resp.Content, &cat); err != nil {
+ err = fmt.Errorf("api error JSON unmarshall categories: %v", err)
+ return cat, err
+ }
+
+ return cat, nil
+}
+
+// List feeds in a category
+func (c *Client) List(category int) ([]Item, error) {
+ if err := c.login(); err != nil {
+ return nil, err
+ }
+
+ var items []Item
+ return items, nil
+}
+
+// Subscribe to feed
+func (c *Client) Subscribe() error {
+ if err := c.login(); err != nil {
+ return err
+ }
+ return nil
+}
+
+// Unsubscribe from feed
+func (c *Client) Unsubscribe() error {
+ if err := c.login(); err != nil {
+ return err
+ }
+ return nil
+}