Go client for the Write.as API https://developers.write.as
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

346 line
10 KiB

  1. package writeas
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "time"
  7. )
  8. type (
  9. // Post represents a published Write.as post, whether anonymous, owned by a
  10. // user, or part of a collection.
  11. Post struct {
  12. ID string `json:"id"`
  13. Slug string `json:"slug"`
  14. Token string `json:"token"`
  15. Font string `json:"appearance"`
  16. Language *string `json:"language"`
  17. RTL *bool `json:"rtl"`
  18. Listed bool `json:"listed"`
  19. Type PostType `json:"type"`
  20. Created time.Time `json:"created"`
  21. Updated time.Time `json:"updated"`
  22. Title string `json:"title"`
  23. Content string `json:"body"`
  24. Views int64 `json:"views"`
  25. Tags []string `json:"tags"`
  26. Images []string `json:"images"`
  27. OwnerName string `json:"owner,omitempty"`
  28. Collection *Collection `json:"collection,omitempty"`
  29. }
  30. // OwnedPostParams are, together, fields only the original post author knows.
  31. OwnedPostParams struct {
  32. ID string `json:"id"`
  33. Token string `json:"token,omitempty"`
  34. }
  35. // PostParams holds values for creating or updating a post.
  36. PostParams struct {
  37. // Parameters only for updating
  38. ID string `json:"-"`
  39. Token string `json:"token,omitempty"`
  40. // Parameters for creating or updating
  41. Slug string `json:"slug"`
  42. Created *time.Time `json:"created,omitempty"`
  43. Updated *time.Time `json:"updated,omitempty"`
  44. Title string `json:"title,omitempty"`
  45. Content string `json:"body,omitempty"`
  46. Font string `json:"font,omitempty"`
  47. IsRTL *bool `json:"rtl,omitempty"`
  48. Language *string `json:"lang,omitempty"`
  49. AuthorSlug *string `json:"author,omitempty"`
  50. Categories []Category `json:"categories,omitempty"`
  51. // Parameters only for creating
  52. Crosspost []map[string]string `json:"crosspost,omitempty"`
  53. // Parameters for collection posts
  54. Collection string `json:"-"`
  55. }
  56. // PinnedPostParams holds values for pinning a post
  57. PinnedPostParams struct {
  58. ID string `json:"id"`
  59. Position int `json:"position"`
  60. }
  61. // BatchPostResult contains the post-specific result as part of a larger
  62. // batch operation.
  63. BatchPostResult struct {
  64. ID string `json:"id,omitempty"`
  65. Code int `json:"code,omitempty"`
  66. ErrorMessage string `json:"error_msg,omitempty"`
  67. }
  68. // ClaimPostResult contains the post-specific result for a request to
  69. // associate a post to an account.
  70. ClaimPostResult struct {
  71. ID string `json:"id,omitempty"`
  72. Code int `json:"code,omitempty"`
  73. ErrorMessage string `json:"error_msg,omitempty"`
  74. Post *Post `json:"post,omitempty"`
  75. }
  76. )
  77. type PostType string
  78. const (
  79. TypePost PostType = "post"
  80. TypePrompt = "prompt"
  81. TypePromptArchive = "prompt-arch"
  82. TypeSubmission = "submission"
  83. TypeSubmissionDraft = "submission-draft"
  84. )
  85. // GetPost retrieves a published post, returning the Post and any error (in
  86. // user-friendly form) that occurs. See
  87. // https://developers.write.as/docs/api/#retrieve-a-post.
  88. func (c *Client) GetPost(ctx context.Context, id string) (*Post, error) {
  89. p := &Post{}
  90. env, err := c.get(ctx, fmt.Sprintf("/posts/%s", id), p)
  91. if err != nil {
  92. return nil, err
  93. }
  94. var ok bool
  95. if p, ok = env.Data.(*Post); !ok {
  96. return nil, fmt.Errorf("Wrong data returned from API.")
  97. }
  98. status := env.Code
  99. if status == http.StatusOK {
  100. return p, nil
  101. } else if status == http.StatusNotFound {
  102. return nil, fmt.Errorf("Post not found.")
  103. } else if status == http.StatusGone {
  104. return nil, fmt.Errorf("Post unpublished.")
  105. }
  106. return nil, fmt.Errorf("Problem getting post: %d. %s\n", status, env.ErrorMessage)
  107. }
  108. // CreatePost publishes a new post, returning a user-friendly error if one comes
  109. // up. See https://developers.write.as/docs/api/#publish-a-post.
  110. func (c *Client) CreatePost(ctx context.Context, sp *PostParams) (*Post, error) {
  111. p := &Post{}
  112. endPre := ""
  113. if sp.Collection != "" {
  114. endPre = "/collections/" + sp.Collection
  115. }
  116. env, err := c.post(ctx, endPre+"/posts", sp, p)
  117. if err != nil {
  118. return nil, err
  119. }
  120. var ok bool
  121. if p, ok = env.Data.(*Post); !ok {
  122. return nil, fmt.Errorf("Wrong data returned from API.")
  123. }
  124. status := env.Code
  125. if status != http.StatusCreated {
  126. if status == http.StatusBadRequest {
  127. return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage)
  128. }
  129. return nil, fmt.Errorf("Problem creating post: %d. %s\n", status, env.ErrorMessage)
  130. }
  131. return p, nil
  132. }
  133. // UpdatePost updates a published post with the given PostParams. See
  134. // https://developers.write.as/docs/api/#update-a-post.
  135. func (c *Client) UpdatePost(ctx context.Context, id, token string, sp *PostParams) (*Post, error) {
  136. return c.updatePost(ctx, "", id, token, sp)
  137. }
  138. func (c *Client) updatePost(ctx context.Context, collection, identifier, token string, sp *PostParams) (*Post, error) {
  139. p := &Post{}
  140. endpoint := "/posts/" + identifier
  141. /*
  142. if collection != "" {
  143. endpoint = "/collections/" + collection + endpoint
  144. } else {
  145. sp.Token = token
  146. }
  147. */
  148. sp.Token = token
  149. env, err := c.put(ctx, endpoint, sp, p)
  150. if err != nil {
  151. return nil, err
  152. }
  153. var ok bool
  154. if p, ok = env.Data.(*Post); !ok {
  155. return nil, fmt.Errorf("Wrong data returned from API.")
  156. }
  157. status := env.Code
  158. if status != http.StatusOK {
  159. if c.isNotLoggedIn(status) {
  160. return nil, fmt.Errorf("Not authenticated.")
  161. } else if status == http.StatusBadRequest {
  162. return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage)
  163. }
  164. return nil, fmt.Errorf("Problem updating post: %d. %s\n", status, env.ErrorMessage)
  165. }
  166. return p, nil
  167. }
  168. // DeletePost permanently deletes a published post. See
  169. // https://developers.write.as/docs/api/#delete-a-post.
  170. func (c *Client) DeletePost(ctx context.Context, id, token string) error {
  171. return c.deletePost(ctx, "", id, token)
  172. }
  173. func (c *Client) deletePost(ctx context.Context, collection, identifier, token string) error {
  174. p := map[string]string{}
  175. endpoint := "/posts/" + identifier
  176. /*
  177. if collection != "" {
  178. endpoint = "/collections/" + collection + endpoint
  179. } else {
  180. p["token"] = token
  181. }
  182. */
  183. p["token"] = token
  184. env, err := c.delete(ctx, endpoint, p)
  185. if err != nil {
  186. return err
  187. }
  188. status := env.Code
  189. if status == http.StatusNoContent {
  190. return nil
  191. } else if c.isNotLoggedIn(status) {
  192. return fmt.Errorf("Not authenticated.")
  193. } else if status == http.StatusBadRequest {
  194. return fmt.Errorf("Bad request: %s", env.ErrorMessage)
  195. }
  196. return fmt.Errorf("Problem deleting post: %d. %s\n", status, env.ErrorMessage)
  197. }
  198. // ClaimPosts associates anonymous posts with a user / account.
  199. // https://developers.write.as/docs/api/#claim-posts.
  200. func (c *Client) ClaimPosts(ctx context.Context, sp *[]OwnedPostParams) (*[]ClaimPostResult, error) {
  201. p := &[]ClaimPostResult{}
  202. env, err := c.post(ctx, "/posts/claim", sp, p)
  203. if err != nil {
  204. return nil, err
  205. }
  206. var ok bool
  207. if p, ok = env.Data.(*[]ClaimPostResult); !ok {
  208. return nil, fmt.Errorf("Wrong data returned from API.")
  209. }
  210. status := env.Code
  211. if status == http.StatusOK {
  212. return p, nil
  213. } else if c.isNotLoggedIn(status) {
  214. return nil, fmt.Errorf("Not authenticated.")
  215. } else if status == http.StatusBadRequest {
  216. return nil, fmt.Errorf("Bad request: %s", env.ErrorMessage)
  217. } else {
  218. return nil, fmt.Errorf("Problem claiming post: %d. %s\n", status, env.ErrorMessage)
  219. }
  220. // TODO: does this also happen with moving posts?
  221. }
  222. // GetUserPosts retrieves the authenticated user's posts.
  223. // See https://developers.write.as/docs/api/#retrieve-user-39-s-posts
  224. func (c *Client) GetUserPosts(ctx context.Context) (*[]Post, error) {
  225. p := &[]Post{}
  226. env, err := c.get(ctx, "/me/posts", p)
  227. if err != nil {
  228. return nil, err
  229. }
  230. var ok bool
  231. if p, ok = env.Data.(*[]Post); !ok {
  232. return nil, fmt.Errorf("Wrong data returned from API.")
  233. }
  234. status := env.Code
  235. if status != http.StatusOK {
  236. if c.isNotLoggedIn(status) {
  237. return nil, fmt.Errorf("Not authenticated.")
  238. }
  239. return nil, fmt.Errorf("Problem getting user posts: %d. %s\n", status, env.ErrorMessage)
  240. }
  241. return p, nil
  242. }
  243. // PinPost pins a post in the given collection.
  244. // See https://developers.write.as/docs/api/#pin-a-post-to-a-collection
  245. func (c *Client) PinPost(ctx context.Context, alias string, pp *PinnedPostParams) error {
  246. res := &[]BatchPostResult{}
  247. env, err := c.post(ctx, fmt.Sprintf("/collections/%s/pin", alias), []*PinnedPostParams{pp}, res)
  248. if err != nil {
  249. return err
  250. }
  251. var ok bool
  252. if res, ok = env.Data.(*[]BatchPostResult); !ok {
  253. return fmt.Errorf("Wrong data returned from API.")
  254. }
  255. // Check for basic request errors on top level response
  256. status := env.Code
  257. if status != http.StatusOK {
  258. if c.isNotLoggedIn(status) {
  259. return fmt.Errorf("Not authenticated.")
  260. }
  261. return fmt.Errorf("Problem pinning post: %d. %s\n", status, env.ErrorMessage)
  262. }
  263. // Check the individual post result
  264. if len(*res) == 0 || len(*res) > 1 {
  265. return fmt.Errorf("Wrong data returned from API.")
  266. }
  267. if (*res)[0].Code != http.StatusOK {
  268. return fmt.Errorf("Problem pinning post: %d", (*res)[0].Code)
  269. // TODO: return ErrorMessage (right now it'll be empty)
  270. // return fmt.Errorf("Problem pinning post: %s", res[0].ErrorMessage)
  271. }
  272. return nil
  273. }
  274. // UnpinPost unpins a post from the given collection.
  275. // See https://developers.write.as/docs/api/#unpin-a-post-from-a-collection
  276. func (c *Client) UnpinPost(ctx context.Context, alias string, pp *PinnedPostParams) error {
  277. res := &[]BatchPostResult{}
  278. env, err := c.post(ctx, fmt.Sprintf("/collections/%s/unpin", alias), []*PinnedPostParams{pp}, res)
  279. if err != nil {
  280. return err
  281. }
  282. var ok bool
  283. if res, ok = env.Data.(*[]BatchPostResult); !ok {
  284. return fmt.Errorf("Wrong data returned from API.")
  285. }
  286. // Check for basic request errors on top level response
  287. status := env.Code
  288. if status != http.StatusOK {
  289. if c.isNotLoggedIn(status) {
  290. return fmt.Errorf("Not authenticated.")
  291. }
  292. return fmt.Errorf("Problem unpinning post: %d. %s\n", status, env.ErrorMessage)
  293. }
  294. // Check the individual post result
  295. if len(*res) == 0 || len(*res) > 1 {
  296. return fmt.Errorf("Wrong data returned from API.")
  297. }
  298. if (*res)[0].Code != http.StatusOK {
  299. return fmt.Errorf("Problem unpinning post: %d", (*res)[0].Code)
  300. // TODO: return ErrorMessage (right now it'll be empty)
  301. // return fmt.Errorf("Problem unpinning post: %s", res[0].ErrorMessage)
  302. }
  303. return nil
  304. }