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.

452 lines
11 KiB

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