remove dependencies and client
This commit is contained in:
parent
5f0c9a21c2
commit
2542bf46ba
1
LICENSE
1
LICENSE
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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=
|
|
|
@ -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())
|
|
||||||
}
|
|
|
@ -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
5
go.mod
|
@ -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
4
go.sum
|
@ -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=
|
|
44
mastodon.go
44
mastodon.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
195
streaming_ws.go
195
streaming_ws.go
|
@ -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)), ¬ification)
|
|
||||||
if err == nil {
|
|
||||||
q <- &NotificationEvent{Notification: ¬ification}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user