Browse Source

Merge branch 'v2'

tags/v2.0.0^0
Matt Baer 1 year ago
parent
commit
8cf5d50560
9 changed files with 286 additions and 169 deletions
  1. +10
    -15
      README.md
  2. +1
    -3
      auth_test.go
  3. +51
    -2
      collection.go
  4. +65
    -21
      collection_test.go
  5. +1
    -1
      go.mod
  6. +56
    -28
      post.go
  7. +57
    -80
      post_test.go
  8. +1
    -3
      user.go
  9. +44
    -16
      writeas.go

+ 10
- 15
README.md View File

@@ -1,13 +1,17 @@
# go-writeas

[![godoc](https://godoc.org/go.code.as/writeas.v1?status.svg)](https://godoc.org/go.code.as/writeas.v1)
[![godoc](https://godoc.org/go.code.as/writeas.v2?status.svg)](https://godoc.org/go.code.as/writeas.v2)

Official Write.as Go client library.

## Installation

**Warning**: the `v2` branch is under heavy development and its API will change without notice.

For a stable API, use `go.code.as/writeas.v1` and upgrade to `v2` once everything is merged into `master`.

```bash
go get go.code.as/writeas.v1
go get go.code.as/writeas.v2
```

## Documentation
@@ -17,7 +21,7 @@ See all functionality and usages in the [API documentation](https://developer.wr
### Example usage

```go
import "go.code.as/writeas.v1"
import "go.code.as/writeas.v2"

func main() {
// Create the client
@@ -37,11 +41,7 @@ func main() {
token := p.Token

// Update a published post
p, err = c.UpdatePost(&writeas.PostParams{
OwnedPostParams: writeas.OwnedPostParams{
ID: p.ID,
Token: token,
},
p, err = c.UpdatePost(p.ID, token, &writeas.PostParams{
Content: "Now it's been updated!",
})
if err != nil {
@@ -55,12 +55,7 @@ func main() {
}

// Delete a post
err = c.DeletePost(&writeas.PostParams{
OwnedPostParams: writeas.OwnedPostParams{
ID: p.ID,
Token: token,
},
})
err = c.DeletePost(p.ID, token)
}
```

@@ -68,7 +63,7 @@ func main() {

The library covers our usage, but might not be comprehensive of the API. So we always welcome contributions and improvements from the community. Before sending pull requests, make sure you've done the following:

* Run `go fmt` on all updated .go files.
* Run `goimports` on all updated .go files.
* Document all exported structs and funcs.

## License


+ 1
- 3
auth_test.go View File

@@ -1,8 +1,6 @@
package writeas

import (
"testing"
)
import "testing"

func TestAuthentication(t *testing.T) {
dwac := NewDevClient()


+ 51
- 2
collection.go View File

@@ -26,8 +26,9 @@ type (

// CollectionParams holds values for creating a collection.
CollectionParams struct {
Alias string `json:"alias"`
Title string `json:"title"`
Alias string `json:"alias"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
}
)

@@ -112,6 +113,30 @@ func (c *Client) GetCollectionPosts(alias string) (*[]Post, error) {
}
}

// GetCollectionPost retrieves a post from a collection
// and any error (in user-friendly form) that occurs). See
// https://developers.write.as/docs/api/#retrieve-a-collection-post
func (c *Client) GetCollectionPost(alias, slug string) (*Post, error) {
post := Post{}

env, err := c.get(fmt.Sprintf("/collections/%s/posts/%s", alias, slug), &post)
if err != nil {
return nil, err
}

if _, ok := env.Data.(*Post); !ok {
return nil, fmt.Errorf("Wrong data returned from API.")
}

if env.Code == http.StatusOK {
return &post, nil
} else if env.Code == http.StatusNotFound {
return nil, fmt.Errorf("Post %s not found in collection %s", slug, alias)
}

return nil, fmt.Errorf("Problem getting post %s from collection %s: %d. %v\n", slug, alias, env.Code, err)
}

// GetUserCollections retrieves the authenticated user's collections.
// See https://developers.write.as/docs/api/#retrieve-user-39-s-collections
func (c *Client) GetUserCollections() (*[]Collection, error) {
@@ -135,3 +160,27 @@ func (c *Client) GetUserCollections() (*[]Collection, error) {
}
return colls, nil
}

// DeleteCollection permanently deletes a collection and makes any posts on it
// anonymous.
//
// See https://developers.write.as/docs/api/#delete-a-collection.
func (c *Client) DeleteCollection(alias string) error {
endpoint := "/collections/" + alias
env, err := c.delete(endpoint, nil /* data */)
if err != nil {
return err
}

status := env.Code
switch status {
case http.StatusNoContent:
return nil
case http.StatusUnauthorized:
return fmt.Errorf("Not authenticated.")
case http.StatusBadRequest:
return fmt.Errorf("Bad request: %s", env.ErrorMessage)
default:
return fmt.Errorf("Problem deleting collection: %d. %s\n", status, env.ErrorMessage)
}
}

+ 65
- 21
collection_test.go View File

@@ -2,34 +2,51 @@ package writeas

import (
"fmt"
"strings"
"testing"
"time"
)

func TestGetCollection(t *testing.T) {
wac := NewClient()
dwac := NewDevClient()

res, err := wac.GetCollection("blog")
res, err := dwac.GetCollection("tester")
if err != nil {
t.Errorf("Unexpected fetch results: %+v, err: %v\n", res, err)
} else {
t.Logf("Collection: %+v", res)
if res.Title != "write.as" {
t.Errorf("Unexpected fetch results: %+v\n", res)
}
}
if res == nil {
t.Error("Expected collection to not be nil")
}
}

func TestGetCollectionPosts(t *testing.T) {
wac := NewClient()
dwac := NewDevClient()
posts := []Post{}

res, err := wac.GetCollectionPosts("blog")
if err != nil {
t.Errorf("Unexpected fetch results: %+v, err: %v\n", res, err)
} else {
t.Run("Get all posts in collection", func(t *testing.T) {
res, err := dwac.GetCollectionPosts("tester")
if err != nil {
t.Errorf("Unexpected fetch results: %+v, err: %v\n", res, err)
}
if len(*res) == 0 {
t.Errorf("No posts returned!")
t.Error("Expected at least on post in collection")
}
}
posts = *res
})
t.Run("Get one post from collection", func(t *testing.T) {
res, err := dwac.GetCollectionPost("tester", posts[0].Slug)
if err != nil {
t.Errorf("Unexpected fetch results: %+v, err: %v\n", res, err)
}

if res == nil {
t.Errorf("No post returned!")
}

if len(res.Content) == 0 {
t.Errorf("Post content is empty!")
}
})
}

func TestGetUserCollections(t *testing.T) {
@@ -51,13 +68,40 @@ func TestGetUserCollections(t *testing.T) {
}
}

func ExampleClient_GetCollection() {
c := NewClient()
coll, err := c.GetCollection("blog")
func TestCreateAndDeleteCollection(t *testing.T) {
wac := NewDevClient()
_, err := wac.LogIn("demo", "demo")
if err != nil {
fmt.Printf("%v", err)
return
t.Fatalf("Unable to log in: %v", err)
}
defer wac.LogOut()

now := time.Now().Unix()
alias := fmt.Sprintf("test-collection-%v", now)
c, err := wac.CreateCollection(&CollectionParams{
Alias: alias,
Title: fmt.Sprintf("Test Collection %v", now),
})
if err != nil {
t.Fatalf("Unable to create collection %q: %v", alias, err)
}

if err := wac.DeleteCollection(c.Alias); err != nil {
t.Fatalf("Unable to delete collection %q: %v", alias, err)
}
}

func TestDeleteCollectionUnauthenticated(t *testing.T) {
wac := NewDevClient()

now := time.Now().Unix()
alias := fmt.Sprintf("test-collection-does-not-exist-%v", now)
err := wac.DeleteCollection(alias)
if err == nil {
t.Fatalf("Should not be able to delete collection %q unauthenticated.", alias)
}

if !strings.Contains(err.Error(), "Not authenticated") {
t.Fatalf("Error message should be more informative: %v", err)
}
fmt.Printf("%s", coll.Title)
// Output: write.as
}

+ 1
- 1
go.mod View File

@@ -1,4 +1,4 @@
module github.com/writeas/go-writeas
module github.com/writeas/go-writeas/v2

go 1.9



+ 56
- 28
post.go View File

@@ -31,7 +31,7 @@ type (

// OwnedPostParams are, together, fields only the original post author knows.
OwnedPostParams struct {
ID string `json:"-"`
ID string `json:"id"`
Token string `json:"token,omitempty"`
}

@@ -42,11 +42,14 @@ type (
Token string `json:"token,omitempty"`

// Parameters for creating or updating
Title string `json:"title,omitempty"`
Content string `json:"body,omitempty"`
Font string `json:"font,omitempty"`
IsRTL *bool `json:"rtl,omitempty"`
Language *string `json:"lang,omitempty"`
Slug string `json:"slug"`
Created *time.Time `json:"created,omitempty"`
Updated *time.Time `json:"updated,omitempty"`
Title string `json:"title,omitempty"`
Content string `json:"body,omitempty"`
Font string `json:"font,omitempty"`
IsRTL *bool `json:"rtl,omitempty"`
Language *string `json:"lang,omitempty"`

// Parameters only for creating
Crosspost []map[string]string `json:"crosspost,omitempty"`
@@ -102,7 +105,7 @@ func (c *Client) GetPost(id string) (*Post, error) {
} else if status == http.StatusGone {
return nil, fmt.Errorf("Post unpublished.")
}
return nil, fmt.Errorf("Problem getting post: %d. %v\n", status, err)
return nil, fmt.Errorf("Problem getting post: %d. %s\n", status, env.ErrorMessage)
}

// CreatePost publishes a new post, returning a user-friendly error if one comes
@@ -124,20 +127,33 @@ func (c *Client) CreatePost(sp *PostParams) (*Post, error) {
}

status := env.Code
if status == http.StatusCreated {
return p, nil
} else if status == http.StatusBadRequest {
return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage)
} else {
return nil, fmt.Errorf("Problem getting post: %d. %v\n", status, err)
if status != http.StatusCreated {
if status == http.StatusBadRequest {
return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage)
}
return nil, fmt.Errorf("Problem creating post: %d. %s\n", status, env.ErrorMessage)
}
return p, nil
}

// UpdatePost updates a published post with the given PostParams. See
// https://developer.write.as/docs/api/#update-a-post.
func (c *Client) UpdatePost(sp *PostParams) (*Post, error) {
func (c *Client) UpdatePost(id, token string, sp *PostParams) (*Post, error) {
return c.updatePost("", id, token, sp)
}

func (c *Client) updatePost(collection, identifier, token string, sp *PostParams) (*Post, error) {
p := &Post{}
env, err := c.put(fmt.Sprintf("/posts/%s", sp.ID), sp, p)
endpoint := "/posts/" + identifier
/*
if collection != "" {
endpoint = "/collections/" + collection + endpoint
} else {
sp.Token = token
}
*/
sp.Token = token
env, err := c.put(endpoint, sp, p)
if err != nil {
return nil, err
}
@@ -154,17 +170,29 @@ func (c *Client) UpdatePost(sp *PostParams) (*Post, error) {
} else if status == http.StatusBadRequest {
return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage)
}
return nil, fmt.Errorf("Problem getting post: %d. %v\n", status, err)
return nil, fmt.Errorf("Problem updating post: %d. %s\n", status, env.ErrorMessage)
}
return p, nil
}

// DeletePost permanently deletes a published post. See
// https://developer.write.as/docs/api/#delete-a-post.
func (c *Client) DeletePost(sp *PostParams) error {
env, err := c.delete(fmt.Sprintf("/posts/%s", sp.ID), map[string]string{
"token": sp.Token,
})
func (c *Client) DeletePost(id, token string) error {
return c.deletePost("", id, token)
}

func (c *Client) deletePost(collection, identifier, token string) error {
p := map[string]string{}
endpoint := "/posts/" + identifier
/*
if collection != "" {
endpoint = "/collections/" + collection + endpoint
} else {
p["token"] = token
}
*/
p["token"] = token
env, err := c.delete(endpoint, p)
if err != nil {
return err
}
@@ -177,14 +205,14 @@ func (c *Client) DeletePost(sp *PostParams) error {
} else if status == http.StatusBadRequest {
return fmt.Errorf("Bad request: %s", env.ErrorMessage)
}
return fmt.Errorf("Problem getting post: %d. %v\n", status, err)
return fmt.Errorf("Problem deleting post: %d. %s\n", status, env.ErrorMessage)
}

// ClaimPosts associates anonymous posts with a user / account.
// https://developer.write.as/docs/api/#claim-posts.
func (c *Client) ClaimPosts(sp *[]OwnedPostParams) (*[]ClaimPostResult, error) {
p := &[]ClaimPostResult{}
env, err := c.put("/posts/claim", sp, p)
env, err := c.post("/posts/claim", sp, p)
if err != nil {
return nil, err
}
@@ -202,7 +230,7 @@ func (c *Client) ClaimPosts(sp *[]OwnedPostParams) (*[]ClaimPostResult, error) {
} else if status == http.StatusBadRequest {
return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage)
} else {
return nil, fmt.Errorf("Problem getting post: %d. %v\n", status, err)
return nil, fmt.Errorf("Problem claiming post: %d. %s\n", status, env.ErrorMessage)
}
// TODO: does this also happen with moving posts?
}
@@ -226,7 +254,7 @@ func (c *Client) GetUserPosts() (*[]Post, error) {
if c.isNotLoggedIn(status) {
return nil, fmt.Errorf("Not authenticated.")
}
return nil, fmt.Errorf("Problem getting posts: %d. %v\n", status, err)
return nil, fmt.Errorf("Problem getting user posts: %d. %s\n", status, env.ErrorMessage)
}
return p, nil
}
@@ -251,7 +279,7 @@ func (c *Client) PinPost(alias string, pp *PinnedPostParams) error {
if c.isNotLoggedIn(status) {
return fmt.Errorf("Not authenticated.")
}
return fmt.Errorf("Problem pinning post: %d. %v\n", status, err)
return fmt.Errorf("Problem pinning post: %d. %s\n", status, env.ErrorMessage)
}

// Check the individual post result
@@ -261,7 +289,7 @@ func (c *Client) PinPost(alias string, pp *PinnedPostParams) error {
if (*res)[0].Code != http.StatusOK {
return fmt.Errorf("Problem pinning post: %d", (*res)[0].Code)
// TODO: return ErrorMessage (right now it'll be empty)
// return fmt.Errorf("Problem pinning post: %v", res[0].ErrorMessage)
// return fmt.Errorf("Problem pinning post: %s", res[0].ErrorMessage)
}
return nil
}
@@ -286,7 +314,7 @@ func (c *Client) UnpinPost(alias string, pp *PinnedPostParams) error {
if c.isNotLoggedIn(status) {
return fmt.Errorf("Not authenticated.")
}
return fmt.Errorf("Problem unpinning post: %d. %v\n", status, err)
return fmt.Errorf("Problem unpinning post: %d. %s\n", status, env.ErrorMessage)
}

// Check the individual post result
@@ -296,7 +324,7 @@ func (c *Client) UnpinPost(alias string, pp *PinnedPostParams) error {
if (*res)[0].Code != http.StatusOK {
return fmt.Errorf("Problem unpinning post: %d", (*res)[0].Code)
// TODO: return ErrorMessage (right now it'll be empty)
// return fmt.Errorf("Problem unpinning post: %v", res[0].ErrorMessage)
// return fmt.Errorf("Problem unpinning post: %s", res[0].ErrorMessage)
}
return nil
}

+ 57
- 80
post_test.go View File

@@ -1,89 +1,58 @@
package writeas

import (
"testing"

"fmt"
"strings"
"testing"
)

func TestCreatePost(t *testing.T) {
wac := NewClient()
p, err := wac.CreatePost(&PostParams{
Title: "Title!",
Content: "This is a post.",
Font: "sans",
})
if err != nil {
t.Errorf("Post create failed: %v", err)
return
}
t.Logf("Post created: %+v", p)

token := p.Token

// Update post
p, err = wac.UpdatePost(&PostParams{
ID: p.ID,
Token: token,
Content: "Now it's been updated!",
func TestPostRoundTrip(t *testing.T) {
var id, token string
dwac := NewClient()
t.Run("Create post", func(t *testing.T) {
p, err := dwac.CreatePost(&PostParams{
Title: "Title!",
Content: "This is a post.",
Font: "sans",
})
if err != nil {
t.Errorf("Post create failed: %v", err)
return
}
t.Logf("Post created: %+v", p)
id, token = p.ID, p.Token
})
if err != nil {
t.Errorf("Post update failed: %v", err)
return
}
t.Logf("Post updated: %+v", p)
// Delete post
err = wac.DeletePost(&PostParams{
ID: p.ID,
Token: token,
t.Run("Get post", func(t *testing.T) {
res, err := dwac.GetPost(id)
if err != nil {
t.Errorf("Unexpected fetch results: %+v, err: %v\n", res, err)
} else {
t.Logf("Post: %+v", res)
if res.Content != "This is a post." {
t.Errorf("Unexpected fetch results: %+v\n", res)
}
}
})
if err != nil {
t.Errorf("Post delete failed: %v", err)
return
}
t.Logf("Post deleted!")
}

func TestGetPost(t *testing.T) {
dwac := NewDevClient()
res, err := dwac.GetPost("zekk5r9apum6p")
if err != nil {
t.Errorf("Unexpected fetch results: %+v, err: %v\n", res, err)
} else {
t.Logf("Post: %+v", res)
if res.Content != "This is a post." {
t.Errorf("Unexpected fetch results: %+v\n", res)
t.Run("Update post", func(t *testing.T) {
p, err := dwac.UpdatePost(id, token, &PostParams{
Content: "Now it's been updated!",
})
if err != nil {
t.Errorf("Post update failed: %v", err)
return
}
}

wac := NewClient()
res, err = wac.GetPost("3psnxyhqxy3hq")
if err != nil {
t.Errorf("Unexpected fetch results: %+v, err: %v\n", res, err)
} else {
if !strings.HasPrefix(res.Content, " Write.as Blog") {
t.Errorf("Unexpected fetch results: %+v\n", res)
t.Logf("Post updated: %+v", p)
})
t.Run("Delete post", func(t *testing.T) {
err := dwac.DeletePost(id, token)
if err != nil {
t.Errorf("Post delete failed: %v", err)
return
}
}
}

func TestPinPost(t *testing.T) {
dwac := NewDevClient()
_, err := dwac.LogIn("demo", "demo")
if err != nil {
t.Fatalf("Unable to log in: %v", err)
}
defer dwac.LogOut()

err = dwac.PinPost("tester", &PinnedPostParams{ID: "olx6uk7064heqltf"})
if err != nil {
t.Fatalf("Pin failed: %v", err)
}
t.Logf("Post deleted!")
})
}

func TestUnpinPost(t *testing.T) {
func TestPinUnPin(t *testing.T) {
dwac := NewDevClient()
_, err := dwac.LogIn("demo", "demo")
if err != nil {
@@ -91,17 +60,25 @@ func TestUnpinPost(t *testing.T) {
}
defer dwac.LogOut()

err = dwac.UnpinPost("tester", &PinnedPostParams{ID: "olx6uk7064heqltf"})
if err != nil {
t.Fatalf("Unpin failed: %v", err)
}
t.Run("Pin post", func(t *testing.T) {
err := dwac.PinPost("tester", &PinnedPostParams{ID: "olx6uk7064heqltf"})
if err != nil {
t.Fatalf("Pin failed: %v", err)
}
})
t.Run("Unpin post", func(t *testing.T) {
err := dwac.UnpinPost("tester", &PinnedPostParams{ID: "olx6uk7064heqltf"})
if err != nil {
t.Fatalf("Unpin failed: %v", err)
}
})
}

func ExampleClient_CreatePost() {
c := NewClient()
dwac := NewDevClient()

// Publish a post
p, err := c.CreatePost(&PostParams{
p, err := dwac.CreatePost(&PostParams{
Title: "Title!",
Content: "This is a post.",
Font: "sans",


+ 1
- 3
user.go View File

@@ -1,8 +1,6 @@
package writeas

import (
"time"
)
import "time"

type (
// AuthUser represents a just-authenticated user. It contains information


+ 44
- 16
writeas.go View File

@@ -3,19 +3,23 @@ package writeas

import (
"bytes"
"code.as/core/socks"
"encoding/json"
"fmt"
"github.com/writeas/impart"
"io"
"net/http"
"time"

"code.as/core/socks"
"github.com/writeas/impart"
)

const (
apiURL = "https://write.as/api"
devAPIURL = "https://development.write.as/api"
torAPIURL = "http://writeas7pm7rcdqg.onion/api"

// Current go-writeas version
Version = "2-dev"
)

// Client is used to interact with the Write.as API. It can be used to make
@@ -41,31 +45,55 @@ const defaultHTTPTimeout = 10 * time.Second
// c := writeas.NewClient()
// c.SetToken("00000000-0000-0000-0000-000000000000")
func NewClient() *Client {
return &Client{
client: &http.Client{Timeout: defaultHTTPTimeout},
baseURL: apiURL,
}
return NewClientWith(Config{URL: apiURL})
}

// NewTorClient creates a new API client for communicating with the Write.as
// Tor hidden service, using the given port to connect to the local SOCKS
// proxy.
func NewTorClient(port int) *Client {
dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, fmt.Sprintf("127.0.0.1:%d", port))
transport := &http.Transport{Dial: dialSocksProxy}
return &Client{
client: &http.Client{Transport: transport},
baseURL: torAPIURL,
}
return NewClientWith(Config{URL: torAPIURL, TorPort: port})
}

// NewDevClient creates a new API client for development and testing. It'll
// communicate with our development servers, and SHOULD NOT be used in
// production.
func NewDevClient() *Client {
return NewClientWith(Config{URL: devAPIURL})
}

// Config configures a Write.as client.
type Config struct {
// URL of the Write.as API service. Defaults to https://write.as/api.
URL string

// If specified, the API client will communicate with the Write.as Tor
// hidden service using the provided port to connect to the local SOCKS
// proxy.
TorPort int

// If specified, requests will be authenticated using this user token.
// This may be provided after making a few anonymous requests with
// SetToken.
Token string
}

// NewClientWith builds a new API client with the provided configuration.
func NewClientWith(c Config) *Client {
if c.URL == "" {
c.URL = apiURL
}

httpClient := &http.Client{Timeout: defaultHTTPTimeout}
if c.TorPort > 0 {
dialSocksProxy := socks.DialSocksProxy(socks.SOCKS5, fmt.Sprintf("127.0.0.1:%d", c.TorPort))
httpClient.Transport = &http.Transport{Dial: dialSocksProxy}
}

return &Client{
client: &http.Client{Timeout: defaultHTTPTimeout},
baseURL: devAPIURL,
client: httpClient,
baseURL: c.URL,
token: c.Token,
}
}

@@ -161,9 +189,9 @@ func (c *Client) doRequest(r *http.Request, result interface{}) (*impart.Envelop
func (c *Client) prepareRequest(r *http.Request) {
ua := c.UserAgent
if ua == "" {
ua = "go-writeas v1"
ua = "go-writeas v" + Version
}
r.Header.Add("User-Agent", ua)
r.Header.Set("User-Agent", ua)
r.Header.Add("Content-Type", "application/json")
if c.token != "" {
r.Header.Add("Authorization", "Token "+c.token)


Loading…
Cancel
Save