go-mastodon/cmd/mstdn/main.go

242 lines
4.8 KiB
Go

package main
import (
"bufio"
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"github.com/fatih/color"
"github.com/mattn/go-mastodon"
"github.com/mattn/go-tty"
"golang.org/x/net/html"
)
var (
toot = flag.String("t", "", "toot text")
stream = flag.Bool("S", false, "streaming public")
fromfile = flag.String("ff", "", "post utf-8 string from a file(\"-\" means STDIN)")
)
func readFile(filename string) ([]byte, error) {
if filename == "-" {
return ioutil.ReadAll(os.Stdin)
}
return ioutil.ReadFile(filename)
}
func textContent(s string) string {
doc, err := html.Parse(strings.NewReader(s))
if err != nil {
log.Fatal(err)
}
var buf bytes.Buffer
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 != "" {
w.WriteString(data)
}
}
for c := node.FirstChild; c != nil; c = c.NextSibling {
extractText(c, w)
}
if node.Type == html.ElementNode {
name := strings.ToLower(node.Data)
if name == "br" {
w.WriteString("\n")
}
}
}
extractText(doc, &buf)
return buf.String()
}
var (
readUsername func() (string, error) = func() (string, error) {
b, _, err := bufio.NewReader(os.Stdin).ReadLine()
if err != nil {
return "", err
}
return string(b), nil
}
readPassword func() (string, error)
)
func prompt() (string, string, error) {
t, err := tty.Open()
if err != nil {
return "", "", err
}
defer t.Close()
fmt.Print("E-Mail: ")
email, err := readUsername()
if err != nil {
return "", "", err
}
fmt.Print("Password: ")
var password string
if readPassword == nil {
password, err = t.ReadPassword()
} else {
password, err = readPassword()
}
if err != nil {
return "", "", err
}
return email, password, nil
}
func getConfig() (string, *mastodon.Config, error) {
dir := os.Getenv("HOME")
if runtime.GOOS == "windows" {
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 {
return "", nil, err
}
file := filepath.Join(dir, "settings.json")
b, err := ioutil.ReadFile(file)
if err != nil && !os.IsNotExist(err) {
return "", nil, err
}
config := &mastodon.Config{
Server: "https://mstdn.jp",
ClientID: "171d45f22068a5dddbd927b9d966f5b97971ed1d3256b03d489f5b3a83cdba59",
ClientSecret: "574a2cf4b3f28a5fa0cfd285fc80cfe9daa419945163ef18f5f3d0022f4add28",
}
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
}
func authenticate(client *mastodon.Client, config *mastodon.Config, file string) {
email, password, err := prompt()
if err != nil {
log.Fatal(err)
}
err = client.Authenticate(email, password)
if err != nil {
log.Fatal(err)
}
b, err := json.MarshalIndent(config, "", " ")
if err != nil {
log.Fatal("failed to store file:", err)
}
err = ioutil.WriteFile(file, b, 0700)
if err != nil {
log.Fatal("failed to store file:", err)
}
}
func streaming(client *mastodon.Client) {
ctx, cancel := context.WithCancel(context.Background())
sc := make(chan os.Signal, 1)
signal.Notify(sc, os.Interrupt)
q, err := client.StreamingPublic(ctx)
if err != nil {
log.Fatal(err)
}
go func() {
<-sc
cancel()
close(q)
}()
for e := range q {
switch t := e.(type) {
case *mastodon.UpdateEvent:
color.Set(color.FgHiRed)
fmt.Println(t.Status.Account.Username)
color.Set(color.Reset)
fmt.Println(textContent(t.Status.Content))
case *mastodon.ErrorEvent:
color.Set(color.FgYellow)
fmt.Println(t.Error())
color.Set(color.Reset)
}
}
}
func init() {
flag.Parse()
if *fromfile != "" {
text, err := readFile(*fromfile)
if err != nil {
log.Fatal(err)
}
*toot = string(text)
}
}
func post(client *mastodon.Client, text string) {
_, err := client.PostStatus(&mastodon.Toot{
Status: text,
})
if err != nil {
log.Fatal(err)
}
}
func timeline(client *mastodon.Client) {
timeline, err := client.GetTimelineHome()
if err != nil {
log.Fatal(err)
}
for i := len(timeline) - 1; i >= 0; i-- {
t := timeline[i]
color.Set(color.FgHiRed)
fmt.Println(t.Account.Username)
color.Set(color.Reset)
fmt.Println(textContent(t.Content))
}
}
func main() {
file, config, err := getConfig()
if err != nil {
log.Fatal(err)
}
client := mastodon.NewClient(config)
if config.AccessToken == "" {
authenticate(client, config, file)
return
}
if *toot != "" {
post(client, *toot)
return
}
if *stream {
streaming(client)
return
}
timeline(client)
}