227 lines
4.5 KiB
Go
227 lines
4.5 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()
|
|
}
|
|
|
|
func prompt() (string, string, error) {
|
|
t, err := tty.Open()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
defer t.Close()
|
|
|
|
fmt.Print("E-Mail: ")
|
|
b, _, err := bufio.NewReader(os.Stdin).ReadLine()
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
email := string(b)
|
|
|
|
fmt.Print("Password: ")
|
|
password, err := t.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: "7d1873f3940af3e9128c81d5a2ddb3f235ccfa1cd11761efd3b8426f40898fe8",
|
|
ClientSecret: "3c8ea997c580f196453e97c1c58f6f5c131f668456bbe1ed37aaccac719397db",
|
|
}
|
|
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)
|
|
}
|