Browse Source

Merge pull request #45 from writeas/develop

WriteFreely CLI v1.0
pull/47/head
Matt Baer 4 years ago
committed by GitHub
parent
commit
c52a2970f8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1576 additions and 341 deletions
  1. +6
    -145
      GUIDE.md
  2. +18
    -74
      README.md
  3. +111
    -36
      api/api.go
  4. +23
    -6
      api/posts.go
  5. +13
    -5
      api/sync.go
  6. +1
    -0
      cmd/wf/.gitignore
  7. +164
    -0
      cmd/wf/GUIDE.md
  8. +95
    -0
      cmd/wf/README.md
  9. +276
    -0
      cmd/wf/commands.go
  10. +5
    -0
      cmd/wf/config_nix.go
  11. +5
    -0
      cmd/wf/config_win.go
  12. +16
    -0
      cmd/wf/flags.go
  13. +250
    -0
      cmd/wf/main.go
  14. +149
    -0
      cmd/writeas/GUIDE.md
  15. +95
    -0
      cmd/writeas/README.md
  16. +1
    -3
      cmd/writeas/config_nix.go
  17. +1
    -3
      cmd/writeas/config_win.go
  18. +13
    -0
      cmd/writeas/flags.go
  19. +7
    -19
      cmd/writeas/main.go
  20. +61
    -24
      commands/commands.go
  21. +20
    -9
      config/config.go
  22. +24
    -4
      config/directories.go
  23. +2
    -1
      config/files_nix.go
  24. +3
    -1
      config/files_win.go
  25. +4
    -0
      config/flags.go
  26. +48
    -4
      config/options.go
  27. +130
    -6
      config/user.go
  28. +13
    -0
      executable/executable.go
  29. +16
    -0
      fileutils/fileutils.go
  30. +1
    -0
      go.mod
  31. +4
    -0
      go.sum
  32. +1
    -1
      log/logging.go

+ 6
- 145
GUIDE.md View File

@@ -1,148 +1,9 @@
# Write.as CLI User Guide
# Write.as / WriteFreely CLI User Guide

The Write.as Command-Line Interface (CLI) is a cross-platform tool for publishing text to [Write.as](https://write.as) and its other sites, like [Paste.as](https://paste.as). It is designed to be simple, scriptable, do one job (publishing) well, and work as you'd expect with other command-line tools.
**This has been split into two user guides:**

Write.as is a text-publishing service that protects your privacy. There's no sign up required to publish, but if you do sign up, you can access posts across devices and compile collections of them in what most people would call a "blog".
## Write.as CLI
See full usage documentation on our [writeas-cli User Guide](https://github.com/writeas/writeas-cli/blob/master/cmd/writeas/GUIDE.md).

## Uses

These are a few common uses for `writeas`. If you get stuck or want to know more, run `writeas [command] --help`. If you still have questions, [ask us](https://write.as/contact).

### Overview

```
writeas [global options] command [command options] [arguments...]

COMMANDS:
post Alias for default action: create post from stdin
new Compose a new post from the command-line and publish
publish Publish a file to Write.as
delete Delete a post
update Update (overwrite) a post
get Read a raw post
add Add an existing post locally
posts List all of your posts
claim Claim local unsynced posts
blogs List blogs
auth Authenticate with Write.as
logout Log out of Write.as
help, h Shows a list of commands or help for one command

GLOBAL OPTIONS:
-c value, -b value Optional blog to post to
--tor, -t Perform action on Tor hidden service
--tor-port value Use a different port to connect to Tor (default: 9150)
--code Specifies this post is code
--md Returns post URL with Markdown enabled
--verbose, -v Make the operation more talkative
--font value Sets post font to given value (default: "mono")
--lang value Sets post language to given ISO 639-1 language code
--user-agent value Sets the User-Agent for API requests
--help, -h show help
--version, -V print the version
```

#### Share something

By default, `writeas` creates a post with a `monospace` typeface that doesn't word wrap (scrolls horizontally). It will return a single line with a URL, and automatically copy that URL to the clipboard:

```bash
$ echo "Hello world!" | writeas
https://write.as/aaaazzzzzzzza
```

This is generally more useful for posting terminal output or code, like so (the `--code` flag turns on syntax highlighting):

macOS / Linux: `cat writeas/cli.go | writeas --code`

Windows: `type writeas/cli.go | writeas.exe --code`

#### Output a post

This outputs any Write.as post with the given ID.

```bash
$ writeas get aaaazzzzzzzza
Hello world!
```

#### Authenticate

This will authenticate with write.as and store the user access token locally, until you explicitly logout.
```bash
$ writeas auth username
Password: ************
```

#### List all blogs

This will output a list of the authenticated user's blogs.
```bash
$ writeas blogs
Alias Title
user An Example Blog
dev My Dev Log
```

#### List posts

This lists all anonymous posts you've published. If authenticated, it will include posts on your account as well as any local / unclaimed posts.

Pass the `--url` flag to show the list with full post URLs, and the `--md` flag to return URLs with Markdown enabled.

To see post IDs with their Edit Tokens pass the `--v` flag.

```bash
$ writeas posts
aaaazzzzzzzza

$ writeas posts -url
https://write.as/aaaazzzzzzzza

$ writeas posts -v
ID Token
aaaazzzzzzzza dhuieoj23894jhf984hdfs9834hdf84j
```

#### Delete a post

This permanently deletes a post you own.

```bash
$ writeas delete aaaazzzzzzzza
```

#### Update a post

This completely overwrites an existing post you own.

```bash
$ echo "See you later!" | writeas update aaaazzzzzzzza
```

#### Claim a post

This moves an unsynced local post to a draft on your account. You will need to authenticate first.
```bash
$ writeas claim aaaazzzzzzzza
```

### Composing posts

If you simply have a penchant for never leaving your keyboard, `writeas` is great for composing new posts from the command-line. Just use the `new` subcommand.

`writeas new` will open your favorite command-line editor, as specified by your `WRITEAS_EDITOR` or `EDITOR` environment variables (in that order), falling back to `vim` on OS X / *nix.

Customize your post's appearance with the `--font` flag:

| Argument | Appearance (Typeface) | Word Wrap? |
| -------- | --------------------- | ---------- |
| `sans` | Sans-serif (Open Sans) | Yes |
| `serif` | Serif (Lora) | Yes |
| `wrap` | Monospace | Yes |
| `mono` | Monospace | No |
| `code` | Syntax-highlighted monospace | No |

Put it all together, e.g. publish with a sans-serif font: `writeas new --font sans`

If you're publishing Markdown, supply the `--md` flag to get a URL back that will render Markdown, e.g.: `writeas new --font sans --md`
## WriteFreely CLI
See full usage documentation on our [wf-cli User Guide](https://github.com/writeas/writeas-cli/blob/master/cmd/wf/GUIDE.md).

+ 18
- 74
README.md View File

@@ -1,19 +1,18 @@
writeas-cli
===========
writeas-cli / wf-cli
====================
![GPL](https://img.shields.io/github/license/writeas/writeas-cli.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/writeas/writeas-cli)](https://goreportcard.com/report/github.com/writeas/writeas-cli) [![#writeas on freenode](https://img.shields.io/badge/freenode-%23writeas-blue.svg)](http://webchat.freenode.net/?channels=writeas) [![Discuss on our forum](https://img.shields.io/discourse/https/discuss.write.as/users.svg?label=forum)](https://discuss.write.as/c/development)

Command line interface for [Write.as](https://write.as). Works on Windows, macOS, and Linux.
Command line utility for publishing to [Write.as](https://write.as) and any other [WriteFreely](https://writefreely.org) instance. Works on Windows, macOS, and Linux.

## Features

* Publish anonymously to Write.as
* Authenticate with a Write.as account
* Authenticate with a Write.as / WriteFreely account
* Publish anonymous posts or drafts to Write.as or WriteFreely, respectively
* A stable, easy back-end for your [GUI app](https://write.as/apps/desktop) or desktop-based workflow
* Compatible with our [Tor hidden service](http://writeas7pm7rcdqg.onion/)
* Locally keeps track of any posts you make
* Update and delete posts, anonymous and authenticated
* Compatible with the [Write.as Tor hidden service](http://writeas7pm7rcdqg.onion/)
* Update and delete posts
* Fetch any post by ID
* Add anonymous post credentials (like for one published with the [Android app](https://play.google.com/store/apps/details?id=com.abunchtell.writeas)) for editing
* ...and more, depending on which client you're using (see respective READMEs for more)

## Installing
The easiest way to get the CLI is to download a pre-built executable for your OS.
@@ -23,74 +22,19 @@ The easiest way to get the CLI is to download a pre-built executable for your OS

Get the latest version for your operating system as a standalone executable.

**Windows**<br />
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_386.zip) executable and put it somewhere in your `%PATH%`.
**Write.as CLI**<br />
See the [writeas-cli README](https://github.com/writeas/writeas-cli/blob/master/cmd/writeas#readme)

**macOS**<br />
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_darwin_amd64.zip) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`.

**Debian-based Linux**<br />
```bash
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys DBE07445
sudo add-apt-repository "deb http://updates.writeas.org xenial main"
sudo apt-get update && sudo apt-get install writeas-cli
```

**Linux (other)**<br />
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`.

### Go get it
```bash
go get github.com/writeas/writeas-cli/cmd/writeas
```

Once this finishes, you'll see `writeas` or `writeas.exe` inside `$GOPATH/bin/`.

## Upgrading

To upgrade the CLI, download and replace the executable you downloaded before.

If you previously installed with `go get`, run it again with the `-u` option.

```bash
go get -u github.com/writeas/writeas-cli/cmd/writeas
```
**WriteFreely CLI**<br />
See the [wf-cli README](https://github.com/writeas/writeas-cli/blob/master/cmd/wf#readme)

## Usage

See full usage documentation on our [User Guide](GUIDE.md).

```
writeas [global options] command [command options] [arguments...]

COMMANDS:
post Alias for default action: create post from stdin
new Compose a new post from the command-line and publish
publish Publish a file to Write.as
delete Delete a post
update Update (overwrite) a post
get Read a raw post
add Add an existing post locally
posts List all of your posts
blogs List blogs
claim Claim local unsynced posts
auth Authenticate with Write.as
logout Log out of Write.as
help, h Shows a list of commands or help for one command

GLOBAL OPTIONS:
-c value, -b value Optional blog to post to
--tor, -t Perform action on Tor hidden service
--tor-port value Use a different port to connect to Tor (default: 9150)
--code Specifies this post is code
--md Returns post URL with Markdown enabled
--verbose, -v Make the operation more talkative
--font value Sets post font to given value (default: "mono")
--lang value Sets post language to given ISO 639-1 language code
--user-agent value Sets the User-Agent for API requests
--help, -h show help
--version, -V print the version
```
**Write.as CLI**<br />
See full usage documentation on our [writeas-cli User Guide](https://github.com/writeas/writeas-cli/blob/master/cmd/writeas/GUIDE.md).

**WriteFreely CLI**<br />
See full usage documentation on our [wf-cli User Guide](https://github.com/writeas/writeas-cli/blob/master/cmd/wf/GUIDE.md).

## Contributing to the CLI

@@ -104,4 +48,4 @@ We're available on [several channels](https://write.as/contact), and prefer our

### Reporting Issues

If you believe you have found a bug in the CLI or its documentation, file an issue on this repo. If you're not sure if it's a bug or not, [reach out to us](https://write.as/contact) in one way or another. Be sure to provide the version of the CLI (with `writeas --version`) in your report.
If you believe you have found a bug in the CLI or its documentation, file an issue on this repo. If you're not sure if it's a bug or not, [reach out to us](https://write.as/contact) in one way or another. Be sure to provide the version of the CLI (with `writeas --version` or `wf --version`) in your report.

+ 111
- 36
api/api.go View File

@@ -2,44 +2,70 @@ package api

import (
"fmt"
"path/filepath"
"strings"

"github.com/atotto/clipboard"
writeas "github.com/writeas/go-writeas/v2"
"github.com/writeas/web-core/posts"
"github.com/writeas/writeas-cli/config"
"github.com/writeas/writeas-cli/fileutils"
"github.com/writeas/writeas-cli/executable"
"github.com/writeas/writeas-cli/log"
cli "gopkg.in/urfave/cli.v1"
)

func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) {
func HostURL(c *cli.Context) string {
host := c.GlobalString("host")
if host == "" {
return ""
}
insecure := c.Bool("insecure")
if parts := strings.Split(host, "://"); len(parts) > 1 {
host = parts[1]
}
scheme := "https://"
if insecure {
scheme = "http://"
}
return scheme + host
}

func newClient(c *cli.Context) (*writeas.Client, error) {
var client *writeas.Client
if config.IsTor(c) {
client = writeas.NewTorClient(config.TorPort(c))
} else {
if config.IsDev() {
client = writeas.NewDevClient()
var clientConfig writeas.Config
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
if err != nil {
return nil, fmt.Errorf("Failed to load configuration file: %v", err)
}
if host := HostURL(c); host != "" {
clientConfig.URL = host + "/api"
} else if cfg.Default.Host != "" && cfg.Default.User != "" {
if parts := strings.Split(cfg.Default.Host, "://"); len(parts) > 1 {
clientConfig.URL = cfg.Default.Host + "/api"
} else {
client = writeas.NewClient()
clientConfig.URL = "https://" + cfg.Default.Host + "/api"
}
} else if config.IsDev() {
clientConfig.URL = config.DevBaseURL + "/api"
} else if c.App.Name == "writeas" {
clientConfig.URL = config.WriteasBaseURL + "/api"
} else {
return nil, fmt.Errorf("Must supply a host. Example: %s --host example.com %s", executable.Name(), c.Command.Name)
}
client.UserAgent = config.UserAgent(c)
// TODO: load user into var shared across the app
u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
if u != nil {
client.SetToken(u.AccessToken)
} else if authRequired {
return nil, fmt.Errorf("Not currently logged in. Authenticate with: writeas auth <username>")
if config.IsTor(c) {
clientConfig.URL = config.TorURL(c)
clientConfig.TorPort = config.TorPort(c)
}

client = writeas.NewClientWith(clientConfig)
client.UserAgent = config.UserAgent(c)

return client, nil
}

// DoFetch retrieves the Write.as post with the given friendlyID,
// optionally via the Tor hidden service.
func DoFetch(c *cli.Context, friendlyID string) error {
cl, err := newClient(c, false)
cl, err := newClient(c)
if err != nil {
return err
}
@@ -59,11 +85,18 @@ func DoFetch(c *cli.Context, friendlyID string) error {
// DoFetchPosts retrieves all remote posts for the
// authenticated user
func DoFetchPosts(c *cli.Context) ([]writeas.Post, error) {
cl, err := newClient(c, true)
cl, err := newClient(c)
if err != nil {
return nil, fmt.Errorf("%v", err)
}

u, _ := config.LoadUser(c)
if u != nil {
cl.SetToken(u.AccessToken)
} else {
return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>")
}

posts, err := cl.GetUserPosts()
if err != nil {
return nil, err
@@ -75,11 +108,18 @@ func DoFetchPosts(c *cli.Context) ([]writeas.Post, error) {
// DoPost creates a Write.as post, returning an error if it was
// unsuccessful.
func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writeas.Post, error) {
cl, err := newClient(c, false)
cl, err := newClient(c)
if err != nil {
return nil, fmt.Errorf("%v", err)
}

u, _ := config.LoadUser(c)
if u != nil {
cl.SetToken(u.AccessToken)
} else {
return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>")
}

pp := &writeas.PostParams{
Font: config.GetFont(code, font),
Collection: config.Collection(c),
@@ -93,14 +133,22 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writ
return nil, fmt.Errorf("Unable to post: %v", err)
}

cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
if err != nil {
return nil, fmt.Errorf("Couldn't check for config file: %v", err)
}
var url string
if p.Collection != nil {
url = p.Collection.URL + p.Slug
} else {
if config.IsTor(c) {
url = config.TorBaseURL
if host := HostURL(c); host != "" {
url = host
} else if cfg.Default.Host != "" {
url = cfg.Default.Host
} else if config.IsDev() {
url = config.DevBaseURL
} else if config.IsTor(c) {
url = config.TorBaseURL
} else {
url = config.WriteasBaseURL
}
@@ -119,7 +167,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writ
// Copy URL to clipboard
err = clipboard.WriteAll(string(url))
if err != nil {
log.Errorln("writeas: Didn't copy to clipboard: %s", err)
log.Errorln(executable.Name()+": Didn't copy to clipboard: %s", err)
} else {
log.Info(c, "Copied to clipboard.")
}
@@ -133,7 +181,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writ
// DoFetchCollections retrieves a list of the currently logged in users
// collections.
func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) {
cl, err := newClient(c, true)
cl, err := newClient(c)
if err != nil {
if config.Debug() {
log.ErrorlnQuit("could not create client: %v", err)
@@ -141,6 +189,13 @@ func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) {
return nil, fmt.Errorf("Couldn't create new client")
}

u, _ := config.LoadUser(c)
if u != nil {
cl.SetToken(u.AccessToken)
} else {
return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>")
}

colls, err := cl.GetUserCollections()
if err != nil {
if config.Debug() {
@@ -165,11 +220,20 @@ func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) {

// DoUpdate updates the given post on Write.as.
func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, code bool) error {
cl, err := newClient(c, false)
cl, err := newClient(c)
if err != nil {
return fmt.Errorf("%v", err)
}

if token == "" {
u, _ := config.LoadUser(c)
if u != nil {
cl.SetToken(u.AccessToken)
} else {
return fmt.Errorf("You must either provide and edit token or log in to delete a post.")
}
}

params := writeas.PostParams{}
params.Title, params.Content = posts.ExtractTitle(string(post))
if lang := config.Language(c, false); lang != "" {
@@ -191,11 +255,20 @@ func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, code

// DoDelete deletes the given post on Write.as, and removes any local references
func DoDelete(c *cli.Context, friendlyID, token string) error {
cl, err := newClient(c, false)
cl, err := newClient(c)
if err != nil {
return fmt.Errorf("%v", err)
}

if token == "" {
u, _ := config.LoadUser(c)
if u != nil {
cl.SetToken(u.AccessToken)
} else {
return fmt.Errorf("You must either provide and edit token or log in to delete a post.")
}
}

err = cl.DeletePost(friendlyID, token)
if err != nil {
if config.Debug() {
@@ -204,13 +277,13 @@ func DoDelete(c *cli.Context, friendlyID, token string) error {
return fmt.Errorf("Post doesn't exist, or bad edit token given.")
}

RemovePost(c.App.ExtraInfo()["configDir"], friendlyID)
RemovePost(c, friendlyID)

return nil
}

func DoLogIn(c *cli.Context, username, password string) error {
cl, err := newClient(c, false)
cl, err := newClient(c)
if err != nil {
return fmt.Errorf("%v", err)
}
@@ -223,7 +296,7 @@ func DoLogIn(c *cli.Context, username, password string) error {
return err
}

err = config.SaveUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]), u)
err = config.SaveUser(c, u)
if err != nil {
return err
}
@@ -232,11 +305,18 @@ func DoLogIn(c *cli.Context, username, password string) error {
}

func DoLogOut(c *cli.Context) error {
cl, err := newClient(c, true)
cl, err := newClient(c)
if err != nil {
return fmt.Errorf("%v", err)
}

u, _ := config.LoadUser(c)
if u != nil {
cl.SetToken(u.AccessToken)
} else if c.App.Name == "writeas" {
return fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>")
}

err = cl.LogOut()
if err != nil {
if config.Debug() {
@@ -245,11 +325,6 @@ func DoLogOut(c *cli.Context) error {
return err
}

// Delete local user data
err = fileutils.DeleteFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), config.UserFile))
if err != nil {
return err
}

return nil
// delete local user file
return config.DeleteUser(c)
}

+ 23
- 6
api/posts.go View File

@@ -12,6 +12,7 @@ import (

writeas "github.com/writeas/go-writeas/v2"
"github.com/writeas/writeas-cli/config"
"github.com/writeas/writeas-cli/executable"
"github.com/writeas/writeas-cli/fileutils"
"github.com/writeas/writeas-cli/log"
cli "gopkg.in/urfave/cli.v1"
@@ -42,7 +43,11 @@ type RemotePost struct {
}

func AddPost(c *cli.Context, id, token string) error {
f, err := os.OpenFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
hostDir, err := config.HostDirectory(c)
if err != nil {
return fmt.Errorf("Error checking for host directory: %v", err)
}
f, err := os.OpenFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
if err != nil {
return fmt.Errorf("Error creating local posts list: %s", err)
}
@@ -60,10 +65,18 @@ func AddPost(c *cli.Context, id, token string) error {
// ClaimPost adds a local post to the authenticated user's account and deletes
// the local reference
func ClaimPosts(c *cli.Context, localPosts *[]Post) (*[]writeas.ClaimPostResult, error) {
cl, err := newClient(c, true)
cl, err := newClient(c)
if err != nil {
return nil, err
}

u, _ := config.LoadUser(c)
if u != nil {
cl.SetToken(u.AccessToken)
} else {
return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>")
}

postsToClaim := make([]writeas.OwnedPostParams, len(*localPosts))
for i, post := range *localPosts {
postsToClaim[i] = writeas.OwnedPostParams{
@@ -76,7 +89,8 @@ func ClaimPosts(c *cli.Context, localPosts *[]Post) (*[]writeas.ClaimPostResult,
}

func TokenFromID(c *cli.Context, id string) string {
post := fileutils.FindLine(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile), id)
hostDir, _ := config.HostDirectory(c)
post := fileutils.FindLine(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile), id)
if post == "" {
return ""
}
@@ -89,12 +103,15 @@ func TokenFromID(c *cli.Context, id string) string {
return parts[1]
}

func RemovePost(path, id string) {
fileutils.RemoveLine(filepath.Join(config.UserDataDir(path), postsFile), id)
func RemovePost(c *cli.Context, id string) {
hostDir, _ := config.HostDirectory(c)
fullPath := filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile)
fileutils.RemoveLine(fullPath, id)
}

func GetPosts(c *cli.Context) *[]Post {
lines := fileutils.ReadData(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile))
hostDir, _ := config.HostDirectory(c)
lines := fileutils.ReadData(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile))

posts := []Post{}



+ 13
- 5
api/sync.go View File

@@ -8,6 +8,7 @@ import (
"path/filepath"

"github.com/writeas/writeas-cli/config"
"github.com/writeas/writeas-cli/executable"
"github.com/writeas/writeas-cli/fileutils"
"github.com/writeas/writeas-cli/log"
cli "gopkg.in/urfave/cli.v1"
@@ -25,14 +26,21 @@ func CmdPull(c *cli.Context) error {
}
// Create posts directory if needed
if cfg.Posts.Directory == "" {
syncSetUp(c.App.ExtraInfo()["configDir"], cfg)
syncSetUp(c, cfg)
}

cl, err := newClient(c, true)
cl, err := newClient(c)
if err != nil {
return err
}

u, _ := config.LoadUser(c)
if u != nil {
cl.SetToken(u.AccessToken)
} else {
return fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>")
}

// Fetch posts
posts, err := cl.GetUserPosts()
if err != nil {
@@ -79,10 +87,10 @@ func CmdPull(c *cli.Context) error {
return nil
}

func syncSetUp(path string, cfg *config.UserConfig) error {
func syncSetUp(c *cli.Context, cfg *config.Config) error {
// Get user information and fail early (before we make the user do
// anything), if we're going to
u, err := config.LoadUser(config.UserDataDir(path))
u, err := config.LoadUser(c)
if err != nil {
return err
}
@@ -118,7 +126,7 @@ func syncSetUp(path string, cfg *config.UserConfig) error {

// Save preference
cfg.Posts.Directory = dir
err = config.SaveConfig(config.UserDataDir(path), cfg)
err = config.SaveConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]), cfg)
if err != nil {
if config.Debug() {
log.Errorln("Unable to save config: %s", err)


+ 1
- 0
cmd/wf/.gitignore View File

@@ -0,0 +1 @@
wf

+ 164
- 0
cmd/wf/GUIDE.md View File

@@ -0,0 +1,164 @@
# WriteFreely CLI User Guide

The WriteFreely Command-Line Interface (CLI) is a cross-platform tool for publishing text to any [WriteFreely](https://writefreely.org) instance. It is designed to be simple, scriptable, do one job (publishing) well, and work as you'd expect with other command-line tools.

WriteFreely is the software behind [Write.as](https://write.as). While the WriteFreely CLI supports publishing to Write.as, we recommend using the dedicated [Write.as CLI](https://github.com/writeas/writeas-cli/tree/master/cmd/writeas#readme) to get the full features of the platform, including anonymous publishing.

**The WriteFreely CLI is compatible with WriteFreely v0.11 or later.**

## Uses

These are a few common uses for `wf`. If you get stuck or want to know more, run `wf [command] --help`. If you still have questions, [ask us](https://write.as/contact).

### Overview

```
wf [global options] command [command options] [arguments...]

COMMANDS:
post Alias for default action: create post from stdin
new Compose a new post from the command-line and publish
publish Publish a file
delete Delete a post
update Update (overwrite) a post
get Read a raw post
posts List all of your posts
blogs List blogs
auth Authenticate with a WriteFreely instance
logout Log out of a WriteFreely instance
help, h Shows a list of commands or help for one command

GLOBAL OPTIONS:
-c value, -b value Optional blog to post to
--insecure Send request insecurely.
--tor, -t Perform action on Tor hidden service
--tor-port value Use a different port to connect to Tor (default: 9150)
--code Specifies this post is code
--verbose, -v Make the operation more talkative
--font value Sets post font to given value (default: "mono")
--lang value Sets post language to given ISO 639-1 language code
--user-agent value Sets the User-Agent for API requests
--host value, -H value Use the given WriteFreely instance hostname
--user value, -u value Use the given account username
--help, -h show help
--version, -V print the version
```

#### Authenticate

To use the WriteFreely CLI, you'll first need to authenticate with the WriteFreely instance you want to interact with.

You may authenticate with as many WriteFreely instances and accounts as you want. But the first account you authenticate with will automatically be set as the default instance to operate on, so you don't have to supply `--host` and `--user` with every command.

```bash
$ wf --host pencil.writefree.ly auth username
Password: ************
```

In this example, you'll be authenticated as the user **username** on the WriteFreely instance **https://pencil.writefree.ly**.

#### Choosing an account

To select the WriteFreely instance and account you want to interact with, supply the `--host` and `--user` flags at the beginning of your `wf` command, e.g.:

```
$ wf --host pencil.writefree.ly --user username <subcommand>
```

If you're authenticated with only one account on any given WriteFreely instance, you only need to supply the `--host`, and `wf` will automatically use the correct account. E.g.:

```
$ wf --host pencil.writefree.ly <subcommand>
```

If a default account is set in `~/.writefreely/config.ini` and you want to use it, you don't need to supply any additional arguments. E.g.:

```
$ wf <subcommand>
```

#### Share something

By default, `wf` creates a post with a `monospace` typeface that doesn't word wrap (scrolls horizontally). It will return a single line with a URL, and automatically copy that URL to the clipboard.

```bash
$ echo "Hello world!" | wf
https://pencil.writefree.ly/aaaaazzzzz
```

This is generally more useful for posting terminal output or code, like so (the `--code` flag turns on syntax highlighting):

macOS / Linux: `cat cmd/wf/cli.go | wf --code`

Windows: `type cmd/wf/cli.go | wf.exe --code`

#### Output a post

This outputs any WriteFreely post with the given ID.

```bash
$ wf get aaaaazzzzz
Hello world!
```

#### List all blogs

This will output a list of the authenticated user's blogs.
```bash
$ wf blogs
Alias Title
user An Example Blog
dev My Dev Log
```

#### List posts

This lists all draft posts you've published.

Pass the `--url` flag to show the list with full post URLs.

```bash
$ wf posts
aaaaazzzzz

$ wf posts -url
https://pencil.writefree.ly/aaaaazzzzz

$ wf posts
ID
aaaaazzzzz
```

#### Delete a post

This permanently deletes a post with the given ID.

```bash
$ wf delete aaaaazzzzz
```

#### Update a post

This completely overwrites an existing post with the given ID.

```bash
$ echo "See you later!" | wf update aaaaazzzzz
```

### Composing posts

If you simply have a penchant for never leaving your keyboard, `wf` is great for composing new posts from the command-line. Just use the `new` subcommand.

`wf new` will open your favorite command-line editor, as specified by your `WRITEAS_EDITOR` or `EDITOR` environment variables (in that order), falling back to `vim` on OS X / *nix.

Customize your post's appearance with the `--font` flag:

| Argument | Appearance (Typeface) | Word Wrap? |
| -------- | --------------------- | ---------- |
| `sans` | Sans-serif (Open Sans) | Yes |
| `serif` | Serif (Lora) | Yes |
| `wrap` | Monospace | Yes |
| `mono` | Monospace | No |
| `code` | Syntax-highlighted monospace | No |

Put it all together, e.g. publish with a sans-serif font: `wf new --font sans`

+ 95
- 0
cmd/wf/README.md View File

@@ -0,0 +1,95 @@
wf-cli
======
![GPL](https://img.shields.io/github/license/writeas/writeas-cli.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/writeas/writeas-cli)](https://goreportcard.com/report/github.com/writeas/writeas-cli) [![#writeas on freenode](https://img.shields.io/badge/freenode-%23writeas-blue.svg)](http://webchat.freenode.net/?channels=writeas) [![Discuss on our forum](https://img.shields.io/discourse/https/discuss.write.as/users.svg?label=forum)](https://discuss.write.as/c/development)

Command line utility for publishing to any [WriteFreely](https://writefreely.org) instance. Works on Windows, macOS, and Linux.

**The WriteFreely CLI is compatible with WriteFreely v0.11 or later.**

## Features

* Authenticate with any WriteFreely instance
* Publish drafts
* Manage multiple WriteFreely accounts on multiple instances
* A stable, easy back-end for your GUI app or desktop-based workflow
* Locally keeps track of any posts you make
* Update and delete posts
* Fetch any post by ID

## Installing
The easiest way to get the CLI is to download a pre-built executable for your OS.

### Download
[![Latest release](https://img.shields.io/github/release/writeas/writeas-cli.svg)](https://github.com/writeas/writeas-cli/releases/latest) ![Total downloads](https://img.shields.io/github/downloads/writeas/writeas-cli/total.svg)

Get the latest version for your operating system as a standalone executable.

**Windows**<br />
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_2.0.0_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_2.0.0_windows_386.zip) executable and put it somewhere in your `%PATH%`.

**macOS**<br />
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_2.0.0_darwin_amd64.zip) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`.

**Debian-based Linux**<br />
```bash
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys DBE07445
sudo add-apt-repository "deb http://updates.writeas.org xenial main"
sudo apt-get update && sudo apt-get install wf-cli
```

**Linux (other)**<br />
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_2.0.0_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/wf_2.0.0_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`.

### Go get it
```bash
go get github.com/writeas/writeas-cli/cmd/wf
```

Once this finishes, you'll see `wf` or `wf.exe` inside `$GOPATH/bin/`.

## Upgrading

To upgrade the CLI, download and replace the executable you downloaded before.

If you previously installed with `go get`, run it again with the `-u` option.

```bash
go get -u github.com/writeas/writeas-cli/cmd/wf
```

## Usage

See full usage documentation on our [User Guide](https://github.com/writeas/writeas-cli/blob/master/cmd/wf/GUIDE.md).

```
wf [global options] command [command options] [arguments...]

COMMANDS:
post Alias for default action: create post from stdin
new Compose a new post from the command-line and publish
publish Publish a file
delete Delete a post
update Update (overwrite) a post
get Read a raw post
posts List draft posts
blogs List blogs
accounts List all currently logged in accounts
auth Authenticate with a WriteFreely instance
logout Log out of a WriteFreely instance
help, h Shows a list of commands or help for one command

GLOBAL OPTIONS:
-c value, -b value Optional blog to post to
--tor, -t Perform action on Tor hidden service
--tor-port value Use a different port to connect to Tor (default: 9150)
--code Specifies this post is code
--md Returns post URL with Markdown enabled
--verbose, -v Make the operation more talkative
--font value Sets post font to given value (default: "mono")
--lang value Sets post language to given ISO 639-1 language code
--user-agent value Sets the User-Agent for API requests
--host value, -H value Operate against a custom hostname
--user value, -u value Use authenticated user, other than default
--help, -h show help
--version, -V print the version
```

+ 276
- 0
cmd/wf/commands.go View File

@@ -0,0 +1,276 @@
package main

import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"text/tabwriter"

"github.com/hashicorp/go-multierror"
"github.com/writeas/writeas-cli/api"
"github.com/writeas/writeas-cli/commands"
"github.com/writeas/writeas-cli/config"
"github.com/writeas/writeas-cli/executable"
"github.com/writeas/writeas-cli/log"
cli "gopkg.in/urfave/cli.v1"
)

func requireAuth(f cli.ActionFunc, action string) cli.ActionFunc {
return func(c *cli.Context) error {
// check for logged in users when host is provided without user
if c.GlobalIsSet("host") && !c.GlobalIsSet("user") {
// multiple users should display a list
if num, users, err := usersLoggedIn(c); num > 1 && err == nil {
return cli.NewExitError(fmt.Sprintf("Multiple logged in users, please use '-u' or '-user' to specify one of:\n%s", strings.Join(users, ", ")), 1)
} else if num == 1 && err == nil {
// single user found for host should be set as user flag so LoadUser can
// succeed, and notify the client
if err := c.GlobalSet("user", users[0]); err != nil {
return cli.NewExitError(fmt.Sprintf("Failed to set user flag for only logged in user at host %s: %v", users[0], err), 1)
}
log.Info(c, "Host specified without user flag, using logged in user: %s\n", users[0])
} else if err != nil {
return cli.NewExitError(fmt.Sprintf("Failed to check for logged in users: %v", err), 1)
}
} else if !c.GlobalIsSet("host") && !c.GlobalIsSet("user") {
// check for global configured pair host/user
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
if err != nil {
return cli.NewExitError(fmt.Sprintf("Failed to load config from file: %v", err), 1)
// set flags if found
}
// set flags if both were found in config
if cfg.Default.Host != "" && cfg.Default.User != "" {
err = c.GlobalSet("host", cfg.Default.Host)
if err != nil {
return cli.NewExitError(fmt.Sprintf("Failed to set host from global config: %v", err), 1)
}
err = c.GlobalSet("user", cfg.Default.User)
if err != nil {
return cli.NewExitError(fmt.Sprintf("Failed to set user from global config: %v", err), 1)
}
} else {
num, err := totalUsersLoggedIn(c)
if err != nil {
return cli.NewExitError(fmt.Sprintf("Failed to check for logged in users: %v", err), 1)
} else if num > 0 {
return cli.NewExitError("You are authenticated, but have no default user/host set. Supply -user and -host flags.", 1)
}
}
}
u, err := config.LoadUser(c)
if err != nil {
return cli.NewExitError(fmt.Sprintf("Couldn't load user: %v", err), 1)
}
if u == nil {
return cli.NewExitError("You must be authenticated to "+action+".\nLog in first with: "+executable.Name()+" auth <username>", 1)
}

return f(c)
}
}

// usersLoggedIn checks for logged in users for the set host flag
// it returns the number of users and a slice of usernames
func usersLoggedIn(c *cli.Context) (int, []string, error) {
path, err := config.UserHostDir(c)
if err != nil {
return 0, nil, err
}
dir, err := os.Open(path)
if err != nil {
return 0, nil, err
}
contents, err := dir.Readdir(0)
if err != nil {
return 0, nil, err
}
var names []string
for _, file := range contents {
if file.IsDir() {
// stat user.json
if _, err := os.Stat(filepath.Join(path, file.Name(), "user.json")); err == nil {
names = append(names, file.Name())
}
}
}
return len(names), names, nil
}

// totalUsersLoggedIn checks for logged in users for any host
// it returns the number of users and an error if any
func totalUsersLoggedIn(c *cli.Context) (int, error) {
path := config.UserDataDir(c.App.ExtraInfo()["configDir"])
dir, err := os.Open(path)
if err != nil {
return 0, err
}
contents, err := dir.Readdir(0)
if err != nil {
return 0, err
}
count := 0
for _, file := range contents {
if file.IsDir() {
subDir, err := os.Open(filepath.Join(path, file.Name()))
if err != nil {
return 0, err
}
subContents, err := subDir.Readdir(0)
if err != nil {
return 0, err
}
for _, subFile := range subContents {
if subFile.IsDir() {
if _, err := os.Stat(filepath.Join(path, file.Name(), subFile.Name(), "user.json")); err == nil {
count++
}
}
}
}
}
return count, nil
}

func cmdAuth(c *cli.Context) error {
err := commands.CmdAuth(c)
if err != nil {
return err
}

// Get the username from the command, just like commands.CmdAuth does
username := c.Args().Get(0)

// Update config if this is user's first auth
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
if err != nil {
log.Errorln("Not saving config. Unable to load config: %s", err)
return err
}
if cfg.Default.Host == "" && cfg.Default.User == "" {
// This is user's first auth, so save defaults
cfg.Default.Host = api.HostURL(c)
cfg.Default.User = username
err = config.SaveConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]), cfg)
if err != nil {
log.Errorln("Not saving config. Unable to save config: %s", err)
return err
}
fmt.Printf("Set %s on %s as default account.\n", username, c.GlobalString("host"))
}

return nil
}

func cmdLogOut(c *cli.Context) error {
err := commands.CmdLogOut(c)
if err != nil {
return err
}

// Remove this from config if it's the default account
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
if err != nil {
log.Errorln("Not updating config. Unable to load: %s", err)
return err
}
username, err := config.CurrentUser(c)
if err != nil {
log.Errorln("Not updating config. Unable to load current user: %s", err)
return err
}
reqHost := api.HostURL(c)
if reqHost == "" {
// No --host given, so we're using the default host
reqHost = cfg.Default.Host
}
if cfg.Default.Host == reqHost && cfg.Default.User == username {
// We're logging out of default username + host, so remove from config file
cfg.Default.Host = ""
cfg.Default.User = ""
err = config.SaveConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]), cfg)
if err != nil {
log.Errorln("Not updating config. Unable to save config: %s", err)
return err
}
}

return nil
}

func cmdAccounts(c *cli.Context) error {
// get user config dir
userDir := config.UserDataDir(c.App.ExtraInfo()["configDir"])
// load defaults
cfg, err := config.LoadConfig(userDir)
if err != nil {
return cli.NewExitError("Could not load default user configuration", 1)
}
defaultUser := cfg.Default.User
defaultHost := cfg.Default.Host
if parts := strings.Split(defaultHost, "://"); len(parts) > 1 {
defaultHost = parts[1]
}
// get each host dir
files, err := ioutil.ReadDir(userDir)
if err != nil {
return cli.NewExitError("Could not read user configuration directory", 1)
}
// accounts will be a slice of slices of string. the first string in
// a subslice should always be the hostname
accounts := [][]string{}
for _, file := range files {
if file.IsDir() {
dirName := file.Name()
// get each user in host dir
users, err := usersFromDir(filepath.Join(userDir, dirName))
if err != nil {
log.Info(c, "Failed to get users from %s: %v", dirName, err)
continue
}
if len(users) != 0 {
// append the slice of users as a new slice in accounts w/ the host prepended
accounts = append(accounts, append([]string{dirName}, users...))
}
}
}

// print out all logged in accounts
tw := tabwriter.NewWriter(os.Stdout, 10, 2, 2, ' ', tabwriter.TabIndent)
if len(accounts) == 0 && (c.Bool("v") || c.Bool("verbose") || c.GlobalBool("v") || c.GlobalBool("verbose")) {
fmt.Fprintf(tw, "%s\t", "No authenticated accounts found.\n")
}
for _, userList := range accounts {
host := userList[0]
for _, username := range userList[1:] {
if host == defaultHost && username == defaultUser {
fmt.Fprintf(tw, "[%s]\t%s (default)\n", host, username)
continue
}
fmt.Fprintf(tw, "[%s]\t%s\n", host, username)
}
}
return tw.Flush()
}

func usersFromDir(path string) ([]string, error) {
users := make([]string, 0, 4)
files, err := ioutil.ReadDir(path)
if err != nil {
return nil, err
}
var errs error
for _, file := range files {
if file.IsDir() {
_, err := os.Stat(filepath.Join(path, file.Name(), "user.json"))
if err != nil {
err = multierror.Append(errs, err)
continue
}
users = append(users, file.Name())
}
}
return users, errs
}

+ 5
- 0
cmd/wf/config_nix.go View File

@@ -0,0 +1,5 @@
// +build !windows

package main

const configDir = ".writefreely"

+ 5
- 0
cmd/wf/config_win.go View File

@@ -0,0 +1,5 @@
// +build windows

package main

const configDir = "WriteFreely"

+ 16
- 0
cmd/wf/flags.go View File

@@ -0,0 +1,16 @@
package main

import (
"gopkg.in/urfave/cli.v1"
)

var flags = []cli.Flag{
cli.StringFlag{
Name: "host, H",
Usage: "Use the given WriteFreely instance hostname",
},
cli.StringFlag{
Name: "user, u",
Usage: "Use the given account username",
},
}

+ 250
- 0
cmd/wf/main.go View File

@@ -0,0 +1,250 @@
package main

import (
"os"

"github.com/writeas/writeas-cli/commands"
"github.com/writeas/writeas-cli/config"
cli "gopkg.in/urfave/cli.v1"
)

func main() {
appInfo := map[string]string{
"configDir": configDir,
"version": "1.0",
}
config.DirMustExist(config.UserDataDir(appInfo["configDir"]))
cli.VersionFlag = cli.BoolFlag{
Name: "version, V",
Usage: "print the version",
}

// Run the app
app := cli.NewApp()
app.Name = "wf"
app.Version = appInfo["version"]
app.Usage = "Publish to any WriteFreely instance from the command-line."
// TODO: who is the author? the contributors? link to GH?
app.Authors = []cli.Author{
{
Name: "Write.as",
Email: "hello@write.as",
},
}
app.ExtraInfo = func() map[string]string {
return appInfo
}
app.Action = requireAuth(commands.CmdPost, "publish")
app.Flags = append(config.PostFlags, flags...)
app.Commands = []cli.Command{
{
Name: "post",
Usage: "Alias for default action: create post from stdin",
Action: requireAuth(commands.CmdPost, "publish"),
Flags: config.PostFlags,
Description: `Create a new post on WriteFreely from stdin.

Use the --code flag to indicate that the post should use syntax
highlighting. Or use the --font [value] argument to set the post's
appearance, where [value] is mono, monospace (default), wrap (monospace
font with word wrapping), serif, or sans.`,
},
{
Name: "new",
Usage: "Compose a new post from the command-line and publish",
Description: `An alternative to piping data to the program.

On Windows, this will use 'copy con' to start reading what you input from the
prompt. Press F6 or Ctrl-Z then Enter to end input.
On *nix, this will use the best available text editor, starting with the
value set to the WRITEAS_EDITOR or EDITOR environment variable, or vim, or
finally nano.

Use the --code flag to indicate that the post should use syntax
highlighting. Or use the --font [value] argument to set the post's
appearance, where [value] is mono, monospace (default), wrap (monospace
font with word wrapping), serif, or sans.
If posting fails for any reason, 'wf' will show you the temporary file
location and how to pipe it to 'wf' to retry.`,
Action: requireAuth(commands.CmdNew, "publish"),
Flags: config.PostFlags,
},
{
Name: "publish",
Usage: "Publish a file",
Action: requireAuth(commands.CmdPublish, "publish"),
Flags: config.PostFlags,
},
{
Name: "delete",
Usage: "Delete a post",
Action: requireAuth(commands.CmdDelete, "delete a post"),
Flags: []cli.Flag{
cli.BoolFlag{
Name: "tor, t",
Usage: "Delete via Tor hidden service",
},
cli.IntFlag{
Name: "tor-port",
Usage: "Use a different port to connect to Tor",
Value: 9150,
},
cli.BoolFlag{
Name: "verbose, v",
Usage: "Make the operation more talkative",
},
},
},
{
Name: "update",
Usage: "Update (overwrite) a post",
Action: requireAuth(commands.CmdUpdate, "update a post"),
Flags: []cli.Flag{
cli.BoolFlag{
Name: "tor, t",
Usage: "Update via Tor hidden service",
},
cli.IntFlag{
Name: "tor-port",
Usage: "Use a different port to connect to Tor",
Value: 9150,
},
cli.BoolFlag{
Name: "code",
Usage: "Specifies this post is code",
},
cli.StringFlag{
Name: "font",
Usage: "Sets post font to given value",
},
cli.BoolFlag{
Name: "verbose, v",
Usage: "Make the operation more talkative",
},
},
},
{
Name: "get",
Usage: "Read a raw post",
Action: commands.CmdGet,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "tor, t",
Usage: "Get from Tor hidden service",
},
cli.IntFlag{
Name: "tor-port",
Usage: "Use a different port to connect to Tor",
Value: 9150,
},
cli.BoolFlag{
Name: "verbose, v",
Usage: "Make the operation more talkative",
},
},
},
{
Name: "posts",
Usage: "List draft posts",
Action: requireAuth(commands.CmdListPosts, "view posts"),
Flags: []cli.Flag{
cli.BoolFlag{
Name: "id",
Usage: "Show list with post IDs (default)",
},
cli.BoolFlag{
Name: "url",
Usage: "Show list with URLs",
},
cli.BoolFlag{
Name: "verbose, v",
Usage: "Show verbose post listing",
},
},
}, {
Name: "blogs",
Usage: "List blogs",
Action: requireAuth(commands.CmdCollections, "view blogs"),
Flags: []cli.Flag{
cli.BoolFlag{
Name: "tor, t",
Usage: "Authenticate via Tor hidden service",
},
cli.IntFlag{
Name: "tor-port",
Usage: "Use a different port to connect to Tor",
Value: 9150,
},
cli.BoolFlag{
Name: "url",
Usage: "Show list with URLs",
},
},
}, {
Name: "accounts",
Usage: "List all currently logged in accounts",
Action: cmdAccounts,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "verbose, v",
Usage: "Make the operation more talkative",
},
},
}, {
Name: "auth",
Usage: "Authenticate with a WriteFreely instance",
Action: cmdAuth,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "tor, t",
Usage: "Authenticate via Tor hidden service",
},
cli.IntFlag{
Name: "tor-port",
Usage: "Use a different port to connect to Tor",
Value: 9150,
},
cli.BoolFlag{
Name: "verbose, v",
Usage: "Make the operation more talkative",
},
},
},
{
Name: "logout",
Usage: "Log out of a WriteFreely instance",
Action: requireAuth(cmdLogOut, "logout"),
Flags: []cli.Flag{
cli.BoolFlag{
Name: "tor, t",
Usage: "Authenticate via Tor hidden service",
},
cli.IntFlag{
Name: "tor-port",
Usage: "Use a different port to connect to Tor",
Value: 9150,
},
cli.BoolFlag{
Name: "verbose, v",
Usage: "Make the operation more talkative",
},
},
},
}

cli.CommandHelpTemplate = `NAME:
{{.Name}} - {{.Usage}}

USAGE:
wf {{.Name}}{{if .Flags}} [command options]{{end}} [arguments...]{{if .Description}}

DESCRIPTION:
{{.Description}}{{end}}{{if .Flags}}

OPTIONS:
{{range .Flags}}{{.}}
{{end}}{{ end }}
`
app.Run(os.Args)
}

+ 149
- 0
cmd/writeas/GUIDE.md View File

@@ -0,0 +1,149 @@
# Write.as CLI User Guide

The Write.as Command-Line Interface (CLI) is a cross-platform tool for publishing text to [Write.as](https://write.as) and its other sites, like [Paste.as](https://paste.as). It is designed to be simple, scriptable, do one job (publishing) well, and work as you'd expect with other command-line tools.

Write.as is a text-publishing service that protects your privacy. There's no sign up required to publish, but if you do sign up, you can access posts across devices and compile collections of them in what most people would call a "blog".

## Uses

These are a few common uses for `writeas`. If you get stuck or want to know more, run `writeas [command] --help`. If you still have questions, [ask us](https://write.as/contact).

### Overview

```
writeas [global options] command [command options] [arguments...]

COMMANDS:
post Alias for default action: create post from stdin
new Compose a new post from the command-line and publish
publish Publish a file to Write.as
delete Delete a post
update Update (overwrite) a post
get Read a raw post
add Add an existing post locally
posts List all of your posts
claim Claim local unsynced posts
blogs List blogs
claim Claim local unsynced posts
auth Authenticate with Write.as
logout Log out of Write.as
help, h Shows a list of commands or help for one command

GLOBAL OPTIONS:
-c value, -b value Optional blog to post to
--tor, -t Perform action on Tor hidden service
--tor-port value Use a different port to connect to Tor (default: 9150)
--code Specifies this post is code
--md Returns post URL with Markdown enabled
--verbose, -v Make the operation more talkative
--font value Sets post font to given value (default: "mono")
--lang value Sets post language to given ISO 639-1 language code
--user-agent value Sets the User-Agent for API requests
--help, -h show help
--version, -V print the version
```

#### Share something

By default, `writeas` creates a post with a `monospace` typeface that doesn't word wrap (scrolls horizontally). It will return a single line with a URL, and automatically copy that URL to the clipboard:

```bash
$ echo "Hello world!" | writeas
https://write.as/aaaazzzzzzzza
```

This is generally more useful for posting terminal output or code, like so (the `--code` flag turns on syntax highlighting):

macOS / Linux: `cat writeas/cli.go | writeas --code`

Windows: `type writeas/cli.go | writeas.exe --code`

#### Output a post

This outputs any Write.as post with the given ID.

```bash
$ writeas get aaaazzzzzzzza
Hello world!
```

#### Authenticate

This will authenticate with write.as and store the user access token locally, until you explicitly logout.
```bash
$ writeas auth username
Password: ************
```

#### List all blogs

This will output a list of the authenticated user's blogs.
```bash
$ writeas blogs
Alias Title
user An Example Blog
dev My Dev Log
```

#### List posts

This lists all anonymous posts you've published. If authenticated, it will include posts on your account as well as any local / unclaimed posts.

Pass the `--url` flag to show the list with full post URLs, and the `--md` flag to return URLs with Markdown enabled.

To see post IDs with their Edit Tokens pass the `--v` flag.

```bash
$ writeas posts
aaaazzzzzzzza

$ writeas posts -url
https://write.as/aaaazzzzzzzza

$ writeas posts -v
ID Token
aaaazzzzzzzza dhuieoj23894jhf984hdfs9834hdf84j
```

#### Delete a post

This permanently deletes a post you own.

```bash
$ writeas delete aaaazzzzzzzza
```

#### Update a post

This completely overwrites an existing post you own.

```bash
$ echo "See you later!" | writeas update aaaazzzzzzzza
```

#### Claim a post

This moves an unsynced local post to a draft on your account. You will need to authenticate first.
```bash
$ writeas claim aaaazzzzzzzza
```

### Composing posts

If you simply have a penchant for never leaving your keyboard, `writeas` is great for composing new posts from the command-line. Just use the `new` subcommand.

`writeas new` will open your favorite command-line editor, as specified by your `WRITEAS_EDITOR` or `EDITOR` environment variables (in that order), falling back to `vim` on OS X / *nix.

Customize your post's appearance with the `--font` flag:

| Argument | Appearance (Typeface) | Word Wrap? |
| -------- | --------------------- | ---------- |
| `sans` | Sans-serif (Open Sans) | Yes |
| `serif` | Serif (Lora) | Yes |
| `wrap` | Monospace | Yes |
| `mono` | Monospace | No |
| `code` | Syntax-highlighted monospace | No |

Put it all together, e.g. publish with a sans-serif font: `writeas new --font sans`

If you're publishing Markdown, supply the `--md` flag to get a URL back that will render Markdown, e.g.: `writeas new --font sans --md`

+ 95
- 0
cmd/writeas/README.md View File

@@ -0,0 +1,95 @@
writeas-cli
===========
![GPL](https://img.shields.io/github/license/writeas/writeas-cli.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/writeas/writeas-cli)](https://goreportcard.com/report/github.com/writeas/writeas-cli) [![#writeas on freenode](https://img.shields.io/badge/freenode-%23writeas-blue.svg)](http://webchat.freenode.net/?channels=writeas) [![Discuss on our forum](https://img.shields.io/discourse/https/discuss.write.as/users.svg?label=forum)](https://discuss.write.as/c/development)

Command line utility for publishing to [Write.as](https://write.as). Works on Windows, macOS, and Linux.

## Features

* Publish anonymously to Write.as
* Authenticate with a Write.as account
* A stable, easy back-end for your [GUI app](https://write.as/apps/desktop) or desktop-based workflow
* Compatible with our [Tor hidden service](http://writeas7pm7rcdqg.onion/)
* Locally keeps track of any posts you make
* Update and delete posts, anonymous and authenticated
* Fetch any post by ID
* Add anonymous post credentials (like for one published with the [Android app](https://play.google.com/store/apps/details?id=com.abunchtell.writeas)) for editing

## Installing
The easiest way to get the CLI is to download a pre-built executable for your OS.

### Download
[![Latest release](https://img.shields.io/github/release/writeas/writeas-cli.svg)](https://github.com/writeas/writeas-cli/releases/latest) ![Total downloads](https://img.shields.io/github/downloads/writeas/writeas-cli/total.svg)

Get the latest version for your operating system as a standalone executable.

**Windows**<br />
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_386.zip) executable and put it somewhere in your `%PATH%`.

**macOS**<br />
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_darwin_amd64.zip) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`.

**Debian-based Linux**<br />
```bash
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys DBE07445
sudo add-apt-repository "deb http://updates.writeas.org xenial main"
sudo apt-get update && sudo apt-get install writeas-cli
```

**Linux (other)**<br />
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`.

### Go get it
```bash
go get github.com/writeas/writeas-cli/cmd/writeas
```

Once this finishes, you'll see `writeas` or `writeas.exe` inside `$GOPATH/bin/`.

## Upgrading

To upgrade the CLI, download and replace the executable you downloaded before.

If you previously installed with `go get`, run it again with the `-u` option.

```bash
go get -u github.com/writeas/writeas-cli/cmd/writeas
```

## Usage

See full usage documentation on our [User Guide](https://github.com/writeas/writeas-cli/blob/master/cmd/writeas/GUIDE.md).

```
writeas [global options] command [command options] [arguments...]

COMMANDS:
post Alias for default action: create post from stdin
new Compose a new post from the command-line and publish
publish Publish a file to Write.as
delete Delete a post
update Update (overwrite) a post
get Read a raw post
add Add an existing post locally
posts List all of your posts
blogs List blogs
claim Claim local unsynced posts
auth Authenticate with Write.as
logout Log out of Write.as
help, h Shows a list of commands or help for one command

GLOBAL OPTIONS:
-c value, -b value Optional blog to post to
--tor, -t Perform action on Tor hidden service
--tor-port value Use a different port to connect to Tor (default: 9150)
--code Specifies this post is code
--md Returns post URL with Markdown enabled
--verbose, -v Make the operation more talkative
--font value Sets post font to given value (default: "mono")
--lang value Sets post language to given ISO 639-1 language code
--user-agent value Sets the User-Agent for API requests
--host value, -H value Operate against a custom hostname
--user value, -u value Use authenticated user, other than default
--help, -h show help
--version, -V print the version
```

+ 1
- 3
cmd/writeas/config_nix.go View File

@@ -2,6 +2,4 @@

package main

var appInfo = map[string]string{
"configDir": ".writeas",
}
const configDir = ".writeas"

+ 1
- 3
cmd/writeas/config_win.go View File

@@ -2,6 +2,4 @@

package main

var appInfo = map[string]string{
"configDir": "Write.as",
}
const configDir = "Write.as"

+ 13
- 0
cmd/writeas/flags.go View File

@@ -0,0 +1,13 @@
package main

import (
"gopkg.in/urfave/cli.v1"
)

var flags = []cli.Flag{
cli.StringFlag{
Name: "user, u",
Hidden: true,
Value: "user",
},
}

+ 7
- 19
cmd/writeas/main.go View File

@@ -5,12 +5,15 @@ import (

"github.com/writeas/writeas-cli/commands"
"github.com/writeas/writeas-cli/config"
"github.com/writeas/writeas-cli/log"
cli "gopkg.in/urfave/cli.v1"
)

func main() {
initialize(appInfo["configDir"])
appInfo := map[string]string{
"configDir": configDir,
"version": "2.0",
}
config.DirMustExist(config.UserDataDir(appInfo["configDir"]))
cli.VersionFlag = cli.BoolFlag{
Name: "version, V",
Usage: "print the version",
@@ -19,7 +22,7 @@ func main() {
// Run the app
app := cli.NewApp()
app.Name = "writeas"
app.Version = config.Version
app.Version = appInfo["version"]
app.Usage = "Publish text quickly"
app.Authors = []cli.Author{
{
@@ -31,7 +34,7 @@ func main() {
return appInfo
}
app.Action = commands.CmdPost
app.Flags = config.PostFlags
app.Flags = append(config.PostFlags, flags...)
app.Commands = []cli.Command{
{
Name: "post",
@@ -274,18 +277,3 @@ OPTIONS:
`
app.Run(os.Args)
}

func initialize(dataDirName string) {
// Ensure we have a data directory to use
if !config.DataDirExists(dataDirName) {
err := config.CreateDataDir(dataDirName)
if err != nil {
if config.Debug() {
panic(err)
} else {
log.Errorln("Error creating data directory: %s", err)
return
}
}
}
}

+ 61
- 24
commands/commands.go View File

@@ -4,11 +4,13 @@ import (
"fmt"
"io/ioutil"
"os"
"strings"
"text/tabwriter"

"github.com/howeyc/gopass"
"github.com/writeas/writeas-cli/api"
"github.com/writeas/writeas-cli/config"
"github.com/writeas/writeas-cli/executable"
"github.com/writeas/writeas-cli/log"
cli "gopkg.in/urfave/cli.v1"
)
@@ -67,7 +69,7 @@ func CmdNew(c *cli.Context) error {
func CmdPublish(c *cli.Context) error {
filename := c.Args().Get(0)
if filename == "" {
return cli.NewExitError("usage: writeas publish <filename>", 1)
return cli.NewExitError("usage: "+executable.Name()+" publish <filename>", 1)
}
content, err := ioutil.ReadFile(filename)
if err != nil {
@@ -92,16 +94,16 @@ func CmdDelete(c *cli.Context) error {
friendlyID := c.Args().Get(0)
token := c.Args().Get(1)
if friendlyID == "" {
return cli.NewExitError("usage: writeas delete <postId> [<token>]", 1)
return cli.NewExitError("usage: "+executable.Name()+" delete <postId> [<token>]", 1)
}

u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
u, _ := config.LoadUser(c)
if token == "" {
// Search for the token locally
token = api.TokenFromID(c, friendlyID)
if token == "" && u == nil {
log.Errorln("Couldn't find an edit token locally. Did you create this post here?")
log.ErrorlnQuit("If you have an edit token, use: writeas delete %s <token>", friendlyID)
log.ErrorlnQuit("If you have an edit token, use: "+executable.Name()+" delete %s <token>", friendlyID)
}
}

@@ -124,16 +126,16 @@ func CmdUpdate(c *cli.Context) error {
friendlyID := c.Args().Get(0)
token := c.Args().Get(1)
if friendlyID == "" {
return cli.NewExitError("usage: writeas update <postId> [<token>]", 1)
return cli.NewExitError("usage: "+executable.Name()+" update <postId> [<token>]", 1)
}

u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
u, _ := config.LoadUser(c)
if token == "" {
// Search for the token locally
token = api.TokenFromID(c, friendlyID)
if token == "" && u == nil {
log.Errorln("Couldn't find an edit token locally. Did you create this post here?")
log.ErrorlnQuit("If you have an edit token, use: writeas update %s <token>", friendlyID)
log.ErrorlnQuit("If you have an edit token, use: "+executable.Name()+" update %s <token>", friendlyID)
}
}

@@ -155,7 +157,7 @@ func CmdUpdate(c *cli.Context) error {
func CmdGet(c *cli.Context) error {
friendlyID := c.Args().Get(0)
if friendlyID == "" {
return cli.NewExitError("usage: writeas get <postId>", 1)
return cli.NewExitError("usage: "+executable.Name()+" get <postId>", 1)
}

if config.IsTor(c) {
@@ -175,7 +177,7 @@ func CmdAdd(c *cli.Context) error {
friendlyID := c.Args().Get(0)
token := c.Args().Get(1)
if friendlyID == "" || token == "" {
return cli.NewExitError("usage: writeas add <postId> <token>", 1)
return cli.NewExitError("usage: "+executable.Name()+" add <postId> <token>", 1)
}

err := api.AddPost(c, friendlyID, token)
@@ -192,7 +194,7 @@ func CmdListPosts(c *cli.Context) error {

posts := api.GetPosts(c)

u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
u, _ := config.LoadUser(c)
if u != nil {
if config.IsTor(c) {
log.Info(c, "Getting posts via hidden service...")
@@ -205,7 +207,11 @@ func CmdListPosts(c *cli.Context) error {
}

if len(remotePosts) > 0 {
fmt.Println("Anonymous Posts")
if c.App.Name == "wf" {
fmt.Println("Draft Posts")
} else {
fmt.Println("Anonymous Posts")
}
if details {
identifier := "URL"
if ids || !urls {
@@ -261,9 +267,28 @@ func CmdListPosts(c *cli.Context) error {
}

func getPostURL(c *cli.Context, slug string) string {
base := config.WriteasBaseURL
if config.IsDev() {
base = config.DevBaseURL
var base string
if c.App.Name == "writeas" {
if config.IsDev() {
base = config.DevBaseURL
} else {
base = config.WriteasBaseURL
}
} else {
if host := api.HostURL(c); host != "" {
base = host
} else {
// TODO handle error, or load config globally, see T601
// https://phabricator.write.as/T601
cfg, _ := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
if cfg.Default.Host != "" && cfg.Default.User != "" {
if parts := strings.Split(cfg.Default.Host, "://"); len(parts) > 1 {
base = cfg.Default.Host
} else {
base = "https://" + cfg.Default.Host
}
}
}
}
ext := ""
// Output URL in requested format
@@ -274,12 +299,12 @@ func getPostURL(c *cli.Context, slug string) string {
}

func CmdCollections(c *cli.Context) error {
u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
u, err := config.LoadUser(c)
if err != nil {
return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1)
}
if u == nil {
return cli.NewExitError("You must be authenticated to view collections.\nLog in first with: writeas auth <username>", 1)
return cli.NewExitError("You must be authenticated to view collections.\nLog in first with: "+executable.Name()+" auth <username>", 1)
}
if config.IsTor(c) {
log.Info(c, "Getting blogs via hidden service...")
@@ -309,12 +334,12 @@ func CmdCollections(c *cli.Context) error {
}

func CmdClaim(c *cli.Context) error {
u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
u, err := config.LoadUser(c)
if err != nil {
return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1)
}
if u == nil {
return cli.NewExitError("You must be authenticated to claim local posts.\nLog in first with: writeas auth <username>", 1)
return cli.NewExitError("You must be authenticated to claim local posts.\nLog in first with: "+executable.Name()+" auth <username>", 1)
}

localPosts := api.GetPosts(c)
@@ -348,7 +373,7 @@ func CmdClaim(c *cli.Context) error {
log.Info(c, "%sOK", status)
okCount++
// only delete local if successful
api.RemovePost(c.App.ExtraInfo()["configDir"], id)
api.RemovePost(c, id)
}
}
log.Info(c, "%d claimed, %d failed", okCount, errCount)
@@ -356,19 +381,31 @@ func CmdClaim(c *cli.Context) error {
}

func CmdAuth(c *cli.Context) error {
username := c.Args().Get(0)
if username == "" && c.GlobalIsSet("user") {
username = c.GlobalString("user")
}
// Check configuration
u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
u, err := config.LoadUser(c)
if err != nil {
return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1)
}
if u != nil && u.AccessToken != "" {
return cli.NewExitError("You're already authenticated as "+u.User.Username+". Log out with: writeas logout", 1)
if u != nil && u.AccessToken != "" && username == u.User.Username {
return cli.NewExitError("You're already authenticated as "+u.User.Username, 1)
}

// Validate arguments and get password
username := c.Args().Get(0)
if username == "" {
return cli.NewExitError("usage: writeas auth <username>", 1)
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]))
if err != nil {
return cli.NewExitError(fmt.Sprintf("Failed to load config: %v", err), 1)
}
if cfg.Default.Host != "" && cfg.Default.User != "" {
username = cfg.Default.User
fmt.Printf("No user provided, using default user %s for host %s...\n", cfg.Default.User, cfg.Default.Host)
} else {
return cli.NewExitError("usage: "+executable.Name()+" auth <username>", 1)
}
}

fmt.Print("Password: ")


+ 20
- 9
config/config.go View File

@@ -8,32 +8,43 @@ import (
)

const (
UserConfigFile = "config.ini"
// ConfigFile is the full filename for application configuration files
ConfigFile = "config.ini"
)

type (
// APIConfig is not currently used
APIConfig struct {
}

// PostsConfig stores the directory for the user post cache
PostsConfig struct {
Directory string `ini:"directory"`
}

UserConfig struct {
API APIConfig `ini:"api"`
Posts PostsConfig `ini:"posts"`
// DefaultConfig stores the default host and user to authenticate with
DefaultConfig struct {
Host string `ini:"host"`
User string `ini:"user"`
}

// Config represents the entire base configuration
Config struct {
API APIConfig `ini:"api"`
Default DefaultConfig `ini:"default"`
Posts PostsConfig `ini:"posts"`
}
)

func LoadConfig(dataDir string) (*UserConfig, error) {
func LoadConfig(dataDir string) (*Config, error) {
// TODO: load config to var shared across app
cfg, err := ini.LooseLoad(filepath.Join(dataDir, UserConfigFile))
cfg, err := ini.LooseLoad(filepath.Join(dataDir, ConfigFile))
if err != nil {
return nil, err
}

// Parse INI file
uc := &UserConfig{}
uc := &Config{}
err = cfg.MapTo(uc)
if err != nil {
return nil, err
@@ -41,14 +52,14 @@ func LoadConfig(dataDir string) (*UserConfig, error) {
return uc, nil
}

func SaveConfig(dataDir string, uc *UserConfig) error {
func SaveConfig(dataDir string, uc *Config) error {
cfg := ini.Empty()
err := ini.ReflectFrom(cfg, uc)
if err != nil {
return err
}

return cfg.SaveTo(filepath.Join(dataDir, UserConfigFile))
return cfg.SaveTo(filepath.Join(dataDir, ConfigFile))
}

var editors = []string{"WRITEAS_EDITOR", "EDITOR"}


+ 24
- 4
config/directories.go View File

@@ -5,16 +5,36 @@ import (
"path/filepath"

"github.com/writeas/writeas-cli/fileutils"
"github.com/writeas/writeas-cli/log"
)

// UserDataDir returns a platform specific directory under the user's home
// directory
func UserDataDir(dataDirName string) string {
return filepath.Join(parentDataDir(), dataDirName)
}

func DataDirExists(dataDirName string) bool {
return fileutils.Exists(UserDataDir(dataDirName))
func dataDirExists(dataDirName string) bool {
return fileutils.Exists(dataDirName)
}

func CreateDataDir(dataDirName string) error {
return os.Mkdir(UserDataDir(dataDirName), 0700)
func createDataDir(dataDirName string) error {
return os.Mkdir(dataDirName, 0700)
}

// DirMustExist checks for a directory, creates it if not found and either
// panics or logs and error depending on the status of Debug
func DirMustExist(dataDirName string) {
// Ensure we have a data directory to use
if !dataDirExists(dataDirName) {
err := createDataDir(dataDirName)
if err != nil {
if Debug() {
panic(err)
} else {
log.Errorln("Error creating data directory: %s", err)
return
}
}
}
}

+ 2
- 1
config/files_nix.go View File

@@ -7,6 +7,7 @@ import (
"os/exec"

homedir "github.com/mitchellh/go-homedir"
"github.com/writeas/writeas-cli/executable"
)

const (
@@ -39,5 +40,5 @@ func EditPostCmd(fname string) *exec.Cmd {
}

func MessageRetryCompose(fname string) string {
return fmt.Sprintf("To retry this post, run:\n cat %s | writeas", fname)
return fmt.Sprintf("To retry this post, run:\n cat %s | %s", fname, executable.Name())
}

+ 3
- 1
config/files_win.go View File

@@ -6,6 +6,8 @@ import (
"fmt"
"os"
"os/exec"

"github.com/writeas/writeas-cli/executable"
)

const (
@@ -22,5 +24,5 @@ func EditPostCmd(fname string) *exec.Cmd {
}

func MessageRetryCompose(fname string) string {
return fmt.Sprintf("To retry this post, run:\n type %s | writeas.exe", fname)
return fmt.Sprintf("To retry this post, run:\n type %s | %s", fname, executable.Name())
}

+ 4
- 0
config/flags.go View File

@@ -12,6 +12,10 @@ var PostFlags = []cli.Flag{
Value: "",
},
cli.BoolFlag{
Name: "insecure",
Usage: "Send request insecurely.",
},
cli.BoolFlag{
Name: "tor, t",
Usage: "Perform action on Tor hidden service",
},


+ 48
- 4
config/options.go View File

@@ -1,6 +1,8 @@
package config

import (
"strings"

"github.com/cloudfoundry/jibber_jabber"
"github.com/writeas/writeas-cli/log"
cli "gopkg.in/urfave/cli.v1"
@@ -8,8 +10,8 @@ import (

// Application constants.
const (
Version = "2.0"
defaultUserAgent = "writeas-cli v" + Version
writeasUserAgent = "writeas-cli v"
wfUserAgent = "wf-cli v"
// Defaults for posts on Write.as.
DefaultFont = PostFontMono
WriteasBaseURL = "https://write.as"
@@ -19,11 +21,16 @@ const (
)

func UserAgent(c *cli.Context) string {
client := wfUserAgent
if c.App.Name == "writeas" {
client = writeasUserAgent
}

ua := c.String("user-agent")
if ua == "" {
return defaultUserAgent
return client + c.App.ExtraInfo()["version"]
}
return ua + " (" + defaultUserAgent + ")"
return ua + " (" + client + c.App.ExtraInfo()["version"] + ")"
}

func IsTor(c *cli.Context) bool {
@@ -37,6 +44,18 @@ func TorPort(c *cli.Context) int {
return torPort
}

func TorURL(c *cli.Context) string {
flagHost := c.String("host")
if flagHost != "" && strings.HasSuffix(flagHost, "onion") {
return flagHost
}
cfg, _ := LoadConfig(c.App.ExtraInfo()["configDir"])
if cfg != nil && cfg.Default.Host != "" && strings.HasSuffix(cfg.Default.Host, "onion") {
return cfg.Default.Host
}
return TorBaseURL
}

func Language(c *cli.Context, auto bool) string {
if l := c.String("lang"); l != "" {
return l
@@ -62,3 +81,28 @@ func Collection(c *cli.Context) string {
}
return ""
}

// HostDirectory returns the sub directory string for the host. Order of
// precedence is a host flag if any, then the configured default, if any
func HostDirectory(c *cli.Context) (string, error) {
cfg, err := LoadConfig(UserDataDir(c.App.ExtraInfo()["configDir"]))
if err != nil {
return "", err
}
// flag takes precedence over defaults
if hostFlag := c.GlobalString("host"); hostFlag != "" {
if parts := strings.Split(hostFlag, "://"); len(parts) > 1 {
return parts[1], nil
}
return hostFlag, nil
}

if cfg.Default.Host != "" && cfg.Default.User != "" {
if parts := strings.Split(cfg.Default.Host, "://"); len(parts) > 1 {
return parts[1], nil
}
return cfg.Default.Host, nil
}

return "", nil
}

+ 130
- 6
config/user.go View File

@@ -7,12 +7,23 @@ import (

writeas "github.com/writeas/go-writeas/v2"
"github.com/writeas/writeas-cli/fileutils"
"gopkg.in/urfave/cli.v1"
)

const UserFile = "user.json"

func LoadUser(dataDir string) (*writeas.AuthUser, error) {
fname := filepath.Join(dataDir, UserFile)
func LoadUser(c *cli.Context) (*writeas.AuthUser, error) {
dir, err := UserHostDir(c)
if err != nil {
return nil, err
}
DirMustExist(dir)
username, err := CurrentUser(c)
if err != nil {
return nil, err
}
if username == "user" {
username = ""
}
fname := filepath.Join(dir, username, "user.json")
userJSON, err := ioutil.ReadFile(fname)
if err != nil {
if !fileutils.Exists(fname) {
@@ -31,17 +42,130 @@ func LoadUser(dataDir string) (*writeas.AuthUser, error) {
return u, nil
}

func SaveUser(dataDir string, u *writeas.AuthUser) error {
func DeleteUser(c *cli.Context) error {
dir, err := UserHostDir(c)
if err != nil {
return err
}

username, err := CurrentUser(c)
if err != nil {
return err
}

if username == "user" {
username = ""
}

// Delete user data
err = fileutils.DeleteFile(filepath.Join(dir, username, "user.json"))
if err != nil {
return err
}

// Do additional cleanup in wf-cli
if c.App.Name == "wf" {
// Delete user dir if it's empty
userEmpty, err := fileutils.IsEmpty(filepath.Join(dir, username))
if err != nil {
return err
}
if !userEmpty {
return nil
}
err = fileutils.DeleteFile(filepath.Join(dir, username))
if err != nil {
return err
}

// Delete host dir if it's empty
hostEmpty, err := fileutils.IsEmpty(dir)
if err != nil {
return err
}
if !hostEmpty {
return nil
}
err = fileutils.DeleteFile(dir)
if err != nil {
return err
}
}

return nil
}

func SaveUser(c *cli.Context, u *writeas.AuthUser) error {
// Marshal struct into pretty-printed JSON
userJSON, err := json.MarshalIndent(u, "", " ")
if err != nil {
return err
}

dir, err := UserHostDir(c)
if err != nil {
return err
}
// Save file
err = ioutil.WriteFile(filepath.Join(dataDir, UserFile), userJSON, 0600)
username, err := CurrentUser(c)
if err != nil {
return err
}
if username != "user" {
dir = filepath.Join(dir, u.User.Username)
}
DirMustExist(dir)
err = ioutil.WriteFile(filepath.Join(dir, "user.json"), userJSON, 0600)
if err != nil {
return err
}
return nil
}

// UserHostDir returns the path to the user data directory with the host based
// subpath if the host flag is set
func UserHostDir(c *cli.Context) (string, error) {
dataDir := UserDataDir(c.App.ExtraInfo()["configDir"])
hostDir, err := HostDirectory(c)
if err != nil {
return "", err
}
return filepath.Join(dataDir, hostDir), nil
}

// CurrentUser returns the username of the user taking action in the current
// cli.Context.
func CurrentUser(c *cli.Context) (string, error) {
if c.App.Name == "writeas" {
return "user", nil
}
// Use user flag value
if c.GlobalString("user") != "" {
return c.GlobalString("user"), nil
}

// Load host-level config, if host flag is set
hostDir, err := UserHostDir(c)
if err != nil {
return "", err
}
cfg, err := LoadConfig(hostDir)
if err != nil {
return "", err
}
if cfg.Default.User == "" {
// Load app-level config
globalCFG, err := LoadConfig(UserDataDir(c.App.ExtraInfo()["configDir"]))
if err != nil {
return "", err
}
// only use global defaults when both are set and no host flag
if globalCFG.Default.User != "" &&
globalCFG.Default.Host != "" &&
!c.GlobalIsSet("host") {
cfg = globalCFG
}
}

return cfg.Default.User, nil
}

+ 13
- 0
executable/executable.go View File

@@ -0,0 +1,13 @@
// Package executable holds utility functions that assist both CLI executables,
// writeas and wf.
package executable

import (
"os"
"path"
)

func Name() string {
n := os.Args[0]
return path.Base(n)
}

+ 16
- 0
fileutils/fileutils.go View File

@@ -3,6 +3,7 @@ package fileutils
import (
"bufio"
"fmt"
"io"
"os"
"strings"
)
@@ -109,3 +110,18 @@ func FindLine(p, startsWith string) string {
func DeleteFile(p string) error {
return os.Remove(p)
}

// IsEmpty returns whether or not the given directory is empty
func IsEmpty(d string) (bool, error) {
f, err := os.Open(d)
if err != nil {
return false, err
}
defer f.Close()

_, err = f.Readdirnames(1)
if err == io.EOF {
return true, nil
}
return false, err
}

+ 1
- 0
go.mod View File

@@ -5,6 +5,7 @@ require (
github.com/atotto/clipboard v0.1.1
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/hashicorp/go-multierror v1.0.0
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/microcosm-cc/bluemonday v1.0.1 // indirect


+ 4
- 0
go.sum View File

@@ -12,6 +12,10 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0=
github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=


+ 1
- 1
log/logging.go View File

@@ -10,7 +10,7 @@ import (
// Info logs general diagnostic messages, shown only when the -v or --verbose
// flag is provided.
func Info(c *cli.Context, s string, p ...interface{}) {
if c.Bool("v") || c.Bool("verbose") {
if c.Bool("v") || c.Bool("verbose") || c.GlobalBool("v") || c.GlobalBool("verbose") {
fmt.Fprintf(os.Stderr, s+"\n", p...)
}
}


Loading…
Cancel
Save