diff --git a/GUIDE.md b/GUIDE.md
index 5ed6e63..68d29d1 100644
--- a/GUIDE.md
+++ b/GUIDE.md
@@ -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.
diff --git a/README.md b/README.md
index 65841f4..a39a162 100644
--- a/README.md
+++ b/README.md
@@ -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**
-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**
-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**
```bash
@@ -39,7 +37,7 @@ sudo apt-get update && sudo apt-get install writeas-cli
```
**Linux (other)**
-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).
diff --git a/api/api.go b/api/api.go
index baa138b..dd550c0 100644
--- a/api/api.go
+++ b/api/api.go
@@ -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 ")
+ 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 ")
+ }
+
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 ")
+ }
+
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 ")
+ }
+
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 ")
+ }
+
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)
}
diff --git a/api/posts.go b/api/posts.go
index 77f82a1..033cac3 100644
--- a/api/posts.go
+++ b/api/posts.go
@@ -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 ")
+ }
+
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{}
diff --git a/api/sync.go b/api/sync.go
index b093be9..e57a31b 100644
--- a/api/sync.go
+++ b/api/sync.go
@@ -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 ")
+ }
+
// 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)
diff --git a/cmd/wf/.gitignore b/cmd/wf/.gitignore
new file mode 100644
index 0000000..89b7a51
--- /dev/null
+++ b/cmd/wf/.gitignore
@@ -0,0 +1 @@
+wf
\ No newline at end of file
diff --git a/cmd/wf/commands.go b/cmd/wf/commands.go
new file mode 100644
index 0000000..1a9e6c4
--- /dev/null
+++ b/cmd/wf/commands.go
@@ -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 ", 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
+}
diff --git a/cmd/wf/config_nix.go b/cmd/wf/config_nix.go
new file mode 100644
index 0000000..ddc1bff
--- /dev/null
+++ b/cmd/wf/config_nix.go
@@ -0,0 +1,5 @@
+// +build !windows
+
+package main
+
+const configDir = ".writefreely"
diff --git a/cmd/wf/config_win.go b/cmd/wf/config_win.go
new file mode 100644
index 0000000..1673fa1
--- /dev/null
+++ b/cmd/wf/config_win.go
@@ -0,0 +1,5 @@
+// +build windows
+
+package main
+
+const configDir = "WriteFreely"
diff --git a/cmd/wf/flags.go b/cmd/wf/flags.go
new file mode 100644
index 0000000..5245bb7
--- /dev/null
+++ b/cmd/wf/flags.go
@@ -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",
+ },
+}
diff --git a/cmd/wf/main.go b/cmd/wf/main.go
new file mode 100644
index 0000000..ef9d8f7
--- /dev/null
+++ b/cmd/wf/main.go
@@ -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)
+}
diff --git a/cmd/writeas/config_nix.go b/cmd/writeas/config_nix.go
index 6b3cb86..6c0ed02 100644
--- a/cmd/writeas/config_nix.go
+++ b/cmd/writeas/config_nix.go
@@ -2,6 +2,4 @@
package main
-var appInfo = map[string]string{
- "configDir": ".writeas",
-}
+const configDir = ".writeas"
diff --git a/cmd/writeas/config_win.go b/cmd/writeas/config_win.go
index 9a7eea1..43d2bca 100644
--- a/cmd/writeas/config_win.go
+++ b/cmd/writeas/config_win.go
@@ -2,6 +2,4 @@
package main
-var appInfo = map[string]string{
- "configDir": "Write.as",
-}
+const configDir = "Write.as"
diff --git a/cmd/writeas/flags.go b/cmd/writeas/flags.go
new file mode 100644
index 0000000..5fdaf8a
--- /dev/null
+++ b/cmd/writeas/flags.go
@@ -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",
+ },
+}
diff --git a/cmd/writeas/main.go b/cmd/writeas/main.go
index eec4500..e9c1019 100644
--- a/cmd/writeas/main.go
+++ b/cmd/writeas/main.go
@@ -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
- }
- }
- }
-}
diff --git a/commands/commands.go b/commands/commands.go
index a275e91..29ddc21 100644
--- a/commands/commands.go
+++ b/commands/commands.go
@@ -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 ", 1)
+ return cli.NewExitError("usage: "+executable.Name()+" publish ", 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 []", 1)
+ return cli.NewExitError("usage: "+executable.Name()+" delete []", 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 ", friendlyID)
+ log.ErrorlnQuit("If you have an edit token, use: "+executable.Name()+" delete %s ", 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 []", 1)
+ return cli.NewExitError("usage: "+executable.Name()+" update []", 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 ", friendlyID)
+ log.ErrorlnQuit("If you have an edit token, use: "+executable.Name()+" update %s ", 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 ", 1)
+ return cli.NewExitError("usage: "+executable.Name()+" get ", 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 ", 1)
+ return cli.NewExitError("usage: "+executable.Name()+" add ", 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 ", 1)
+ return cli.NewExitError("You must be authenticated to view collections.\nLog in first with: "+executable.Name()+" auth ", 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 ", 1)
+ return cli.NewExitError("You must be authenticated to claim local posts.\nLog in first with: "+executable.Name()+" auth ", 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 ", 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 ", 1)
+ }
}
fmt.Print("Password: ")
diff --git a/config/config.go b/config/config.go
index 4c9c59d..6ded22b 100644
--- a/config/config.go
+++ b/config/config.go
@@ -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"}
diff --git a/config/directories.go b/config/directories.go
index 8373835..e2a68f3 100644
--- a/config/directories.go
+++ b/config/directories.go
@@ -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
+ }
+ }
+ }
}
diff --git a/config/files_nix.go b/config/files_nix.go
index 0c10f04..13fb338 100644
--- a/config/files_nix.go
+++ b/config/files_nix.go
@@ -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())
}
diff --git a/config/files_win.go b/config/files_win.go
index 026b803..db2f459 100644
--- a/config/files_win.go
+++ b/config/files_win.go
@@ -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())
}
diff --git a/config/flags.go b/config/flags.go
index 2c17e23..195eb8d 100644
--- a/config/flags.go
+++ b/config/flags.go
@@ -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",
},
diff --git a/config/options.go b/config/options.go
index c3bffb5..9a163a6 100644
--- a/config/options.go
+++ b/config/options.go
@@ -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
+}
diff --git a/config/user.go b/config/user.go
index 21dd8f9..1a90c55 100644
--- a/config/user.go
+++ b/config/user.go
@@ -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
+}
diff --git a/executable/executable.go b/executable/executable.go
new file mode 100644
index 0000000..697f419
--- /dev/null
+++ b/executable/executable.go
@@ -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)
+}
diff --git a/fileutils/fileutils.go b/fileutils/fileutils.go
index 7e4c354..eda996b 100644
--- a/fileutils/fileutils.go
+++ b/fileutils/fileutils.go
@@ -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
+}
diff --git a/go.mod b/go.mod
index 346f6ef..90d8f91 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index d135529..86825a8 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/log/logging.go b/log/logging.go
index a514a7c..42da556 100644
--- a/log/logging.go
+++ b/log/logging.go
@@ -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...)
}
}