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.

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