go-mastodon/cmd/mstdn/main.go

370 lines
7.1 KiB
Go
Raw Normal View History

2017-04-13 18:00:02 +02:00
package main
import (
"bufio"
"bytes"
"context"
2017-04-13 18:00:02 +02:00
"encoding/json"
"fmt"
2017-04-19 16:21:19 +02:00
"io"
2017-04-13 18:00:02 +02:00
"io/ioutil"
2017-04-19 16:21:19 +02:00
"net/url"
2017-04-13 18:00:02 +02:00
"os"
"path/filepath"
"runtime"
"strings"
2017-04-19 16:21:19 +02:00
"github.com/fatih/color"
2017-04-13 18:00:02 +02:00
"github.com/mattn/go-mastodon"
"github.com/mattn/go-tty"
2017-04-15 14:48:20 +02:00
"github.com/urfave/cli"
2017-04-13 18:00:02 +02:00
"golang.org/x/net/html"
)
func readFile(filename string) ([]byte, error) {
if filename == "-" {
return ioutil.ReadAll(os.Stdin)
}
2017-04-14 05:23:45 +02:00
return ioutil.ReadFile(filename)
}
2017-04-14 02:32:46 +02:00
func textContent(s string) string {
doc, err := html.Parse(strings.NewReader(s))
if err != nil {
2017-04-15 14:48:20 +02:00
return s
2017-04-14 02:32:46 +02:00
}
var buf bytes.Buffer
2017-04-14 05:30:13 +02:00
var extractText func(node *html.Node, w *bytes.Buffer)
extractText = func(node *html.Node, w *bytes.Buffer) {
if node.Type == html.TextNode {
data := strings.Trim(node.Data, "\r\n")
if data != "" {
2017-04-14 06:53:56 +02:00
w.WriteString(data)
2017-04-14 05:30:13 +02:00
}
}
for c := node.FirstChild; c != nil; c = c.NextSibling {
extractText(c, w)
}
2017-04-14 06:53:56 +02:00
if node.Type == html.ElementNode {
name := strings.ToLower(node.Data)
if name == "br" {
w.WriteString("\n")
}
}
2017-04-14 05:30:13 +02:00
}
2017-04-14 02:32:46 +02:00
extractText(doc, &buf)
return buf.String()
}
2017-04-14 13:05:18 +02:00
var (
2017-04-16 16:38:53 +02:00
readUsername = func() (string, error) {
2017-04-14 13:05:18 +02:00
b, _, err := bufio.NewReader(os.Stdin).ReadLine()
if err != nil {
return "", err
}
return string(b), nil
}
readPassword func() (string, error)
)
2017-04-13 18:00:02 +02:00
func prompt() (string, string, error) {
fmt.Print("E-Mail: ")
2017-04-14 13:05:18 +02:00
email, err := readUsername()
2017-04-13 18:00:02 +02:00
if err != nil {
return "", "", err
}
fmt.Print("Password: ")
2017-04-14 13:05:18 +02:00
var password string
if readPassword == nil {
2017-04-20 14:29:10 +02:00
var t *tty.TTY
t, err = tty.Open()
2017-04-17 15:39:57 +02:00
if err != nil {
return "", "", err
}
defer t.Close()
2017-04-14 13:05:18 +02:00
password, err = t.ReadPassword()
} else {
password, err = readPassword()
}
2017-04-13 18:00:02 +02:00
if err != nil {
return "", "", err
}
return email, password, nil
}
2017-04-26 17:56:13 +02:00
func configFile(c *cli.Context) (string, error) {
2017-04-13 18:00:02 +02:00
dir := os.Getenv("HOME")
2017-04-13 19:17:05 +02:00
if runtime.GOOS == "windows" {
2017-04-13 18:00:02 +02:00
dir = os.Getenv("APPDATA")
if dir == "" {
dir = filepath.Join(os.Getenv("USERPROFILE"), "Application Data", "mstdn")
}
dir = filepath.Join(dir, "mstdn")
} else {
dir = filepath.Join(dir, ".config", "mstdn")
}
if err := os.MkdirAll(dir, 0700); err != nil {
2017-04-26 17:56:13 +02:00
return "", err
2017-04-13 18:00:02 +02:00
}
2017-04-19 09:52:26 +02:00
var file string
profile := c.String("profile")
if profile != "" {
2017-04-24 10:49:56 +02:00
file = filepath.Join(dir, "settings-"+profile+".json")
2017-04-19 09:52:26 +02:00
} else {
file = filepath.Join(dir, "settings.json")
}
2017-04-26 17:56:13 +02:00
return file, nil
}
func getConfig(c *cli.Context) (string, *mastodon.Config, error) {
file, err := configFile(c)
if err != nil {
return "", nil, err
}
2017-04-13 18:00:02 +02:00
b, err := ioutil.ReadFile(file)
if err != nil && !os.IsNotExist(err) {
return "", nil, err
}
config := &mastodon.Config{
Server: "https://mstdn.jp",
2017-04-25 08:32:56 +02:00
ClientID: "1e463436008428a60ed14ff1f7bc0b4d923e14fc4a6827fa99560b0c0222612f",
ClientSecret: "72b63de5bc11111a5aa1a7b690672d78ad6a207ce32e16ea26115048ec5d234d",
2017-04-13 18:00:02 +02:00
}
if err == nil {
err = json.Unmarshal(b, &config)
if err != nil {
return "", nil, fmt.Errorf("could not unmarshal %v: %v", file, err)
}
}
return file, config, nil
}
2017-04-15 14:48:20 +02:00
func authenticate(client *mastodon.Client, config *mastodon.Config, file string) error {
2017-04-14 05:30:13 +02:00
email, password, err := prompt()
if err != nil {
2017-04-15 14:48:20 +02:00
return err
2017-04-14 05:30:13 +02:00
}
err = client.Authenticate(context.Background(), email, password)
2017-04-14 05:30:13 +02:00
if err != nil {
2017-04-15 14:48:20 +02:00
return err
2017-04-14 05:30:13 +02:00
}
b, err := json.MarshalIndent(config, "", " ")
if err != nil {
2017-04-16 16:38:53 +02:00
return fmt.Errorf("failed to store file: %v", err)
2017-04-14 05:30:13 +02:00
}
err = ioutil.WriteFile(file, b, 0700)
if err != nil {
2017-04-16 16:38:53 +02:00
return fmt.Errorf("failed to store file: %v", err)
2017-04-15 14:48:20 +02:00
}
return nil
}
2017-04-15 16:21:45 +02:00
func argstr(c *cli.Context) string {
a := []string{}
for i := 0; i < c.NArg(); i++ {
a = append(a, c.Args().Get(i))
}
return strings.Join(a, " ")
}
2017-04-15 14:48:20 +02:00
func fatalIf(err error) {
if err == nil {
return
}
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err)
os.Exit(1)
}
2017-04-16 15:26:05 +02:00
func makeApp() *cli.App {
2017-04-15 14:48:20 +02:00
app := cli.NewApp()
app.Name = "mstdn"
app.Usage = "mastodon client"
app.Version = "0.0.1"
2017-04-19 09:52:26 +02:00
app.Flags = []cli.Flag{
cli.StringFlag{
2017-04-19 10:05:00 +02:00
Name: "profile",
2017-04-19 09:52:26 +02:00
Usage: "profile name",
Value: "",
},
}
2017-04-15 14:48:20 +02:00
app.Commands = []cli.Command{
{
Name: "toot",
Usage: "post toot",
Flags: []cli.Flag{
cli.StringFlag{
Name: "ff",
Usage: "post utf-8 string from a file(\"-\" means STDIN)",
Value: "",
},
2017-04-19 08:17:26 +02:00
cli.IntFlag{
Name: "i",
Usage: "in-reply-to",
Value: 0,
},
2017-04-15 14:48:20 +02:00
},
Action: cmdToot,
},
{
2017-04-17 16:29:44 +02:00
Name: "stream",
Usage: "stream statuses",
Flags: []cli.Flag{
cli.StringFlag{
Name: "type",
Usage: "stream type (public,public/local,user:NAME,hashtag:TAG)",
},
2017-04-17 16:29:44 +02:00
cli.BoolFlag{
Name: "json",
Usage: "output JSON",
},
2017-04-17 16:43:11 +02:00
cli.BoolFlag{
Name: "simplejson",
Usage: "output simple JSON",
},
cli.StringFlag{
Name: "template",
Usage: "output with tamplate format",
},
2017-04-17 16:29:44 +02:00
},
2017-04-15 14:48:20 +02:00
Action: cmdStream,
},
{
Name: "timeline",
Usage: "show timeline",
Action: cmdTimeline,
},
2017-04-15 15:12:07 +02:00
{
Name: "notification",
Usage: "show notification",
Action: cmdNotification,
},
2017-04-15 15:25:20 +02:00
{
Name: "instance",
Usage: "show instance information",
Action: cmdInstance,
},
2017-04-15 15:58:46 +02:00
{
Name: "account",
Usage: "show account information",
Action: cmdAccount,
},
2017-04-15 16:21:45 +02:00
{
Name: "search",
Usage: "search content",
Action: cmdSearch,
},
2017-04-19 07:17:18 +02:00
{
Name: "follow",
Usage: "follow account",
Action: cmdFollow,
},
2017-04-19 11:25:01 +02:00
{
Name: "followers",
Usage: "show followers",
Action: cmdFollowers,
},
2017-04-17 06:56:06 +02:00
{
Name: "upload",
Usage: "upload file",
Action: cmdUpload,
},
2017-04-19 19:04:20 +02:00
{
Name: "delete",
Usage: "delete status",
Action: cmdDelete,
},
2017-04-26 17:56:13 +02:00
{
Name: "init",
Usage: "initialize profile",
Action: func(c *cli.Context) error { return nil },
},
2017-04-15 14:48:20 +02:00
}
2017-04-19 09:52:26 +02:00
app.Setup()
2017-04-16 15:26:05 +02:00
return app
}
2017-04-19 16:21:19 +02:00
type screen struct {
host string
}
func newScreen(config *mastodon.Config) *screen {
var host string
u, err := url.Parse(config.Server)
if err == nil {
host = u.Host
}
return &screen{host}
}
2017-04-19 16:32:23 +02:00
func (s *screen) acct(a string) string {
if !strings.Contains(a, "@") {
a += "@" + s.host
}
return a
}
2017-04-19 16:21:19 +02:00
func (s *screen) displayError(w io.Writer, e error) {
color.Set(color.FgYellow)
fmt.Fprintln(w, e.Error())
color.Set(color.Reset)
}
func (s *screen) displayStatus(w io.Writer, t *mastodon.Status) {
2017-04-19 16:58:58 +02:00
if t == nil {
return
}
2017-04-19 16:21:19 +02:00
if t.Reblog != nil {
color.Set(color.FgHiRed)
2017-04-19 16:32:23 +02:00
fmt.Fprint(w, s.acct(t.Account.Acct))
2017-04-19 16:21:19 +02:00
color.Set(color.Reset)
fmt.Fprint(w, " reblogged ")
color.Set(color.FgHiBlue)
2017-04-19 16:32:23 +02:00
fmt.Fprintln(w, s.acct(t.Reblog.Account.Acct))
2017-04-19 16:21:19 +02:00
fmt.Fprintln(w, textContent(t.Reblog.Content))
color.Set(color.Reset)
} else {
color.Set(color.FgHiRed)
2017-04-19 16:32:23 +02:00
fmt.Fprintln(w, s.acct(t.Account.Acct))
2017-04-19 16:21:19 +02:00
color.Set(color.Reset)
fmt.Fprintln(w, textContent(t.Content))
}
}
2017-04-16 15:26:05 +02:00
func run() int {
app := makeApp()
2017-04-19 09:52:26 +02:00
app.Before = func(c *cli.Context) error {
2017-04-26 17:56:13 +02:00
if c.Args().Get(0) == "init" {
file, err := configFile(c)
if err != nil {
return err
}
os.Remove(file)
}
2017-04-19 09:52:26 +02:00
file, config, err := getConfig(c)
if err != nil {
return err
}
2017-04-16 15:26:05 +02:00
2017-04-19 09:52:26 +02:00
client := mastodon.NewClient(config)
app.Metadata = map[string]interface{}{
"client": client,
"config": config,
}
2017-04-23 12:17:18 +02:00
if config.AccessToken == "" {
return authenticate(client, config, file)
}
2017-04-19 09:52:26 +02:00
return nil
2017-04-16 15:26:05 +02:00
}
2017-04-26 05:38:11 +02:00
fatalIf(app.Run(os.Args))
2017-04-15 14:48:20 +02:00
return 0
}
2017-04-14 05:33:12 +02:00
func main() {
2017-04-15 14:48:20 +02:00
os.Exit(run())
2017-04-13 18:00:02 +02:00
}