diff --git a/cmd/mstdn/main.go b/cmd/mstdn/main.go index b4dc265..423269d 100644 --- a/cmd/mstdn/main.go +++ b/cmd/mstdn/main.go @@ -3,23 +3,27 @@ package main import ( "bufio" "bytes" + "context" "encoding/json" "flag" "fmt" "io/ioutil" "log" "os" + "os/signal" "path/filepath" "runtime" "strings" + "github.com/fatih/color" "github.com/mattn/go-mastodon" "github.com/mattn/go-tty" "golang.org/x/net/html" ) var ( - toot = flag.String("t", "", "toot text") + toot = flag.String("t", "", "toot text") + stream = flag.Bool("S", false, "streaming public") ) func extractText(node *html.Node, w *bytes.Buffer) { @@ -38,6 +42,16 @@ func extractText(node *html.Node, w *bytes.Buffer) { } } +func textContent(s string) string { + doc, err := html.Parse(strings.NewReader(s)) + if err != nil { + log.Fatal(err) + } + var buf bytes.Buffer + extractText(doc, &buf) + return buf.String() +} + func prompt() (string, string, error) { t, err := tty.Open() if err != nil { @@ -132,19 +146,39 @@ func main() { } return } + if *stream { + ctx, cancel := context.WithCancel(context.Background()) + sc := make(chan os.Signal, 1) + signal.Notify(sc, os.Interrupt) + q, err := client.StreamingPublic(ctx) + if err != nil { + log.Fatal(err) + } + go func() { + <-sc + cancel() + close(q) + }() + for e := range q { + switch t := e.(type) { + case *mastodon.UpdateEvent: + color.Set(color.FgHiRed) + fmt.Println(t.Status.Account.Username) + color.Set(color.Reset) + fmt.Println(textContent(t.Status.Content)) + } + } + return + } timeline, err := client.GetTimelineHome() if err != nil { log.Fatal(err) } for _, t := range timeline { - doc, err := html.Parse(strings.NewReader(t.Content)) - if err != nil { - log.Fatal(err) - } - var buf bytes.Buffer - extractText(doc, &buf) + color.Set(color.FgHiRed) fmt.Println(t.Account.Username) - fmt.Println(buf.String()) + color.Set(color.Reset) + fmt.Println(textContent(t.Content)) } } diff --git a/mastodon.go b/mastodon.go index 1f950b4..a410ecf 100644 --- a/mastodon.go +++ b/mastodon.go @@ -1,6 +1,8 @@ package mastodon import ( + "bufio" + "context" "encoding/json" "fmt" "net/http" @@ -65,6 +67,63 @@ func (c *client) Authenticate(username, password string) error { return nil } +// AppConfig is a setting for registering applications. +type AppConfig struct { + http.Client + Server string + ClientName string + + // Where the user should be redirected after authorization (for no redirect, use urn:ietf:wg:oauth:2.0:oob) + RedirectURIs string + + // This can be a space-separated list of the following items: "read", "write" and "follow". + Scopes string + + // Optional. + Website string +} + +// Application is mastodon application. +type Application struct { + ID int64 `json:"id"` + RedirectURI string `json:"redirect_uri"` + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` +} + +// RegisterApp returns the mastodon application. +func RegisterApp(appConfig *AppConfig) (*Application, error) { + params := url.Values{} + params.Set("client_name", appConfig.ClientName) + params.Set("redirect_uris", appConfig.RedirectURIs) + params.Set("scopes", appConfig.Scopes) + params.Set("website", appConfig.Website) + + url, err := url.Parse(appConfig.Server) + if err != nil { + return nil, err + } + url.Path = path.Join(url.Path, "/api/v1/apps") + + req, err := http.NewRequest("POST", url.String(), strings.NewReader(params.Encode())) + if err != nil { + return nil, err + } + resp, err := appConfig.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + app := &Application{} + err = json.NewDecoder(resp.Body).Decode(app) + if err != nil { + return nil, err + } + + return app, nil +} + type Account struct { ID int64 `json:"id"` Username string `json:"username"` @@ -83,32 +142,6 @@ type Account struct { HeaderStatic string `json:"header_static"` } -func (c *client) GetAccount(id int) (*Account, error) { - url, err := url.Parse(c.config.Server) - if err != nil { - return nil, err - } - url.Path = path.Join(url.Path, fmt.Sprintf("/api/v1/accounts/%d", id)) - - req, err := http.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Authorization", "Bearer "+c.config.AccessToken) - resp, err := c.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - account := &Account{} - err = json.NewDecoder(resp.Body).Decode(account) - if err != nil { - return nil, err - } - return account, nil -} - type Visibility int64 type Toot struct { @@ -143,6 +176,32 @@ type Status struct { Reblogged interface{} `json:"reblogged"` } +func (c *client) GetAccount(id int) (*Account, error) { + url, err := url.Parse(c.config.Server) + if err != nil { + return nil, err + } + url.Path = path.Join(url.Path, fmt.Sprintf("/api/v1/accounts/%d", id)) + + req, err := http.NewRequest("GET", url.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+c.config.AccessToken) + resp, err := c.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + account := &Account{} + err = json.NewDecoder(resp.Body).Decode(account) + if err != nil { + return nil, err + } + return account, nil +} + func (c *client) GetTimelineHome() ([]*Status, error) { url, err := url.Parse(c.config.Server) if err != nil { @@ -201,59 +260,74 @@ func (c *client) PostStatus(toot *Toot) (*Status, error) { return &status, nil } -// AppConfig is a setting for registering applications. -type AppConfig struct { - http.Client - Server string - ClientName string - - // Where the user should be redirected after authorization (for no redirect, use urn:ietf:wg:oauth:2.0:oob) - RedirectURIs string - - // This can be a space-separated list of the following items: "read", "write" and "follow". - Scopes string - - // Optional. - Website string +type UpdateEvent struct { + Status *Status } -// Application is mastodon application. -type Application struct { - ID int64 `json:"id"` - RedirectURI string `json:"redirect_uri"` - ClientID string `json:"client_id"` - ClientSecret string `json:"client_secret"` +func (e *UpdateEvent) event() {} + +type NotificationEvent struct { } -// RegisterApp returns the mastodon application. -func RegisterApp(appConfig *AppConfig) (*Application, error) { - params := url.Values{} - params.Set("client_name", appConfig.ClientName) - params.Set("redirect_uris", appConfig.RedirectURIs) - params.Set("scopes", appConfig.Scopes) - params.Set("website", appConfig.Website) +func (e *NotificationEvent) event() {} - url, err := url.Parse(appConfig.Server) - if err != nil { - return nil, err - } - url.Path = path.Join(url.Path, "/api/v1/apps") - - req, err := http.NewRequest("POST", url.String(), strings.NewReader(params.Encode())) - if err != nil { - return nil, err - } - resp, err := appConfig.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - app := &Application{} - err = json.NewDecoder(resp.Body).Decode(app) - if err != nil { - return nil, err - } - - return app, nil +type DeleteEvent struct { + ID int64 +} + +func (e *DeleteEvent) event() {} + +type Event interface { + event() +} + +func (c *client) StreamingPublic(ctx context.Context) (chan Event, error) { + url, err := url.Parse(c.config.Server) + if err != nil { + return nil, err + } + url.Path = path.Join(url.Path, "/api/v1/streaming/public") + + req, err := http.NewRequest("GET", url.String(), nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+c.config.AccessToken) + resp, err := c.Do(req) + if err != nil { + return nil, err + } + + q := make(chan Event) + go func() { + defer ctx.Done() + name := "" + s := bufio.NewScanner(resp.Body) + for s.Scan() { + line := s.Text() + token := strings.SplitN(line, ":", 2) + if len(token) != 2 { + continue + } + switch strings.TrimSpace(token[0]) { + case "event": + name = strings.TrimSpace(token[1]) + case "data": + switch name { + case "update": + var status Status + json.Unmarshal([]byte(token[1]), &status) + q <- &UpdateEvent{&status} + case "notification": + case "delete": + } + } + } + fmt.Println(s.Err()) + }() + go func() { + <-ctx.Done() + resp.Body.Close() + }() + return q, nil }