diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index f4684ce..9f9e72e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,7 +1,7 @@ ## Overview ## Implementation -[API documentation](https://developer.write.as/docs/api/?go#TODO-ENTER-SECTION-HERE). +[API documentation](https://developers.write.as/docs/api/?go#TODO-ENTER-SECTION-HERE). diff --git a/README.md b/README.md index 5288001..029e1ee 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,71 @@ # go-writeas [![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.v2 ``` ## Documentation -See all functionality and usages in the [API documentation](https://developer.write.as/docs/api/). +See all functionality and usages in the [API documentation](https://developers.write.as/docs/api/). ### Example usage ```go 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(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(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 `goimports` on all updated .go files. * Document all exported structs and funcs. ## License MIT diff --git a/auth.go b/auth.go index 3cf4249..26b5eb9 100644 --- a/auth.go +++ b/auth.go @@ -1,75 +1,75 @@ package writeas import ( "fmt" "net/http" ) // LogIn authenticates a user with Write.as. -// See https://developer.write.as/docs/api/#authenticate-a-user +// See https://developers.write.as/docs/api/#authenticate-a-user func (c *Client) LogIn(username, pass string) (*AuthUser, error) { u := &AuthUser{} up := struct { Alias string `json:"alias"` Pass string `json:"pass"` }{ Alias: username, Pass: pass, } env, err := c.post("/auth/login", up, u) if err != nil { return nil, err } var ok bool if u, ok = env.Data.(*AuthUser); !ok { return nil, fmt.Errorf("Wrong data returned from API.") } status := env.Code if status != http.StatusOK { if status == http.StatusBadRequest { return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage) } else if status == http.StatusUnauthorized { return nil, fmt.Errorf("Incorrect password.") } else if status == http.StatusNotFound { return nil, fmt.Errorf("User does not exist.") } else if status == http.StatusTooManyRequests { return nil, fmt.Errorf("Too many log in attempts in a short period of time.") } return nil, fmt.Errorf("Problem authenticating: %d. %v\n", status, err) } c.SetToken(u.AccessToken) return u, nil } // LogOut logs the current user out, making the Client's current access token // invalid. func (c *Client) LogOut() error { env, err := c.delete("/auth/me", nil) if err != nil { return err } status := env.Code if status != http.StatusNoContent { if status == http.StatusNotFound { return fmt.Errorf("Access token is invalid or doesn't exist") } return fmt.Errorf("Unable to log out: %v", env.ErrorMessage) } // Logout successful, so update the Client c.token = "" return nil } func (c *Client) isNotLoggedIn(code int) bool { if c.token == "" { return false } return code == http.StatusUnauthorized } diff --git a/collection.go b/collection.go index 9b4a925..da44638 100644 --- a/collection.go +++ b/collection.go @@ -1,186 +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"` 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 +// https://developers.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 +// https://developers.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 +// https://developers.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/post.go b/post.go index 1f8a55b..33d650f 100644 --- a/post.go +++ b/post.go @@ -1,330 +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"` 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 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. +// https://developers.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. %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. +// up. See https://developers.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 { 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. +// https://developers.write.as/docs/api/#update-a-post. 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{} 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 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. +// https://developers.write.as/docs/api/#delete-a-post. 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 deleting post: %d. %s\n", status, env.ErrorMessage) } // ClaimPosts associates anonymous posts with a user / account. -// https://developer.write.as/docs/api/#claim-posts. +// https://developers.write.as/docs/api/#claim-posts. func (c *Client) ClaimPosts(sp *[]OwnedPostParams) (*[]ClaimPostResult, error) { p := &[]ClaimPostResult{} 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 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 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. %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: %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. %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: %s", res[0].ErrorMessage) } return nil }