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.

353 lines
8.5 KiB

  1. package commands
  2. import (
  3. "fmt"
  4. "io/ioutil"
  5. "os"
  6. "text/tabwriter"
  7. "github.com/howeyc/gopass"
  8. "github.com/writeas/writeas-cli/api"
  9. "github.com/writeas/writeas-cli/config"
  10. "github.com/writeas/writeas-cli/log"
  11. cli "gopkg.in/urfave/cli.v1"
  12. )
  13. func CmdPost(c *cli.Context) error {
  14. _, err := api.DoPost(c, api.ReadStdIn(), c.String("font"), false, c.Bool("code"))
  15. if config.IsTor(c) {
  16. log.Info(c, "Posted to hidden service...")
  17. } else {
  18. log.Info(c, "Posted...")
  19. }
  20. return err
  21. }
  22. func CmdNew(c *cli.Context) error {
  23. fname, p := api.ComposeNewPost()
  24. if p == nil {
  25. // Assume composeNewPost already told us what the error was. Abort now.
  26. os.Exit(1)
  27. }
  28. // Ensure we have something to post
  29. if len(*p) == 0 {
  30. // Clean up temporary post
  31. if fname != "" {
  32. os.Remove(fname)
  33. }
  34. log.InfolnQuit("Empty post. Bye!")
  35. }
  36. if config.IsTor(c) {
  37. log.Info(c, "Posting to hidden service...")
  38. } else {
  39. log.Info(c, "Posting...")
  40. }
  41. _, err := api.DoPost(c, *p, c.String("font"), false, c.Bool("code"))
  42. if err != nil {
  43. log.Errorln("Error posting: %s\n%s", err, config.MessageRetryCompose(fname))
  44. return cli.NewExitError("", 1)
  45. }
  46. // Clean up temporary post
  47. if fname != "" {
  48. os.Remove(fname)
  49. }
  50. return nil
  51. }
  52. func CmdPublish(c *cli.Context) error {
  53. filename := c.Args().Get(0)
  54. if filename == "" {
  55. return cli.NewExitError("usage: writeas publish <filename>", 1)
  56. }
  57. content, err := ioutil.ReadFile(filename)
  58. if err != nil {
  59. return err
  60. }
  61. if config.IsTor(c) {
  62. log.Info(c, "Publishing to hidden service...")
  63. } else {
  64. log.Info(c, "Publishing...")
  65. }
  66. _, err = api.DoPost(c, content, c.String("font"), false, c.Bool("code"))
  67. // TODO: write local file if directory is set
  68. return err
  69. }
  70. func CmdDelete(c *cli.Context) error {
  71. friendlyID := c.Args().Get(0)
  72. token := c.Args().Get(1)
  73. if friendlyID == "" {
  74. return cli.NewExitError("usage: writeas delete <postId> [<token>]", 1)
  75. }
  76. u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
  77. if token == "" {
  78. // Search for the token locally
  79. token = api.TokenFromID(c, friendlyID)
  80. if token == "" && u == nil {
  81. log.Errorln("Couldn't find an edit token locally. Did you create this post here?")
  82. log.ErrorlnQuit("If you have an edit token, use: writeas delete %s <token>", friendlyID)
  83. }
  84. }
  85. if config.IsTor(c) {
  86. log.Info(c, "Deleting via hidden service...")
  87. } else {
  88. log.Info(c, "Deleting...")
  89. }
  90. err := api.DoDelete(c, friendlyID, token)
  91. if err != nil {
  92. return cli.NewExitError(fmt.Sprintf("Couldn't delete remote copy: %v", err), 1)
  93. }
  94. // TODO: Delete local file, if necessary
  95. return nil
  96. }
  97. func CmdUpdate(c *cli.Context) error {
  98. friendlyID := c.Args().Get(0)
  99. token := c.Args().Get(1)
  100. if friendlyID == "" {
  101. return cli.NewExitError("usage: writeas update <postId> [<token>]", 1)
  102. }
  103. u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
  104. if token == "" {
  105. // Search for the token locally
  106. token = api.TokenFromID(c, friendlyID)
  107. if token == "" && u == nil {
  108. log.Errorln("Couldn't find an edit token locally. Did you create this post here?")
  109. log.ErrorlnQuit("If you have an edit token, use: writeas update %s <token>", friendlyID)
  110. }
  111. }
  112. // Read post body
  113. fullPost := api.ReadStdIn()
  114. if config.IsTor(c) {
  115. log.Info(c, "Updating via hidden service...")
  116. } else {
  117. log.Info(c, "Updating...")
  118. }
  119. return api.DoUpdate(c, fullPost, friendlyID, token, c.String("font"), c.Bool("code"))
  120. }
  121. func CmdGet(c *cli.Context) error {
  122. friendlyID := c.Args().Get(0)
  123. if friendlyID == "" {
  124. return cli.NewExitError("usage: writeas get <postId>", 1)
  125. }
  126. if config.IsTor(c) {
  127. log.Info(c, "Getting via hidden service...")
  128. } else {
  129. log.Info(c, "Getting...")
  130. }
  131. return api.DoFetch(c, friendlyID)
  132. }
  133. func CmdAdd(c *cli.Context) error {
  134. friendlyID := c.Args().Get(0)
  135. token := c.Args().Get(1)
  136. if friendlyID == "" || token == "" {
  137. return cli.NewExitError("usage: writeas add <postId> <token>", 1)
  138. }
  139. err := api.AddPost(c, friendlyID, token)
  140. return err
  141. }
  142. func CmdListPosts(c *cli.Context) error {
  143. urls := c.Bool("url")
  144. ids := c.Bool("id")
  145. details := c.Bool("v")
  146. posts := api.GetPosts(c)
  147. if details {
  148. var p api.Post
  149. tw := tabwriter.NewWriter(os.Stdout, 10, 0, 2, ' ', tabwriter.TabIndent)
  150. numPosts := len(*posts)
  151. if ids || !urls && numPosts != 0 {
  152. fmt.Fprintf(tw, "%s\t%s\t\n", "ID", "Token")
  153. } else if numPosts != 0 {
  154. fmt.Fprintf(tw, "%s\t%s\t\n", "URL", "Token")
  155. } else {
  156. fmt.Fprintf(tw, "No local posts found\n")
  157. }
  158. for i := range *posts {
  159. p = (*posts)[numPosts-1-i]
  160. if ids || !urls {
  161. fmt.Fprintf(tw, "%s\t%s\t\n", p.ID, p.EditToken)
  162. } else {
  163. fmt.Fprintf(tw, "%s\t%s\t\n", getPostURL(c, p.ID), p.EditToken)
  164. }
  165. }
  166. return tw.Flush()
  167. }
  168. for _, p := range *posts {
  169. if ids || !urls {
  170. fmt.Printf("%s\n", p.ID)
  171. } else {
  172. fmt.Printf("%s\n", getPostURL(c, p.ID))
  173. }
  174. }
  175. return nil
  176. }
  177. func getPostURL(c *cli.Context, slug string) string {
  178. base := config.WriteasBaseURL
  179. if config.IsDev() {
  180. base = config.DevBaseURL
  181. }
  182. ext := ""
  183. // Output URL in requested format
  184. if c.Bool("md") {
  185. ext = ".md"
  186. }
  187. return fmt.Sprintf("%s/%s%s", base, slug, ext)
  188. }
  189. func CmdCollections(c *cli.Context) error {
  190. u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
  191. if err != nil {
  192. return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1)
  193. }
  194. if u == nil {
  195. return cli.NewExitError("You must be authenticated to view collections.\nLog in first with: writeas auth <username>", 1)
  196. }
  197. if config.IsTor(c) {
  198. log.Info(c, "Getting blogs via hidden service...")
  199. } else {
  200. log.Info(c, "Getting blogs...")
  201. }
  202. colls, err := api.DoFetchCollections(c)
  203. if err != nil {
  204. return cli.NewExitError(fmt.Sprintf("Couldn't get collections for user %s: %v", u.User.Username, err), 1)
  205. }
  206. urls := c.Bool("url")
  207. tw := tabwriter.NewWriter(os.Stdout, 8, 0, 2, ' ', tabwriter.TabIndent)
  208. detail := "Title"
  209. if urls {
  210. detail = "URL"
  211. }
  212. fmt.Fprintf(tw, "%s\t%s\t\n", "Alias", detail)
  213. for _, c := range colls {
  214. dData := c.Title
  215. if urls {
  216. dData = c.URL
  217. }
  218. fmt.Fprintf(tw, "%s\t%s\t\n", c.Alias, dData)
  219. }
  220. tw.Flush()
  221. return nil
  222. }
  223. func CmdClaim(c *cli.Context) error {
  224. u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
  225. if err != nil {
  226. return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1)
  227. }
  228. if u == nil {
  229. return cli.NewExitError("You must be authenticated to claim local posts.\nLog in first with: writeas auth <username>", 1)
  230. }
  231. localPosts := api.GetPosts(c)
  232. if len(*localPosts) == 0 {
  233. return nil
  234. }
  235. log.Info(c, "Claiming %d post(s) for %s...", len(*localPosts), u.User.Username)
  236. if config.IsTor(c) {
  237. log.Info(c, "...via hidden service...")
  238. }
  239. results, err := api.ClaimPosts(c, localPosts)
  240. if err != nil {
  241. return cli.NewExitError(fmt.Sprintf("Failed to claim posts: %v", err), 1)
  242. }
  243. var okCount, errCount int
  244. for _, r := range *results {
  245. id := r.ID
  246. if id == "" {
  247. // No top-level ID, so the claim was successful
  248. id = r.Post.ID
  249. }
  250. status := fmt.Sprintf("Post %s...", id)
  251. if r.ErrorMessage != "" {
  252. log.Errorln("%serror: %v", status, r.ErrorMessage)
  253. errCount++
  254. } else {
  255. log.Info(c, "%sOK", status)
  256. okCount++
  257. // only delete local if successful
  258. api.RemovePost(c.App.ExtraInfo()["configDir"], id)
  259. }
  260. }
  261. log.Info(c, "%d claimed, %d failed", okCount, errCount)
  262. return nil
  263. }
  264. func CmdAuth(c *cli.Context) error {
  265. // Check configuration
  266. u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
  267. if err != nil {
  268. return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1)
  269. }
  270. if u != nil && u.AccessToken != "" {
  271. return cli.NewExitError("You're already authenticated as "+u.User.Username+". Log out with: writeas logout", 1)
  272. }
  273. // Validate arguments and get password
  274. username := c.Args().Get(0)
  275. if username == "" {
  276. return cli.NewExitError("usage: writeas auth <username>", 1)
  277. }
  278. fmt.Print("Password: ")
  279. pass, err := gopass.GetPasswdMasked()
  280. if err != nil {
  281. return cli.NewExitError(fmt.Sprintf("error reading password: %v", err), 1)
  282. }
  283. // Validate password
  284. if len(pass) == 0 {
  285. return cli.NewExitError("Please enter your password.", 1)
  286. }
  287. if config.IsTor(c) {
  288. log.Info(c, "Logging in to hidden service...")
  289. } else {
  290. log.Info(c, "Logging in...")
  291. }
  292. err = api.DoLogIn(c, username, string(pass))
  293. if err != nil {
  294. return cli.NewExitError(fmt.Sprintf("error logging in: %v", err), 1)
  295. }
  296. return nil
  297. }
  298. func CmdLogOut(c *cli.Context) error {
  299. if config.IsTor(c) {
  300. log.Info(c, "Logging out of hidden service...")
  301. } else {
  302. log.Info(c, "Logging out...")
  303. }
  304. return api.DoLogOut(c)
  305. }