Page MenuHomeMusing Studio

No OneTemporary

diff --git a/README.md b/README.md
index 91fee2c..1ee20fe 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,34 @@
writeas-cli
===========
Command line interface for [Write.as](https://write.as) and [Write.as on Tor](http://writeas7pm7rcdqg.onion/). Works on Windows, OS X, and Linux.
-Like the [Android app](https://play.google.com/store/apps/details?id=com.abunchtell.writeas), the command line client keeps track of the posts you make, so future editing / deleting is easier than [doing it with cURL](http://cmd.write.as/). It is currently **ALPHA**, so only basic functionality is available. But the goal is for this to hold the logic behind any future GUI app we build for the desktop.
+Like the [Android app](https://play.google.com/store/apps/details?id=com.abunchtell.writeas), the command line client keeps track of the posts you make, so future editing / deleting is easier than [doing it with cURL](http://cmd.write.as/). The goal is for this to serve as the backend for any future GUI app we build for the desktop.
+
+It is currently **alpha**, so a) functionality is basic and b) everything is subject to change — i.e., watch the [changelog](https://write.as/changelog-cli.html).
## Usage
```
writeas [global options] command [command options] [arguments...]
COMMANDS:
post Alias for default action: create post from stdin
delete Delete a post
get Read a raw post
+ add Add a post locally
+ list List local posts
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--tor, -t Perform action on Tor hidden service
--tor-port "9150" Use a different port to connect to Tor
--help, -h show help
--version, -v print the version
```
## Download
Get it on the [web](https://write.as/cli.html) or [hidden service](http://writeas7pm7rcdqg.onion/cli.html).
## Go get it
`go get github.com/writeas/writeas-cli`
diff --git a/utils/fileutils.go b/utils/fileutils.go
index 5bc14f0..4686606 100644
--- a/utils/fileutils.go
+++ b/utils/fileutils.go
@@ -1,80 +1,101 @@
package fileutils
import (
"bufio"
"fmt"
"os"
"strings"
)
// Exists returns whether or not the given file exists
func Exists(p string) bool {
if _, err := os.Stat(p); err == nil {
return true
}
return false
}
func WriteData(path string, data []byte) {
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
fmt.Println(err)
}
// TODO: check for Close() errors
// https://github.com/ncw/swift/blob/master/swift.go#L170
defer f.Close()
_, err = f.Write(data)
if err != nil {
fmt.Println(err)
}
}
+func ReadData(p string) *[]string {
+ f, err := os.Open(p)
+ if err != nil {
+ return nil
+ }
+ defer f.Close()
+
+ lines := []string{}
+
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ lines = append(lines, scanner.Text())
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil
+ }
+
+ return &lines
+}
+
func RemoveLine(p, startsWith string) {
f, err := os.Open(p)
if err != nil {
return
}
defer f.Close()
var outText string
found := false
scanner := bufio.NewScanner(f)
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), startsWith) {
found = true
} else {
outText += scanner.Text() + string('\n')
}
}
if err := scanner.Err(); err != nil {
return
}
if found {
WriteData(p, []byte(outText))
}
}
func FindLine(p, startsWith string) string {
f, err := os.Open(p)
if err != nil {
return ""
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), startsWith) {
return scanner.Text()
}
}
if err := scanner.Err(); err != nil {
return ""
}
return ""
}
diff --git a/writeas/cli.go b/writeas/cli.go
index 8970d68..0d645db 100644
--- a/writeas/cli.go
+++ b/writeas/cli.go
@@ -1,317 +1,366 @@
package main
import (
"bufio"
"bytes"
"fmt"
"github.com/codegangsta/cli"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"os"
"strconv"
"strings"
)
const (
apiUrl = "http://i.write.as"
hiddenApiUrl = "http://writeas7pm7rcdqg.onion"
readApiUrl = "http://i.write.as"
VERSION = "0.1.1"
)
func main() {
initialize()
// Run the app
app := cli.NewApp()
app.Name = "writeas"
app.Version = VERSION
app.Usage = "Simple text pasting and publishing"
app.Authors = []cli.Author{
{
Name: "Write.as",
Email: "hello@write.as",
},
}
app.Action = cmdPost
app.Flags = []cli.Flag{
cli.BoolFlag{
Name: "tor, t",
Usage: "Perform action on Tor hidden service",
},
cli.IntFlag{
Name: "tor-port",
Usage: "Use a different port to connect to Tor",
Value: 9150,
},
}
app.Commands = []cli.Command{
{
Name: "post",
Usage: "Alias for default action: create post from stdin",
Action: cmdPost,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "tor, t",
Usage: "Post to Tor hidden service",
},
cli.IntFlag{
Name: "tor-port",
Usage: "Use a different port to connect to Tor",
Value: 9150,
},
},
},
{
Name: "delete",
Usage: "Delete a post",
Action: cmdDelete,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "tor, t",
Usage: "Delete from Tor hidden service",
},
cli.IntFlag{
Name: "tor-port",
Usage: "Use a different port to connect to Tor",
Value: 9150,
},
},
},
{
Name: "get",
Usage: "Read a raw post",
Action: 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,
},
},
},
+ {
+ Name: "add",
+ Usage: "Add a post locally",
+ Action: cmdAdd,
+ },
+ {
+ Name: "list",
+ Usage: "List local posts",
+ Action: cmdList,
+ Flags: []cli.Flag{
+ cli.BoolFlag{
+ Name: "id",
+ Usage: "Show list with post IDs (default)",
+ },
+ cli.BoolFlag{
+ Name: "url",
+ Usage: "Show list with URLs",
+ },
+ },
+ },
}
app.Run(os.Args)
}
func initialize() {
// Ensure we have a data directory to use
if !dataDirExists() {
createDataDir()
}
}
func readStdIn() []byte {
numBytes, numChunks := int64(0), int64(0)
r := bufio.NewReader(os.Stdin)
fullPost := []byte{}
buf := make([]byte, 0, 1024)
for {
n, err := r.Read(buf[:cap(buf)])
buf = buf[:n]
if n == 0 {
if err == nil {
continue
}
if err == io.EOF {
break
}
log.Fatal(err)
}
numChunks++
numBytes += int64(len(buf))
fullPost = append(fullPost, buf...)
if err != nil && err != io.EOF {
log.Fatal(err)
}
}
return fullPost
}
func check(err error) {
if err != nil {
fmt.Printf("%s\n", err)
os.Exit(1)
}
}
func cmdPost(c *cli.Context) {
fullPost := readStdIn()
tor := c.Bool("tor") || c.Bool("t")
if c.Int("tor-port") != 0 {
torPort = c.Int("tor-port")
}
if tor {
fmt.Println("Posting to hidden service...")
} else {
fmt.Println("Posting...")
}
DoPost(fullPost, false, tor)
}
func cmdDelete(c *cli.Context) {
friendlyId := c.Args().Get(0)
token := c.Args().Get(1)
if friendlyId == "" {
fmt.Println("usage: writeas delete <postId> [<token>]")
os.Exit(1)
}
if token == "" {
// Search for the token locally
token = tokenFromID(friendlyId)
if token == "" {
fmt.Println("Couldn't find an edit token locally. Did you create this post here?")
fmt.Printf("If you have an edit token, use: writeas delete %s <token>\n", friendlyId)
os.Exit(1)
}
}
tor := c.Bool("tor") || c.Bool("t")
if c.Int("tor-port") != 0 {
torPort = c.Int("tor-port")
}
DoDelete(friendlyId, token, tor)
}
func cmdGet(c *cli.Context) {
friendlyId := c.Args().Get(0)
if friendlyId == "" {
fmt.Println("usage: writeas get <postId>")
os.Exit(1)
}
tor := c.Bool("tor") || c.Bool("t")
if c.Int("tor-port") != 0 {
torPort = c.Int("tor-port")
}
DoFetch(friendlyId, tor)
}
+func cmdAdd(c *cli.Context) {
+ friendlyId := c.Args().Get(0)
+ token := c.Args().Get(1)
+ if friendlyId == "" || token == "" {
+ fmt.Println("usage: writeas add <postId> <token>")
+ os.Exit(1)
+ }
+
+ addPost(friendlyId, token)
+}
+
+func cmdList(c *cli.Context) {
+ urls := c.Bool("url")
+ ids := c.Bool("id")
+
+ var p Post
+ posts := getPosts()
+ for i := range *posts {
+ p = (*posts)[len(*posts)-1-i]
+ if ids || !urls {
+ fmt.Printf("%s ", p.ID)
+ }
+ if urls {
+ fmt.Printf("https://write.as/%s ", p.ID)
+ }
+ fmt.Print("\n")
+ }
+}
+
func client(read, tor bool, path, query string) (string, *http.Client) {
var u *url.URL
var client *http.Client
if tor {
u, _ = url.ParseRequestURI(hiddenApiUrl)
if len(path) != 12 {
// Handle alpha phase HTML-based URLs
path += ".txt"
}
if read {
u.Path = "/" + path
} else {
u.Path = "/api"
}
client = torClient()
} else {
u, _ = url.ParseRequestURI(apiUrl)
u.Path = "/" + path
client = &http.Client{}
}
if query != "" {
u.RawQuery = query
}
urlStr := fmt.Sprintf("%v", u)
return urlStr, client
}
func DoFetch(friendlyId string, tor bool) {
path := friendlyId
urlStr, client := client(true, tor, path, "")
r, _ := http.NewRequest("GET", urlStr, nil)
r.Header.Add("User-Agent", "writeas-cli v"+VERSION)
resp, err := client.Do(r)
check(err)
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
content, err := ioutil.ReadAll(resp.Body)
check(err)
fmt.Printf("%s\n", string(content))
} else if resp.StatusCode == http.StatusNotFound {
fmt.Printf("Post not found.\n")
} else {
fmt.Printf("Problem getting post: %s\n", resp.Status)
}
}
func DoPost(post []byte, encrypt, tor bool) {
data := url.Values{}
data.Set("w", string(post))
if encrypt {
data.Add("e", "")
}
urlStr, client := client(false, tor, "", "")
r, _ := http.NewRequest("POST", urlStr, bytes.NewBufferString(data.Encode()))
r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
r.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
resp, err := client.Do(r)
check(err)
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
content, err := ioutil.ReadAll(resp.Body)
check(err)
nlPos := strings.Index(string(content), "\n")
url := content[:nlPos]
idPos := strings.LastIndex(string(url), "/") + 1
id := string(url[idPos:])
token := string(content[nlPos+1 : len(content)-1])
addPost(id, token)
fmt.Printf("%s\n", url)
} else {
fmt.Printf("Unable to post: %s\n", resp.Status)
}
}
func DoDelete(friendlyId, token string, tor bool) {
urlStr, client := client(false, tor, "", fmt.Sprintf("id=%s&t=%s", friendlyId, token))
r, _ := http.NewRequest("DELETE", urlStr, nil)
r.Header.Add("Content-Type", "application/x-www-form-urlencoded")
resp, err := client.Do(r)
check(err)
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
if tor {
fmt.Println("Post deleted from hidden service.")
} else {
fmt.Println("Post deleted.")
}
removePost(friendlyId)
} else {
if DEBUG {
fmt.Printf("Problem deleting: %s\n", resp.Status)
} else {
fmt.Printf("Post doesn't exist, or bad edit token given.\n")
}
}
}
diff --git a/writeas/posts.go b/writeas/posts.go
index 668c633..bf0606f 100644
--- a/writeas/posts.go
+++ b/writeas/posts.go
@@ -1,65 +1,88 @@
package main
import (
"fmt"
"github.com/writeas/writeas-cli/utils"
"os"
"path/filepath"
"strings"
)
const (
POSTS_FILE = "posts.psv"
SEPARATOR = `|`
)
+type Post struct {
+ ID string
+ EditToken string
+}
+
func userDataDir() string {
return filepath.Join(parentDataDir(), DATA_DIR_NAME)
}
func dataDirExists() bool {
return fileutils.Exists(userDataDir())
}
func createDataDir() {
err := os.Mkdir(userDataDir(), 0700)
if err != nil && DEBUG {
panic(err)
}
}
func addPost(id, token string) {
f, err := os.OpenFile(filepath.Join(userDataDir(), POSTS_FILE), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
if err != nil {
if DEBUG {
panic(err)
} else {
return
}
}
defer f.Close()
l := fmt.Sprintf("%s%s%s\n", id, SEPARATOR, token)
if _, err = f.WriteString(l); err != nil && DEBUG {
panic(err)
}
}
func tokenFromID(id string) string {
post := fileutils.FindLine(filepath.Join(userDataDir(), POSTS_FILE), id)
if post == "" {
return ""
}
parts := strings.Split(post, SEPARATOR)
if len(parts) < 2 {
return ""
}
return parts[1]
}
func removePost(id string) {
fileutils.RemoveLine(filepath.Join(userDataDir(), POSTS_FILE), id)
}
+
+func getPosts() *[]Post {
+ lines := fileutils.ReadData(filepath.Join(userDataDir(), POSTS_FILE))
+
+ posts := []Post{}
+ parts := make([]string, 2)
+
+ for _, l := range *lines {
+ parts = strings.Split(l, SEPARATOR)
+ if len(parts) < 2 {
+ continue
+ }
+ posts = append(posts, Post{ID: parts[0], EditToken: parts[1]})
+ }
+
+ return &posts
+
+}

File Metadata

Mime Type
text/x-diff
Expires
Fri, May 16, 8:06 PM (8 h, 53 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
3240427

Event Timeline