remove dependencies and client

This commit is contained in:
gutmet 2022-11-19 20:02:47 +01:00
parent 5f0c9a21c2
commit 2542bf46ba
41 changed files with 23 additions and 2405 deletions

View File

@ -1,5 +1,6 @@
MIT License MIT License
Copyright (c) 2022 Alexander Weinhold
Copyright (c) 2017 Yasuhiro Matsumoto Copyright (c) 2017 Yasuhiro Matsumoto
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy

View File

@ -1,10 +1,6 @@
# go-mastodon # go-mastodon
[![Build Status](https://github.com/mattn/go-mastodon/workflows/test/badge.svg?branch=master)](https://github.com/mattn/go-mastodon/actions?query=workflow%3Atest) Fork of https://github.com/mattn/go-mastodon
[![Codecov](https://codecov.io/gh/mattn/go-mastodon/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-mastodon)
[![Go Reference](https://pkg.go.dev/badge/github.com/mattn/go-mastodon.svg)](https://pkg.go.dev/github.com/mattn/go-mastodon)
[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-mastodon)](https://goreportcard.com/report/github.com/mattn/go-mastodon)
## Usage ## Usage

View File

@ -1,52 +0,0 @@
# mstdn
command line tool for mstdn.jp
## Usage
```
NAME:
mstdn - mastodon client
USAGE:
mstdn [global options] command [command options] [arguments...]
VERSION:
0.0.1
COMMANDS:
toot post toot
stream stream statuses
timeline show timeline
notification show notification
instance show instance information
account show account information
search search content
follow follow account
followers show followers
upload upload file
delete delete status
init initialize profile
mikami search mikami
xsearch cross search
help, h Shows a list of commands or help for one command
GLOBAL OPTIONS:
--profile value profile name
--help, -h show help
--version, -v print the version
```
## Installation
```
$ go get github.com/mattn/go-mastodon/cmd/mstdn
```
## License
MIT
## Author
Yasuhiro Matsumoto (a.k.a. mattn)

View File

@ -1,30 +0,0 @@
package main
import (
"context"
"fmt"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli"
)
func cmdAccount(c *cli.Context) error {
client := c.App.Metadata["client"].(*mastodon.Client)
account, err := client.GetAccountCurrentUser(context.Background())
if err != nil {
return err
}
fmt.Fprintf(c.App.Writer, "URI : %v\n", account.Acct)
fmt.Fprintf(c.App.Writer, "ID : %v\n", account.ID)
fmt.Fprintf(c.App.Writer, "Username : %v\n", account.Username)
fmt.Fprintf(c.App.Writer, "Acct : %v\n", account.Acct)
fmt.Fprintf(c.App.Writer, "DisplayName : %v\n", account.DisplayName)
fmt.Fprintf(c.App.Writer, "Locked : %v\n", account.Locked)
fmt.Fprintf(c.App.Writer, "CreatedAt : %v\n", account.CreatedAt.Local())
fmt.Fprintf(c.App.Writer, "FollowersCount: %v\n", account.FollowersCount)
fmt.Fprintf(c.App.Writer, "FollowingCount: %v\n", account.FollowingCount)
fmt.Fprintf(c.App.Writer, "StatusesCount : %v\n", account.StatusesCount)
fmt.Fprintf(c.App.Writer, "Note : %v\n", textContent(account.Note))
fmt.Fprintf(c.App.Writer, "URL : %v\n", account.URL)
return nil
}

View File

@ -1,30 +0,0 @@
package main
import (
"fmt"
"net/http"
"strings"
"testing"
"github.com/urfave/cli"
)
func TestCmdAccount(t *testing.T) {
out := testWithServer(
func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/accounts/verify_credentials":
fmt.Fprintln(w, `{"username": "zzz"}`)
return
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
},
func(app *cli.App) {
app.Run([]string{"mstdn", "account"})
},
)
if !strings.Contains(out, "zzz") {
t.Fatalf("%q should be contained in output of command: %v", "zzz", out)
}
}

View File

@ -1,23 +0,0 @@
package main
import (
"context"
"errors"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli"
)
func cmdDelete(c *cli.Context) error {
client := c.App.Metadata["client"].(*mastodon.Client)
if !c.Args().Present() {
return errors.New("arguments required")
}
for i := 0; i < c.NArg(); i++ {
err := client.DeleteStatus(context.Background(), mastodon.ID(c.Args().Get(i)))
if err != nil {
return err
}
}
return nil
}

View File

@ -1,41 +0,0 @@
package main
import (
"fmt"
"net/http"
"testing"
"github.com/urfave/cli"
)
func TestCmdDelete(t *testing.T) {
ok := false
f := func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/statuses/123":
fmt.Fprintln(w, `{}`)
ok = true
return
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
testWithServer(
f, func(app *cli.App) {
app.Run([]string{"mstdn", "delete", "122"})
},
)
if ok {
t.Fatal("something wrong to sequence to follow account")
}
ok = false
testWithServer(
f, func(app *cli.App) {
app.Run([]string{"mstdn", "delete", "123"})
},
)
if !ok {
t.Fatal("something wrong to sequence to follow account")
}
}

View File

@ -1,30 +0,0 @@
package main
import (
"context"
"errors"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli"
)
func cmdFollow(c *cli.Context) error {
client := c.App.Metadata["client"].(*mastodon.Client)
if !c.Args().Present() {
return errors.New("arguments required")
}
for i := 0; i < c.NArg(); i++ {
account, err := client.AccountsSearch(context.Background(), c.Args().Get(i), 1)
if err != nil {
return err
}
if len(account) == 0 {
continue
}
_, err = client.AccountFollow(context.Background(), account[0].ID)
if err != nil {
return err
}
}
return nil
}

View File

@ -1,70 +0,0 @@
package main
import (
"fmt"
"net/http"
"testing"
"github.com/urfave/cli"
)
func TestCmdFollow(t *testing.T) {
ok := false
testWithServer(
func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/accounts/search":
q := r.URL.Query().Get("q")
if q == "mattn" {
fmt.Fprintln(w, `[{"id": 123}]`)
return
} else if q == "different_id" {
fmt.Fprintln(w, `[{"id": 1234567}]`)
return
} else if q == "empty" {
fmt.Fprintln(w, `[]`)
return
}
case "/api/v1/accounts/123/follow":
fmt.Fprintln(w, `{"id": 123}`)
ok = true
return
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
},
func(app *cli.App) {
err := app.Run([]string{"mstdn", "follow", "mattn"})
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
},
func(app *cli.App) {
err := app.Run([]string{"mstdn", "follow"})
if err == nil {
t.Fatalf("should be fail: %v", err)
}
},
func(app *cli.App) {
err := app.Run([]string{"mstdn", "follow", "fail"})
if err == nil {
t.Fatalf("should be fail: %v", err)
}
},
func(app *cli.App) {
err := app.Run([]string{"mstdn", "follow", "empty"})
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
},
func(app *cli.App) {
err := app.Run([]string{"mstdn", "follow", "different_id"})
if err == nil {
t.Fatalf("should be fail: %v", err)
}
},
)
if !ok {
t.Fatal("something wrong to sequence to follow account")
}
}

View File

@ -1,40 +0,0 @@
package main
import (
"context"
"fmt"
"time"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli"
)
func cmdFollowers(c *cli.Context) error {
client := c.App.Metadata["client"].(*mastodon.Client)
config := c.App.Metadata["config"].(*mastodon.Config)
account, err := client.GetAccountCurrentUser(context.Background())
if err != nil {
return err
}
var followers []*mastodon.Account
var pg mastodon.Pagination
for {
fs, err := client.GetAccountFollowers(context.Background(), account.ID, &pg)
if err != nil {
return err
}
followers = append(followers, fs...)
if pg.MaxID == "" {
break
}
pg.SinceID = ""
pg.MinID = ""
time.Sleep(10 * time.Second)
}
s := newScreen(config)
for _, follower := range followers {
fmt.Fprintf(c.App.Writer, "%v,%v\n", follower.ID, s.acct(follower.Acct))
}
return nil
}

View File

@ -1,34 +0,0 @@
package main
import (
"fmt"
"net/http"
"strings"
"testing"
"github.com/urfave/cli"
)
func TestCmdFollowers(t *testing.T) {
out := testWithServer(
func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/accounts/verify_credentials":
fmt.Fprintln(w, `{"id": 123}`)
return
case "/api/v1/accounts/123/followers":
w.Header().Set("Link", `<http://example.com?since_id=890>; rel="prev"`)
fmt.Fprintln(w, `[{"id": 234, "username": "ZZZ", "acct": "zzz"}]`)
return
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
},
func(app *cli.App) {
app.Run([]string{"mstdn", "followers"})
},
)
if !strings.Contains(out, "zzz") {
t.Fatalf("%q should be contained in output of command: %v", "zzz", out)
}
}

View File

@ -1,44 +0,0 @@
package main
import (
"context"
"fmt"
"sort"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli"
)
func cmdInstance(c *cli.Context) error {
client := c.App.Metadata["client"].(*mastodon.Client)
instance, err := client.GetInstance(context.Background())
if err != nil {
return err
}
fmt.Fprintf(c.App.Writer, "URI : %s\n", instance.URI)
fmt.Fprintf(c.App.Writer, "Title : %s\n", instance.Title)
fmt.Fprintf(c.App.Writer, "Description: %s\n", instance.Description)
fmt.Fprintf(c.App.Writer, "EMail : %s\n", instance.EMail)
if instance.Version != "" {
fmt.Fprintf(c.App.Writer, "Version : %s\n", instance.Version)
}
if instance.Thumbnail != "" {
fmt.Fprintf(c.App.Writer, "Thumbnail : %s\n", instance.Thumbnail)
}
if instance.URLs != nil {
var keys []string
for _, k := range instance.URLs {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Fprintf(c.App.Writer, "%s: %s\n", k, instance.URLs[k])
}
}
if instance.Stats != nil {
fmt.Fprintf(c.App.Writer, "User Count : %v\n", instance.Stats.UserCount)
fmt.Fprintf(c.App.Writer, "Status Count : %v\n", instance.Stats.StatusCount)
fmt.Fprintf(c.App.Writer, "Domain Count : %v\n", instance.Stats.DomainCount)
}
return nil
}

View File

@ -1,24 +0,0 @@
package main
import (
"context"
"fmt"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli"
)
func cmdInstanceActivity(c *cli.Context) error {
client := c.App.Metadata["client"].(*mastodon.Client)
activities, err := client.GetInstanceActivity(context.Background())
if err != nil {
return err
}
for _, activity := range activities {
fmt.Fprintf(c.App.Writer, "Logins : %v\n", activity.Logins)
fmt.Fprintf(c.App.Writer, "Registrations : %v\n", activity.Registrations)
fmt.Fprintf(c.App.Writer, "Statuses : %v\n", activity.Statuses)
fmt.Fprintf(c.App.Writer, "Week : %v\n", activity.Week)
}
return nil
}

View File

@ -1,21 +0,0 @@
package main
import (
"context"
"fmt"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli"
)
func cmdInstancePeers(c *cli.Context) error {
client := c.App.Metadata["client"].(*mastodon.Client)
peers, err := client.GetInstancePeers(context.Background())
if err != nil {
return err
}
for _, peer := range peers {
fmt.Fprintln(c.App.Writer, peer)
}
return nil
}

View File

@ -1,30 +0,0 @@
package main
import (
"fmt"
"net/http"
"strings"
"testing"
"github.com/urfave/cli"
)
func TestCmdInstance(t *testing.T) {
out := testWithServer(
func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/instance":
fmt.Fprintln(w, `{"title": "zzz"}`)
return
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
},
func(app *cli.App) {
app.Run([]string{"mstdn", "instance"})
},
)
if !strings.Contains(out, "zzz") {
t.Fatalf("%q should be contained in output of command: %v", "zzz", out)
}
}

View File

@ -1,9 +0,0 @@
package main
import (
"github.com/urfave/cli"
)
func cmdMikami(c *cli.Context) error {
return xSearch(c.App.Metadata["xsearch_url"].(string), "三上", c.App.Writer)
}

View File

@ -1,41 +0,0 @@
package main
import (
"bytes"
"fmt"
"net/http"
"strings"
"testing"
"github.com/urfave/cli"
)
func TestCmdMikami(t *testing.T) {
ok := false
buf := bytes.NewBuffer(nil)
testWithServer(
func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("q") == "三上" {
ok = true
fmt.Fprintln(w, `<div class="post"><div class="mst_content"><a href="http://example.com/@test/1"><p>三上</p></a></div></div>`)
}
},
func(app *cli.App) {
app.Writer = buf
err := app.Run([]string{"mstdn", "mikami"})
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
},
)
if !ok {
t.Fatal("should be search Mikami")
}
result := buf.String()
if !strings.Contains(result, "http://example.com/@test/1") {
t.Fatalf("%q should be contained in output of search: %s", "http://example.com/@test/1", result)
}
if !strings.Contains(result, "三上") {
t.Fatalf("%q should be contained in output of search: %s", "三上", result)
}
}

View File

@ -1,29 +0,0 @@
package main
import (
"context"
"fmt"
"github.com/fatih/color"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli"
)
func cmdNotification(c *cli.Context) error {
client := c.App.Metadata["client"].(*mastodon.Client)
notifications, err := client.GetNotifications(context.Background(), nil)
if err != nil {
return err
}
for _, n := range notifications {
if n.Status != nil {
color.Set(color.FgHiRed)
fmt.Fprint(c.App.Writer, n.Account.Acct)
color.Set(color.Reset)
fmt.Fprintln(c.App.Writer, " "+n.Type)
s := n.Status
fmt.Fprintln(c.App.Writer, textContent(s.Content))
}
}
return nil
}

View File

@ -1,30 +0,0 @@
package main
import (
"fmt"
"net/http"
"strings"
"testing"
"github.com/urfave/cli"
)
func TestCmdNotification(t *testing.T) {
out := testWithServer(
func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/notifications":
fmt.Fprintln(w, `[{"type": "rebloged", "status": {"content": "foo"}}]`)
return
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
},
func(app *cli.App) {
app.Run([]string{"mstdn", "notification"})
},
)
if !strings.Contains(out, "rebloged") {
t.Fatalf("%q should be contained in output of command: %v", "rebloged", out)
}
}

View File

@ -1,47 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli"
)
func cmdSearch(c *cli.Context) error {
if !c.Args().Present() {
return errors.New("arguments required")
}
client := c.App.Metadata["client"].(*mastodon.Client)
config := c.App.Metadata["config"].(*mastodon.Config)
results, err := client.Search(context.Background(), argstr(c), false)
if err != nil {
return err
}
s := newScreen(config)
if len(results.Accounts) > 0 {
fmt.Fprintln(c.App.Writer, "===ACCOUNT===")
for _, result := range results.Accounts {
fmt.Fprintf(c.App.Writer, "%v,%v\n", result.ID, s.acct(result.Acct))
}
fmt.Fprintln(c.App.Writer)
}
if len(results.Statuses) > 0 {
fmt.Fprintln(c.App.Writer, "===STATUS===")
for _, result := range results.Statuses {
s.displayStatus(c.App.Writer, result)
}
fmt.Fprintln(c.App.Writer)
}
if len(results.Hashtags) > 0 {
fmt.Fprintln(c.App.Writer, "===HASHTAG===")
for _, result := range results.Hashtags {
fmt.Fprintf(c.App.Writer, "#%v\n", result)
}
fmt.Fprintln(c.App.Writer)
}
return nil
}

View File

@ -1,32 +0,0 @@
package main
import (
"fmt"
"net/http"
"strings"
"testing"
"github.com/urfave/cli"
)
func TestCmdSearch(t *testing.T) {
out := testWithServer(
func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v2/search":
fmt.Fprintln(w, `{"accounts": [{"id": 234, "acct": "zzz"}], "statuses":[{"id": 345, "content": "yyy"}], "hashtags": [{"name": "www"}, {"name": "わろす"}]}`)
return
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
},
func(app *cli.App) {
app.Run([]string{"mstdn", "search", "zzz"})
},
)
for _, s := range []string{"zzz", "yyy", "www", "わろす"} {
if !strings.Contains(out, s) {
t.Fatalf("%q should be contained in output of command: %v", s, out)
}
}
}

View File

@ -1,113 +0,0 @@
package main
import (
"context"
"encoding/json"
"errors"
"os"
"os/signal"
"strings"
"text/template"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli"
)
// SimpleJSON is a struct for output JSON for data to be simple used
type SimpleJSON struct {
ID mastodon.ID `json:"id"`
Username string `json:"username"`
Acct string `json:"acct"`
Avatar string `json:"avatar"`
Content string `json:"content"`
}
func checkFlag(f ...bool) bool {
n := 0
for _, on := range f {
if on {
n++
}
}
return n > 1
}
func cmdStream(c *cli.Context) error {
asJSON := c.Bool("json")
asSimpleJSON := c.Bool("simplejson")
asFormat := c.String("template")
if checkFlag(asJSON, asSimpleJSON, asFormat != "") {
return errors.New("cannot speicify two or three options in --json/--simplejson/--template")
}
tx, err := template.New("mstdn").Funcs(template.FuncMap{
"nl": func(s string) string {
return s + "\n"
},
"text": textContent,
}).Parse(asFormat)
if err != nil {
return err
}
client := c.App.Metadata["client"].(*mastodon.Client)
config := c.App.Metadata["config"].(*mastodon.Config)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sc := make(chan os.Signal, 1)
signal.Notify(sc, os.Interrupt)
var q chan mastodon.Event
t := c.String("type")
if t == "public" {
q, err = client.StreamingPublic(ctx, false)
} else if t == "" || t == "public/local" {
q, err = client.StreamingPublic(ctx, true)
} else if strings.HasPrefix(t, "user:") {
q, err = client.StreamingUser(ctx)
} else if strings.HasPrefix(t, "hashtag:") {
q, err = client.StreamingHashtag(ctx, t[8:], false)
} else {
return errors.New("invalid type")
}
if err != nil {
return err
}
go func() {
<-sc
cancel()
}()
c.App.Metadata["signal"] = sc
s := newScreen(config)
for e := range q {
if asJSON {
json.NewEncoder(c.App.Writer).Encode(e)
} else if asSimpleJSON {
if t, ok := e.(*mastodon.UpdateEvent); ok {
json.NewEncoder(c.App.Writer).Encode(&SimpleJSON{
ID: t.Status.ID,
Username: t.Status.Account.Username,
Acct: t.Status.Account.Acct,
Avatar: t.Status.Account.AvatarStatic,
Content: textContent(t.Status.Content),
})
}
} else if asFormat != "" {
tx.ExecuteTemplate(c.App.Writer, "mstdn", e)
} else {
switch t := e.(type) {
case *mastodon.UpdateEvent:
s.displayStatus(c.App.Writer, t.Status)
case *mastodon.NotificationEvent:
// TODO s.displayStatus(c.App.Writer, t.Notification.Status)
case *mastodon.ErrorEvent:
s.displayError(c.App.Writer, t)
}
}
}
return nil
}

View File

@ -1,84 +0,0 @@
package main
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/mattn/go-mastodon"
)
func TestCmdStream(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/streaming/public/local" {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
f, _ := w.(http.Flusher)
fmt.Fprintln(w, `
event: update
data: {"content": "foo", "account":{"acct":"FOO"}}
`)
f.Flush()
fmt.Fprintln(w, `
event: update
data: {"content": "bar", "account":{"acct":"BAR"}}
`)
f.Flush()
return
}))
defer ts.Close()
config := &mastodon.Config{
Server: ts.URL,
ClientID: "foo",
ClientSecret: "bar",
AccessToken: "zoo",
}
client := mastodon.NewClient(config)
var buf bytes.Buffer
app := makeApp()
app.Writer = &buf
app.Metadata = map[string]interface{}{
"client": client,
"config": config,
}
stop := func() {
time.Sleep(5 * time.Second)
if sig, ok := app.Metadata["signal"]; ok {
sig.(chan os.Signal) <- os.Interrupt
return
}
panic("timeout")
}
var out string
go stop()
app.Run([]string{"mstdn", "stream"})
out = buf.String()
if !strings.Contains(out, "FOO@") {
t.Fatalf("%q should be contained in output of command: %v", "FOO@", out)
}
if !strings.Contains(out, "foo") {
t.Fatalf("%q should be contained in output of command: %v", "foo", out)
}
go stop()
app.Run([]string{"mstdn", "stream", "--simplejson"})
out = buf.String()
if !strings.Contains(out, "FOO@") {
t.Fatalf("%q should be contained in output of command: %v", "FOO@", out)
}
if !strings.Contains(out, "foo") {
t.Fatalf("%q should be contained in output of command: %v", "foo", out)
}
}

View File

@ -1,41 +0,0 @@
package main
import (
"bytes"
"net/http"
"net/http/httptest"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli"
)
func testWithServer(h http.HandlerFunc, testFuncs ...func(*cli.App)) string {
ts := httptest.NewServer(h)
defer ts.Close()
cli.OsExiter = func(n int) {}
client := mastodon.NewClient(&mastodon.Config{
Server: ts.URL,
ClientID: "foo",
ClientSecret: "bar",
AccessToken: "zoo",
})
var buf bytes.Buffer
app := makeApp()
app.Writer = &buf
app.Metadata = map[string]interface{}{
"client": client,
"config": &mastodon.Config{
Server: "https://example.com",
},
"xsearch_url": ts.URL,
}
for _, f := range testFuncs {
f(app)
}
return buf.String()
}

View File

@ -1,68 +0,0 @@
package main
import (
"context"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli"
)
func cmdTimeline(c *cli.Context) error {
client := c.App.Metadata["client"].(*mastodon.Client)
config := c.App.Metadata["config"].(*mastodon.Config)
timeline, err := client.GetTimelineHome(context.Background(), nil)
if err != nil {
return err
}
s := newScreen(config)
for i := len(timeline) - 1; i >= 0; i-- {
s.displayStatus(c.App.Writer, timeline[i])
}
return nil
}
func cmdTimelineHome(c *cli.Context) error {
return cmdTimeline(c)
}
func cmdTimelinePublic(c *cli.Context) error {
client := c.App.Metadata["client"].(*mastodon.Client)
config := c.App.Metadata["config"].(*mastodon.Config)
timeline, err := client.GetTimelinePublic(context.Background(), false, nil)
if err != nil {
return err
}
s := newScreen(config)
for i := len(timeline) - 1; i >= 0; i-- {
s.displayStatus(c.App.Writer, timeline[i])
}
return nil
}
func cmdTimelineLocal(c *cli.Context) error {
client := c.App.Metadata["client"].(*mastodon.Client)
config := c.App.Metadata["config"].(*mastodon.Config)
timeline, err := client.GetTimelinePublic(context.Background(), true, nil)
if err != nil {
return err
}
s := newScreen(config)
for i := len(timeline) - 1; i >= 0; i-- {
s.displayStatus(c.App.Writer, timeline[i])
}
return nil
}
func cmdTimelineDirect(c *cli.Context) error {
client := c.App.Metadata["client"].(*mastodon.Client)
config := c.App.Metadata["config"].(*mastodon.Config)
timeline, err := client.GetTimelineDirect(context.Background(), nil)
if err != nil {
return err
}
s := newScreen(config)
for i := len(timeline) - 1; i >= 0; i-- {
s.displayStatus(c.App.Writer, timeline[i])
}
return nil
}

View File

@ -1,52 +0,0 @@
package main
import (
"fmt"
"net/http"
"strings"
"testing"
"github.com/urfave/cli"
)
func TestCmdTimeline(t *testing.T) {
out := testWithServer(
func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/timelines/home":
fmt.Fprintln(w, `[{"content": "home"}]`)
return
case "/api/v1/timelines/public":
fmt.Fprintln(w, `[{"content": "public"}]`)
return
case "/api/v1/conversations":
fmt.Fprintln(w, `[{"id": "4", "unread":false, "last_status" : {"content": "direct"}}]`)
return
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
},
func(app *cli.App) {
app.Run([]string{"mstdn", "timeline"})
app.Run([]string{"mstdn", "timeline-home"})
app.Run([]string{"mstdn", "timeline-public"})
app.Run([]string{"mstdn", "timeline-local"})
app.Run([]string{"mstdn", "timeline-direct"})
},
)
want := strings.Join([]string{
"@example.com",
"home",
"@example.com",
"home",
"@example.com",
"public",
"@example.com",
"public",
"@example.com",
"direct",
}, "\n") + "\n"
if !strings.Contains(out, want) {
t.Fatalf("%q should be contained in output of command: %v", want, out)
}
}

View File

@ -1,33 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli"
)
func cmdToot(c *cli.Context) error {
var toot string
ff := c.String("ff")
if ff != "" {
text, err := readFile(ff)
if err != nil {
return err
}
toot = string(text)
} else {
if !c.Args().Present() {
return errors.New("arguments required")
}
toot = argstr(c)
}
client := c.App.Metadata["client"].(*mastodon.Client)
_, err := client.PostStatus(context.Background(), &mastodon.Toot{
Status: toot,
InReplyToID: mastodon.ID(fmt.Sprint(c.String("i"))),
})
return err
}

View File

@ -1,52 +0,0 @@
package main
import (
"fmt"
"net/http"
"testing"
"github.com/urfave/cli"
)
func TestCmdToot(t *testing.T) {
toot := ""
testWithServer(
func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/statuses":
toot = r.FormValue("status")
fmt.Fprintln(w, `{"id": 2345}`)
return
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
},
func(app *cli.App) {
app.Run([]string{"mstdn", "toot", "foo"})
},
)
if toot != "foo" {
t.Fatalf("want %q, got %q", "foo", toot)
}
}
func TestCmdTootFileNotFound(t *testing.T) {
var err error
testWithServer(
func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/statuses":
fmt.Fprintln(w, `{"id": 2345}`)
return
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
},
func(app *cli.App) {
err = app.Run([]string{"mstdn", "toot", "-ff", "not-found"})
},
)
if err == nil {
t.Fatal("should be fail")
}
}

View File

@ -1,33 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli"
)
func cmdUpload(c *cli.Context) error {
if !c.Args().Present() {
return errors.New("arguments required")
}
client := c.App.Metadata["client"].(*mastodon.Client)
for i := 0; i < c.NArg(); i++ {
attachment, err := client.UploadMedia(context.Background(), c.Args().Get(i))
if err != nil {
return err
}
if i > 0 {
fmt.Fprintln(c.App.Writer)
}
fmt.Fprintf(c.App.Writer, "ID : %v\n", attachment.ID)
fmt.Fprintf(c.App.Writer, "Type : %v\n", attachment.Type)
fmt.Fprintf(c.App.Writer, "URL : %v\n", attachment.URL)
fmt.Fprintf(c.App.Writer, "RemoteURL : %v\n", attachment.RemoteURL)
fmt.Fprintf(c.App.Writer, "PreviewURL: %v\n", attachment.PreviewURL)
fmt.Fprintf(c.App.Writer, "TextURL : %v\n", attachment.TextURL)
}
return nil
}

View File

@ -1,30 +0,0 @@
package main
import (
"fmt"
"net/http"
"strings"
"testing"
"github.com/urfave/cli"
)
func TestCmdUpload(t *testing.T) {
out := testWithServer(
func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/media":
fmt.Fprintln(w, `{"id": 123}`)
return
}
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
},
func(app *cli.App) {
app.Run([]string{"mstdn", "upload", "../../testdata/logo.png"})
},
)
if !strings.Contains(out, "123") {
t.Fatalf("%q should be contained in output of command: %v", "123", out)
}
}

View File

@ -1,38 +0,0 @@
package main
import (
"fmt"
"io"
"net/url"
"github.com/PuerkitoBio/goquery"
"github.com/urfave/cli"
)
func cmdXSearch(c *cli.Context) error {
return xSearch(c.App.Metadata["xsearch_url"].(string), c.Args().First(), c.App.Writer)
}
func xSearch(xsearchRawurl, query string, w io.Writer) error {
u, err := url.Parse(xsearchRawurl)
if err != nil {
return err
}
params := url.Values{}
params.Set("q", query)
u.RawQuery = params.Encode()
doc, err := goquery.NewDocument(u.String())
if err != nil {
return err
}
doc.Find(".post").Each(func(n int, elem *goquery.Selection) {
href, ok := elem.Find(".mst_content a").Attr("href")
if !ok {
return
}
text := elem.Find(".mst_content p").Text()
fmt.Fprintf(w, "%s\n", href)
fmt.Fprintf(w, "%s\n\n", text)
})
return nil
}

View File

@ -1,76 +0,0 @@
package main
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/urfave/cli"
)
func TestCmdXSearch(t *testing.T) {
testWithServer(
func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `<div class="post"><div class="mst_content"><a href="http://example.com/@test/1"><p>test status</p></a></div></div>`)
},
func(app *cli.App) {
err := app.Run([]string{"mstdn", "xsearch", "test"})
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
},
)
}
func TestXSearch(t *testing.T) {
canErr := true
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if canErr {
canErr = false
http.Error(w, http.StatusText(http.StatusInternalServerError), 9999)
return
} else if r.URL.Query().Get("q") == "empty" {
fmt.Fprintln(w, `<div class="post"><div class="mst_content"><a><p>test status</p></a></div></div>`)
return
}
fmt.Fprintln(w, `<div class="post"><div class="mst_content"><a href="http://example.com/@test/1"><p>test status</p></a></div></div>`)
}))
defer ts.Close()
err := xSearch(":", "", nil)
if err == nil {
t.Fatalf("should be fail: %v", err)
}
err = xSearch(ts.URL, "", nil)
if err == nil {
t.Fatalf("should be fail: %v", err)
}
buf := bytes.NewBuffer(nil)
err = xSearch(ts.URL, "empty", buf)
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
result := buf.String()
if result != "" {
t.Fatalf("the search result should be empty: %s", result)
}
buf = bytes.NewBuffer(nil)
err = xSearch(ts.URL, "test", buf)
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
result = buf.String()
if !strings.Contains(result, "http://example.com/@test/1") {
t.Fatalf("%q should be contained in output of search: %s", "http://example.com/@test/1", result)
}
if !strings.Contains(result, "test status") {
t.Fatalf("%q should be contained in output of search: %s", "test status", result)
}
}

View File

@ -1,14 +0,0 @@
module github.com/mattn/go-mastodon/cmd/mstdn
go 1.16
replace github.com/mattn/go-mastodon => ../..
require (
github.com/PuerkitoBio/goquery v1.8.0
github.com/fatih/color v1.13.0
github.com/mattn/go-mastodon v0.0.4
github.com/mattn/go-tty v0.0.4
github.com/urfave/cli v1.22.9
golang.org/x/net v0.0.0-20220531201128-c960675eff93
)

View File

@ -1,55 +0,0 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-tty v0.0.4 h1:NVikla9X8MN0SQAqCYzpGyXv0jY7MNl3HOWD2dkle7E=
github.com/mattn/go-tty v0.0.4/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=
github.com/urfave/cli v1.22.9 h1:cv3/KhXGBGjEXLC4bH0sLuJ9BewaAbpk5oyMOveu4pw=
github.com/urfave/cli v1.22.9/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220531201128-c960675eff93 h1:MYimHLfoXEpOhqd/zgoA/uoXzHB86AEky4LAx5ij9xA=
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,411 +0,0 @@
package main
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/fatih/color"
"github.com/mattn/go-mastodon"
"github.com/mattn/go-tty"
"github.com/urfave/cli"
"golang.org/x/net/html"
)
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 {
return s
}
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) {
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) {
fmt.Print("E-Mail: ")
email, err := readUsername()
if err != nil {
return "", "", err
}
fmt.Print("Password: ")
var password string
if readPassword == nil {
var t *tty.TTY
t, err = tty.Open()
if err != nil {
return "", "", err
}
defer t.Close()
password, err = t.ReadPassword()
} else {
password, err = readPassword()
}
if err != nil {
return "", "", err
}
return email, password, nil
}
func configFile(c *cli.Context) (string, 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 "", err
}
var file string
profile := c.String("profile")
if profile != "" {
file = filepath.Join(dir, "settings-"+profile+".json")
} else {
file = filepath.Join(dir, "settings.json")
}
return file, nil
}
func getConfig(c *cli.Context) (string, *mastodon.Config, error) {
file, err := configFile(c)
if err != nil {
return "", nil, err
}
b, err := ioutil.ReadFile(file)
if err != nil && !os.IsNotExist(err) {
return "", nil, err
}
config := &mastodon.Config{
Server: "https://mstdn.jp",
ClientID: "1e463436008428a60ed14ff1f7bc0b4d923e14fc4a6827fa99560b0c0222612f",
ClientSecret: "72b63de5bc11111a5aa1a7b690672d78ad6a207ce32e16ea26115048ec5d234d",
}
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) error {
email, password, err := prompt()
if err != nil {
return err
}
err = client.Authenticate(context.Background(), email, password)
if err != nil {
return err
}
b, err := json.MarshalIndent(config, "", " ")
if err != nil {
return fmt.Errorf("failed to store file: %v", err)
}
err = ioutil.WriteFile(file, b, 0700)
if err != nil {
return fmt.Errorf("failed to store file: %v", err)
}
return nil
}
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, " ")
}
func fatalIf(err error) {
if err == nil {
return
}
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err)
os.Exit(1)
}
func makeApp() *cli.App {
app := cli.NewApp()
app.Name = "mstdn"
app.Usage = "mastodon client"
app.Version = "0.0.1"
app.Flags = []cli.Flag{
cli.StringFlag{
Name: "profile",
Usage: "profile name",
Value: "",
},
}
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: "",
},
cli.StringFlag{
Name: "i",
Usage: "in-reply-to",
Value: "",
},
},
Action: cmdToot,
},
{
Name: "stream",
Usage: "stream statuses",
Flags: []cli.Flag{
cli.StringFlag{
Name: "type",
Usage: "stream type (public,public/local,user:NAME,hashtag:TAG)",
},
cli.BoolFlag{
Name: "json",
Usage: "output JSON",
},
cli.BoolFlag{
Name: "simplejson",
Usage: "output simple JSON",
},
cli.StringFlag{
Name: "template",
Usage: "output with tamplate format",
},
},
Action: cmdStream,
},
{
Name: "timeline",
Usage: "show timeline",
Action: cmdTimeline,
},
{
Name: "timeline-home",
Usage: "show timeline home",
Action: cmdTimelineHome,
},
{
Name: "timeline-local",
Usage: "show timeline local",
Action: cmdTimelineLocal,
},
{
Name: "timeline-public",
Usage: "show timeline public",
Action: cmdTimelinePublic,
},
{
Name: "timeline-direct",
Usage: "show timeline direct",
Action: cmdTimelineDirect,
},
{
Name: "notification",
Usage: "show notification",
Action: cmdNotification,
},
{
Name: "instance",
Usage: "show instance information",
Action: cmdInstance,
},
{
Name: "instance_activity",
Usage: "show instance activity information",
Action: cmdInstanceActivity,
},
{
Name: "instance_peers",
Usage: "show instance peers information",
Action: cmdInstancePeers,
},
{
Name: "account",
Usage: "show account information",
Action: cmdAccount,
},
{
Name: "search",
Usage: "search content",
Action: cmdSearch,
},
{
Name: "follow",
Usage: "follow account",
Action: cmdFollow,
},
{
Name: "followers",
Usage: "show followers",
Action: cmdFollowers,
},
{
Name: "upload",
Usage: "upload file",
Action: cmdUpload,
},
{
Name: "delete",
Usage: "delete status",
Action: cmdDelete,
},
{
Name: "init",
Usage: "initialize profile",
Action: func(c *cli.Context) error { return nil },
},
{
Name: "mikami",
Usage: "search mikami",
Action: cmdMikami,
},
{
Name: "xsearch",
Usage: "cross search",
Action: cmdXSearch,
},
}
app.Setup()
return app
}
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}
}
func (s *screen) acct(a string) string {
if !strings.Contains(a, "@") {
a += "@" + s.host
}
return a
}
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) {
if t == nil {
return
}
if t.Reblog != nil {
color.Set(color.FgHiRed)
fmt.Fprint(w, s.acct(t.Account.Acct))
color.Set(color.Reset)
fmt.Fprint(w, " reblogged ")
color.Set(color.FgHiBlue)
fmt.Fprintln(w, s.acct(t.Reblog.Account.Acct))
fmt.Fprintln(w, textContent(t.Reblog.Content))
color.Set(color.Reset)
} else {
color.Set(color.FgHiRed)
fmt.Fprintln(w, s.acct(t.Account.Acct))
color.Set(color.Reset)
fmt.Fprintln(w, textContent(t.Content))
}
}
func run() int {
app := makeApp()
app.Before = func(c *cli.Context) error {
if c.Args().Get(0) == "init" {
file, err := configFile(c)
if err != nil {
return err
}
os.Remove(file)
}
file, config, err := getConfig(c)
if err != nil {
return err
}
client := mastodon.NewClient(config)
client.UserAgent = "mstdn"
app.Metadata = map[string]interface{}{
"client": client,
"config": config,
"xsearch_url": "http://mastodonsearch.jp/cross/",
}
if config.AccessToken == "" {
return authenticate(client, config, file)
}
return nil
}
fatalIf(app.Run(os.Args))
return 0
}
func main() {
os.Exit(run())
}

View File

@ -1,135 +0,0 @@
package main
import (
"encoding/json"
"flag"
"io/ioutil"
"os"
"testing"
"github.com/urfave/cli"
)
func TestReadFileFile(t *testing.T) {
b, err := readFile("main.go")
if err != nil {
t.Fatal(err)
}
if len(b) == 0 {
t.Fatalf("should read something: %v", err)
}
}
func TestReadFileStdin(t *testing.T) {
f, err := os.Open("main.go")
if err != nil {
t.Fatal(err)
}
defer f.Close()
stdin := os.Stdin
os.Stdin = f
defer func() {
os.Stdin = stdin
}()
b, err := readFile("-")
if err != nil {
t.Fatal(err)
}
if len(b) == 0 {
t.Fatalf("should read something: %v", err)
}
}
func TestTextContent(t *testing.T) {
tests := []struct {
input string
want string
}{
{input: "", want: ""},
{input: "<p>foo</p>", want: "foo"},
{input: "<p>foo<span>\nbar\n</span>baz</p>", want: "foobarbaz"},
{input: "<p>foo<span>\nbar<br></span>baz</p>", want: "foobar\nbaz"},
}
for _, test := range tests {
got := textContent(test.input)
if got != test.want {
t.Fatalf("want %q but %q", test.want, got)
}
}
}
func TestGetConfig(t *testing.T) {
tmpdir, err := ioutil.TempDir("", "mstdn")
if err != nil {
t.Fatal(err)
}
home := os.Getenv("HOME")
appdata := os.Getenv("APPDATA")
os.Setenv("HOME", tmpdir)
os.Setenv("APPDATA", tmpdir)
defer func() {
os.RemoveAll(tmpdir)
os.Setenv("HOME", home)
os.Setenv("APPDATA", appdata)
}()
app := makeApp()
set := flag.NewFlagSet("test", 0)
set.Parse([]string{"mstdn", "-profile", ""})
c := cli.NewContext(app, set, nil)
file, config, err := getConfig(c)
if err != nil {
t.Fatal(err)
}
if _, err := os.Stat(file); err == nil {
t.Fatal("should not exists")
}
if config.AccessToken != "" {
t.Fatalf("should be empty: %v", config.AccessToken)
}
if config.ClientID == "" {
t.Fatalf("should not be empty")
}
if config.ClientSecret == "" {
t.Fatalf("should not be empty")
}
config.AccessToken = "foo"
b, err := json.MarshalIndent(config, "", " ")
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile(file, b, 0700)
if err != nil {
t.Fatal(err)
}
file, config, err = getConfig(c)
if err != nil {
t.Fatal(err)
}
if _, err := os.Stat(file); err != nil {
t.Fatalf("should exists: %v", err)
}
if got := config.AccessToken; got != "foo" {
t.Fatalf("want %q but %q", "foo", got)
}
}
func TestPrompt(t *testing.T) {
readUsername = func() (string, error) {
return "foo", nil
}
readPassword = func() (string, error) {
return "bar", nil
}
username, password, err := prompt()
if err != nil {
t.Fatal(err)
}
if username != "foo" {
t.Fatalf("want %q but %q", "foo", username)
}
if password != "bar" {
t.Fatalf("want %q but %q", "bar", password)
}
}

5
go.mod
View File

@ -1,8 +1,3 @@
module github.com/mattn/go-mastodon module github.com/mattn/go-mastodon
go 1.16 go 1.16
require (
github.com/gorilla/websocket v1.5.0
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
)

4
go.sum
View File

@ -1,4 +0,0 @@
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y=
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE=

View File

@ -12,8 +12,6 @@ import (
"path" "path"
"strings" "strings"
"time" "time"
"github.com/tomnomnom/linkheader"
) )
// Config is a setting for access mastodon APIs. // Config is a setting for access mastodon APIs.
@ -320,28 +318,28 @@ func newPagination(rawlink string) (*Pagination, error) {
} }
p := &Pagination{} p := &Pagination{}
for _, link := range linkheader.Parse(rawlink) { // for _, link := range linkheader.Parse(rawlink) {
switch link.Rel { // switch link.Rel {
case "next": // case "next":
maxID, err := getPaginationID(link.URL, "max_id") // maxID, err := getPaginationID(link.URL, "max_id")
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
p.MaxID = maxID // p.MaxID = maxID
case "prev": // case "prev":
sinceID, err := getPaginationID(link.URL, "since_id") // sinceID, err := getPaginationID(link.URL, "since_id")
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
p.SinceID = sinceID // p.SinceID = sinceID
minID, err := getPaginationID(link.URL, "min_id") // minID, err := getPaginationID(link.URL, "min_id")
if err != nil { // if err != nil {
return nil, err // return nil, err
} // }
p.MinID = minID // p.MinID = minID
} // }
} // }
return p, nil return p, nil
} }

View File

@ -1,195 +0,0 @@
package mastodon
import (
"context"
"encoding/json"
"fmt"
"net/url"
"path"
"strings"
"github.com/gorilla/websocket"
)
// WSClient is a WebSocket client.
type WSClient struct {
websocket.Dialer
client *Client
}
// NewWSClient return WebSocket client.
func (c *Client) NewWSClient() *WSClient { return &WSClient{client: c} }
// Stream is a struct of data that flows in streaming.
type Stream struct {
Event string `json:"event"`
Payload interface{} `json:"payload"`
}
// StreamingWSUser return channel to read events on home using WebSocket.
func (c *WSClient) StreamingWSUser(ctx context.Context) (chan Event, error) {
return c.streamingWS(ctx, "user", "")
}
// StreamingWSPublic return channel to read events on public using WebSocket.
func (c *WSClient) StreamingWSPublic(ctx context.Context, isLocal bool) (chan Event, error) {
s := "public"
if isLocal {
s += ":local"
}
return c.streamingWS(ctx, s, "")
}
// StreamingWSHashtag return channel to read events on tagged timeline using WebSocket.
func (c *WSClient) StreamingWSHashtag(ctx context.Context, tag string, isLocal bool) (chan Event, error) {
s := "hashtag"
if isLocal {
s += ":local"
}
return c.streamingWS(ctx, s, tag)
}
// StreamingWSList return channel to read events on a list using WebSocket.
func (c *WSClient) StreamingWSList(ctx context.Context, id ID) (chan Event, error) {
return c.streamingWS(ctx, "list", string(id))
}
func (c *WSClient) streamingWS(ctx context.Context, stream, tag string) (chan Event, error) {
params := url.Values{}
params.Set("access_token", c.client.Config.AccessToken)
params.Set("stream", stream)
if tag != "" {
params.Set("tag", tag)
}
u, err := changeWebSocketScheme(c.client.Config.Server)
if err != nil {
return nil, err
}
u.Path = path.Join(u.Path, "/api/v1/streaming")
u.RawQuery = params.Encode()
q := make(chan Event)
go func() {
defer close(q)
for {
err := c.handleWS(ctx, u.String(), q)
if err != nil {
return
}
}
}()
return q, nil
}
func (c *WSClient) handleWS(ctx context.Context, rawurl string, q chan Event) error {
conn, err := c.dialRedirect(rawurl)
if err != nil {
q <- &ErrorEvent{err: err}
// End.
return err
}
// Close the WebSocket when the context is canceled.
go func() {
<-ctx.Done()
conn.Close()
}()
for {
select {
case <-ctx.Done():
q <- &ErrorEvent{err: ctx.Err()}
// End.
return ctx.Err()
default:
}
var s Stream
err := conn.ReadJSON(&s)
if err != nil {
q <- &ErrorEvent{err: err}
// Reconnect.
break
}
err = nil
switch s.Event {
case "update":
var status Status
err = json.Unmarshal([]byte(s.Payload.(string)), &status)
if err == nil {
q <- &UpdateEvent{Status: &status}
}
case "notification":
var notification Notification
err = json.Unmarshal([]byte(s.Payload.(string)), &notification)
if err == nil {
q <- &NotificationEvent{Notification: &notification}
}
case "delete":
if f, ok := s.Payload.(float64); ok {
q <- &DeleteEvent{ID: ID(fmt.Sprint(int64(f)))}
} else {
q <- &DeleteEvent{ID: ID(strings.TrimSpace(s.Payload.(string)))}
}
}
if err != nil {
q <- &ErrorEvent{err}
}
}
return nil
}
func (c *WSClient) dialRedirect(rawurl string) (conn *websocket.Conn, err error) {
for {
conn, rawurl, err = c.dial(rawurl)
if err != nil {
return nil, err
} else if conn != nil {
return conn, nil
}
}
}
func (c *WSClient) dial(rawurl string) (*websocket.Conn, string, error) {
conn, resp, err := c.Dial(rawurl, nil)
if err != nil && err != websocket.ErrBadHandshake {
return nil, "", err
}
defer resp.Body.Close()
if loc := resp.Header.Get("Location"); loc != "" {
u, err := changeWebSocketScheme(loc)
if err != nil {
return nil, "", err
}
return nil, u.String(), nil
}
return conn, "", err
}
func changeWebSocketScheme(rawurl string) (*url.URL, error) {
u, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
switch u.Scheme {
case "http":
u.Scheme = "ws"
case "https":
u.Scheme = "wss"
}
return u, nil
}

View File

@ -1,281 +0,0 @@
package mastodon
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gorilla/websocket"
)
func TestStreamingWSUser(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(wsMock))
defer ts.Close()
client := NewClient(&Config{Server: ts.URL}).NewWSClient()
ctx, cancel := context.WithCancel(context.Background())
q, err := client.StreamingWSUser(ctx)
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
wsTest(t, q, cancel)
}
func TestStreamingWSPublic(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(wsMock))
defer ts.Close()
client := NewClient(&Config{Server: ts.URL}).NewWSClient()
ctx, cancel := context.WithCancel(context.Background())
q, err := client.StreamingWSPublic(ctx, false)
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
wsTest(t, q, cancel)
}
func TestStreamingWSHashtag(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(wsMock))
defer ts.Close()
client := NewClient(&Config{Server: ts.URL}).NewWSClient()
ctx, cancel := context.WithCancel(context.Background())
q, err := client.StreamingWSHashtag(ctx, "zzz", true)
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
wsTest(t, q, cancel)
ctx, cancel = context.WithCancel(context.Background())
q, err = client.StreamingWSHashtag(ctx, "zzz", false)
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
wsTest(t, q, cancel)
}
func wsMock(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/streaming" {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
u := websocket.Upgrader{}
conn, err := u.Upgrade(w, r, nil)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
defer conn.Close()
err = conn.WriteMessage(websocket.TextMessage,
[]byte(`{"event":"update","payload":"{\"content\":\"foo\"}"}`))
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
err = conn.WriteMessage(websocket.TextMessage,
[]byte(`{"event":"notification","payload":"{\"id\":123}"}`))
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
err = conn.WriteMessage(websocket.TextMessage,
[]byte(`{"event":"delete","payload":1234567}`))
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
err = conn.WriteMessage(websocket.TextMessage,
[]byte(`{"event":"update","payload":"<html></html>"}`))
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
time.Sleep(10 * time.Second)
}
func wsTest(t *testing.T, q chan Event, cancel func()) {
time.AfterFunc(time.Second, func() {
cancel()
})
events := []Event{}
for e := range q {
events = append(events, e)
}
if len(events) != 6 {
t.Fatalf("result should be four: %d", len(events))
}
if events[0].(*UpdateEvent).Status.Content != "foo" {
t.Fatalf("want %q but %q", "foo", events[0].(*UpdateEvent).Status.Content)
}
if events[1].(*NotificationEvent).Notification.ID != "123" {
t.Fatalf("want %q but %q", "123", events[1].(*NotificationEvent).Notification.ID)
}
if events[2].(*DeleteEvent).ID != "1234567" {
t.Fatalf("want %q but %q", "1234567", events[2].(*DeleteEvent).ID)
}
if errorEvent, ok := events[3].(*ErrorEvent); !ok {
t.Fatalf("should be fail: %v", errorEvent.err)
}
if errorEvent, ok := events[4].(*ErrorEvent); !ok {
t.Fatalf("should be fail: %v", errorEvent.err)
}
if errorEvent, ok := events[5].(*ErrorEvent); !ok {
t.Fatalf("should be fail: %v", errorEvent.err)
}
}
func TestStreamingWS(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(wsMock))
defer ts.Close()
client := NewClient(&Config{Server: ":"}).NewWSClient()
_, err := client.StreamingWSPublic(context.Background(), true)
if err == nil {
t.Fatalf("should be fail: %v", err)
}
client = NewClient(&Config{Server: ts.URL}).NewWSClient()
ctx, cancel := context.WithCancel(context.Background())
cancel()
q, err := client.StreamingWSPublic(ctx, true)
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
go func() {
e := <-q
if errorEvent, ok := e.(*ErrorEvent); !ok {
t.Fatalf("should be fail: %v", errorEvent.err)
}
}()
}
func TestHandleWS(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
u := websocket.Upgrader{}
conn, err := u.Upgrade(w, r, nil)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
defer conn.Close()
err = conn.WriteMessage(websocket.TextMessage,
[]byte(`<html></html>`))
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
time.Sleep(10 * time.Second)
}))
defer ts.Close()
q := make(chan Event)
client := NewClient(&Config{}).NewWSClient()
go func() {
e := <-q
if errorEvent, ok := e.(*ErrorEvent); !ok {
t.Fatalf("should be fail: %v", errorEvent.err)
}
}()
err := client.handleWS(context.Background(), ":", q)
if err == nil {
t.Fatalf("should be fail: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
go func() {
e := <-q
if errorEvent, ok := e.(*ErrorEvent); !ok {
t.Fatalf("should be fail: %v", errorEvent.err)
}
}()
err = client.handleWS(ctx, "ws://"+ts.Listener.Addr().String(), q)
if err == nil {
t.Fatalf("should be fail: %v", err)
}
go func() {
e := <-q
if errorEvent, ok := e.(*ErrorEvent); !ok {
t.Fatalf("should be fail: %v", errorEvent.err)
}
}()
client.handleWS(context.Background(), "ws://"+ts.Listener.Addr().String(), q)
}
func TestDialRedirect(t *testing.T) {
client := NewClient(&Config{}).NewWSClient()
_, err := client.dialRedirect(":")
if err == nil {
t.Fatalf("should be fail: %v", err)
}
}
func TestDial(t *testing.T) {
canErr := true
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if canErr {
canErr = false
http.Redirect(w, r, ":", http.StatusMovedPermanently)
return
}
http.Redirect(w, r, "http://www.example.com/", http.StatusMovedPermanently)
}))
defer ts.Close()
client := NewClient(&Config{}).NewWSClient()
_, _, err := client.dial(":")
if err == nil {
t.Fatalf("should be fail: %v", err)
}
_, _, err = client.dial("ws://" + ts.Listener.Addr().String())
if err == nil {
t.Fatalf("should be fail: %v", err)
}
_, rawurl, err := client.dial("ws://" + ts.Listener.Addr().String())
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
if rawurl != "ws://www.example.com/" {
t.Fatalf("want %q but %q", "ws://www.example.com/", rawurl)
}
}
func TestChangeWebSocketScheme(t *testing.T) {
_, err := changeWebSocketScheme(":")
if err == nil {
t.Fatalf("should be fail: %v", err)
}
u, err := changeWebSocketScheme("http://example.com/")
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
if u.Scheme != "ws" {
t.Fatalf("want %q but %q", "ws", u.Scheme)
}
u, err = changeWebSocketScheme("https://example.com/")
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
if u.Scheme != "wss" {
t.Fatalf("want %q but %q", "wss", u.Scheme)
}
}