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...) } }