Command line client for Write.as https://write.as/apps/cli
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.

291 lines
5.9 KiB

  1. package api
  2. import (
  3. "bufio"
  4. "fmt"
  5. "io"
  6. "io/ioutil"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "time"
  11. writeas "github.com/writeas/go-writeas/v2"
  12. "github.com/writeas/writeas-cli/config"
  13. "github.com/writeas/writeas-cli/fileutils"
  14. "github.com/writeas/writeas-cli/log"
  15. cli "gopkg.in/urfave/cli.v1"
  16. )
  17. const (
  18. postsFile = "posts.psv"
  19. separator = `|`
  20. )
  21. // Post holds the basic authentication information for a Write.as post.
  22. type Post struct {
  23. ID string
  24. EditToken string
  25. }
  26. // RemotePost holds addition information about published
  27. // posts
  28. type RemotePost struct {
  29. Post
  30. Title,
  31. Excerpt,
  32. Slug,
  33. Collection,
  34. EditToken string
  35. Synced bool
  36. Updated time.Time
  37. }
  38. func AddPost(c *cli.Context, id, token string) error {
  39. f, err := os.OpenFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
  40. if err != nil {
  41. return fmt.Errorf("Error creating local posts list: %s", err)
  42. }
  43. defer f.Close()
  44. l := fmt.Sprintf("%s%s%s\n", id, separator, token)
  45. if _, err = f.WriteString(l); err != nil {
  46. return fmt.Errorf("Error writing to local posts list: %s", err)
  47. }
  48. return nil
  49. }
  50. // ClaimPost adds a local post to the authenticated user's account and deletes
  51. // the local reference
  52. func ClaimPosts(c *cli.Context, localPosts *[]Post) (*[]writeas.ClaimPostResult, error) {
  53. cl, err := newClient(c, true)
  54. if err != nil {
  55. return nil, err
  56. }
  57. postsToClaim := make([]writeas.OwnedPostParams, len(*localPosts))
  58. for i, post := range *localPosts {
  59. postsToClaim[i] = writeas.OwnedPostParams{
  60. ID: post.ID,
  61. Token: post.EditToken,
  62. }
  63. }
  64. return cl.ClaimPosts(&postsToClaim)
  65. }
  66. func TokenFromID(c *cli.Context, id string) string {
  67. post := fileutils.FindLine(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile), id)
  68. if post == "" {
  69. return ""
  70. }
  71. parts := strings.Split(post, separator)
  72. if len(parts) < 2 {
  73. return ""
  74. }
  75. return parts[1]
  76. }
  77. func RemovePost(path, id string) {
  78. fileutils.RemoveLine(filepath.Join(config.UserDataDir(path), postsFile), id)
  79. }
  80. func GetPosts(c *cli.Context) *[]Post {
  81. lines := fileutils.ReadData(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile))
  82. posts := []Post{}
  83. if lines != nil && len(*lines) > 0 {
  84. parts := make([]string, 2)
  85. for _, l := range *lines {
  86. parts = strings.Split(l, separator)
  87. if len(parts) < 2 {
  88. continue
  89. }
  90. posts = append(posts, Post{ID: parts[0], EditToken: parts[1]})
  91. }
  92. }
  93. return &posts
  94. }
  95. func GetUserPosts(c *cli.Context) ([]RemotePost, error) {
  96. waposts, err := DoFetchPosts(c)
  97. if err != nil {
  98. return nil, err
  99. }
  100. if len(waposts) == 0 {
  101. return nil, nil
  102. }
  103. posts := []RemotePost{}
  104. for _, p := range waposts {
  105. post := RemotePost{
  106. Post: Post{
  107. ID: p.ID,
  108. EditToken: p.Token,
  109. },
  110. Title: p.Title,
  111. Excerpt: getExcerpt(p.Content),
  112. Slug: p.Slug,
  113. Synced: p.Slug != "",
  114. Updated: p.Updated,
  115. }
  116. if p.Collection != nil {
  117. post.Collection = p.Collection.Alias
  118. }
  119. posts = append(posts, post)
  120. }
  121. return posts, nil
  122. }
  123. // getExcerpt takes in a content string and returns
  124. // a concatenated version. limited to no more than
  125. // two lines of 80 chars each. delimited by '...'
  126. func getExcerpt(input string) string {
  127. length := len(input)
  128. if length <= 80 {
  129. return input
  130. } else if length < 160 {
  131. ln1, idx := trimToLength(input, 80)
  132. if idx == -1 {
  133. idx = 80
  134. }
  135. ln2, _ := trimToLength(input[idx:], 80)
  136. return ln1 + "\n" + ln2
  137. } else {
  138. excerpt := input[:158]
  139. ln1, idx := trimToLength(excerpt, 80)
  140. if idx == -1 {
  141. idx = 80
  142. }
  143. ln2, _ := trimToLength(excerpt[idx:], 80)
  144. return ln1 + "\n" + ln2 + "..."
  145. }
  146. }
  147. func trimToLength(in string, l int) (string, int) {
  148. c := []rune(in)
  149. spaceIdx := -1
  150. length := len(c)
  151. if length <= l {
  152. return in, spaceIdx
  153. }
  154. for i := l; i > 0; i-- {
  155. if c[i] == ' ' {
  156. spaceIdx = i
  157. break
  158. }
  159. }
  160. if spaceIdx > -1 {
  161. c = c[:spaceIdx]
  162. }
  163. return string(c), spaceIdx
  164. }
  165. func ComposeNewPost() (string, *[]byte) {
  166. f, err := fileutils.TempFile(os.TempDir(), "WApost", "txt")
  167. if err != nil {
  168. if config.Debug() {
  169. panic(err)
  170. } else {
  171. log.Errorln("Error creating temp file: %s", err)
  172. return "", nil
  173. }
  174. }
  175. f.Close()
  176. cmd := config.EditPostCmd(f.Name())
  177. if cmd == nil {
  178. os.Remove(f.Name())
  179. fmt.Println(config.NoEditorErr)
  180. return "", nil
  181. }
  182. cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
  183. if err := cmd.Start(); err != nil {
  184. os.Remove(f.Name())
  185. if config.Debug() {
  186. panic(err)
  187. } else {
  188. log.Errorln("Error starting editor: %s", err)
  189. return "", nil
  190. }
  191. }
  192. // If something fails past this point, the temporary post file won't be
  193. // removed automatically. Calling function should handle this.
  194. if err := cmd.Wait(); err != nil {
  195. if config.Debug() {
  196. panic(err)
  197. } else {
  198. log.Errorln("Editor finished with error: %s", err)
  199. return "", nil
  200. }
  201. }
  202. post, err := ioutil.ReadFile(f.Name())
  203. if err != nil {
  204. if config.Debug() {
  205. panic(err)
  206. } else {
  207. log.Errorln("Error reading post: %s", err)
  208. return "", nil
  209. }
  210. }
  211. return f.Name(), &post
  212. }
  213. func WritePost(postsDir string, p *writeas.Post) error {
  214. postFilename := p.ID
  215. collDir := ""
  216. if p.Collection != nil {
  217. postFilename = p.Slug
  218. collDir = p.Collection.Alias
  219. }
  220. postFilename += PostFileExt
  221. txtFile := p.Content
  222. if p.Title != "" {
  223. txtFile = "# " + p.Title + "\n\n" + txtFile
  224. }
  225. return ioutil.WriteFile(filepath.Join(postsDir, collDir, postFilename), []byte(txtFile), 0644)
  226. }
  227. func ReadStdIn() []byte {
  228. numBytes, numChunks := int64(0), int64(0)
  229. r := bufio.NewReader(os.Stdin)
  230. fullPost := []byte{}
  231. buf := make([]byte, 0, 1024)
  232. for {
  233. n, err := r.Read(buf[:cap(buf)])
  234. buf = buf[:n]
  235. if n == 0 {
  236. if err == nil {
  237. continue
  238. }
  239. if err == io.EOF {
  240. break
  241. }
  242. log.ErrorlnQuit("Error reading from stdin: %v", err)
  243. }
  244. numChunks++
  245. numBytes += int64(len(buf))
  246. fullPost = append(fullPost, buf...)
  247. if err != nil && err != io.EOF {
  248. log.ErrorlnQuit("Error appending to end of post: %v", err)
  249. }
  250. }
  251. return fullPost
  252. }