Support any writefreely instance Resolves T586 T594 T595 T635pull/45/head
@@ -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. | |||
@@ -4,8 +4,6 @@ writeas-cli | |||
Command line interface for [Write.as](https://write.as). Works on Windows, macOS, and Linux. | |||
**NOTE: the `master` branch is currently unstable while we prepare the v2.0 release! You should install via official release channel, or build from the `v1.2` tag.** | |||
## Features | |||
* Publish anonymously to Write.as | |||
@@ -26,10 +24,10 @@ The easiest way to get the CLI is to download a pre-built executable for your OS | |||
Get the latest version for your operating system as a standalone executable. | |||
**Windows**<br /> | |||
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_windows_386.zip) executable and put it somewhere in your `%PATH%`. | |||
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_amd64.zip) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_windows_386.zip) executable and put it somewhere in your `%PATH%`. | |||
**macOS**<br /> | |||
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_darwin_amd64.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. | |||
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_darwin_amd64.zip) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. | |||
**Debian-based Linux**<br /> | |||
```bash | |||
@@ -39,7 +37,7 @@ sudo apt-get update && sudo apt-get install writeas-cli | |||
``` | |||
**Linux (other)**<br /> | |||
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v1.2/writeas_1.2_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. | |||
Download the [64-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_linux_amd64.tar.gz) or [32-bit](https://github.com/writeas/writeas-cli/releases/download/v2.0.0/writeas_2.0.0_linux_386.tar.gz) executable and put it somewhere in your `$PATH`, like `/usr/local/bin`. | |||
### Go get it | |||
```bash | |||
@@ -81,19 +79,23 @@ COMMANDS: | |||
help, h Shows a list of commands or help for one command | |||
GLOBAL OPTIONS: | |||
-c value, -b value Optional blog to post to | |||
--tor, -t Perform action on Tor hidden service | |||
--tor-port value Use a different port to connect to Tor (default: 9150) | |||
--code Specifies this post is code | |||
--md Returns post URL with Markdown enabled | |||
--verbose, -v Make the operation more talkative | |||
--font value Sets post font to given value (default: "mono") | |||
--lang value Sets post language to given ISO 639-1 language code | |||
--user-agent value Sets the User-Agent for API requests | |||
--help, -h show help | |||
--version, -V print the version | |||
-c value, -b value Optional blog to post to | |||
--tor, -t Perform action on Tor hidden service | |||
--tor-port value Use a different port to connect to Tor (default: 9150) | |||
--code Specifies this post is code | |||
--md Returns post URL with Markdown enabled | |||
--verbose, -v Make the operation more talkative | |||
--font value Sets post font to given value (default: "mono") | |||
--lang value Sets post language to given ISO 639-1 language code | |||
--user-agent value Sets the User-Agent for API requests | |||
--host value, -H value Operate against a custom hostname | |||
--user value, -u value Use authenticated user, other than default | |||
--help, -h show help | |||
--version, -V print the version | |||
``` | |||
> Note: the host and user flags are only available in `wf` the community edition | |||
## Contributing to the CLI | |||
For a complete guide to contributing, see the [Contribution Guide](.github/CONTRIBUTING.md). | |||
@@ -2,44 +2,70 @@ package api | |||
import ( | |||
"fmt" | |||
"path/filepath" | |||
"strings" | |||
"github.com/atotto/clipboard" | |||
writeas "github.com/writeas/go-writeas/v2" | |||
"github.com/writeas/web-core/posts" | |||
"github.com/writeas/writeas-cli/config" | |||
"github.com/writeas/writeas-cli/fileutils" | |||
"github.com/writeas/writeas-cli/executable" | |||
"github.com/writeas/writeas-cli/log" | |||
cli "gopkg.in/urfave/cli.v1" | |||
) | |||
func newClient(c *cli.Context, authRequired bool) (*writeas.Client, error) { | |||
func HostURL(c *cli.Context) string { | |||
host := c.GlobalString("host") | |||
if host == "" { | |||
return "" | |||
} | |||
insecure := c.Bool("insecure") | |||
if parts := strings.Split(host, "://"); len(parts) > 1 { | |||
host = parts[1] | |||
} | |||
scheme := "https://" | |||
if insecure { | |||
scheme = "http://" | |||
} | |||
return scheme + host | |||
} | |||
func newClient(c *cli.Context) (*writeas.Client, error) { | |||
var client *writeas.Client | |||
if config.IsTor(c) { | |||
client = writeas.NewTorClient(config.TorPort(c)) | |||
} else { | |||
if config.IsDev() { | |||
client = writeas.NewDevClient() | |||
var clientConfig writeas.Config | |||
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||
if err != nil { | |||
return nil, fmt.Errorf("Failed to load configuration file: %v", err) | |||
} | |||
if host := HostURL(c); host != "" { | |||
clientConfig.URL = host + "/api" | |||
} else if cfg.Default.Host != "" && cfg.Default.User != "" { | |||
if parts := strings.Split(cfg.Default.Host, "://"); len(parts) > 1 { | |||
clientConfig.URL = cfg.Default.Host + "/api" | |||
} else { | |||
client = writeas.NewClient() | |||
clientConfig.URL = "https://" + cfg.Default.Host + "/api" | |||
} | |||
} else if config.IsDev() { | |||
clientConfig.URL = config.DevBaseURL + "/api" | |||
} else if c.App.Name == "writeas" { | |||
clientConfig.URL = config.WriteasBaseURL + "/api" | |||
} else { | |||
return nil, fmt.Errorf("Must supply a host. Example: %s --host example.com %s", executable.Name(), c.Command.Name) | |||
} | |||
client.UserAgent = config.UserAgent(c) | |||
// TODO: load user into var shared across the app | |||
u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||
if u != nil { | |||
client.SetToken(u.AccessToken) | |||
} else if authRequired { | |||
return nil, fmt.Errorf("Not currently logged in. Authenticate with: writeas auth <username>") | |||
if config.IsTor(c) { | |||
clientConfig.URL = config.TorURL(c) | |||
clientConfig.TorPort = config.TorPort(c) | |||
} | |||
client = writeas.NewClientWith(clientConfig) | |||
client.UserAgent = config.UserAgent(c) | |||
return client, nil | |||
} | |||
// DoFetch retrieves the Write.as post with the given friendlyID, | |||
// optionally via the Tor hidden service. | |||
func DoFetch(c *cli.Context, friendlyID string) error { | |||
cl, err := newClient(c, false) | |||
cl, err := newClient(c) | |||
if err != nil { | |||
return err | |||
} | |||
@@ -59,11 +85,18 @@ func DoFetch(c *cli.Context, friendlyID string) error { | |||
// DoFetchPosts retrieves all remote posts for the | |||
// authenticated user | |||
func DoFetchPosts(c *cli.Context) ([]writeas.Post, error) { | |||
cl, err := newClient(c, true) | |||
cl, err := newClient(c) | |||
if err != nil { | |||
return nil, fmt.Errorf("%v", err) | |||
} | |||
u, _ := config.LoadUser(c) | |||
if u != nil { | |||
cl.SetToken(u.AccessToken) | |||
} else { | |||
return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>") | |||
} | |||
posts, err := cl.GetUserPosts() | |||
if err != nil { | |||
return nil, err | |||
@@ -75,11 +108,18 @@ func DoFetchPosts(c *cli.Context) ([]writeas.Post, error) { | |||
// DoPost creates a Write.as post, returning an error if it was | |||
// unsuccessful. | |||
func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writeas.Post, error) { | |||
cl, err := newClient(c, false) | |||
cl, err := newClient(c) | |||
if err != nil { | |||
return nil, fmt.Errorf("%v", err) | |||
} | |||
u, _ := config.LoadUser(c) | |||
if u != nil { | |||
cl.SetToken(u.AccessToken) | |||
} else { | |||
return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>") | |||
} | |||
pp := &writeas.PostParams{ | |||
Font: config.GetFont(code, font), | |||
Collection: config.Collection(c), | |||
@@ -93,14 +133,22 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writ | |||
return nil, fmt.Errorf("Unable to post: %v", err) | |||
} | |||
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||
if err != nil { | |||
return nil, fmt.Errorf("Couldn't check for config file: %v", err) | |||
} | |||
var url string | |||
if p.Collection != nil { | |||
url = p.Collection.URL + p.Slug | |||
} else { | |||
if config.IsTor(c) { | |||
url = config.TorBaseURL | |||
if host := HostURL(c); host != "" { | |||
url = host | |||
} else if cfg.Default.Host != "" { | |||
url = cfg.Default.Host | |||
} else if config.IsDev() { | |||
url = config.DevBaseURL | |||
} else if config.IsTor(c) { | |||
url = config.TorBaseURL | |||
} else { | |||
url = config.WriteasBaseURL | |||
} | |||
@@ -119,7 +167,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writ | |||
// Copy URL to clipboard | |||
err = clipboard.WriteAll(string(url)) | |||
if err != nil { | |||
log.Errorln("writeas: Didn't copy to clipboard: %s", err) | |||
log.Errorln(executable.Name()+": Didn't copy to clipboard: %s", err) | |||
} else { | |||
log.Info(c, "Copied to clipboard.") | |||
} | |||
@@ -133,7 +181,7 @@ func DoPost(c *cli.Context, post []byte, font string, encrypt, code bool) (*writ | |||
// DoFetchCollections retrieves a list of the currently logged in users | |||
// collections. | |||
func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { | |||
cl, err := newClient(c, true) | |||
cl, err := newClient(c) | |||
if err != nil { | |||
if config.Debug() { | |||
log.ErrorlnQuit("could not create client: %v", err) | |||
@@ -141,6 +189,13 @@ func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { | |||
return nil, fmt.Errorf("Couldn't create new client") | |||
} | |||
u, _ := config.LoadUser(c) | |||
if u != nil { | |||
cl.SetToken(u.AccessToken) | |||
} else { | |||
return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>") | |||
} | |||
colls, err := cl.GetUserCollections() | |||
if err != nil { | |||
if config.Debug() { | |||
@@ -165,11 +220,20 @@ func DoFetchCollections(c *cli.Context) ([]RemoteColl, error) { | |||
// DoUpdate updates the given post on Write.as. | |||
func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, code bool) error { | |||
cl, err := newClient(c, false) | |||
cl, err := newClient(c) | |||
if err != nil { | |||
return fmt.Errorf("%v", err) | |||
} | |||
if token == "" { | |||
u, _ := config.LoadUser(c) | |||
if u != nil { | |||
cl.SetToken(u.AccessToken) | |||
} else { | |||
return fmt.Errorf("You must either provide and edit token or log in to delete a post.") | |||
} | |||
} | |||
params := writeas.PostParams{} | |||
params.Title, params.Content = posts.ExtractTitle(string(post)) | |||
if lang := config.Language(c, false); lang != "" { | |||
@@ -191,11 +255,20 @@ func DoUpdate(c *cli.Context, post []byte, friendlyID, token, font string, code | |||
// DoDelete deletes the given post on Write.as, and removes any local references | |||
func DoDelete(c *cli.Context, friendlyID, token string) error { | |||
cl, err := newClient(c, false) | |||
cl, err := newClient(c) | |||
if err != nil { | |||
return fmt.Errorf("%v", err) | |||
} | |||
if token == "" { | |||
u, _ := config.LoadUser(c) | |||
if u != nil { | |||
cl.SetToken(u.AccessToken) | |||
} else { | |||
return fmt.Errorf("You must either provide and edit token or log in to delete a post.") | |||
} | |||
} | |||
err = cl.DeletePost(friendlyID, token) | |||
if err != nil { | |||
if config.Debug() { | |||
@@ -204,13 +277,13 @@ func DoDelete(c *cli.Context, friendlyID, token string) error { | |||
return fmt.Errorf("Post doesn't exist, or bad edit token given.") | |||
} | |||
RemovePost(c.App.ExtraInfo()["configDir"], friendlyID) | |||
RemovePost(c, friendlyID) | |||
return nil | |||
} | |||
func DoLogIn(c *cli.Context, username, password string) error { | |||
cl, err := newClient(c, false) | |||
cl, err := newClient(c) | |||
if err != nil { | |||
return fmt.Errorf("%v", err) | |||
} | |||
@@ -223,7 +296,7 @@ func DoLogIn(c *cli.Context, username, password string) error { | |||
return err | |||
} | |||
err = config.SaveUser(config.UserDataDir(c.App.ExtraInfo()["configDir"]), u) | |||
err = config.SaveUser(c, u) | |||
if err != nil { | |||
return err | |||
} | |||
@@ -232,11 +305,18 @@ func DoLogIn(c *cli.Context, username, password string) error { | |||
} | |||
func DoLogOut(c *cli.Context) error { | |||
cl, err := newClient(c, true) | |||
cl, err := newClient(c) | |||
if err != nil { | |||
return fmt.Errorf("%v", err) | |||
} | |||
u, _ := config.LoadUser(c) | |||
if u != nil { | |||
cl.SetToken(u.AccessToken) | |||
} else if c.App.Name == "writeas" { | |||
return fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>") | |||
} | |||
err = cl.LogOut() | |||
if err != nil { | |||
if config.Debug() { | |||
@@ -245,11 +325,6 @@ func DoLogOut(c *cli.Context) error { | |||
return err | |||
} | |||
// Delete local user data | |||
err = fileutils.DeleteFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), config.UserFile)) | |||
if err != nil { | |||
return err | |||
} | |||
return nil | |||
// delete local user file | |||
return config.DeleteUser(c) | |||
} |
@@ -12,6 +12,7 @@ import ( | |||
writeas "github.com/writeas/go-writeas/v2" | |||
"github.com/writeas/writeas-cli/config" | |||
"github.com/writeas/writeas-cli/executable" | |||
"github.com/writeas/writeas-cli/fileutils" | |||
"github.com/writeas/writeas-cli/log" | |||
cli "gopkg.in/urfave/cli.v1" | |||
@@ -42,7 +43,11 @@ type RemotePost struct { | |||
} | |||
func AddPost(c *cli.Context, id, token string) error { | |||
f, err := os.OpenFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) | |||
hostDir, err := config.HostDirectory(c) | |||
if err != nil { | |||
return fmt.Errorf("Error checking for host directory: %v", err) | |||
} | |||
f, err := os.OpenFile(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600) | |||
if err != nil { | |||
return fmt.Errorf("Error creating local posts list: %s", err) | |||
} | |||
@@ -60,10 +65,18 @@ func AddPost(c *cli.Context, id, token string) error { | |||
// ClaimPost adds a local post to the authenticated user's account and deletes | |||
// the local reference | |||
func ClaimPosts(c *cli.Context, localPosts *[]Post) (*[]writeas.ClaimPostResult, error) { | |||
cl, err := newClient(c, true) | |||
cl, err := newClient(c) | |||
if err != nil { | |||
return nil, err | |||
} | |||
u, _ := config.LoadUser(c) | |||
if u != nil { | |||
cl.SetToken(u.AccessToken) | |||
} else { | |||
return nil, fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>") | |||
} | |||
postsToClaim := make([]writeas.OwnedPostParams, len(*localPosts)) | |||
for i, post := range *localPosts { | |||
postsToClaim[i] = writeas.OwnedPostParams{ | |||
@@ -76,7 +89,8 @@ func ClaimPosts(c *cli.Context, localPosts *[]Post) (*[]writeas.ClaimPostResult, | |||
} | |||
func TokenFromID(c *cli.Context, id string) string { | |||
post := fileutils.FindLine(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile), id) | |||
hostDir, _ := config.HostDirectory(c) | |||
post := fileutils.FindLine(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile), id) | |||
if post == "" { | |||
return "" | |||
} | |||
@@ -89,12 +103,15 @@ func TokenFromID(c *cli.Context, id string) string { | |||
return parts[1] | |||
} | |||
func RemovePost(path, id string) { | |||
fileutils.RemoveLine(filepath.Join(config.UserDataDir(path), postsFile), id) | |||
func RemovePost(c *cli.Context, id string) { | |||
hostDir, _ := config.HostDirectory(c) | |||
fullPath := filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile) | |||
fileutils.RemoveLine(fullPath, id) | |||
} | |||
func GetPosts(c *cli.Context) *[]Post { | |||
lines := fileutils.ReadData(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), postsFile)) | |||
hostDir, _ := config.HostDirectory(c) | |||
lines := fileutils.ReadData(filepath.Join(config.UserDataDir(c.App.ExtraInfo()["configDir"]), hostDir, postsFile)) | |||
posts := []Post{} | |||
@@ -8,6 +8,7 @@ import ( | |||
"path/filepath" | |||
"github.com/writeas/writeas-cli/config" | |||
"github.com/writeas/writeas-cli/executable" | |||
"github.com/writeas/writeas-cli/fileutils" | |||
"github.com/writeas/writeas-cli/log" | |||
cli "gopkg.in/urfave/cli.v1" | |||
@@ -25,14 +26,21 @@ func CmdPull(c *cli.Context) error { | |||
} | |||
// Create posts directory if needed | |||
if cfg.Posts.Directory == "" { | |||
syncSetUp(c.App.ExtraInfo()["configDir"], cfg) | |||
syncSetUp(c, cfg) | |||
} | |||
cl, err := newClient(c, true) | |||
cl, err := newClient(c) | |||
if err != nil { | |||
return err | |||
} | |||
u, _ := config.LoadUser(c) | |||
if u != nil { | |||
cl.SetToken(u.AccessToken) | |||
} else { | |||
return fmt.Errorf("Not currently logged in. Authenticate with: " + executable.Name() + " auth <username>") | |||
} | |||
// Fetch posts | |||
posts, err := cl.GetUserPosts() | |||
if err != nil { | |||
@@ -79,10 +87,10 @@ func CmdPull(c *cli.Context) error { | |||
return nil | |||
} | |||
func syncSetUp(path string, cfg *config.UserConfig) error { | |||
func syncSetUp(c *cli.Context, cfg *config.Config) error { | |||
// Get user information and fail early (before we make the user do | |||
// anything), if we're going to | |||
u, err := config.LoadUser(config.UserDataDir(path)) | |||
u, err := config.LoadUser(c) | |||
if err != nil { | |||
return err | |||
} | |||
@@ -118,7 +126,7 @@ func syncSetUp(path string, cfg *config.UserConfig) error { | |||
// Save preference | |||
cfg.Posts.Directory = dir | |||
err = config.SaveConfig(config.UserDataDir(path), cfg) | |||
err = config.SaveConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]), cfg) | |||
if err != nil { | |||
if config.Debug() { | |||
log.Errorln("Unable to save config: %s", err) | |||
@@ -0,0 +1 @@ | |||
wf |
@@ -0,0 +1,198 @@ | |||
package main | |||
import ( | |||
"fmt" | |||
"os" | |||
"path/filepath" | |||
"strings" | |||
"github.com/writeas/writeas-cli/api" | |||
"github.com/writeas/writeas-cli/commands" | |||
"github.com/writeas/writeas-cli/config" | |||
"github.com/writeas/writeas-cli/executable" | |||
"github.com/writeas/writeas-cli/log" | |||
cli "gopkg.in/urfave/cli.v1" | |||
) | |||
func requireAuth(f cli.ActionFunc, action string) cli.ActionFunc { | |||
return func(c *cli.Context) error { | |||
// check for logged in users when host is provided without user | |||
if c.GlobalIsSet("host") && !c.GlobalIsSet("user") { | |||
// multiple users should display a list | |||
if num, users, err := usersLoggedIn(c); num > 1 && err == nil { | |||
return cli.NewExitError(fmt.Sprintf("Multiple logged in users, please use '-u' or '-user' to specify one of:\n%s", strings.Join(users, ", ")), 1) | |||
} else if num == 1 && err == nil { | |||
// single user found for host should be set as user flag so LoadUser can | |||
// succeed, and notify the client | |||
if err := c.GlobalSet("user", users[0]); err != nil { | |||
return cli.NewExitError(fmt.Sprintf("Failed to set user flag for only logged in user at host %s: %v", users[0], err), 1) | |||
} | |||
log.Info(c, "Host specified without user flag, using logged in user: %s\n", users[0]) | |||
} else if err != nil { | |||
return cli.NewExitError(fmt.Sprintf("Failed to check for logged in users: %v", err), 1) | |||
} | |||
} else if !c.GlobalIsSet("host") && !c.GlobalIsSet("user") { | |||
// check for global configured pair host/user | |||
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||
if err != nil { | |||
return cli.NewExitError(fmt.Sprintf("Failed to load config from file: %v", err), 1) | |||
// set flags if found | |||
} | |||
// set flags if both were found in config | |||
if cfg.Default.Host != "" && cfg.Default.User != "" { | |||
err = c.GlobalSet("host", cfg.Default.Host) | |||
if err != nil { | |||
return cli.NewExitError(fmt.Sprintf("Failed to set host from global config: %v", err), 1) | |||
} | |||
err = c.GlobalSet("user", cfg.Default.User) | |||
if err != nil { | |||
return cli.NewExitError(fmt.Sprintf("Failed to set user from global config: %v", err), 1) | |||
} | |||
} else { | |||
num, err := totalUsersLoggedIn(c) | |||
if err != nil { | |||
return cli.NewExitError(fmt.Sprintf("Failed to check for logged in users: %v", err), 1) | |||
} else if num > 0 { | |||
return cli.NewExitError("You are authenticated, but have no default user/host set. Supply -user and -host flags.", 1) | |||
} | |||
} | |||
} | |||
u, err := config.LoadUser(c) | |||
if err != nil { | |||
return cli.NewExitError(fmt.Sprintf("Couldn't load user: %v", err), 1) | |||
} | |||
if u == nil { | |||
return cli.NewExitError("You must be authenticated to "+action+".\nLog in first with: "+executable.Name()+" auth <username>", 1) | |||
} | |||
return f(c) | |||
} | |||
} | |||
// usersLoggedIn checks for logged in users for the set host flag | |||
// it returns the number of users and a slice of usernames | |||
func usersLoggedIn(c *cli.Context) (int, []string, error) { | |||
path, err := config.UserHostDir(c) | |||
if err != nil { | |||
return 0, nil, err | |||
} | |||
dir, err := os.Open(path) | |||
if err != nil { | |||
return 0, nil, err | |||
} | |||
contents, err := dir.Readdir(0) | |||
if err != nil { | |||
return 0, nil, err | |||
} | |||
var names []string | |||
for _, file := range contents { | |||
if file.IsDir() { | |||
// stat user.json | |||
if _, err := os.Stat(filepath.Join(path, file.Name(), "user.json")); err == nil { | |||
names = append(names, file.Name()) | |||
} | |||
} | |||
} | |||
return len(names), names, nil | |||
} | |||
// totalUsersLoggedIn checks for logged in users for any host | |||
// it returns the number of users and an error if any | |||
func totalUsersLoggedIn(c *cli.Context) (int, error) { | |||
path := config.UserDataDir(c.App.ExtraInfo()["configDir"]) | |||
dir, err := os.Open(path) | |||
if err != nil { | |||
return 0, err | |||
} | |||
contents, err := dir.Readdir(0) | |||
if err != nil { | |||
return 0, err | |||
} | |||
count := 0 | |||
for _, file := range contents { | |||
if file.IsDir() { | |||
subDir, err := os.Open(filepath.Join(path, file.Name())) | |||
if err != nil { | |||
return 0, err | |||
} | |||
subContents, err := subDir.Readdir(0) | |||
if err != nil { | |||
return 0, err | |||
} | |||
for _, subFile := range subContents { | |||
if subFile.IsDir() { | |||
if _, err := os.Stat(filepath.Join(path, file.Name(), subFile.Name(), "user.json")); err == nil { | |||
count++ | |||
} | |||
} | |||
} | |||
} | |||
} | |||
return count, nil | |||
} | |||
func cmdAuth(c *cli.Context) error { | |||
err := commands.CmdAuth(c) | |||
if err != nil { | |||
return err | |||
} | |||
// Get the username from the command, just like commands.CmdAuth does | |||
username := c.Args().Get(0) | |||
// Update config if this is user's first auth | |||
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||
if err != nil { | |||
log.Errorln("Not saving config. Unable to load config: %s", err) | |||
return err | |||
} | |||
if cfg.Default.Host == "" && cfg.Default.User == "" { | |||
// This is user's first auth, so save defaults | |||
cfg.Default.Host = api.HostURL(c) | |||
cfg.Default.User = username | |||
err = config.SaveConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]), cfg) | |||
if err != nil { | |||
log.Errorln("Not saving config. Unable to save config: %s", err) | |||
return err | |||
} | |||
fmt.Printf("Set %s on %s as default account.\n", username, c.GlobalString("host")) | |||
} | |||
return nil | |||
} | |||
func cmdLogOut(c *cli.Context) error { | |||
err := commands.CmdLogOut(c) | |||
if err != nil { | |||
return err | |||
} | |||
// Remove this from config if it's the default account | |||
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||
if err != nil { | |||
log.Errorln("Not updating config. Unable to load: %s", err) | |||
return err | |||
} | |||
username, err := config.CurrentUser(c) | |||
if err != nil { | |||
log.Errorln("Not updating config. Unable to load current user: %s", err) | |||
return err | |||
} | |||
reqHost := api.HostURL(c) | |||
if reqHost == "" { | |||
// No --host given, so we're using the default host | |||
reqHost = cfg.Default.Host | |||
} | |||
if cfg.Default.Host == reqHost && cfg.Default.User == username { | |||
// We're logging out of default username + host, so remove from config file | |||
cfg.Default.Host = "" | |||
cfg.Default.User = "" | |||
err = config.SaveConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"]), cfg) | |||
if err != nil { | |||
log.Errorln("Not updating config. Unable to save config: %s", err) | |||
return err | |||
} | |||
} | |||
return nil | |||
} |
@@ -0,0 +1,5 @@ | |||
// +build !windows | |||
package main | |||
const configDir = ".writefreely" |
@@ -0,0 +1,5 @@ | |||
// +build windows | |||
package main | |||
const configDir = "WriteFreely" |
@@ -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", | |||
}, | |||
} |
@@ -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) | |||
} |
@@ -2,6 +2,4 @@ | |||
package main | |||
var appInfo = map[string]string{ | |||
"configDir": ".writeas", | |||
} | |||
const configDir = ".writeas" |
@@ -2,6 +2,4 @@ | |||
package main | |||
var appInfo = map[string]string{ | |||
"configDir": "Write.as", | |||
} | |||
const configDir = "Write.as" |
@@ -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", | |||
}, | |||
} |
@@ -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 | |||
} | |||
} | |||
} | |||
} |
@@ -4,11 +4,13 @@ import ( | |||
"fmt" | |||
"io/ioutil" | |||
"os" | |||
"strings" | |||
"text/tabwriter" | |||
"github.com/howeyc/gopass" | |||
"github.com/writeas/writeas-cli/api" | |||
"github.com/writeas/writeas-cli/config" | |||
"github.com/writeas/writeas-cli/executable" | |||
"github.com/writeas/writeas-cli/log" | |||
cli "gopkg.in/urfave/cli.v1" | |||
) | |||
@@ -67,7 +69,7 @@ func CmdNew(c *cli.Context) error { | |||
func CmdPublish(c *cli.Context) error { | |||
filename := c.Args().Get(0) | |||
if filename == "" { | |||
return cli.NewExitError("usage: writeas publish <filename>", 1) | |||
return cli.NewExitError("usage: "+executable.Name()+" publish <filename>", 1) | |||
} | |||
content, err := ioutil.ReadFile(filename) | |||
if err != nil { | |||
@@ -92,16 +94,16 @@ func CmdDelete(c *cli.Context) error { | |||
friendlyID := c.Args().Get(0) | |||
token := c.Args().Get(1) | |||
if friendlyID == "" { | |||
return cli.NewExitError("usage: writeas delete <postId> [<token>]", 1) | |||
return cli.NewExitError("usage: "+executable.Name()+" delete <postId> [<token>]", 1) | |||
} | |||
u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||
u, _ := config.LoadUser(c) | |||
if token == "" { | |||
// Search for the token locally | |||
token = api.TokenFromID(c, friendlyID) | |||
if token == "" && u == nil { | |||
log.Errorln("Couldn't find an edit token locally. Did you create this post here?") | |||
log.ErrorlnQuit("If you have an edit token, use: writeas delete %s <token>", friendlyID) | |||
log.ErrorlnQuit("If you have an edit token, use: "+executable.Name()+" delete %s <token>", friendlyID) | |||
} | |||
} | |||
@@ -124,16 +126,16 @@ func CmdUpdate(c *cli.Context) error { | |||
friendlyID := c.Args().Get(0) | |||
token := c.Args().Get(1) | |||
if friendlyID == "" { | |||
return cli.NewExitError("usage: writeas update <postId> [<token>]", 1) | |||
return cli.NewExitError("usage: "+executable.Name()+" update <postId> [<token>]", 1) | |||
} | |||
u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||
u, _ := config.LoadUser(c) | |||
if token == "" { | |||
// Search for the token locally | |||
token = api.TokenFromID(c, friendlyID) | |||
if token == "" && u == nil { | |||
log.Errorln("Couldn't find an edit token locally. Did you create this post here?") | |||
log.ErrorlnQuit("If you have an edit token, use: writeas update %s <token>", friendlyID) | |||
log.ErrorlnQuit("If you have an edit token, use: "+executable.Name()+" update %s <token>", friendlyID) | |||
} | |||
} | |||
@@ -155,7 +157,7 @@ func CmdUpdate(c *cli.Context) error { | |||
func CmdGet(c *cli.Context) error { | |||
friendlyID := c.Args().Get(0) | |||
if friendlyID == "" { | |||
return cli.NewExitError("usage: writeas get <postId>", 1) | |||
return cli.NewExitError("usage: "+executable.Name()+" get <postId>", 1) | |||
} | |||
if config.IsTor(c) { | |||
@@ -175,7 +177,7 @@ func CmdAdd(c *cli.Context) error { | |||
friendlyID := c.Args().Get(0) | |||
token := c.Args().Get(1) | |||
if friendlyID == "" || token == "" { | |||
return cli.NewExitError("usage: writeas add <postId> <token>", 1) | |||
return cli.NewExitError("usage: "+executable.Name()+" add <postId> <token>", 1) | |||
} | |||
err := api.AddPost(c, friendlyID, token) | |||
@@ -192,7 +194,7 @@ func CmdListPosts(c *cli.Context) error { | |||
posts := api.GetPosts(c) | |||
u, _ := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||
u, _ := config.LoadUser(c) | |||
if u != nil { | |||
if config.IsTor(c) { | |||
log.Info(c, "Getting posts via hidden service...") | |||
@@ -205,7 +207,11 @@ func CmdListPosts(c *cli.Context) error { | |||
} | |||
if len(remotePosts) > 0 { | |||
fmt.Println("Anonymous Posts") | |||
if c.App.Name == "wf" { | |||
fmt.Println("Draft Posts") | |||
} else { | |||
fmt.Println("Anonymous Posts") | |||
} | |||
if details { | |||
identifier := "URL" | |||
if ids || !urls { | |||
@@ -261,9 +267,28 @@ func CmdListPosts(c *cli.Context) error { | |||
} | |||
func getPostURL(c *cli.Context, slug string) string { | |||
base := config.WriteasBaseURL | |||
if config.IsDev() { | |||
base = config.DevBaseURL | |||
var base string | |||
if c.App.Name == "writeas" { | |||
if config.IsDev() { | |||
base = config.DevBaseURL | |||
} else { | |||
base = config.WriteasBaseURL | |||
} | |||
} else { | |||
if host := api.HostURL(c); host != "" { | |||
base = host | |||
} else { | |||
// TODO handle error, or load config globally, see T601 | |||
// https://phabricator.write.as/T601 | |||
cfg, _ := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||
if cfg.Default.Host != "" && cfg.Default.User != "" { | |||
if parts := strings.Split(cfg.Default.Host, "://"); len(parts) > 1 { | |||
base = cfg.Default.Host | |||
} else { | |||
base = "https://" + cfg.Default.Host | |||
} | |||
} | |||
} | |||
} | |||
ext := "" | |||
// Output URL in requested format | |||
@@ -274,12 +299,12 @@ func getPostURL(c *cli.Context, slug string) string { | |||
} | |||
func CmdCollections(c *cli.Context) error { | |||
u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||
u, err := config.LoadUser(c) | |||
if err != nil { | |||
return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) | |||
} | |||
if u == nil { | |||
return cli.NewExitError("You must be authenticated to view collections.\nLog in first with: writeas auth <username>", 1) | |||
return cli.NewExitError("You must be authenticated to view collections.\nLog in first with: "+executable.Name()+" auth <username>", 1) | |||
} | |||
if config.IsTor(c) { | |||
log.Info(c, "Getting blogs via hidden service...") | |||
@@ -309,12 +334,12 @@ func CmdCollections(c *cli.Context) error { | |||
} | |||
func CmdClaim(c *cli.Context) error { | |||
u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||
u, err := config.LoadUser(c) | |||
if err != nil { | |||
return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) | |||
} | |||
if u == nil { | |||
return cli.NewExitError("You must be authenticated to claim local posts.\nLog in first with: writeas auth <username>", 1) | |||
return cli.NewExitError("You must be authenticated to claim local posts.\nLog in first with: "+executable.Name()+" auth <username>", 1) | |||
} | |||
localPosts := api.GetPosts(c) | |||
@@ -348,7 +373,7 @@ func CmdClaim(c *cli.Context) error { | |||
log.Info(c, "%sOK", status) | |||
okCount++ | |||
// only delete local if successful | |||
api.RemovePost(c.App.ExtraInfo()["configDir"], id) | |||
api.RemovePost(c, id) | |||
} | |||
} | |||
log.Info(c, "%d claimed, %d failed", okCount, errCount) | |||
@@ -356,19 +381,31 @@ func CmdClaim(c *cli.Context) error { | |||
} | |||
func CmdAuth(c *cli.Context) error { | |||
username := c.Args().Get(0) | |||
if username == "" && c.GlobalIsSet("user") { | |||
username = c.GlobalString("user") | |||
} | |||
// Check configuration | |||
u, err := config.LoadUser(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||
u, err := config.LoadUser(c) | |||
if err != nil { | |||
return cli.NewExitError(fmt.Sprintf("couldn't load config: %v", err), 1) | |||
} | |||
if u != nil && u.AccessToken != "" { | |||
return cli.NewExitError("You're already authenticated as "+u.User.Username+". Log out with: writeas logout", 1) | |||
if u != nil && u.AccessToken != "" && username == u.User.Username { | |||
return cli.NewExitError("You're already authenticated as "+u.User.Username, 1) | |||
} | |||
// Validate arguments and get password | |||
username := c.Args().Get(0) | |||
if username == "" { | |||
return cli.NewExitError("usage: writeas auth <username>", 1) | |||
cfg, err := config.LoadConfig(config.UserDataDir(c.App.ExtraInfo()["configDir"])) | |||
if err != nil { | |||
return cli.NewExitError(fmt.Sprintf("Failed to load config: %v", err), 1) | |||
} | |||
if cfg.Default.Host != "" && cfg.Default.User != "" { | |||
username = cfg.Default.User | |||
fmt.Printf("No user provided, using default user %s for host %s...\n", cfg.Default.User, cfg.Default.Host) | |||
} else { | |||
return cli.NewExitError("usage: "+executable.Name()+" auth <username>", 1) | |||
} | |||
} | |||
fmt.Print("Password: ") | |||
@@ -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"} | |||
@@ -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 | |||
} | |||
} | |||
} | |||
} |
@@ -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()) | |||
} |
@@ -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()) | |||
} |
@@ -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", | |||
}, | |||
@@ -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 | |||
} |
@@ -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 | |||
} |
@@ -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) | |||
} |
@@ -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 | |||
} |
@@ -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 | |||
@@ -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= | |||
@@ -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...) | |||
} | |||
} | |||