diff --git a/README.md b/README.md index d1ee8d3..5288001 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,71 @@ # 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 See all functionality and usages in the [API documentation](https://developer.write.as/docs/api/). ### Example usage ```go -import "go.code.as/writeas.v1" +import "go.code.as/writeas.v2" func main() { // Create the client c := writeas.NewClient() // Publish a post p, err := c.CreatePost(&writeas.PostParams{ Title: "Title!", Content: "This is a post.", Font: "sans", }) if err != nil { // Perhaps show err.Error() } // Save token for later, since it won't ever be returned again 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 { // handle } // Get a published post p, err = c.GetPost(p.ID) if err != nil { // handle } // Delete a post - err = c.DeletePost(&writeas.PostParams{ - OwnedPostParams: writeas.OwnedPostParams{ - ID: p.ID, - Token: token, - }, - }) + err = c.DeletePost(p.ID, token) } ``` ## Contributing 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 MIT diff --git a/auth_test.go b/auth_test.go index 3c78c7e..a9ece6f 100644 --- a/auth_test.go +++ b/auth_test.go @@ -1,21 +1,19 @@ package writeas -import ( - "testing" -) +import "testing" func TestAuthentication(t *testing.T) { dwac := NewDevClient() // Log in _, err := dwac.LogIn("demo", "demo") if err != nil { t.Fatalf("Unable to log in: %v", err) } // Log out err = dwac.LogOut() if err != nil { t.Fatalf("Unable to log out: %v", err) } } diff --git a/collection.go b/collection.go index d63e116..9b4a925 100644 --- a/collection.go +++ b/collection.go @@ -1,137 +1,186 @@ package writeas import ( "fmt" "net/http" ) type ( // Collection represents a collection of posts. Blogs are a type of collection // on Write.as. Collection struct { Alias string `json:"alias"` Title string `json:"title"` Description string `json:"description"` StyleSheet string `json:"style_sheet"` Private bool `json:"private"` Views int64 `json:"views"` Domain string `json:"domain,omitempty"` Email string `json:"email,omitempty"` URL string `json:"url,omitempty"` TotalPosts int `json:"total_posts"` Posts *[]Post `json:"posts,omitempty"` } // 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"` } ) // CreateCollection creates a new collection, returning a user-friendly error // if one comes up. Requires a Write.as subscription. See // https://developer.write.as/docs/api/#create-a-collection func (c *Client) CreateCollection(sp *CollectionParams) (*Collection, error) { p := &Collection{} env, err := c.post("/collections", sp, p) if err != nil { return nil, err } var ok bool if p, ok = env.Data.(*Collection); !ok { return nil, fmt.Errorf("Wrong data returned from API.") } status := env.Code if status != http.StatusCreated { if status == http.StatusBadRequest { return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) } else if status == http.StatusForbidden { return nil, fmt.Errorf("Casual or Pro user required.") } else if status == http.StatusConflict { return nil, fmt.Errorf("Collection name is already taken.") } else if status == http.StatusPreconditionFailed { return nil, fmt.Errorf("Reached max collection quota.") } return nil, fmt.Errorf("Problem getting post: %d. %v\n", status, err) } return p, nil } // GetCollection retrieves a collection, returning the Collection and any error // (in user-friendly form) that occurs. See // https://developer.write.as/docs/api/#retrieve-a-collection func (c *Client) GetCollection(alias string) (*Collection, error) { coll := &Collection{} env, err := c.get(fmt.Sprintf("/collections/%s", alias), coll) if err != nil { return nil, err } var ok bool if coll, ok = env.Data.(*Collection); !ok { return nil, fmt.Errorf("Wrong data returned from API.") } status := env.Code if status == http.StatusOK { return coll, nil } else if status == http.StatusNotFound { return nil, fmt.Errorf("Collection not found.") } else { return nil, fmt.Errorf("Problem getting collection: %d. %v\n", status, err) } } // GetCollectionPosts retrieves a collection's posts, returning the Posts // and any error (in user-friendly form) that occurs. See // https://developer.write.as/docs/api/#retrieve-collection-posts func (c *Client) GetCollectionPosts(alias string) (*[]Post, error) { coll := &Collection{} env, err := c.get(fmt.Sprintf("/collections/%s/posts", alias), coll) if err != nil { return nil, err } var ok bool if coll, ok = env.Data.(*Collection); !ok { return nil, fmt.Errorf("Wrong data returned from API.") } status := env.Code if status == http.StatusOK { return coll.Posts, nil } else if status == http.StatusNotFound { return nil, fmt.Errorf("Collection not found.") } else { return nil, fmt.Errorf("Problem getting collection: %d. %v\n", status, err) } } +// 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) { colls := &[]Collection{} env, err := c.get("/me/collections", colls) if err != nil { return nil, err } var ok bool if colls, ok = env.Data.(*[]Collection); !ok { return nil, fmt.Errorf("Wrong data returned from API.") } status := env.Code if status != http.StatusOK { if c.isNotLoggedIn(status) { return nil, fmt.Errorf("Not authenticated.") } return nil, fmt.Errorf("Problem getting collections: %d. %v\n", status, err) } 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) + } +} diff --git a/collection_test.go b/collection_test.go index da9a7f3..d6bb49b 100644 --- a/collection_test.go +++ b/collection_test.go @@ -1,63 +1,107 @@ 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) { wac := NewDevClient() _, err := wac.LogIn("demo", "demo") if err != nil { t.Fatalf("Unable to log in: %v", err) } defer wac.LogOut() res, err := wac.GetUserCollections() if err != nil { t.Errorf("Unexpected fetch results: %+v, err: %v\n", res, err) } else { t.Logf("User collections: %+v", res) if len(*res) == 0 { t.Errorf("No collections returned!") } } } -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 } diff --git a/go.mod b/go.mod index 40ab49a..b88b28a 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ -module github.com/writeas/go-writeas +module github.com/writeas/go-writeas/v2 go 1.9 require ( code.as/core/socks v1.0.0 github.com/writeas/impart v1.1.0 ) diff --git a/post.go b/post.go index 977918e..1f8a55b 100644 --- a/post.go +++ b/post.go @@ -1,302 +1,330 @@ package writeas import ( "fmt" "net/http" "time" ) type ( // Post represents a published Write.as post, whether anonymous, owned by a // user, or part of a collection. Post struct { ID string `json:"id"` Slug string `json:"slug"` Token string `json:"token"` Font string `json:"appearance"` Language *string `json:"language"` RTL *bool `json:"rtl"` Listed bool `json:"listed"` Created time.Time `json:"created"` Updated time.Time `json:"updated"` Title string `json:"title"` Content string `json:"body"` Views int64 `json:"views"` Tags []string `json:"tags"` Images []string `json:"images"` OwnerName string `json:"owner,omitempty"` Collection *Collection `json:"collection,omitempty"` } // OwnedPostParams are, together, fields only the original post author knows. OwnedPostParams struct { - ID string `json:"-"` + ID string `json:"id"` Token string `json:"token,omitempty"` } // PostParams holds values for creating or updating a post. PostParams struct { // Parameters only for updating ID string `json:"-"` 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"` // Parameters for collection posts Collection string `json:"-"` } // PinnedPostParams holds values for pinning a post PinnedPostParams struct { ID string `json:"id"` Position int `json:"position"` } // BatchPostResult contains the post-specific result as part of a larger // batch operation. BatchPostResult struct { ID string `json:"id,omitempty"` Code int `json:"code,omitempty"` ErrorMessage string `json:"error_msg,omitempty"` } // ClaimPostResult contains the post-specific result for a request to // associate a post to an account. ClaimPostResult struct { ID string `json:"id,omitempty"` Code int `json:"code,omitempty"` ErrorMessage string `json:"error_msg,omitempty"` Post *Post `json:"post,omitempty"` } ) // GetPost retrieves a published post, returning the Post and any error (in // user-friendly form) that occurs. See // https://developer.write.as/docs/api/#retrieve-a-post. func (c *Client) GetPost(id string) (*Post, error) { p := &Post{} env, err := c.get(fmt.Sprintf("/posts/%s", id), p) if err != nil { return nil, err } var ok bool if p, ok = env.Data.(*Post); !ok { return nil, fmt.Errorf("Wrong data returned from API.") } status := env.Code if status == http.StatusOK { return p, nil } else if status == http.StatusNotFound { return nil, fmt.Errorf("Post not found.") } 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 // up. See https://developer.write.as/docs/api/#publish-a-post. func (c *Client) CreatePost(sp *PostParams) (*Post, error) { p := &Post{} endPre := "" if sp.Collection != "" { endPre = "/collections/" + sp.Collection } env, err := c.post(endPre+"/posts", sp, p) if err != nil { return nil, err } var ok bool if p, ok = env.Data.(*Post); !ok { return nil, fmt.Errorf("Wrong data returned from API.") } 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 } var ok bool if p, ok = env.Data.(*Post); !ok { return nil, fmt.Errorf("Wrong data returned from API.") } status := env.Code if status != http.StatusOK { if c.isNotLoggedIn(status) { return nil, fmt.Errorf("Not authenticated.") } 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 } status := env.Code if status == http.StatusNoContent { return nil } else if c.isNotLoggedIn(status) { return fmt.Errorf("Not authenticated.") } 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 } var ok bool if p, ok = env.Data.(*[]ClaimPostResult); !ok { return nil, fmt.Errorf("Wrong data returned from API.") } status := env.Code if status == http.StatusOK { return p, nil } else if c.isNotLoggedIn(status) { return nil, fmt.Errorf("Not authenticated.") } 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? } // GetUserPosts retrieves the authenticated user's posts. // See https://developers.write.as/docs/api/#retrieve-user-39-s-posts func (c *Client) GetUserPosts() (*[]Post, error) { p := &[]Post{} env, err := c.get("/me/posts", p) if err != nil { return nil, err } var ok bool if p, ok = env.Data.(*[]Post); !ok { return nil, fmt.Errorf("Wrong data returned from API.") } status := env.Code if status != http.StatusOK { 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 } // PinPost pins a post in the given collection. // See https://developers.write.as/docs/api/#pin-a-post-to-a-collection func (c *Client) PinPost(alias string, pp *PinnedPostParams) error { res := &[]BatchPostResult{} env, err := c.post(fmt.Sprintf("/collections/%s/pin", alias), []*PinnedPostParams{pp}, res) if err != nil { return err } var ok bool if res, ok = env.Data.(*[]BatchPostResult); !ok { return fmt.Errorf("Wrong data returned from API.") } // Check for basic request errors on top level response status := env.Code if status != http.StatusOK { 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 if len(*res) == 0 || len(*res) > 1 { return fmt.Errorf("Wrong data returned from API.") } 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 } // UnpinPost unpins a post from the given collection. // See https://developers.write.as/docs/api/#unpin-a-post-from-a-collection func (c *Client) UnpinPost(alias string, pp *PinnedPostParams) error { res := &[]BatchPostResult{} env, err := c.post(fmt.Sprintf("/collections/%s/unpin", alias), []*PinnedPostParams{pp}, res) if err != nil { return err } var ok bool if res, ok = env.Data.(*[]BatchPostResult); !ok { return fmt.Errorf("Wrong data returned from API.") } // Check for basic request errors on top level response status := env.Code if status != http.StatusOK { 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 if len(*res) == 0 || len(*res) > 1 { return fmt.Errorf("Wrong data returned from API.") } 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 } diff --git a/post_test.go b/post_test.go index abdfa3a..9d2fb47 100644 --- a/post_test.go +++ b/post_test.go @@ -1,116 +1,93 @@ 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 { t.Fatalf("Unable to log in: %v", err) } 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", }) if err != nil { fmt.Printf("Unable to create: %v", err) return } fmt.Printf("%s", p.Content) // Output: This is a post. } diff --git a/user.go b/user.go index e10f3c8..5973d9c 100644 --- a/user.go +++ b/user.go @@ -1,36 +1,34 @@ package writeas -import ( - "time" -) +import "time" type ( // AuthUser represents a just-authenticated user. It contains information // that'll only be returned once (now) per user session. AuthUser struct { AccessToken string `json:"access_token,omitempty"` Password string `json:"password,omitempty"` User *User `json:"user"` } // User represents a registered Write.as user. User struct { Username string `json:"username"` Email string `json:"email"` Created time.Time `json:"created"` // Optional properties Subscription *UserSubscription `json:"subscription"` } // UserSubscription contains information about a user's Write.as // subscription. UserSubscription struct { Name string `json:"name"` Begin time.Time `json:"begin"` End time.Time `json:"end"` AutoRenew bool `json:"auto_renew"` Active bool `json:"is_active"` Delinquent bool `json:"is_delinquent"` } ) diff --git a/writeas.go b/writeas.go index a31b636..fa87ae1 100644 --- a/writeas.go +++ b/writeas.go @@ -1,171 +1,199 @@ // Package writeas provides the binding for the Write.as API 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 // authenticated or unauthenticated calls. type Client struct { baseURL string // Access token for the user making requests. token string // Client making requests to the API client *http.Client // UserAgent overrides the default User-Agent header UserAgent string } // defaultHTTPTimeout is the default http.Client timeout. const defaultHTTPTimeout = 10 * time.Second // NewClient creates a new API client. By default, all requests are made // unauthenticated. To optionally make authenticated requests, call `SetToken`. // // 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, } } // SetToken sets the user token for all future Client requests. Setting this to // an empty string will change back to unauthenticated requests. func (c *Client) SetToken(token string) { c.token = token } // Token returns the user token currently set to the Client. func (c *Client) Token() string { return c.token } func (c *Client) get(path string, r interface{}) (*impart.Envelope, error) { method := "GET" if method != "GET" && method != "HEAD" { return nil, fmt.Errorf("Method %s not currently supported by library (only HEAD and GET).\n", method) } return c.request(method, path, nil, r) } func (c *Client) post(path string, data, r interface{}) (*impart.Envelope, error) { b := new(bytes.Buffer) json.NewEncoder(b).Encode(data) return c.request("POST", path, b, r) } func (c *Client) put(path string, data, r interface{}) (*impart.Envelope, error) { b := new(bytes.Buffer) json.NewEncoder(b).Encode(data) return c.request("PUT", path, b, r) } func (c *Client) delete(path string, data map[string]string) (*impart.Envelope, error) { r, err := c.buildRequest("DELETE", path, nil) if err != nil { return nil, err } q := r.URL.Query() for k, v := range data { q.Add(k, v) } r.URL.RawQuery = q.Encode() return c.doRequest(r, nil) } func (c *Client) request(method, path string, data io.Reader, result interface{}) (*impart.Envelope, error) { r, err := c.buildRequest(method, path, data) if err != nil { return nil, err } return c.doRequest(r, result) } func (c *Client) buildRequest(method, path string, data io.Reader) (*http.Request, error) { url := fmt.Sprintf("%s%s", c.baseURL, path) r, err := http.NewRequest(method, url, data) if err != nil { return nil, fmt.Errorf("Create request: %v", err) } c.prepareRequest(r) return r, nil } func (c *Client) doRequest(r *http.Request, result interface{}) (*impart.Envelope, error) { resp, err := c.client.Do(r) if err != nil { return nil, fmt.Errorf("Request: %v", err) } defer resp.Body.Close() env := &impart.Envelope{ Code: resp.StatusCode, } if result != nil { env.Data = result err = json.NewDecoder(resp.Body).Decode(&env) if err != nil { return nil, err } } return env, nil } 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) } }