Browse Source

Merge pull request #36 from writeas/T586

Support any writefreely instance

Resolves T586 T594 T595 T635
pull/45/head
Matt Baer 4 years ago
committed by GitHub
parent
commit
de2ec24c13
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1019 additions and 153 deletions
  1. +17
    -12
      GUIDE.md
  2. +18
    -16
      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. +198
    -0
      cmd/wf/commands.go
  8. +5
    -0
      cmd/wf/config_nix.go
  9. +5
    -0
      cmd/wf/config_win.go
  10. +16
    -0
      cmd/wf/flags.go
  11. +265
    -0
      cmd/wf/main.go
  12. +1
    -3
      cmd/writeas/config_nix.go
  13. +1
    -3
      cmd/writeas/config_win.go
  14. +13
    -0
      cmd/writeas/flags.go
  15. +7
    -19
      cmd/writeas/main.go
  16. +61
    -24
      commands/commands.go
  17. +20
    -9
      config/config.go
  18. +24
    -4
      config/directories.go
  19. +2
    -1
      config/files_nix.go
  20. +3
    -1
      config/files_win.go
  21. +4
    -0
      config/flags.go
  22. +48
    -4
      config/options.go
  23. +130
    -6
      config/user.go
  24. +13
    -0
      executable/executable.go
  25. +16
    -0
      fileutils/fileutils.go
  26. +1
    -1
      go.mod
  27. +2
    -2
      go.sum
  28. +1
    -1
      log/logging.go

+ 17
- 12
GUIDE.md View File

@@ -24,24 +24,29 @@ COMMANDS:
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
-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
```

> Note: the host and user flags are only available in `writefreely`.

#### 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:
@@ -86,7 +91,7 @@ dev My Dev Log

#### List posts

This lists all posts you've published from your device
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.



+ 18
- 16
README.md View File

@@ -4,8 +4,6 @@ writeas-cli

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

**NOTE: the `master` branch is currently unstable while we prepare the v2.0 release! You should install via official release channel, or build from the `v1.2` tag.**

## Features

* Publish anonymously to Write.as
@@ -26,10 +24,10 @@ 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/v1.2/writeas_1.2_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_windows_386.zip) executable and put it somewhere in your `%PATH%`.
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/v1.2/writeas_1.2_darwin_amd64.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`.
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
@@ -39,7 +37,7 @@ 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/v1.2/writeas_1.2_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`.
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
@@ -81,19 +79,23 @@ COMMANDS:
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
-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
```

> Note: the host and user flags are only available in `wf` the community edition

## Contributing to the CLI

For a complete guide to contributing, see the [Contribution Guide](.github/CONTRIBUTING.md).


+ 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

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

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

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

"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
}

+ 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: "Operate against a custom hostname",
},
cli.StringFlag{
Name: "user, u",
Usage: "Use authenticated user, other than default",
},
}

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

@@ -0,0 +1,265 @@
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 all of your posts",
Description: "This will list only local posts.",
Action: requireAuth(commands.CmdListPosts, "view posts"),
Flags: []cli.Flag{
cli.BoolFlag{
Name: "id",
Usage: "Show list with post IDs (default)",
},
cli.BoolFlag{
Name: "md",
Usage: "Use with --url to return URLs with Markdown enabled",
},
cli.BoolFlag{
Name: "url",
Usage: "Show list with URLs",
},
cli.BoolFlag{
Name: "verbose, v",
Usage: "Show verbose post listing, including Edit Tokens",
},
},
}, {
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: "claim",
Usage: "Claim local unsynced posts",
Action: requireAuth(commands.CmdClaim, "claim unsynced posts"),
Description: "This will claim any unsynced posts local to this machine. To see which posts these are run: wf posts.",
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: "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)
}

+ 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
- 1
go.mod View File

@@ -14,7 +14,7 @@ require (
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect
github.com/writeas/go-writeas/v2 v2.0.0
github.com/writeas/go-writeas/v2 v2.0.2
github.com/writeas/saturday v0.0.0-20170402010311-f455b05c043f // indirect
github.com/writeas/web-core v0.0.0-20181111165528-05f387ffa1b3
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect


+ 2
- 2
go.sum View File

@@ -33,8 +33,8 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w=
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/writeas/go-writeas/v2 v2.0.0 h1:KjDI5bQSAIH0IzkKW3uGoY98I1A4DrBsSqBklgyOvHw=
github.com/writeas/go-writeas/v2 v2.0.0/go.mod h1:9sjczQJKmru925fLzg0usrU1R1tE4vBmQtGnItUMR0M=
github.com/writeas/go-writeas/v2 v2.0.2 h1:akvdMg89U5oBJiCkBwOXljVLTqP354uN6qnG2oOMrbk=
github.com/writeas/go-writeas/v2 v2.0.2/go.mod h1:9sjczQJKmru925fLzg0usrU1R1tE4vBmQtGnItUMR0M=
github.com/writeas/impart v0.0.0-20180808220913-fef51864677b h1:vsZIsYneuNwXMsnh0lKviEVc8WeIqBG4RTmGWU86HpI=
github.com/writeas/impart v0.0.0-20180808220913-fef51864677b/go.mod h1:sUkQZZHJfrVNsoD4QbkrYrRSQtCN3SaUPWKdohmFKT8=
github.com/writeas/impart v1.1.0 h1:nPnoO211VscNkp/gnzir5UwCDEvdHThL5uELU60NFSE=


+ 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