From 730317321abbdea25c96311c94eee46ca253dfc9 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 25 Oct 2017 10:22:39 +0900 Subject: [PATCH 001/127] also convert to string in attachments, pagenation --- cmd/mstdn/cmd_followers.go | 2 +- cmd/mstdn/cmd_toot.go | 3 ++- example_test.go | 2 +- mastodon.go | 36 ++++++++++++++--------------- mastodon_test.go | 46 +++++++++++++++++++------------------- status.go | 7 +++--- status_test.go | 4 ++-- 7 files changed, 50 insertions(+), 50 deletions(-) diff --git a/cmd/mstdn/cmd_followers.go b/cmd/mstdn/cmd_followers.go index ecb4a79..1b94430 100644 --- a/cmd/mstdn/cmd_followers.go +++ b/cmd/mstdn/cmd_followers.go @@ -25,7 +25,7 @@ func cmdFollowers(c *cli.Context) error { return err } followers = append(followers, fs...) - if pg.MaxID == 0 { + if pg.MaxID == "" { break } time.Sleep(10 * time.Second) diff --git a/cmd/mstdn/cmd_toot.go b/cmd/mstdn/cmd_toot.go index 29b6f4c..b2e41f4 100644 --- a/cmd/mstdn/cmd_toot.go +++ b/cmd/mstdn/cmd_toot.go @@ -3,6 +3,7 @@ package main import ( "context" "errors" + "fmt" "github.com/mattn/go-mastodon" "github.com/urfave/cli" @@ -26,7 +27,7 @@ func cmdToot(c *cli.Context) error { client := c.App.Metadata["client"].(*mastodon.Client) _, err := client.PostStatus(context.Background(), &mastodon.Toot{ Status: toot, - InReplyToID: c.Int64("i"), + InReplyToID: mastodon.ID(fmt.Sprint(c.Int64("i"))), }) return err } diff --git a/example_test.go b/example_test.go index 18716a2..a25a3ca 100644 --- a/example_test.go +++ b/example_test.go @@ -56,7 +56,7 @@ func ExamplePagination() { log.Fatal(err) } followers = append(followers, fs...) - if pg.MaxID == 0 { + if pg.MaxID == "" { break } time.Sleep(10 * time.Second) diff --git a/mastodon.go b/mastodon.go index a81d9bd..118cdcd 100644 --- a/mastodon.go +++ b/mastodon.go @@ -173,12 +173,12 @@ func (c *Client) Authenticate(ctx context.Context, username, password string) er // Toot is struct to post status. type Toot struct { - Status string `json:"status"` - InReplyToID int64 `json:"in_reply_to_id"` - MediaIDs []int64 `json:"media_ids"` - Sensitive bool `json:"sensitive"` - SpoilerText string `json:"spoiler_text"` - Visibility string `json:"visibility"` + Status string `json:"status"` + InReplyToID ID `json:"in_reply_to_id"` + MediaIDs []ID `json:"media_ids"` + Sensitive bool `json:"sensitive"` + SpoilerText string `json:"spoiler_text"` + Visibility string `json:"visibility"` } // Mention hold information for mention. @@ -186,7 +186,7 @@ type Mention struct { URL string `json:"url"` Username string `json:"username"` Acct string `json:"acct"` - ID int64 `json:"id"` + ID ID `json:"id"` } // Tag hold information for tag. @@ -197,7 +197,7 @@ type Tag struct { // Attachment hold information for attachment. type Attachment struct { - ID int64 `json:"id"` + ID ID `json:"id"` Type string `json:"type"` URL string `json:"url"` RemoteURL string `json:"remote_url"` @@ -214,8 +214,8 @@ type Results struct { // Pagination is a struct for specifying the get range. type Pagination struct { - MaxID int64 - SinceID int64 + MaxID ID + SinceID ID Limit int64 } @@ -245,18 +245,18 @@ func newPagination(rawlink string) (*Pagination, error) { return p, nil } -func getPaginationID(rawurl, key string) (int64, error) { +func getPaginationID(rawurl, key string) (ID, error) { u, err := url.Parse(rawurl) if err != nil { - return 0, err + return "", err } id, err := strconv.ParseInt(u.Query().Get(key), 10, 64) if err != nil { - return 0, err + return "", err } - return id, nil + return ID(fmt.Sprint(id)), nil } func (p *Pagination) toValues() url.Values { @@ -264,10 +264,10 @@ func (p *Pagination) toValues() url.Values { } func (p *Pagination) setValues(params url.Values) url.Values { - if p.MaxID > 0 { - params.Set("max_id", fmt.Sprint(p.MaxID)) - } else if p.SinceID > 0 { - params.Set("since_id", fmt.Sprint(p.SinceID)) + if p.MaxID != "" { + params.Set("max_id", string(p.MaxID)) + } else if p.SinceID != "" { + params.Set("since_id", string(p.SinceID)) } if p.Limit > 0 { params.Set("limit", fmt.Sprint(p.Limit)) diff --git a/mastodon_test.go b/mastodon_test.go index eb6c476..4651cae 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -25,26 +25,26 @@ func TestDoAPI(t *testing.T) { c := NewClient(&Config{Server: ts.URL}) var accounts []Account err := c.doAPI(context.Background(), http.MethodGet, "/", nil, &accounts, &Pagination{ - MaxID: 999, + MaxID: "999", }) if err == nil { t.Fatalf("should be fail: %v", err) } pg := &Pagination{ - MaxID: 123, - SinceID: 789, + MaxID: "123", + SinceID: "789", Limit: 10, } err = c.doAPI(context.Background(), http.MethodGet, "/", url.Values{}, &accounts, pg) if err != nil { t.Fatalf("should not be fail: %v", err) } - if pg.MaxID != 234 { - t.Fatalf("want %d but %d", 234, pg.MaxID) + if pg.MaxID != "234" { + t.Fatalf("want %q but %q", "234", pg.MaxID) } - if pg.SinceID != 890 { - t.Fatalf("want %d but %d", 890, pg.SinceID) + if pg.SinceID != "890" { + t.Fatalf("want %q but %q", "890", pg.SinceID) } if accounts[0].Username != "foo" { t.Fatalf("want %q but %q", "foo", accounts[0].Username) @@ -54,19 +54,19 @@ func TestDoAPI(t *testing.T) { } pg = &Pagination{ - MaxID: 123, - SinceID: 789, + MaxID: "123", + SinceID: "789", Limit: 10, } err = c.doAPI(context.Background(), http.MethodGet, "/", nil, &accounts, pg) if err != nil { t.Fatalf("should not be fail: %v", err) } - if pg.MaxID != 234 { - t.Fatalf("want %d but %d", 234, pg.MaxID) + if pg.MaxID != "234" { + t.Fatalf("want %q but %q", "234", pg.MaxID) } - if pg.SinceID != 890 { - t.Fatalf("want %d but %d", 890, pg.SinceID) + if pg.SinceID != "890" { + t.Fatalf("want %q but %q", "890", pg.SinceID) } if accounts[0].Username != "foo" { t.Fatalf("want %q but %q", "foo", accounts[0].Username) @@ -297,11 +297,11 @@ func TestNewPagination(t *testing.T) { if err != nil { t.Fatalf("should not be fail: %v", err) } - if pg.MaxID != 123 { - t.Fatalf("want %d but %d", 123, pg.MaxID) + if pg.MaxID != "123" { + t.Fatalf("want %q but %q", "123", pg.MaxID) } - if pg.SinceID != 789 { - t.Fatalf("want %d but %d", 789, pg.SinceID) + if pg.SinceID != "789" { + t.Fatalf("want %q but %q", "789", pg.SinceID) } } @@ -320,15 +320,15 @@ func TestGetPaginationID(t *testing.T) { if err != nil { t.Fatalf("should not be fail: %v", err) } - if id != 123 { - t.Fatalf("want %d but %d", 123, id) + if id != "123" { + t.Fatalf("want %q but %q", "123", id) } } func TestPaginationSetValues(t *testing.T) { p := &Pagination{ - MaxID: 123, - SinceID: 789, + MaxID: "123", + SinceID: "789", Limit: 10, } before := url.Values{"key": {"value"}} @@ -347,8 +347,8 @@ func TestPaginationSetValues(t *testing.T) { } p = &Pagination{ - MaxID: 0, - SinceID: 789, + MaxID: "", + SinceID: "789", } before = url.Values{} after = p.setValues(before) diff --git a/status.go b/status.go index 38ea606..b04a687 100644 --- a/status.go +++ b/status.go @@ -5,7 +5,6 @@ import ( "fmt" "net/http" "net/url" - "strconv" "time" ) @@ -208,12 +207,12 @@ func (c *Client) GetTimelineMedia(ctx context.Context, isLocal bool, pg *Paginat func (c *Client) PostStatus(ctx context.Context, toot *Toot) (*Status, error) { params := url.Values{} params.Set("status", toot.Status) - if toot.InReplyToID > 0 { - params.Set("in_reply_to_id", fmt.Sprint(toot.InReplyToID)) + if toot.InReplyToID != "" { + params.Set("in_reply_to_id", string(toot.InReplyToID)) } if toot.MediaIDs != nil { for _, media := range toot.MediaIDs { - params.Add("media_ids[]", strconv.FormatInt(media, 10)) + params.Add("media_ids[]", string(media)) } } if toot.Visibility != "" { diff --git a/status_test.go b/status_test.go index 1359bb9..e6fd767 100644 --- a/status_test.go +++ b/status_test.go @@ -534,7 +534,7 @@ func TestUploadMedia(t *testing.T) { if err != nil { t.Fatalf("should not be fail: %v", err) } - if attachment.ID != 123 { - t.Fatalf("want %q but %q", 123, attachment.ID) + if attachment.ID != "123" { + t.Fatalf("want %q but %q", "123", attachment.ID) } } From abf6939992049c9d54ee430e6289ead4303b027c Mon Sep 17 00:00:00 2001 From: Kaoru HAYAMA Date: Wed, 25 Oct 2017 13:47:36 +0900 Subject: [PATCH 002/127] Fix: Type of Notification.ID was int64. Change to ID --- notification.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification.go b/notification.go index 15e6cf7..155e417 100644 --- a/notification.go +++ b/notification.go @@ -9,7 +9,7 @@ import ( // Notification hold information for mastodon notification. type Notification struct { - ID int64 `json:"id"` + ID ID `json:"id"` Type string `json:"type"` CreatedAt time.Time `json:"created_at"` Account Account `json:"account"` From 74f003d4a4cdb85187571c071955201deccd6fa0 Mon Sep 17 00:00:00 2001 From: Kaoru HAYAMA Date: Wed, 25 Oct 2017 14:24:00 +0900 Subject: [PATCH 003/127] Fix tests about notification.ID (which type was changed on #63) --- notification_test.go | 6 +++--- streaming_ws_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/notification_test.go b/notification_test.go index 740dec9..414ebff 100644 --- a/notification_test.go +++ b/notification_test.go @@ -39,17 +39,17 @@ func TestGetNotifications(t *testing.T) { if len(ns) != 2 { t.Fatalf("result should be two: %d", len(ns)) } - if ns[0].ID != 122 { + if ns[0].ID != "122" { t.Fatalf("want %v but %v", 122, ns[0].ID) } - if ns[1].ID != 123 { + if ns[1].ID != "123" { t.Fatalf("want %v but %v", 123, ns[1].ID) } n, err := client.GetNotification(context.Background(), 123) if err != nil { t.Fatalf("should not be fail: %v", err) } - if n.ID != 123 { + if n.ID != "123" { t.Fatalf("want %v but %v", 123, n.ID) } err = client.ClearNotifications(context.Background()) diff --git a/streaming_ws_test.go b/streaming_ws_test.go index f52477c..3c8974b 100644 --- a/streaming_ws_test.go +++ b/streaming_ws_test.go @@ -117,7 +117,7 @@ func wsTest(t *testing.T, q chan Event, cancel func()) { 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 { + if events[1].(*NotificationEvent).Notification.ID != "123" { t.Fatalf("want %d but %d", 123, events[1].(*NotificationEvent).Notification.ID) } if events[2].(*DeleteEvent).ID != 1234567 { From fe913e01e56f7a0d1105126450f9ffe9a30e774b Mon Sep 17 00:00:00 2001 From: Kaoru HAYAMA Date: Wed, 25 Oct 2017 14:35:34 +0900 Subject: [PATCH 004/127] Fix: forgot to (*Client)GetNotification's parameter: int64 -> ID --- notification.go | 4 ++-- notification_test.go | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/notification.go b/notification.go index 155e417..0dfd8f8 100644 --- a/notification.go +++ b/notification.go @@ -27,9 +27,9 @@ func (c *Client) GetNotifications(ctx context.Context, pg *Pagination) ([]*Notif } // GetNotification return notification. -func (c *Client) GetNotification(ctx context.Context, id int64) (*Notification, error) { +func (c *Client) GetNotification(ctx context.Context, id ID) (*Notification, error) { var notification Notification - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/notifications/%d", id), nil, ¬ification, nil) + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/notifications/%v", id), nil, ¬ification, nil) if err != nil { return nil, err } diff --git a/notification_test.go b/notification_test.go index 414ebff..3329d02 100644 --- a/notification_test.go +++ b/notification_test.go @@ -40,17 +40,17 @@ func TestGetNotifications(t *testing.T) { t.Fatalf("result should be two: %d", len(ns)) } if ns[0].ID != "122" { - t.Fatalf("want %v but %v", 122, ns[0].ID) + t.Fatalf("want %v but %v", "122", ns[0].ID) } if ns[1].ID != "123" { - t.Fatalf("want %v but %v", 123, ns[1].ID) + t.Fatalf("want %v but %v", "123", ns[1].ID) } - n, err := client.GetNotification(context.Background(), 123) + n, err := client.GetNotification(context.Background(), "123") if err != nil { t.Fatalf("should not be fail: %v", err) } if n.ID != "123" { - t.Fatalf("want %v but %v", 123, n.ID) + t.Fatalf("want %v but %v", "123", n.ID) } err = client.ClearNotifications(context.Background()) if err != nil { From aef736e9910e95f7931aeb57e4d6171fd63a60cb Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 25 Oct 2017 15:22:17 +0900 Subject: [PATCH 005/127] fix IDs --- report.go | 7 +++---- report_test.go | 10 +++++----- status.go | 40 ++++++++++++++++++++-------------------- status_test.go | 40 ++++++++++++++++++++-------------------- streaming.go | 5 +++-- streaming_test.go | 4 ++-- streaming_ws.go | 3 ++- streaming_ws_test.go | 6 +++--- 8 files changed, 58 insertions(+), 57 deletions(-) diff --git a/report.go b/report.go index 1e6debf..94ae0dc 100644 --- a/report.go +++ b/report.go @@ -2,7 +2,6 @@ package mastodon import ( "context" - "fmt" "net/http" "net/url" ) @@ -24,11 +23,11 @@ func (c *Client) GetReports(ctx context.Context) ([]*Report, error) { } // Report reports the report -func (c *Client) Report(ctx context.Context, accountID int64, ids []int64, comment string) (*Report, error) { +func (c *Client) Report(ctx context.Context, accountID ID, ids []ID, comment string) (*Report, error) { params := url.Values{} - params.Set("account_id", fmt.Sprint(accountID)) + params.Set("account_id", string(accountID)) for _, id := range ids { - params.Add("status_ids[]", fmt.Sprint(id)) + params.Add("status_ids[]", string(id)) } params.Set("comment", comment) var report Report diff --git a/report_test.go b/report_test.go index 2d78bcf..db7fadb 100644 --- a/report_test.go +++ b/report_test.go @@ -65,26 +65,26 @@ func TestReport(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - rp, err := client.Report(context.Background(), 121, nil, "") + rp, err := client.Report(context.Background(), "121", nil, "") if err == nil { t.Fatalf("should be fail: %v", err) } - rp, err = client.Report(context.Background(), 122, nil, "") + rp, err = client.Report(context.Background(), "122", nil, "") if err != nil { t.Fatalf("should not be fail: %v", err) } if rp.ID != 1234 { - t.Fatalf("want %v but %v", 1234, rp.ID) + t.Fatalf("want %q but %q", "1234", rp.ID) } if rp.ActionTaken { t.Fatalf("want %v but %v", true, rp.ActionTaken) } - rp, err = client.Report(context.Background(), 123, []int64{567}, "") + rp, err = client.Report(context.Background(), "123", []ID{"567"}, "") if err != nil { t.Fatalf("should not be fail: %v", err) } if rp.ID != 1234 { - t.Fatalf("want %v but %v", 1234, rp.ID) + t.Fatalf("want %q but %q", "1234", rp.ID) } if !rp.ActionTaken { t.Fatalf("want %v but %v", false, rp.ActionTaken) diff --git a/status.go b/status.go index b04a687..1bfb0cd 100644 --- a/status.go +++ b/status.go @@ -57,9 +57,9 @@ func (c *Client) GetFavourites(ctx context.Context, pg *Pagination) ([]*Status, } // GetStatus return status specified by id. -func (c *Client) GetStatus(ctx context.Context, id int64) (*Status, error) { +func (c *Client) GetStatus(ctx context.Context, id ID) (*Status, error) { var status Status - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%d", id), nil, &status, nil) + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s", id), nil, &status, nil) if err != nil { return nil, err } @@ -67,9 +67,9 @@ func (c *Client) GetStatus(ctx context.Context, id int64) (*Status, error) { } // GetStatusContext return status specified by id. -func (c *Client) GetStatusContext(ctx context.Context, id int64) (*Context, error) { +func (c *Client) GetStatusContext(ctx context.Context, id ID) (*Context, error) { var context Context - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%d/context", id), nil, &context, nil) + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/context", id), nil, &context, nil) if err != nil { return nil, err } @@ -77,9 +77,9 @@ func (c *Client) GetStatusContext(ctx context.Context, id int64) (*Context, erro } // GetStatusCard return status specified by id. -func (c *Client) GetStatusCard(ctx context.Context, id int64) (*Card, error) { +func (c *Client) GetStatusCard(ctx context.Context, id ID) (*Card, error) { var card Card - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%d/card", id), nil, &card, nil) + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/card", id), nil, &card, nil) if err != nil { return nil, err } @@ -87,9 +87,9 @@ func (c *Client) GetStatusCard(ctx context.Context, id int64) (*Card, error) { } // GetRebloggedBy returns the account list of the user who reblogged the toot of id. -func (c *Client) GetRebloggedBy(ctx context.Context, id int64, pg *Pagination) ([]*Account, error) { +func (c *Client) GetRebloggedBy(ctx context.Context, id ID, pg *Pagination) ([]*Account, error) { var accounts []*Account - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%d/reblogged_by", id), nil, &accounts, pg) + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/reblogged_by", id), nil, &accounts, pg) if err != nil { return nil, err } @@ -97,9 +97,9 @@ func (c *Client) GetRebloggedBy(ctx context.Context, id int64, pg *Pagination) ( } // GetFavouritedBy returns the account list of the user who liked the toot of id. -func (c *Client) GetFavouritedBy(ctx context.Context, id int64, pg *Pagination) ([]*Account, error) { +func (c *Client) GetFavouritedBy(ctx context.Context, id ID, pg *Pagination) ([]*Account, error) { var accounts []*Account - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%d/favourited_by", id), nil, &accounts, pg) + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/favourited_by", id), nil, &accounts, pg) if err != nil { return nil, err } @@ -107,9 +107,9 @@ func (c *Client) GetFavouritedBy(ctx context.Context, id int64, pg *Pagination) } // Reblog is reblog the toot of id and return status of reblog. -func (c *Client) Reblog(ctx context.Context, id int64) (*Status, error) { +func (c *Client) Reblog(ctx context.Context, id ID) (*Status, error) { var status Status - err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%d/reblog", id), nil, &status, nil) + err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/reblog", id), nil, &status, nil) if err != nil { return nil, err } @@ -117,9 +117,9 @@ func (c *Client) Reblog(ctx context.Context, id int64) (*Status, error) { } // Unreblog is unreblog the toot of id and return status of the original toot. -func (c *Client) Unreblog(ctx context.Context, id int64) (*Status, error) { +func (c *Client) Unreblog(ctx context.Context, id ID) (*Status, error) { var status Status - err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%d/unreblog", id), nil, &status, nil) + err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unreblog", id), nil, &status, nil) if err != nil { return nil, err } @@ -127,9 +127,9 @@ func (c *Client) Unreblog(ctx context.Context, id int64) (*Status, error) { } // Favourite is favourite the toot of id and return status of the favourite toot. -func (c *Client) Favourite(ctx context.Context, id int64) (*Status, error) { +func (c *Client) Favourite(ctx context.Context, id ID) (*Status, error) { var status Status - err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%d/favourite", id), nil, &status, nil) + err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/favourite", id), nil, &status, nil) if err != nil { return nil, err } @@ -137,9 +137,9 @@ func (c *Client) Favourite(ctx context.Context, id int64) (*Status, error) { } // Unfavourite is unfavourite the toot of id and return status of the unfavourite toot. -func (c *Client) Unfavourite(ctx context.Context, id int64) (*Status, error) { +func (c *Client) Unfavourite(ctx context.Context, id ID) (*Status, error) { var status Status - err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%d/unfavourite", id), nil, &status, nil) + err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unfavourite", id), nil, &status, nil) if err != nil { return nil, err } @@ -234,8 +234,8 @@ func (c *Client) PostStatus(ctx context.Context, toot *Toot) (*Status, error) { } // DeleteStatus delete the toot. -func (c *Client) DeleteStatus(ctx context.Context, id int64) error { - return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/statuses/%d", id), nil, nil, nil) +func (c *Client) DeleteStatus(ctx context.Context, id ID) error { + return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/statuses/%s", id), nil, nil, nil) } // Search search content with query. diff --git a/status_test.go b/status_test.go index e6fd767..7c1ac29 100644 --- a/status_test.go +++ b/status_test.go @@ -53,11 +53,11 @@ func TestGetStatus(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - _, err := client.GetStatus(context.Background(), 123) + _, err := client.GetStatus(context.Background(), "123") if err == nil { t.Fatalf("should be fail: %v", err) } - status, err := client.GetStatus(context.Background(), 1234567) + status, err := client.GetStatus(context.Background(), "1234567") if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -83,11 +83,11 @@ func TestGetStatusCard(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - _, err := client.GetStatusCard(context.Background(), 123) + _, err := client.GetStatusCard(context.Background(), "123") if err == nil { t.Fatalf("should be fail: %v", err) } - card, err := client.GetStatusCard(context.Background(), 1234567) + card, err := client.GetStatusCard(context.Background(), "1234567") if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -113,11 +113,11 @@ func TestGetStatusContext(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - _, err := client.GetStatusContext(context.Background(), 123) + _, err := client.GetStatusContext(context.Background(), "123") if err == nil { t.Fatalf("should be fail: %v", err) } - context, err := client.GetStatusContext(context.Background(), 1234567) + context, err := client.GetStatusContext(context.Background(), "1234567") if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -152,11 +152,11 @@ func TestGetRebloggedBy(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - _, err := client.GetRebloggedBy(context.Background(), 123, nil) + _, err := client.GetRebloggedBy(context.Background(), "123", nil) if err == nil { t.Fatalf("should be fail: %v", err) } - rbs, err := client.GetRebloggedBy(context.Background(), 1234567, nil) + rbs, err := client.GetRebloggedBy(context.Background(), "1234567", nil) if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -188,11 +188,11 @@ func TestGetFavouritedBy(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - _, err := client.GetFavouritedBy(context.Background(), 123, nil) + _, err := client.GetFavouritedBy(context.Background(), "123", nil) if err == nil { t.Fatalf("should be fail: %v", err) } - fbs, err := client.GetFavouritedBy(context.Background(), 1234567, nil) + fbs, err := client.GetFavouritedBy(context.Background(), "1234567", nil) if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -224,11 +224,11 @@ func TestReblog(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - _, err := client.Reblog(context.Background(), 123) + _, err := client.Reblog(context.Background(), "123") if err == nil { t.Fatalf("should be fail: %v", err) } - status, err := client.Reblog(context.Background(), 1234567) + status, err := client.Reblog(context.Background(), "1234567") if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -254,11 +254,11 @@ func TestUnreblog(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - _, err := client.Unreblog(context.Background(), 123) + _, err := client.Unreblog(context.Background(), "123") if err == nil { t.Fatalf("should be fail: %v", err) } - status, err := client.Unreblog(context.Background(), 1234567) + status, err := client.Unreblog(context.Background(), "1234567") if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -284,11 +284,11 @@ func TestFavourite(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - _, err := client.Favourite(context.Background(), 123) + _, err := client.Favourite(context.Background(), "123") if err == nil { t.Fatalf("should be fail: %v", err) } - status, err := client.Favourite(context.Background(), 1234567) + status, err := client.Favourite(context.Background(), "1234567") if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -314,11 +314,11 @@ func TestUnfavourite(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - _, err := client.Unfavourite(context.Background(), 123) + _, err := client.Unfavourite(context.Background(), "123") if err == nil { t.Fatalf("should be fail: %v", err) } - status, err := client.Unfavourite(context.Background(), 1234567) + status, err := client.Unfavourite(context.Background(), "1234567") if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -449,11 +449,11 @@ func TestDeleteStatus(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - err := client.DeleteStatus(context.Background(), 123) + err := client.DeleteStatus(context.Background(), "123") if err == nil { t.Fatalf("should be fail: %v", err) } - err = client.DeleteStatus(context.Background(), 1234567) + err = client.DeleteStatus(context.Background(), "1234567") if err != nil { t.Fatalf("should not be fail: %v", err) } diff --git a/streaming.go b/streaming.go index 0d3e559..5b99180 100644 --- a/streaming.go +++ b/streaming.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "encoding/json" + "fmt" "io" "net/http" "net/url" @@ -27,7 +28,7 @@ type NotificationEvent struct { func (e *NotificationEvent) event() {} // DeleteEvent is struct for passing deletion event to app. -type DeleteEvent struct{ ID int64 } +type DeleteEvent struct{ ID ID } func (e *DeleteEvent) event() {} @@ -73,7 +74,7 @@ func handleReader(q chan Event, r io.Reader) error { var id int64 id, err = strconv.ParseInt(strings.TrimSpace(token[1]), 10, 64) if err == nil { - q <- &DeleteEvent{id} + q <- &DeleteEvent{ID(fmt.Sprintf("%20d", id))} } } if err != nil { diff --git a/streaming_test.go b/streaming_test.go index 9f5b715..686ae40 100644 --- a/streaming_test.go +++ b/streaming_test.go @@ -45,8 +45,8 @@ data: 1234567 } case *DeleteEvent: passDelete = true - if event.ID != 1234567 { - t.Fatalf("want %d but %d", 1234567, event.ID) + if event.ID != "1234567" { + t.Fatalf("want %q but %q", "1234567", event.ID) } case *ErrorEvent: passError = true diff --git a/streaming_ws.go b/streaming_ws.go index 5553a25..80e357b 100644 --- a/streaming_ws.go +++ b/streaming_ws.go @@ -3,6 +3,7 @@ package mastodon import ( "context" "encoding/json" + "fmt" "net/url" "path" @@ -127,7 +128,7 @@ func (c *WSClient) handleWS(ctx context.Context, rawurl string, q chan Event) er q <- &NotificationEvent{Notification: ¬ification} } case "delete": - q <- &DeleteEvent{ID: int64(s.Payload.(float64))} + q <- &DeleteEvent{ID: ID(fmt.Sprint(int64(s.Payload.(float64))))} } if err != nil { q <- &ErrorEvent{err} diff --git a/streaming_ws_test.go b/streaming_ws_test.go index 3c8974b..cf60530 100644 --- a/streaming_ws_test.go +++ b/streaming_ws_test.go @@ -118,10 +118,10 @@ func wsTest(t *testing.T, q chan Event, cancel func()) { t.Fatalf("want %q but %q", "foo", events[0].(*UpdateEvent).Status.Content) } if events[1].(*NotificationEvent).Notification.ID != "123" { - t.Fatalf("want %d but %d", 123, events[1].(*NotificationEvent).Notification.ID) + t.Fatalf("want %q but %q", "123", events[1].(*NotificationEvent).Notification.ID) } - if events[2].(*DeleteEvent).ID != 1234567 { - t.Fatalf("want %d but %d", 1234567, events[2].(*DeleteEvent).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) From a7e10ddba45dc3c2d78898c86580fe8e32ba8592 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 25 Oct 2017 15:23:53 +0900 Subject: [PATCH 006/127] fix ID --- cmd/mstdn/cmd_delete.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cmd/mstdn/cmd_delete.go b/cmd/mstdn/cmd_delete.go index 83755c5..092bad6 100644 --- a/cmd/mstdn/cmd_delete.go +++ b/cmd/mstdn/cmd_delete.go @@ -3,7 +3,6 @@ package main import ( "context" "errors" - "strconv" "github.com/mattn/go-mastodon" "github.com/urfave/cli" @@ -15,11 +14,7 @@ func cmdDelete(c *cli.Context) error { return errors.New("arguments required") } for i := 0; i < c.NArg(); i++ { - id, err := strconv.ParseInt(c.Args().Get(i), 10, 64) - if err != nil { - return err - } - err = client.DeleteStatus(context.Background(), id) + err := client.DeleteStatus(context.Background(), mastodon.ID(c.Args().Get(i))) if err != nil { return err } From d38a3e6dfcc25e799eaecd9fc4e7bbe5b6262ea3 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Wed, 25 Oct 2017 17:29:02 +0900 Subject: [PATCH 007/127] fix ID --- streaming.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/streaming.go b/streaming.go index 5b99180..4689ece 100644 --- a/streaming.go +++ b/streaming.go @@ -4,12 +4,10 @@ import ( "bufio" "context" "encoding/json" - "fmt" "io" "net/http" "net/url" "path" - "strconv" "strings" ) @@ -71,11 +69,7 @@ func handleReader(q chan Event, r io.Reader) error { q <- &NotificationEvent{¬ification} } case "delete": - var id int64 - id, err = strconv.ParseInt(strings.TrimSpace(token[1]), 10, 64) - if err == nil { - q <- &DeleteEvent{ID(fmt.Sprintf("%20d", id))} - } + q <- &DeleteEvent{ID(strings.TrimSpace(token[1]))} } if err != nil { q <- &ErrorEvent{err} From aaeb9f1de2219bfa992afbd862cb1e6b59f309d0 Mon Sep 17 00:00:00 2001 From: Kaoru HAYAMA Date: Wed, 25 Oct 2017 20:26:28 +0900 Subject: [PATCH 008/127] Fix: on `mstdn.exe toot -i ID`: ID (and default) was int64 -> string --- cmd/mstdn/cmd_toot.go | 2 +- cmd/mstdn/main.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/mstdn/cmd_toot.go b/cmd/mstdn/cmd_toot.go index b2e41f4..7c2ee63 100644 --- a/cmd/mstdn/cmd_toot.go +++ b/cmd/mstdn/cmd_toot.go @@ -27,7 +27,7 @@ func cmdToot(c *cli.Context) error { client := c.App.Metadata["client"].(*mastodon.Client) _, err := client.PostStatus(context.Background(), &mastodon.Toot{ Status: toot, - InReplyToID: mastodon.ID(fmt.Sprint(c.Int64("i"))), + InReplyToID: mastodon.ID(fmt.Sprint(c.String("i"))), }) return err } diff --git a/cmd/mstdn/main.go b/cmd/mstdn/main.go index 392f748..7c67584 100644 --- a/cmd/mstdn/main.go +++ b/cmd/mstdn/main.go @@ -199,10 +199,10 @@ func makeApp() *cli.App { Usage: "post utf-8 string from a file(\"-\" means STDIN)", Value: "", }, - cli.IntFlag{ + cli.StringFlag{ Name: "i", Usage: "in-reply-to", - Value: 0, + Value: "", }, }, Action: cmdToot, From 1d165763fae1f4f6dc22f78c321b47d51bfec76b Mon Sep 17 00:00:00 2001 From: utam0k Date: Wed, 25 Oct 2017 21:19:38 +0900 Subject: [PATCH 009/127] Add Emojis to Status --- mastodon.go | 7 +++++++ status.go | 1 + 2 files changed, 8 insertions(+) diff --git a/mastodon.go b/mastodon.go index 118cdcd..1ca8d6e 100644 --- a/mastodon.go +++ b/mastodon.go @@ -205,6 +205,13 @@ type Attachment struct { TextURL string `json:"text_url"` } +// Emoji hold information for CustomEmoji. +type Emoji struct { + ShortCode string `json:"shortcode"` + URL string `json:"url"` + StaticURL string `json:"static_url"` +} + // Results hold information for search result. type Results struct { Accounts []*Account `json:"accounts"` diff --git a/status.go b/status.go index 1bfb0cd..55183d6 100644 --- a/status.go +++ b/status.go @@ -20,6 +20,7 @@ type Status struct { Application Application `json:"application"` Account Account `json:"account"` MediaAttachments []Attachment `json:"media_attachments"` + Emojis []Emoji `json:"emojis"` Mentions []Mention `json:"mentions"` Tags []Tag `json:"tags"` URI string `json:"uri"` From a4d531815ff4e896b9f26bccd5fcdef0f4665610 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 26 Oct 2017 15:00:17 +0900 Subject: [PATCH 010/127] add test for CustomEmoji --- status_test.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/status_test.go b/status_test.go index 7c1ac29..283e290 100644 --- a/status_test.go +++ b/status_test.go @@ -42,7 +42,7 @@ func TestGetStatus(t *testing.T) { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } - fmt.Fprintln(w, `{"content": "zzz"}`) + fmt.Fprintln(w, `{"content": "zzz", "emojis":[{"shortcode":"💩", "url":"http://example.com", "static_url": "http://example.com/static"}]}`) return })) defer ts.Close() @@ -64,6 +64,18 @@ func TestGetStatus(t *testing.T) { if status.Content != "zzz" { t.Fatalf("want %q but %q", "zzz", status.Content) } + if len(status.Emojis) != 1 { + t.Fatal("should have emojis") + } + if status.Emojis[0].ShortCode != "💩" { + t.Fatalf("want %q but %q", "💩", status.Emojis[0]) + } + if status.Emojis[0].URL != "http://example.com" { + t.Fatalf("want %q but %q", "💩", "https://example.com") + } + if status.Emojis[0].StaticURL != "http://example.com/static" { + t.Fatalf("want %q but %q", "💩", "https://example.com/static") + } } func TestGetStatusCard(t *testing.T) { From 2866ebbdc5489783793dc6f09082ff3ad77020a2 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 26 Oct 2017 15:02:33 +0900 Subject: [PATCH 011/127] fix test --- status_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/status_test.go b/status_test.go index 283e290..5240671 100644 --- a/status_test.go +++ b/status_test.go @@ -71,10 +71,10 @@ func TestGetStatus(t *testing.T) { t.Fatalf("want %q but %q", "💩", status.Emojis[0]) } if status.Emojis[0].URL != "http://example.com" { - t.Fatalf("want %q but %q", "💩", "https://example.com") + t.Fatalf("want %q but %q", "https://example.com", static.Emojis[0].URL) } if status.Emojis[0].StaticURL != "http://example.com/static" { - t.Fatalf("want %q but %q", "💩", "https://example.com/static") + t.Fatalf("want %q but %q", "https://example.com/static", static.Emojis[0].URL) } } From bd72aa305d634a8a4a4e7f9a2bde741c21249230 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 26 Oct 2017 15:18:07 +0900 Subject: [PATCH 012/127] fix test --- status_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/status_test.go b/status_test.go index 5240671..d01531f 100644 --- a/status_test.go +++ b/status_test.go @@ -71,10 +71,10 @@ func TestGetStatus(t *testing.T) { t.Fatalf("want %q but %q", "💩", status.Emojis[0]) } if status.Emojis[0].URL != "http://example.com" { - t.Fatalf("want %q but %q", "https://example.com", static.Emojis[0].URL) + t.Fatalf("want %q but %q", "https://example.com", status.Emojis[0].URL) } if status.Emojis[0].StaticURL != "http://example.com/static" { - t.Fatalf("want %q but %q", "https://example.com/static", static.Emojis[0].URL) + t.Fatalf("want %q but %q", "https://example.com/static", status.Emojis[0].StaticURL) } } From 3002812c021427a6285a46b1d14d299e5aedf767 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 20 Nov 2017 09:58:20 +0900 Subject: [PATCH 013/127] fix #66 --- streaming.go | 2 +- streaming_ws.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/streaming.go b/streaming.go index 4689ece..b3242a4 100644 --- a/streaming.go +++ b/streaming.go @@ -69,7 +69,7 @@ func handleReader(q chan Event, r io.Reader) error { q <- &NotificationEvent{¬ification} } case "delete": - q <- &DeleteEvent{ID(strings.TrimSpace(token[1]))} + q <- &DeleteEvent{ID: ID(strings.TrimSpace(token[1]))} } if err != nil { q <- &ErrorEvent{err} diff --git a/streaming_ws.go b/streaming_ws.go index 80e357b..d027f89 100644 --- a/streaming_ws.go +++ b/streaming_ws.go @@ -3,9 +3,9 @@ package mastodon import ( "context" "encoding/json" - "fmt" "net/url" "path" + "strings" "github.com/gorilla/websocket" ) @@ -128,7 +128,7 @@ func (c *WSClient) handleWS(ctx context.Context, rawurl string, q chan Event) er q <- &NotificationEvent{Notification: ¬ification} } case "delete": - q <- &DeleteEvent{ID: ID(fmt.Sprint(int64(s.Payload.(float64))))} + q <- &DeleteEvent{ID: ID(strings.TrimSpace(s.Payload.(string)))} } if err != nil { q <- &ErrorEvent{err} From 81259792b723e0ca85222dd294949fd2b2e9d9fb Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 20 Nov 2017 10:04:53 +0900 Subject: [PATCH 014/127] handle float64/string both --- streaming_ws.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/streaming_ws.go b/streaming_ws.go index d027f89..d37fcfe 100644 --- a/streaming_ws.go +++ b/streaming_ws.go @@ -3,6 +3,7 @@ package mastodon import ( "context" "encoding/json" + "fmt" "net/url" "path" "strings" @@ -128,7 +129,11 @@ func (c *WSClient) handleWS(ctx context.Context, rawurl string, q chan Event) er q <- &NotificationEvent{Notification: ¬ification} } case "delete": - q <- &DeleteEvent{ID: ID(strings.TrimSpace(s.Payload.(string)))} + 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} From 83242d96ca5a2220bd6654f23b5c27b2b38c93e6 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 30 Nov 2017 14:53:54 +0900 Subject: [PATCH 015/127] fix #68 --- apps.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps.go b/apps.go index 6d7552f..301fa6f 100644 --- a/apps.go +++ b/apps.go @@ -27,7 +27,7 @@ type AppConfig struct { // Application is mastodon application. type Application struct { - ID int64 `json:"id"` + ID ID `json:"id"` RedirectURI string `json:"redirect_uri"` ClientID string `json:"client_id"` ClientSecret string `json:"client_secret"` From a98b28c817fa2c903fa8d825c1c77bb03cafc76d Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 30 Nov 2017 14:55:18 +0900 Subject: [PATCH 016/127] add test --- apps_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps_test.go b/apps_test.go index bab20bf..f62bf15 100644 --- a/apps_test.go +++ b/apps_test.go @@ -26,7 +26,7 @@ func TestRegisterApp(t *testing.T) { fmt.Fprintln(w, `Apps`) return } - fmt.Fprintln(w, `{"client_id": "foo", "client_secret": "bar"}`) + fmt.Fprintln(w, `{"id": 123, "client_id": "foo", "client_secret": "bar"}`) return })) defer ts.Close() @@ -64,6 +64,9 @@ func TestRegisterApp(t *testing.T) { if err != nil { t.Fatalf("should not be fail: %v", err) } + if string(app.ID) != "123" { + t.Fatalf("want %q but %q", "bar", app.ClientSecret) + } if app.ClientID != "foo" { t.Fatalf("want %q but %q", "foo", app.ClientID) } From e0de6af20986117d5466e233bc162a2dbbf8824f Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Mon, 29 Jan 2018 12:29:39 +0900 Subject: [PATCH 017/127] Add GetInstanceActivity and GetInstancePeers --- README.md | 2 ++ instance.go | 28 +++++++++++++++++++++ instance_test.go | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ unixtime.go | 20 +++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 unixtime.go diff --git a/README.md b/README.md index a66ccb7..efc5630 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,8 @@ func main() { * [x] POST /api/v1/follow_requests/:id/reject * [x] POST /api/v1/follows * [x] GET /api/v1/instance +* [x] GET /api/v1/instance/activity +* [x] GET /api/v1/instance/peers * [x] POST /api/v1/media * [x] GET /api/v1/mutes * [x] GET /api/v1/notifications diff --git a/instance.go b/instance.go index 9b45c25..3d45591 100644 --- a/instance.go +++ b/instance.go @@ -22,3 +22,31 @@ func (c *Client) GetInstance(ctx context.Context) (*Instance, error) { } return &instance, nil } + +// WeeklyActivity hold information for mastodon weekly activity. +type WeeklyActivity struct { + Week Unixtime `json:"week"` + Statuses int64 `json:"statuses,string"` + Logins int64 `json:"logins,string"` + Registrations int64 `json:"registrations,string"` +} + +// GetInstanceActivity return instance activity. +func (c *Client) GetInstanceActivity(ctx context.Context) ([]*WeeklyActivity, error) { + var activity []*WeeklyActivity + err := c.doAPI(ctx, http.MethodGet, "/api/v1/instance/activity", nil, &activity, nil) + if err != nil { + return nil, err + } + return activity, nil +} + +// GetInstancePeers return instance peers. +func (c *Client) GetInstancePeers(ctx context.Context) ([]string, error) { + var peers []string + err := c.doAPI(ctx, http.MethodGet, "/api/v1/instance/peers", nil, &peers, nil) + if err != nil { + return nil, err + } + return peers, nil +} diff --git a/instance_test.go b/instance_test.go index 6724b53..b811c6c 100644 --- a/instance_test.go +++ b/instance_test.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" ) func TestGetInstance(t *testing.T) { @@ -38,3 +39,65 @@ func TestGetInstance(t *testing.T) { t.Fatalf("want %q but %q", "mastodon", ins.Title) } } + +func TestGetInstanceActivity(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), http.StatusInternalServerError) + return + } + fmt.Fprintln(w, `[{"week":"1516579200","statuses":"1","logins":"1","registrations":"0"}]`) + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + }) + _, err := client.GetInstanceActivity(context.Background()) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + activity, err := client.GetInstanceActivity(context.Background()) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if activity[0].Week != Unixtime(time.Unix(1516579200, 0)) { + t.Fatalf("want %q but %q", Unixtime(time.Unix(1516579200, 0)), activity[0].Week) + } + if activity[0].Logins != 1 { + t.Fatalf("want %q but %q", 1, activity[0].Logins) + } +} + +func TestGetInstancePeers(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), http.StatusInternalServerError) + return + } + fmt.Fprintln(w, `["mastodon.social","mstdn.jp"]`) + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + }) + _, err := client.GetInstancePeers(context.Background()) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + peers, err := client.GetInstancePeers(context.Background()) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if peers[0] != "mastodon.social" { + t.Fatalf("want %q but %q", "mastodon.social", peers[0]) + } + if peers[1] != "mstdn.jp" { + t.Fatalf("want %q but %q", "mstdn.jp", peers[1]) + } +} diff --git a/unixtime.go b/unixtime.go new file mode 100644 index 0000000..a935a9e --- /dev/null +++ b/unixtime.go @@ -0,0 +1,20 @@ +package mastodon + +import ( + "strconv" + "time" +) + +type Unixtime time.Time + +func (t *Unixtime) UnmarshalJSON(data []byte) error { + if len(data) > 0 && data[0] == '"' && data[len(data)-1] == '"' { + data = data[1 : len(data)-1] + } + ts, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return err + } + *t = Unixtime(time.Unix(ts, 0)) + return nil +} From f505a4f6ae937ed72ef3b0b257d0a776b1dd5a2d Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 29 Jan 2018 13:21:56 +0900 Subject: [PATCH 018/127] fix test --- instance_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instance_test.go b/instance_test.go index b811c6c..55c94be 100644 --- a/instance_test.go +++ b/instance_test.go @@ -64,7 +64,7 @@ func TestGetInstanceActivity(t *testing.T) { t.Fatalf("should not be fail: %v", err) } if activity[0].Week != Unixtime(time.Unix(1516579200, 0)) { - t.Fatalf("want %q but %q", Unixtime(time.Unix(1516579200, 0)), activity[0].Week) + t.Fatalf("want %v but %v", Unixtime(time.Unix(1516579200, 0)), activity[0].Week) } if activity[0].Logins != 1 { t.Fatalf("want %q but %q", 1, activity[0].Logins) From 0022a536491584edc2132b4e39b5e6e547b6d3e6 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 29 Jan 2018 13:15:06 +0900 Subject: [PATCH 019/127] add instance_activity/instance_peers command --- cmd/mstdn/cmd_instance_activity.go | 24 ++++++++++++++++++++++++ cmd/mstdn/cmd_instance_peers.go | 21 +++++++++++++++++++++ cmd/mstdn/main.go | 10 ++++++++++ 3 files changed, 55 insertions(+) create mode 100644 cmd/mstdn/cmd_instance_activity.go create mode 100644 cmd/mstdn/cmd_instance_peers.go diff --git a/cmd/mstdn/cmd_instance_activity.go b/cmd/mstdn/cmd_instance_activity.go new file mode 100644 index 0000000..53a71ff --- /dev/null +++ b/cmd/mstdn/cmd_instance_activity.go @@ -0,0 +1,24 @@ +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, "EMail : %v\n", activity.Week) + } + return nil +} diff --git a/cmd/mstdn/cmd_instance_peers.go b/cmd/mstdn/cmd_instance_peers.go new file mode 100644 index 0000000..7a73d1d --- /dev/null +++ b/cmd/mstdn/cmd_instance_peers.go @@ -0,0 +1,21 @@ +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 +} diff --git a/cmd/mstdn/main.go b/cmd/mstdn/main.go index 7c67584..f17e27a 100644 --- a/cmd/mstdn/main.go +++ b/cmd/mstdn/main.go @@ -245,6 +245,16 @@ func makeApp() *cli.App { 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", From b9e51b29169f3552249eec2d15f7755a93b27940 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 29 Jan 2018 13:16:31 +0900 Subject: [PATCH 020/127] fix typo --- cmd/mstdn/cmd_instance_activity.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/mstdn/cmd_instance_activity.go b/cmd/mstdn/cmd_instance_activity.go index 53a71ff..199ba99 100644 --- a/cmd/mstdn/cmd_instance_activity.go +++ b/cmd/mstdn/cmd_instance_activity.go @@ -18,7 +18,7 @@ func cmdInstanceActivity(c *cli.Context) error { 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, "EMail : %v\n", activity.Week) + fmt.Fprintf(c.App.Writer, "Week : %v\n", activity.Week) } return nil } From faab8cdc271e3647a94b0efa3f2424202f8db65f Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Mon, 29 Jan 2018 13:09:30 +0900 Subject: [PATCH 021/127] Add new attributes for instance --- instance.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/instance.go b/instance.go index 3d45591..526983b 100644 --- a/instance.go +++ b/instance.go @@ -7,10 +7,21 @@ import ( // Instance hold information for mastodon instance. type Instance struct { - URI string `json:"uri"` - Title string `json:"title"` - Description string `json:"description"` - EMail string `json:"email"` + URI string `json:"uri"` + Title string `json:"title"` + Description string `json:"description"` + EMail string `json:"email"` + Version string `json:"version,omitempty"` + URLs map[string]string `json:"urls,omitempty"` + Stats InstanceStats `json:"stats,omitempty"` + Thumbnail string `json:"thumbnail,omitempty"` +} + +// InstanceStats hold information for mastodon instance stats. +type InstanceStats struct { + UserCount int64 `json:"user_count"` + StatusCount int64 `json:"status_count"` + DomainCount int64 `json:"domain_count"` } // GetInstance return Instance. From f0445dd4da0219d461323be164bc72c17ec0116c Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Mon, 29 Jan 2018 13:19:36 +0900 Subject: [PATCH 022/127] fix --- instance.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/instance.go b/instance.go index 526983b..cfd33bd 100644 --- a/instance.go +++ b/instance.go @@ -13,7 +13,7 @@ type Instance struct { EMail string `json:"email"` Version string `json:"version,omitempty"` URLs map[string]string `json:"urls,omitempty"` - Stats InstanceStats `json:"stats,omitempty"` + Stats *InstanceStats `json:"stats,omitempty"` Thumbnail string `json:"thumbnail,omitempty"` } From 5d863ccf7964630c4b27da1100ab938f9296f848 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 29 Jan 2018 13:44:30 +0900 Subject: [PATCH 023/127] add tests for instance --- instance_test.go | 75 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/instance_test.go b/instance_test.go index 55c94be..4a87ae3 100644 --- a/instance_test.go +++ b/instance_test.go @@ -17,7 +17,7 @@ func TestGetInstance(t *testing.T) { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - fmt.Fprintln(w, `{"title": "mastodon"}`) + fmt.Fprintln(w, `{"title": "mastodon", "uri": "http://mstdn.example.com", "description": "test mastodon", "email": "mstdn@mstdn.example.com"}`) })) defer ts.Close() @@ -38,6 +38,79 @@ func TestGetInstance(t *testing.T) { if ins.Title != "mastodon" { t.Fatalf("want %q but %q", "mastodon", ins.Title) } + if ins.URI != "http://mstdn.example.com" { + t.Fatalf("want %q but %q", "http://mstdn.example.com", ins.URI) + } + if ins.Description != "test mastodon" { + t.Fatalf("want %q but %q", "test mastodon", ins.Description) + } + if ins.EMail != "mstdn@mstdn.example.com" { + t.Fatalf("want %q but %q", "mstdn@mstdn.example.com", ins.EMail) + } +} + +func TestGetInstanceMore(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), http.StatusInternalServerError) + return + } + fmt.Fprintln(w, `{"title": "mastodon", "uri": "http://mstdn.example.com", "description": "test mastodon", "email": "mstdn@mstdn.example.com", "version": "0.0.1", "urls":{"foo":"http://stream1.example.com", "bar": "http://stream2.example.com"}, "thumbnail": "http://mstdn.example.com/logo.png", "stats":{"user_count":1, "status_count":2, "domain_count":3}}}`) + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + _, err := client.GetInstance(context.Background()) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + ins, err := client.GetInstance(context.Background()) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if ins.Title != "mastodon" { + t.Fatalf("want %q but %q", "mastodon", ins.Title) + } + if ins.URI != "http://mstdn.example.com" { + t.Fatalf("want %q but %q", "mastodon", ins.URI) + } + if ins.Description != "test mastodon" { + t.Fatalf("want %q but %q", "test mastodon", ins.Description) + } + if ins.EMail != "mstdn@mstdn.example.com" { + t.Fatalf("want %q but %q", "mstdn@mstdn.example.com", ins.EMail) + } + if ins.Version != "0.0.1" { + t.Fatalf("want %q but %q", "0.0.1", ins.Version) + } + if ins.URLs["foo"] != "http://stream1.example.com" { + t.Fatalf("want %q but %q", "http://stream1.example.com", ins.Version) + } + if ins.URLs["bar"] != "http://stream2.example.com" { + t.Fatalf("want %q but %q", "http://stream2.example.com", ins.Version) + } + if ins.Thumbnail != "http://mstdn.example.com/logo.png" { + t.Fatalf("want %q but %q", "http://mstdn.example.com/logo.png", ins.Thumbnail) + } + if ins.Stats == nil { + t.Fatal("status should be nil") + } + if ins.Stats.UserCount != 1 { + t.Fatalf("want %v but %v", 1, ins.Stats.UserCount) + } + if ins.Stats.StatusCount != 2 { + t.Fatalf("want %v but %v", 2, ins.Stats.StatusCount) + } + if ins.Stats.DomainCount != 3 { + t.Fatalf("want %v but %v", 3, ins.Stats.DomainCount) + } } func TestGetInstanceActivity(t *testing.T) { From 2ccbcfe14d7ae9129fa84fbf37d030b046abbfdf Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 29 Jan 2018 13:57:17 +0900 Subject: [PATCH 024/127] add some fields for instance command --- cmd/mstdn/cmd_instance.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/cmd/mstdn/cmd_instance.go b/cmd/mstdn/cmd_instance.go index b287efc..5932c68 100644 --- a/cmd/mstdn/cmd_instance.go +++ b/cmd/mstdn/cmd_instance.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "sort" "github.com/mattn/go-mastodon" "github.com/urfave/cli" @@ -18,5 +19,26 @@ func cmdInstance(c *cli.Context) error { 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 } From 48920165ef264ed2fcdd68b796c7c5a6811c3ba2 Mon Sep 17 00:00:00 2001 From: Spotlight Date: Sun, 22 Jul 2018 12:36:42 -0500 Subject: [PATCH 025/127] Correct the spelling of sensitive --- status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/status.go b/status.go index 55183d6..3f7c57c 100644 --- a/status.go +++ b/status.go @@ -220,7 +220,7 @@ func (c *Client) PostStatus(ctx context.Context, toot *Toot) (*Status, error) { params.Set("visibility", fmt.Sprint(toot.Visibility)) } if toot.Sensitive { - params.Set("senstitive", "true") + params.Set("sensitive", "true") } if toot.SpoilerText != "" { params.Set("spoiler_text", toot.SpoilerText) From 61705d1f2b8c5fe255df9f3070d10c9ad5b0d99c Mon Sep 17 00:00:00 2001 From: Ben Lubar Date: Sat, 20 Oct 2018 16:47:08 -0500 Subject: [PATCH 026/127] Add missing fields in Account. See https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md#account Also fixes a spurious pagination-related error. --- accounts.go | 11 +++++++++++ mastodon.go | 7 ++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/accounts.go b/accounts.go index 1e9abb3..56890f6 100644 --- a/accounts.go +++ b/accounts.go @@ -25,6 +25,17 @@ type Account struct { AvatarStatic string `json:"avatar_static"` Header string `json:"header"` HeaderStatic string `json:"header_static"` + Emojis []Emoji `json:"emojis"` + Moved *Account `json:"moved"` + Fields []Field `json:"fields"` + Bot bool `json:"bot"` +} + +// Field is a Mastodon account profile field. +type Field struct { + Name string `json:"name"` + Value string `json:"value"` + VerifiedAt time.Time `json:"verified_at"` } // GetAccount return Account. diff --git a/mastodon.go b/mastodon.go index 1ca8d6e..68db1ba 100644 --- a/mastodon.go +++ b/mastodon.go @@ -258,7 +258,12 @@ func getPaginationID(rawurl, key string) (ID, error) { return "", err } - id, err := strconv.ParseInt(u.Query().Get(key), 10, 64) + val := u.Query().Get(key) + if val == "" { + return "", nil + } + + id, err := strconv.ParseInt(val, 10, 64) if err != nil { return "", err } From 2ebf34adaea93dd857bc5dbd3d736020541d5cf4 Mon Sep 17 00:00:00 2001 From: Strubbl <97055+Strubbl@users.noreply.github.com> Date: Tue, 13 Nov 2018 19:44:17 +0100 Subject: [PATCH 027/127] add info about missing lists APIs in Readme https://docs.joinmastodon.org/api/rest/lists/ --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index efc5630..d09de3c 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ func main() { * [x] GET /api/v1/accounts/:id/unblock * [x] GET /api/v1/accounts/:id/mute * [x] GET /api/v1/accounts/:id/unmute +* [ ] GET /api/v1/accounts/:id/lists * [x] GET /api/v1/accounts/relationships * [x] GET /api/v1/accounts/search * [x] POST /api/v1/apps @@ -94,6 +95,14 @@ func main() { * [x] GET /api/v1/instance * [x] GET /api/v1/instance/activity * [x] GET /api/v1/instance/peers +* [ ] GET /api/v1/lists +* [ ] GET /api/v1/lists/:id/accounts +* [ ] GET /api/v1/lists/:id +* [ ] POST /api/v1/lists +* [ ] PUT /api/v1/lists/:id +* [ ] DELETE /api/v1/lists/:id +* [ ] POST /api/v1/lists/:id/accounts +* [ ] DELETE /api/v1/lists/:id/accounts * [x] POST /api/v1/media * [x] GET /api/v1/mutes * [x] GET /api/v1/notifications From c5945152ec75beb206ec117434e57084a4fca2dc Mon Sep 17 00:00:00 2001 From: Ben Lubar Date: Wed, 21 Nov 2018 22:38:04 -0600 Subject: [PATCH 028/127] Add convenience function to authenticate using OAuth2. This is required for users who have 2-factor authentication enabled, and is generally safer because users don't need to give their password to third-party software. --- apps.go | 21 ++++++++++++++++++++- mastodon.go | 38 +++++++++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/apps.go b/apps.go index 301fa6f..b03f9b1 100644 --- a/apps.go +++ b/apps.go @@ -18,7 +18,9 @@ type AppConfig struct { // 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". + // This can be a space-separated list of items listed on the /settings/applications/new page of any Mastodon + // instance. "read", "write", and "follow" are top-level scopes that include all the permissions of the more + // specific scopes like "read:favourites", "write:statuses", and "write:follows". Scopes string // Optional. @@ -31,6 +33,9 @@ type Application struct { RedirectURI string `json:"redirect_uri"` ClientID string `json:"client_id"` ClientSecret string `json:"client_secret"` + + // AuthURI is not part of the Mastodon API; it is generated by go-mastodon. + AuthURI string `json:"auth_uri,omitempty"` } // RegisterApp returns the mastodon application. @@ -73,5 +78,19 @@ func RegisterApp(ctx context.Context, appConfig *AppConfig) (*Application, error return nil, err } + u, err = url.Parse(appConfig.Server) + if err != nil { + return nil, err + } + u.Path = path.Join(u.Path, "/oauth/authorize") + u.RawQuery = url.Values{ + "scope": {appConfig.Scopes}, + "response_type": {"code"}, + "redirect_uri": {app.RedirectURI}, + "client_id": {app.ClientID}, + }.Encode() + + app.AuthURI = u.String() + return &app, nil } diff --git a/mastodon.go b/mastodon.go index 68db1ba..a856606 100644 --- a/mastodon.go +++ b/mastodon.go @@ -130,14 +130,34 @@ func NewClient(config *Config) *Client { // Authenticate get access-token to the API. func (c *Client) Authenticate(ctx context.Context, username, password string) error { - params := url.Values{} - params.Set("client_id", c.config.ClientID) - params.Set("client_secret", c.config.ClientSecret) - params.Set("grant_type", "password") - params.Set("username", username) - params.Set("password", password) - params.Set("scope", "read write follow") + params := url.Values{ + "client_id": {c.config.ClientID}, + "client_secret": {c.config.ClientSecret}, + "grant_type": {"password"}, + "username": {username}, + "password": {password}, + "scope": {"read write follow"}, + } + return c.authenticate(ctx, params) +} + +// AuthenticateToken logs in using a grant token returned by Application.AuthURI. +// +// redirectURI should be the same as Application.RedirectURI. +func (c *Client) AuthenticateToken(ctx context.Context, authCode, redirectURI string) error { + params := url.Values{ + "client_id": {c.config.ClientID}, + "client_secret": {c.config.ClientSecret}, + "grant_type": {"authorization_code"}, + "code": {authCode}, + "redirect_uri": {redirectURI}, + } + + return c.authenticate(ctx, params) +} + +func (c *Client) authenticate(ctx context.Context, params url.Values) error { u, err := url.Parse(c.config.Server) if err != nil { return err @@ -160,9 +180,9 @@ func (c *Client) Authenticate(ctx context.Context, username, password string) er return parseAPIError("bad authorization", resp) } - res := struct { + var res struct { AccessToken string `json:"access_token"` - }{} + } err = json.NewDecoder(resp.Body).Decode(&res) if err != nil { return err From 3daf61de23f0f1b078f5e1ad25d9b785bb9d3fc3 Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Sat, 24 Nov 2018 06:51:54 +0100 Subject: [PATCH 029/127] Fixed tests for Go 1.11 --- status_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/status_test.go b/status_test.go index d01531f..eb6a403 100644 --- a/status_test.go +++ b/status_test.go @@ -68,7 +68,7 @@ func TestGetStatus(t *testing.T) { t.Fatal("should have emojis") } if status.Emojis[0].ShortCode != "💩" { - t.Fatalf("want %q but %q", "💩", status.Emojis[0]) + t.Fatalf("want %q but %q", "💩", status.Emojis[0].ShortCode) } if status.Emojis[0].URL != "http://example.com" { t.Fatalf("want %q but %q", "https://example.com", status.Emojis[0].URL) From 4def10a243942ff1ce05750f4d1846f3917bd502 Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Sat, 24 Nov 2018 06:46:49 +0100 Subject: [PATCH 030/127] Updated entities and json structs for API v2.6.0 Added missing fields to various structs and sorted them in the same order as the original mastodon API documentation does. This should make it easier to compare go-mastodon's structs with the original documentation. --- accounts.go | 16 ++++++++++------ instance.go | 18 ++++++++++-------- mastodon.go | 32 +++++++++++++++++++++----------- status.go | 45 +++++++++++++++++++++++++++++---------------- 4 files changed, 70 insertions(+), 41 deletions(-) diff --git a/accounts.go b/accounts.go index 56890f6..ceb3674 100644 --- a/accounts.go +++ b/accounts.go @@ -136,12 +136,16 @@ func (c *Client) GetBlocks(ctx context.Context, pg *Pagination) ([]*Account, err // Relationship hold information for relation-ship to the account. type Relationship struct { - ID ID `json:"id"` - Following bool `json:"following"` - FollowedBy bool `json:"followed_by"` - Blocking bool `json:"blocking"` - Muting bool `json:"muting"` - Requested bool `json:"requested"` + ID ID `json:"id"` + Following bool `json:"following"` + FollowedBy bool `json:"followed_by"` + Blocking bool `json:"blocking"` + Muting bool `json:"muting"` + MutingNotifications bool `json:"muting_notifications"` + Requested bool `json:"requested"` + DomainBlocking bool `json:"domain_blocking"` + ShowingReblogs bool `json:"showing_reblogs"` + Endorsed bool `json:"endorsed"` } // AccountFollow follow the account. diff --git a/instance.go b/instance.go index cfd33bd..3217450 100644 --- a/instance.go +++ b/instance.go @@ -7,14 +7,16 @@ import ( // Instance hold information for mastodon instance. type Instance struct { - URI string `json:"uri"` - Title string `json:"title"` - Description string `json:"description"` - EMail string `json:"email"` - Version string `json:"version,omitempty"` - URLs map[string]string `json:"urls,omitempty"` - Stats *InstanceStats `json:"stats,omitempty"` - Thumbnail string `json:"thumbnail,omitempty"` + URI string `json:"uri"` + Title string `json:"title"` + Description string `json:"description"` + EMail string `json:"email"` + Version string `json:"version,omitempty"` + Thumbnail string `json:"thumbnail,omitempty"` + URLs map[string]string `json:"urls,omitempty"` + Stats *InstanceStats `json:"stats,omitempty"` + Languages []string `json:"languages"` + ContactAccount *Account `json:"account"` } // InstanceStats hold information for mastodon instance stats. diff --git a/mastodon.go b/mastodon.go index a856606..12acedb 100644 --- a/mastodon.go +++ b/mastodon.go @@ -211,25 +211,35 @@ type Mention struct { // Tag hold information for tag. type Tag struct { - Name string `json:"name"` - URL string `json:"url"` + Name string `json:"name"` + URL string `json:"url"` + History []History `json:"history"` +} + +// History hold information for history. +type History struct { + Day string `json:"day"` + Uses int64 `json:"uses"` + Accounts int64 `json:"accounts"` } // Attachment hold information for attachment. type Attachment struct { - ID ID `json:"id"` - Type string `json:"type"` - URL string `json:"url"` - RemoteURL string `json:"remote_url"` - PreviewURL string `json:"preview_url"` - TextURL string `json:"text_url"` + ID ID `json:"id"` + Type string `json:"type"` + URL string `json:"url"` + RemoteURL string `json:"remote_url"` + PreviewURL string `json:"preview_url"` + TextURL string `json:"text_url"` + Description string `json:"description"` } // Emoji hold information for CustomEmoji. type Emoji struct { - ShortCode string `json:"shortcode"` - URL string `json:"url"` - StaticURL string `json:"static_url"` + ShortCode string `json:"shortcode"` + StaticURL string `json:"static_url"` + URL string `json:"url"` + VisibleInPicker bool `json:"visible_in_picker"` } // Results hold information for search result. diff --git a/status.go b/status.go index 3f7c57c..cfa9911 100644 --- a/status.go +++ b/status.go @@ -11,26 +11,31 @@ import ( // Status is struct to hold status. type Status struct { ID ID `json:"id"` - CreatedAt time.Time `json:"created_at"` + URI string `json:"uri"` + URL string `json:"url"` + Account Account `json:"account"` InReplyToID interface{} `json:"in_reply_to_id"` InReplyToAccountID interface{} `json:"in_reply_to_account_id"` + Reblog *Status `json:"reblog"` + Content string `json:"content"` + CreatedAt time.Time `json:"created_at"` + Emojis []Emoji `json:"emojis"` + RepliesCount int64 `json:"replies_count"` + ReblogsCount int64 `json:"reblogs_count"` + FavouritesCount int64 `json:"favourites_count"` + Reblogged interface{} `json:"reblogged"` + Favourited interface{} `json:"favourited"` + Muted interface{} `json:"muted"` Sensitive bool `json:"sensitive"` SpoilerText string `json:"spoiler_text"` Visibility string `json:"visibility"` - Application Application `json:"application"` - Account Account `json:"account"` MediaAttachments []Attachment `json:"media_attachments"` - Emojis []Emoji `json:"emojis"` Mentions []Mention `json:"mentions"` Tags []Tag `json:"tags"` - URI string `json:"uri"` - Content string `json:"content"` - URL string `json:"url"` - ReblogsCount int64 `json:"reblogs_count"` - FavouritesCount int64 `json:"favourites_count"` - Reblog *Status `json:"reblog"` - Favourited interface{} `json:"favourited"` - Reblogged interface{} `json:"reblogged"` + Card *Card `json:"card"` + Application Application `json:"application"` + Language string `json:"language"` + Pinned interface{} `json:"pinned"` } // Context hold information for mastodon context. @@ -41,10 +46,18 @@ type Context struct { // Card hold information for mastodon card. type Card struct { - URL string `json:"url"` - Title string `json:"title"` - Description string `json:"description"` - Image string `json:"image"` + URL string `json:"url"` + Title string `json:"title"` + Description string `json:"description"` + Image string `json:"image"` + Type string `json:"type"` + AuthorName string `json:"author_name"` + AuthorURL string `json:"author_url"` + ProviderName string `json:"provider_name"` + ProviderURL string `json:"provider_url"` + HTML string `json:"html"` + Width int64 `json:"width"` + Height int64 `json:"height"` } // GetFavourites return the favorite list of the current user. From 814e71920d6a491e25dd72eb227f9f9e85655bcc Mon Sep 17 00:00:00 2001 From: Jessica Paczuski Date: Tue, 29 Jan 2019 10:24:07 +0100 Subject: [PATCH 031/127] Add Meta field to Attachment --- mastodon.go | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/mastodon.go b/mastodon.go index 12acedb..0599445 100644 --- a/mastodon.go +++ b/mastodon.go @@ -225,13 +225,28 @@ type History struct { // Attachment hold information for attachment. type Attachment struct { - ID ID `json:"id"` - Type string `json:"type"` - URL string `json:"url"` - RemoteURL string `json:"remote_url"` - PreviewURL string `json:"preview_url"` - TextURL string `json:"text_url"` - Description string `json:"description"` + ID ID `json:"id"` + Type string `json:"type"` + URL string `json:"url"` + RemoteURL string `json:"remote_url"` + PreviewURL string `json:"preview_url"` + TextURL string `json:"text_url"` + Description string `json:"description"` + Meta AttachmentMeta `json:"meta"` +} + +// AttachmentMeta holds information for attachment metadata. +type AttachmentMeta struct { + Original Size `json:"original"` + Small Size `json:"small"` +} + +// Size holds information for attatchment size. +type Size struct { + Width int64 `json:"width"` + Height int64 `json:"height"` + Size string `json:"size"` + Aspect float64 `json:"aspect"` } // Emoji hold information for CustomEmoji. From 6bf95fc751916ce7a8d8c70bc7571a19ed60a8fb Mon Sep 17 00:00:00 2001 From: Jessica Paczuski Date: Tue, 29 Jan 2019 11:24:00 +0100 Subject: [PATCH 032/127] Rename Size -> AttachmentSize --- mastodon.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mastodon.go b/mastodon.go index 0599445..3919f85 100644 --- a/mastodon.go +++ b/mastodon.go @@ -237,12 +237,12 @@ type Attachment struct { // AttachmentMeta holds information for attachment metadata. type AttachmentMeta struct { - Original Size `json:"original"` - Small Size `json:"small"` + Original AttachmentSize `json:"original"` + Small AttachmentSize `json:"small"` } -// Size holds information for attatchment size. -type Size struct { +// AttachmentSize holds information for attatchment size. +type AttachmentSize struct { Width int64 `json:"width"` Height int64 `json:"height"` Size string `json:"size"` From b8bb5ae68c410198be4934bc93c110c666746c46 Mon Sep 17 00:00:00 2001 From: 178inaba <178inaba@users.noreply.github.com> Date: Thu, 14 Feb 2019 22:23:25 +0900 Subject: [PATCH 033/127] Add MinID to Pagination --- mastodon.go | 13 ++++++++++++- mastodon_test.go | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/mastodon.go b/mastodon.go index 3919f85..d6f8d60 100644 --- a/mastodon.go +++ b/mastodon.go @@ -268,6 +268,7 @@ type Results struct { type Pagination struct { MaxID ID SinceID ID + MinID ID Limit int64 } @@ -291,6 +292,12 @@ func newPagination(rawlink string) (*Pagination, error) { return nil, err } p.SinceID = sinceID + + minID, err := getPaginationID(link.URL, "min_id") + if err != nil { + return nil, err + } + p.MinID = minID } } @@ -323,9 +330,13 @@ func (p *Pagination) toValues() url.Values { func (p *Pagination) setValues(params url.Values) url.Values { if p.MaxID != "" { params.Set("max_id", string(p.MaxID)) - } else if p.SinceID != "" { + } + if p.SinceID != "" { params.Set("since_id", string(p.SinceID)) } + if p.MinID != "" { + params.Set("min_id", string(p.MinID)) + } if p.Limit > 0 { params.Set("limit", fmt.Sprint(p.Limit)) } diff --git a/mastodon_test.go b/mastodon_test.go index 4651cae..e76cf3c 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -339,7 +339,7 @@ func TestPaginationSetValues(t *testing.T) { if after.Get("max_id") != "123" { t.Fatalf("want %q but %q", "123", after.Get("max_id")) } - if after.Get("since_id") != "" { + if after.Get("since_id") != "789" { t.Fatalf("result should be empty string: %q", after.Get("since_id")) } if after.Get("limit") != "10" { From 5fd7d16157835f504da1c997e260a29f68dad97b Mon Sep 17 00:00:00 2001 From: 178inaba <178inaba@users.noreply.github.com> Date: Thu, 14 Feb 2019 22:54:45 +0900 Subject: [PATCH 034/127] Fix followers command Set empty for SinceID. --- cmd/mstdn/cmd_followers.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/mstdn/cmd_followers.go b/cmd/mstdn/cmd_followers.go index 1b94430..ee5a86b 100644 --- a/cmd/mstdn/cmd_followers.go +++ b/cmd/mstdn/cmd_followers.go @@ -28,6 +28,7 @@ func cmdFollowers(c *cli.Context) error { if pg.MaxID == "" { break } + pg.SinceID = "" time.Sleep(10 * time.Second) } s := newScreen(config) From 1fcdf9f501428b76a70824a6eb286708fb52f553 Mon Sep 17 00:00:00 2001 From: 178inaba <178inaba@users.noreply.github.com> Date: Thu, 14 Feb 2019 23:21:45 +0900 Subject: [PATCH 035/127] Fix TestPaginationSetValues --- mastodon_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mastodon_test.go b/mastodon_test.go index e76cf3c..62ec165 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -328,7 +328,8 @@ func TestGetPaginationID(t *testing.T) { func TestPaginationSetValues(t *testing.T) { p := &Pagination{ MaxID: "123", - SinceID: "789", + SinceID: "456", + MinID: "789", Limit: 10, } before := url.Values{"key": {"value"}} @@ -339,7 +340,10 @@ func TestPaginationSetValues(t *testing.T) { if after.Get("max_id") != "123" { t.Fatalf("want %q but %q", "123", after.Get("max_id")) } - if after.Get("since_id") != "789" { + if after.Get("since_id") != "456" { + t.Fatalf("result should be empty string: %q", after.Get("since_id")) + } + if after.Get("min_id") != "789" { t.Fatalf("result should be empty string: %q", after.Get("since_id")) } if after.Get("limit") != "10" { From 460b971f5479f8de35b748fd9a7213f53dbc4178 Mon Sep 17 00:00:00 2001 From: 178inaba <178inaba@users.noreply.github.com> Date: Thu, 14 Feb 2019 23:27:32 +0900 Subject: [PATCH 036/127] Fix TestPaginationSetValues --- mastodon_test.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mastodon_test.go b/mastodon_test.go index 62ec165..5fdefe4 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -341,10 +341,10 @@ func TestPaginationSetValues(t *testing.T) { t.Fatalf("want %q but %q", "123", after.Get("max_id")) } if after.Get("since_id") != "456" { - t.Fatalf("result should be empty string: %q", after.Get("since_id")) + t.Fatalf("want %q but %q", "456", after.Get("since_id")) } if after.Get("min_id") != "789" { - t.Fatalf("result should be empty string: %q", after.Get("since_id")) + t.Fatalf("want %q but %q", "789", after.Get("min_id")) } if after.Get("limit") != "10" { t.Fatalf("want %q but %q", "10", after.Get("limit")) @@ -362,4 +362,7 @@ func TestPaginationSetValues(t *testing.T) { if after.Get("since_id") != "789" { t.Fatalf("want %q but %q", "789", after.Get("since_id")) } + if after.Get("min_id") != "" { + t.Fatalf("result should be empty string: %q", after.Get("min_id")) + } } From efa05aa949f5602163868a351e1e20a988506925 Mon Sep 17 00:00:00 2001 From: 178inaba <178inaba@users.noreply.github.com> Date: Thu, 14 Feb 2019 23:36:14 +0900 Subject: [PATCH 037/127] Fix TestNewPagination Add min_id error pattern. --- mastodon_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mastodon_test.go b/mastodon_test.go index 5fdefe4..ee294be 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -293,6 +293,11 @@ func TestNewPagination(t *testing.T) { t.Fatalf("should be fail: %v", err) } + _, err = newPagination(`; rel="prev"`) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + pg, err := newPagination(`; rel="next", ; rel="prev"`) if err != nil { t.Fatalf("should not be fail: %v", err) From e804ee7eb264c7bbd58ad9c80291e4d9e2c131a5 Mon Sep 17 00:00:00 2001 From: "Brian C. Lindner" Date: Sun, 10 Mar 2019 22:55:15 -0400 Subject: [PATCH 038/127] Added UploadMediaFromReader --- mastodon.go | 20 ++++++++++++++++++++ status.go | 13 ++++++++++++- status_test.go | 10 ++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/mastodon.go b/mastodon.go index d6f8d60..30a0516 100644 --- a/mastodon.go +++ b/mastodon.go @@ -83,6 +83,26 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in return err } ct = mw.FormDataContentType() + } else if reader, ok := params.(io.Reader); ok { + var buf bytes.Buffer + mw := multipart.NewWriter(&buf) + part, err := mw.CreateFormFile("file", "upload") + if err != nil { + return err + } + _, err = io.Copy(part, reader) + if err != nil { + return err + } + err = mw.Close() + if err != nil { + return err + } + req, err = http.NewRequest(method, u.String(), &buf) + if err != nil { + return err + } + ct = mw.FormDataContentType() } else { if method == http.MethodGet && pg != nil { u.RawQuery = pg.toValues().Encode() diff --git a/status.go b/status.go index cfa9911..ce280c4 100644 --- a/status.go +++ b/status.go @@ -6,6 +6,7 @@ import ( "net/http" "net/url" "time" + "io" ) // Status is struct to hold status. @@ -265,7 +266,7 @@ func (c *Client) Search(ctx context.Context, q string, resolve bool) (*Results, return &results, nil } -// UploadMedia upload a media attachment. +// UploadMedia upload a media attachment from a file. func (c *Client) UploadMedia(ctx context.Context, file string) (*Attachment, error) { var attachment Attachment err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", file, &attachment, nil) @@ -274,3 +275,13 @@ func (c *Client) UploadMedia(ctx context.Context, file string) (*Attachment, err } return &attachment, nil } + +// UploadMediaFromReader uploads a media attachment from a io.Reader. +func (c *Client) UploadMediaFromReader(ctx context.Context, reader io.Reader) (*Attachment, error) { + var attachment Attachment + err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", reader, &attachment, nil) + if err != nil { + return nil, err + } + return &attachment, nil +} diff --git a/status_test.go b/status_test.go index eb6a403..1da1bf1 100644 --- a/status_test.go +++ b/status_test.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "testing" + "os" ) func TestGetFavourites(t *testing.T) { @@ -549,4 +550,13 @@ func TestUploadMedia(t *testing.T) { if attachment.ID != "123" { t.Fatalf("want %q but %q", "123", attachment.ID) } + file, err := os.Open("testdata/logo.png") + defer file.Close() + writerAttachment, err := client.UploadMediaFromReader(context.Background(), file) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if writerAttachment.ID != "123" { + t.Fatalf("want %q but %q", "123", attachment.ID) + } } From 6f05c48bf62c95d31949b766eb35b04e4bbc688f Mon Sep 17 00:00:00 2001 From: Ben Lubar Date: Fri, 26 Apr 2019 20:27:43 -0500 Subject: [PATCH 039/127] Add list API support. --- README.md | 18 ++++----- lists.go | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 lists.go diff --git a/README.md b/README.md index d09de3c..4932901 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ func main() { * [x] GET /api/v1/accounts/:id/unblock * [x] GET /api/v1/accounts/:id/mute * [x] GET /api/v1/accounts/:id/unmute -* [ ] GET /api/v1/accounts/:id/lists +* [x] GET /api/v1/accounts/:id/lists * [x] GET /api/v1/accounts/relationships * [x] GET /api/v1/accounts/search * [x] POST /api/v1/apps @@ -95,14 +95,14 @@ func main() { * [x] GET /api/v1/instance * [x] GET /api/v1/instance/activity * [x] GET /api/v1/instance/peers -* [ ] GET /api/v1/lists -* [ ] GET /api/v1/lists/:id/accounts -* [ ] GET /api/v1/lists/:id -* [ ] POST /api/v1/lists -* [ ] PUT /api/v1/lists/:id -* [ ] DELETE /api/v1/lists/:id -* [ ] POST /api/v1/lists/:id/accounts -* [ ] DELETE /api/v1/lists/:id/accounts +* [x] GET /api/v1/lists +* [x] GET /api/v1/lists/:id/accounts +* [x] GET /api/v1/lists/:id +* [x] POST /api/v1/lists +* [x] PUT /api/v1/lists/:id +* [x] DELETE /api/v1/lists/:id +* [x] POST /api/v1/lists/:id/accounts +* [x] DELETE /api/v1/lists/:id/accounts * [x] POST /api/v1/media * [x] GET /api/v1/mutes * [x] GET /api/v1/notifications diff --git a/lists.go b/lists.go new file mode 100644 index 0000000..59610cc --- /dev/null +++ b/lists.go @@ -0,0 +1,107 @@ +package mastodon + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +// List is metadata for a list of users. +type List struct { + ID ID `json:"id"` + Title string `json:"title"` +} + +// GetLists returns all the lists on the current account. +func (c *Client) GetLists(ctx context.Context) ([]*List, error) { + var lists []*List + err := c.doAPI(ctx, http.MethodGet, "/api/v1/lists", nil, &lists, nil) + if err != nil { + return nil, err + } + return lists, nil +} + +// GetAccountLists returns the lists containing a given account. +func (c *Client) GetAccountLists(ctx context.Context, id ID) ([]*List, error) { + var lists []*List + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/lists", url.PathEscape(string(id))), nil, &lists, nil) + if err != nil { + return nil, err + } + return lists, nil +} + +// GetListAccounts returns the accounts in a given list. +func (c *Client) GetListAccounts(ctx context.Context, id ID) ([]*Account, error) { + var accounts []*Account + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/lists/%s/accounts", url.PathEscape(string(id))), url.Values{"limit": {"0"}}, &accounts, nil) + if err != nil { + return nil, err + } + return accounts, nil +} + +// GetList retrieves a list by ID. +func (c *Client) GetList(ctx context.Context, id ID) (*List, error) { + var list List + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/lists/%s", url.PathEscape(string(id))), nil, &list, nil) + if err != nil { + return nil, err + } + return &list, nil +} + +// CreateList creates a new list with a given title. +func (c *Client) CreateList(ctx context.Context, title string) (*List, error) { + params := url.Values{} + params.Set("title", title) + + var list List + err := c.doAPI(ctx, http.MethodPost, "/api/v1/lists", params, &list, nil) + if err != nil { + return nil, err + } + return &list, nil +} + +// RenameList assigns a new title to a list. +func (c *Client) RenameList(ctx context.Context, id ID, title string) (*List, error) { + params := url.Values{} + params.Set("title", title) + + var list List + err := c.doAPI(ctx, http.MethodPut, fmt.Sprintf("/api/v1/lists/%s", url.PathEscape(string(id))), params, &list, nil) + if err != nil { + return nil, err + } + return &list, nil +} + +// DeleteList removes a list. +func (c *Client) DeleteList(ctx context.Context, id ID) error { + return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/lists/%s", url.PathEscape(string(id))), nil, nil, nil) +} + +// AddToList adds accounts to a list. +// +// Only accounts already followed by the user can be added to a list. +func (c *Client) AddToList(ctx context.Context, list ID, accounts ...ID) error { + params := url.Values{} + for _, acct := range accounts { + params.Add("account_ids", string(acct)) + } + + return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/lists/%s/accounts", url.PathEscape(string(list))), params, nil, nil) +} + +// RemoveFromList removes accounts from a list. +func (c *Client) RemoveFromList(ctx context.Context, list ID, accounts ...ID) error { + params := url.Values{} + for _, acct := range accounts { + params.Add("account_ids", string(acct)) + } + + return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/lists/%s/accounts", url.PathEscape(string(list))), params, nil, nil) +} From 3e2bdc63c758aac3a4e407ee8b08b93bd2a8c5bb Mon Sep 17 00:00:00 2001 From: Ben Lubar Date: Fri, 26 Apr 2019 20:53:58 -0500 Subject: [PATCH 040/127] Add streaming list support. --- streaming.go | 8 ++++++++ streaming_ws.go | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/streaming.go b/streaming.go index b3242a4..2e389a1 100644 --- a/streaming.go +++ b/streaming.go @@ -156,3 +156,11 @@ func (c *Client) StreamingHashtag(ctx context.Context, tag string, isLocal bool) return c.streaming(ctx, p, params) } + +// StreamingList return channel to read events on a list. +func (c *Client) StreamingList(ctx context.Context, id ID) (chan Event, error) { + params := url.Values{} + params.Set("list", string(id)) + + return c.streaming(ctx, "list", params) +} diff --git a/streaming_ws.go b/streaming_ws.go index d37fcfe..3d8927c 100644 --- a/streaming_ws.go +++ b/streaming_ws.go @@ -51,6 +51,11 @@ func (c *WSClient) StreamingWSHashtag(ctx context.Context, tag string, isLocal b 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) From 9427a55316f7e503aa71ba12eef8d065aa56bd0d Mon Sep 17 00:00:00 2001 From: Ben Lubar Date: Fri, 26 Apr 2019 20:56:47 -0500 Subject: [PATCH 041/127] Add list timeline support. --- README.md | 1 + status.go | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4932901..9be937f 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ func main() { * [x] GET /api/v1/timelines/home * [x] GET /api/v1/timelines/public * [x] GET /api/v1/timelines/tag/:hashtag +* [x] GET /api/v1/timelines/list/:id ## Installation diff --git a/status.go b/status.go index ce280c4..fa95ab8 100644 --- a/status.go +++ b/status.go @@ -3,10 +3,10 @@ package mastodon import ( "context" "fmt" + "io" "net/http" "net/url" "time" - "io" ) // Status is struct to hold status. @@ -201,6 +201,16 @@ func (c *Client) GetTimelineHashtag(ctx context.Context, tag string, isLocal boo return statuses, nil } +// GetTimelineList return statuses from a list timeline. +func (c *Client) GetTimelineList(ctx context.Context, id ID, pg *Pagination) ([]*Status, error) { + var statuses []*Status + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/timelines/list/%s", url.PathEscape(string(id))), nil, &statuses, pg) + if err != nil { + return nil, err + } + return statuses, nil +} + // GetTimelineMedia return statuses from media timeline. // NOTE: This is an experimental feature of pawoo.net. func (c *Client) GetTimelineMedia(ctx context.Context, isLocal bool, pg *Pagination) ([]*Status, error) { From e725c814500729bbc5c998e827de744997ed2236 Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Sun, 12 May 2019 16:32:17 +0200 Subject: [PATCH 042/127] Added tests for list API calls --- lists_test.go | 289 ++++++++++++++++++++++++++++++++++++++++++++++ status_test.go | 38 +++++- streaming_test.go | 40 +++++++ 3 files changed, 366 insertions(+), 1 deletion(-) create mode 100644 lists_test.go diff --git a/lists_test.go b/lists_test.go new file mode 100644 index 0000000..ec98cc7 --- /dev/null +++ b/lists_test.go @@ -0,0 +1,289 @@ +package mastodon + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +func TestGetLists(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/lists" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + fmt.Fprintln(w, `[{"id": "1", "title": "foo"}, {"id": "2", "title": "bar"}]`) + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + lists, err := client.GetLists(context.Background()) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if len(lists) != 2 { + t.Fatalf("result should be two: %d", len(lists)) + } + if lists[0].Title != "foo" { + t.Fatalf("want %q but %q", "foo", lists[0].Title) + } + if lists[1].Title != "bar" { + t.Fatalf("want %q but %q", "bar", lists[1].Title) + } +} + +func TestGetAccountLists(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/accounts/1/lists" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + fmt.Fprintln(w, `[{"id": "1", "title": "foo"}, {"id": "2", "title": "bar"}]`) + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + lists, err := client.GetAccountLists(context.Background(), "2") + if err == nil { + t.Fatalf("should be fail: %v", err) + } + lists, err = client.GetAccountLists(context.Background(), "1") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if len(lists) != 2 { + t.Fatalf("result should be two: %d", len(lists)) + } + if lists[0].Title != "foo" { + t.Fatalf("want %q but %q", "foo", lists[0].Title) + } + if lists[1].Title != "bar" { + t.Fatalf("want %q but %q", "bar", lists[1].Title) + } +} + +func TestGetListAccounts(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/lists/1/accounts" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + accounts, err := client.GetListAccounts(context.Background(), "2") + if err == nil { + t.Fatalf("should be fail: %v", err) + } + accounts, err = client.GetListAccounts(context.Background(), "1") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if len(accounts) != 2 { + t.Fatalf("result should be two: %d", len(accounts)) + } + if accounts[0].Username != "foo" { + t.Fatalf("want %q but %q", "foo", accounts[0].Username) + } + if accounts[1].Username != "bar" { + t.Fatalf("want %q but %q", "bar", accounts[1].Username) + } +} + +func TestGetList(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/lists/1" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + fmt.Fprintln(w, `{"id": "1", "title": "foo"}`) + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + list, err := client.GetList(context.Background(), "2") + if err == nil { + t.Fatalf("should be fail: %v", err) + } + list, err = client.GetList(context.Background(), "1") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if list.Title != "foo" { + t.Fatalf("want %q but %q", "foo", list.Title) + } +} + +func TestCreateList(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.PostFormValue("title") != "foo" { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + fmt.Fprintln(w, `{"id": "1", "title": "foo"}`) + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + list, err := client.CreateList(context.Background(), "") + if err == nil { + t.Fatalf("should be fail: %v", err) + } + list, err = client.CreateList(context.Background(), "foo") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if list.Title != "foo" { + t.Fatalf("want %q but %q", "foo", list.Title) + } +} + +func TestRenameList(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/lists/1" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + if r.PostFormValue("title") != "bar" { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + fmt.Fprintln(w, `{"id": "1", "title": "bar"}`) + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + list, err := client.RenameList(context.Background(), "2", "bar") + if err == nil { + t.Fatalf("should be fail: %v", err) + } + list, err = client.RenameList(context.Background(), "1", "bar") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if list.Title != "bar" { + t.Fatalf("want %q but %q", "bar", list.Title) + } +} + +func TestDeleteList(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/lists/1" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + if r.Method != "DELETE" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusMethodNotAllowed) + return + } + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + err := client.DeleteList(context.Background(), "2") + if err == nil { + t.Fatalf("should be fail: %v", err) + } + err = client.DeleteList(context.Background(), "1") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } +} + +func TestAddToList(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/lists/1/accounts" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + if r.PostFormValue("account_ids") != "1" { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + err := client.AddToList(context.Background(), "1", "1") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } +} + +func TestRemoveFromList(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/lists/1/accounts" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + if r.Method != "DELETE" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusMethodNotAllowed) + return + } + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + err := client.RemoveFromList(context.Background(), "1", "1") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } +} diff --git a/status_test.go b/status_test.go index 1da1bf1..9fca529 100644 --- a/status_test.go +++ b/status_test.go @@ -5,8 +5,8 @@ import ( "fmt" "net/http" "net/http/httptest" - "testing" "os" + "testing" ) func TestGetFavourites(t *testing.T) { @@ -406,6 +406,42 @@ func TestGetTimelineHashtag(t *testing.T) { } } +func TestGetTimelineList(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/timelines/list/1" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + fmt.Fprintln(w, `[{"content": "zzz"},{"content": "yyy"}]`) + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + _, err := client.GetTimelineList(context.Background(), "notfound", nil) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + tags, err := client.GetTimelineList(context.Background(), "1", nil) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if len(tags) != 2 { + t.Fatalf("should have %q entries but %q", "2", len(tags)) + } + if tags[0].Content != "zzz" { + t.Fatalf("want %q but %q", "zzz", tags[0].Content) + } + if tags[1].Content != "yyy" { + t.Fatalf("want %q but %q", "zzz", tags[1].Content) + } +} + func TestGetTimelineMedia(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("local") == "" { diff --git a/streaming_test.go b/streaming_test.go index 686ae40..bc0e867 100644 --- a/streaming_test.go +++ b/streaming_test.go @@ -293,3 +293,43 @@ data: {"content": "foo"} t.Fatalf("want %q but %q", "foo", events[0].(*UpdateEvent).Status.Content) } } + +func TestStreamingList(t *testing.T) { + var isEnd bool + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if isEnd { + return + } else if r.URL.Path != "/api/v1/streaming/list" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + f, _ := w.(http.Flusher) + fmt.Fprintln(w, ` +event: update +data: {"content": "foo"} + `) + f.Flush() + isEnd = true + })) + defer ts.Close() + + client := NewClient(&Config{Server: ts.URL}) + ctx, cancel := context.WithCancel(context.Background()) + time.AfterFunc(time.Second, cancel) + q, err := client.StreamingList(ctx, "1") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + events := []Event{} + for e := range q { + if _, ok := e.(*ErrorEvent); !ok { + events = append(events, e) + } + } + if len(events) != 1 { + t.Fatalf("result should be one: %d", len(events)) + } + if events[0].(*UpdateEvent).Status.Content != "foo" { + t.Fatalf("want %q but %q", "foo", events[0].(*UpdateEvent).Status.Content) + } +} From bb2662b33c702bf141dcb4d02733d976b7bfcd4d Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Mon, 26 Nov 2018 04:26:27 +0100 Subject: [PATCH 043/127] Handle HTTP 429 responses with a request backoff approach Since it's difficult to wrap all possible go-mastodon API calls in a backoff algorithm outside of the package itself, I decided to implement a simple version of it in go-mastodon's doAPI itself. This works nicely, but could be improved in two ways still: - Abort sleeping when context gets cancelled - Make backoff optional / configurable Personally, I still think this is a good start and probably fits most of go-mastodon's use-cases. It certainly beats string-grepping for status code "429" in clients. --- mastodon.go | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/mastodon.go b/mastodon.go index 30a0516..a91da4d 100644 --- a/mastodon.go +++ b/mastodon.go @@ -16,6 +16,7 @@ import ( "path/filepath" "strconv" "strings" + "time" "github.com/tomnomnom/linkheader" ) @@ -118,11 +119,32 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in req.Header.Set("Content-Type", ct) } - resp, err := c.Do(req) - if err != nil { - return err + var resp *http.Response + backoff := 1000 * time.Millisecond + for { + resp, err = c.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + // handle status code 429, which indicates the server is throttling + // our requests. Do an exponential backoff and retry the request. + if resp.StatusCode == 429 { + if backoff > time.Hour { + break + } + backoff *= 2 + + select { + case <-time.After(backoff): + case <-ctx.Done(): + return ctx.Err() + } + continue + } + break } - defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return parseAPIError("bad request", resp) From 68ca31fccd03b186d8d71432416885747c9b1c61 Mon Sep 17 00:00:00 2001 From: buckket Date: Tue, 14 May 2019 02:51:33 +0200 Subject: [PATCH 044/127] Add support for Field, Source and Locked parameters to AccountUpdate() --- accounts.go | 33 +++++++++++++++++++++++++++++++++ accounts_test.go | 7 +++++++ 2 files changed, 40 insertions(+) diff --git a/accounts.go b/accounts.go index ceb3674..76525aa 100644 --- a/accounts.go +++ b/accounts.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "strconv" "time" ) @@ -38,6 +39,15 @@ type Field struct { VerifiedAt time.Time `json:"verified_at"` } +// Source is a Mastodon account profile field +type Source struct { + Privacy string `json:"privacy"` + Sensitive *bool `json:"sensitive"` + Language string `json:"language"` + Note string `json:"note"` + Fields *[]Field `json:"fields"` +} + // GetAccount return Account. func (c *Client) GetAccount(ctx context.Context, id ID) (*Account, error) { var account Account @@ -64,6 +74,9 @@ type Profile struct { // If it is empty, update it with empty. DisplayName *string Note *string + Locked *bool + Fields *[]Field + Source *Source // Set the base64 encoded character string of the image. Avatar string @@ -79,6 +92,26 @@ func (c *Client) AccountUpdate(ctx context.Context, profile *Profile) (*Account, if profile.Note != nil { params.Set("note", *profile.Note) } + if profile.Locked != nil { + params.Set("locked", strconv.FormatBool(*profile.Locked)) + } + if profile.Fields != nil { + for idx, field := range *profile.Fields { + params.Set(fmt.Sprintf("fields_attributes[%d][name]", idx), field.Name) + params.Set(fmt.Sprintf("fields_attributes[%d][value]", idx), field.Value) + } + } + if profile.Source != nil { + if len(profile.Source.Privacy) > 0 { + params.Set("source[privacy]", profile.Source.Privacy) + } + if profile.Source.Sensitive != nil { + params.Set("source[sensitive]", strconv.FormatBool(*profile.Source.Sensitive)) + } + if len(profile.Source.Language) > 0 { + params.Set("source[language]", profile.Source.Language) + } + } if profile.Avatar != "" { params.Set("avatar", profile.Avatar) } diff --git a/accounts_test.go b/accounts_test.go index 7603cc6..b59968f 100644 --- a/accounts_test.go +++ b/accounts_test.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "testing" + "time" ) func TestGetAccount(t *testing.T) { @@ -93,9 +94,15 @@ func TestAccountUpdate(t *testing.T) { if err == nil { t.Fatalf("should be fail: %v", err) } + tbool := true + fields := []Field{{"foo", "bar", time.Time{}}, {"dum", "baz", time.Time{}}} + source := Source{Language: "de", Privacy: "public", Sensitive: &tbool} a, err := client.AccountUpdate(context.Background(), &Profile{ DisplayName: String("display_name"), Note: String("note"), + Locked: &tbool, + Fields: &fields, + Source: &source, Avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUoAAADrCAYAAAA...", Header: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUoAAADrCAYAAAA...", }) From 636b33ad1cf5d9a310af4748631abe336f0f26b3 Mon Sep 17 00:00:00 2001 From: buckket Date: Tue, 14 May 2019 04:21:14 +0200 Subject: [PATCH 045/127] Renamed Source to AccountSource --- accounts.go | 6 +++--- accounts_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/accounts.go b/accounts.go index 76525aa..d6210cc 100644 --- a/accounts.go +++ b/accounts.go @@ -39,8 +39,8 @@ type Field struct { VerifiedAt time.Time `json:"verified_at"` } -// Source is a Mastodon account profile field -type Source struct { +// AccountSource is a Mastodon account profile field. +type AccountSource struct { Privacy string `json:"privacy"` Sensitive *bool `json:"sensitive"` Language string `json:"language"` @@ -76,7 +76,7 @@ type Profile struct { Note *string Locked *bool Fields *[]Field - Source *Source + Source *AccountSource // Set the base64 encoded character string of the image. Avatar string diff --git a/accounts_test.go b/accounts_test.go index b59968f..781e3f2 100644 --- a/accounts_test.go +++ b/accounts_test.go @@ -96,7 +96,7 @@ func TestAccountUpdate(t *testing.T) { } tbool := true fields := []Field{{"foo", "bar", time.Time{}}, {"dum", "baz", time.Time{}}} - source := Source{Language: "de", Privacy: "public", Sensitive: &tbool} + source := AccountSource{Language: "de", Privacy: "public", Sensitive: &tbool} a, err := client.AccountUpdate(context.Background(), &Profile{ DisplayName: String("display_name"), Note: String("note"), From e71411ef9650411f9ef8b6c16ab05ae56e6e74a0 Mon Sep 17 00:00:00 2001 From: buckket Date: Tue, 14 May 2019 04:44:39 +0200 Subject: [PATCH 046/127] All parameters are now of pointer type and thus can be nil --- accounts.go | 14 +++++++------- accounts_test.go | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/accounts.go b/accounts.go index d6210cc..3e5bd60 100644 --- a/accounts.go +++ b/accounts.go @@ -41,10 +41,10 @@ type Field struct { // AccountSource is a Mastodon account profile field. type AccountSource struct { - Privacy string `json:"privacy"` + Privacy *string `json:"privacy"` Sensitive *bool `json:"sensitive"` - Language string `json:"language"` - Note string `json:"note"` + Language *string `json:"language"` + Note *string `json:"note"` Fields *[]Field `json:"fields"` } @@ -102,14 +102,14 @@ func (c *Client) AccountUpdate(ctx context.Context, profile *Profile) (*Account, } } if profile.Source != nil { - if len(profile.Source.Privacy) > 0 { - params.Set("source[privacy]", profile.Source.Privacy) + if profile.Source.Privacy != nil { + params.Set("source[privacy]", *profile.Source.Privacy) } if profile.Source.Sensitive != nil { params.Set("source[sensitive]", strconv.FormatBool(*profile.Source.Sensitive)) } - if len(profile.Source.Language) > 0 { - params.Set("source[language]", profile.Source.Language) + if profile.Source.Language != nil { + params.Set("source[language]", *profile.Source.Language) } } if profile.Avatar != "" { diff --git a/accounts_test.go b/accounts_test.go index 781e3f2..08907e6 100644 --- a/accounts_test.go +++ b/accounts_test.go @@ -96,7 +96,7 @@ func TestAccountUpdate(t *testing.T) { } tbool := true fields := []Field{{"foo", "bar", time.Time{}}, {"dum", "baz", time.Time{}}} - source := AccountSource{Language: "de", Privacy: "public", Sensitive: &tbool} + source := AccountSource{Language: String("de"), Privacy: String("public"), Sensitive: &tbool} a, err := client.AccountUpdate(context.Background(), &Profile{ DisplayName: String("display_name"), Note: String("note"), From f51571807ddabd3db787bc03a012e1b90ae1d4c0 Mon Sep 17 00:00:00 2001 From: 178inaba <178inaba@users.noreply.github.com> Date: Tue, 14 May 2019 12:14:13 +0900 Subject: [PATCH 047/127] Add go.mod --- go.mod | 16 ++++++++++++++++ go.sum | 31 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c70bdef --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module github.com/mattn/go-mastodon + +go 1.12 + +require ( + github.com/PuerkitoBio/goquery v1.5.0 + github.com/fatih/color v1.7.0 + github.com/gorilla/websocket v1.4.0 + github.com/mattn/go-colorable v0.1.1 // indirect + github.com/mattn/go-isatty v0.0.7 // indirect + github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859 + github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 + github.com/urfave/cli v1.20.0 + golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 + golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..63d5b07 --- /dev/null +++ b/go.sum @@ -0,0 +1,31 @@ +github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk= +github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= +github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= +github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859 h1:smQbSzmT3EHl4EUwtFwFGmGIpiYgIiiPeVv1uguIQEE= +github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= +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.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak= +golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug= +golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From c09198f7c9bb5d000e1bf097f8a1697f9070f37a Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Tue, 14 May 2019 22:11:57 +0200 Subject: [PATCH 048/127] Fixed pagination parsing for non-numeric IDs Mastodon API's pagination IDs are not guaranteed to be in a numeric format. This happens to be the case for the tootsuite implementation, but others use UUIDs or flake IDs for pagination. This also simplifies the code a bit and luckily shouldn't break backwards compatibility since they're already of type ID in the Pagination struct. --- mastodon.go | 13 +------------ mastodon_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/mastodon.go b/mastodon.go index a91da4d..d2f6182 100644 --- a/mastodon.go +++ b/mastodon.go @@ -14,7 +14,6 @@ import ( "os" "path" "path/filepath" - "strconv" "strings" "time" @@ -352,17 +351,7 @@ func getPaginationID(rawurl, key string) (ID, error) { return "", err } - val := u.Query().Get(key) - if val == "" { - return "", nil - } - - id, err := strconv.ParseInt(val, 10, 64) - if err != nil { - return "", err - } - - return ID(fmt.Sprint(id)), nil + return ID(u.Query().Get(key)), nil } func (p *Pagination) toValues() url.Values { diff --git a/mastodon_test.go b/mastodon_test.go index ee294be..1379230 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -294,8 +294,8 @@ func TestNewPagination(t *testing.T) { } _, err = newPagination(`; rel="prev"`) - if err == nil { - t.Fatalf("should be fail: %v", err) + if err != nil { + t.Fatalf("should not be fail: %v", err) } pg, err := newPagination(`; rel="next", ; rel="prev"`) @@ -317,8 +317,8 @@ func TestGetPaginationID(t *testing.T) { } _, err = getPaginationID("http://example.com?max_id=abc", "max_id") - if err == nil { - t.Fatalf("should be fail: %v", err) + if err != nil { + t.Fatalf("should not be fail: %v", err) } id, err := getPaginationID("http://example.com?max_id=123", "max_id") From 8f6192e26b66e06c5eea3a8d17e342ea57c4a86e Mon Sep 17 00:00:00 2001 From: buckket Date: Thu, 16 May 2019 01:59:33 +0200 Subject: [PATCH 049/127] Add /api/v1/notifications/dismiss --- README.md | 1 + notification.go | 12 ++++++++++++ notification_test.go | 7 +++++++ 3 files changed, 20 insertions(+) diff --git a/README.md b/README.md index 9be937f..59c6e5c 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ func main() { * [x] GET /api/v1/mutes * [x] GET /api/v1/notifications * [x] GET /api/v1/notifications/:id +* [x] POST /api/v1/notifications/dismiss * [x] POST /api/v1/notifications/clear * [x] GET /api/v1/reports * [x] POST /api/v1/reports diff --git a/notification.go b/notification.go index 0dfd8f8..c16f3be 100644 --- a/notification.go +++ b/notification.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "net/url" "time" ) @@ -36,6 +37,17 @@ func (c *Client) GetNotification(ctx context.Context, id ID) (*Notification, err return ¬ification, nil } +// DismissNotification deletes a single notification. +func (c *Client) DismissNotification(ctx context.Context, id ID) error { + params := url.Values{} + params.Add("id", string(id)) + err := c.doAPI(ctx, http.MethodPost, "/api/v1/notifications/dismiss", params, nil, nil) + if err != nil { + return err + } + return nil +} + // ClearNotifications clear notifications. func (c *Client) ClearNotifications(ctx context.Context) error { return c.doAPI(ctx, http.MethodPost, "/api/v1/notifications/clear", nil, nil, nil) diff --git a/notification_test.go b/notification_test.go index 3329d02..024e1b4 100644 --- a/notification_test.go +++ b/notification_test.go @@ -20,6 +20,9 @@ func TestGetNotifications(t *testing.T) { case "/api/v1/notifications/clear": fmt.Fprintln(w, `{}`) return + case "/api/v1/notifications/dismiss": + fmt.Fprintln(w, `{}`) + return } http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return @@ -56,4 +59,8 @@ func TestGetNotifications(t *testing.T) { if err != nil { t.Fatalf("should not be fail: %v", err) } + err = client.DismissNotification(context.Background(), "123") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } } From 3268207afe0b190825046d1832194d32cf81f16c Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sat, 22 Jun 2019 01:14:52 +0900 Subject: [PATCH 050/127] Set User-Agent --- cmd/mstdn/main.go | 1 + mastodon.go | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cmd/mstdn/main.go b/cmd/mstdn/main.go index f17e27a..4386768 100644 --- a/cmd/mstdn/main.go +++ b/cmd/mstdn/main.go @@ -370,6 +370,7 @@ func run() int { } client := mastodon.NewClient(config) + client.UserAgent = "mstdn" app.Metadata = map[string]interface{}{ "client": client, "config": config, diff --git a/mastodon.go b/mastodon.go index d2f6182..d0a94d4 100644 --- a/mastodon.go +++ b/mastodon.go @@ -31,7 +31,8 @@ type Config struct { // Client is a API client for mastodon. type Client struct { http.Client - config *Config + config *Config + UserAgent string } func (c *Client) doAPI(ctx context.Context, method string, uri string, params interface{}, res interface{}, pg *Pagination) error { @@ -117,6 +118,9 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in if params != nil { req.Header.Set("Content-Type", ct) } + if c.UserAgent != "" { + req.Header.Set("User-Agent", c.UserAgent) + } var resp *http.Response backoff := 1000 * time.Millisecond @@ -211,6 +215,9 @@ func (c *Client) authenticate(ctx context.Context, params url.Values) error { } req = req.WithContext(ctx) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if c.UserAgent != "" { + req.Header.Set("User-Agent", c.UserAgent) + } resp, err := c.Do(req) if err != nil { return err From 559ed99cdfe84c737d0942f158d608939692bba7 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sat, 22 Jun 2019 01:44:24 +0900 Subject: [PATCH 051/127] Add direct Closes #102 --- cmd/mstdn/cmd_timeline.go | 46 ++++++++++++++++++++++++++++++++++ cmd/mstdn/cmd_timeline_test.go | 28 ++++++++++++++++++--- cmd/mstdn/main.go | 20 +++++++++++++++ status.go | 12 +++++++++ status_test.go | 22 ++++++++++++++++ 5 files changed, 125 insertions(+), 3 deletions(-) diff --git a/cmd/mstdn/cmd_timeline.go b/cmd/mstdn/cmd_timeline.go index d9a0024..c610966 100644 --- a/cmd/mstdn/cmd_timeline.go +++ b/cmd/mstdn/cmd_timeline.go @@ -20,3 +20,49 @@ func cmdTimeline(c *cli.Context) error { } 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 +} diff --git a/cmd/mstdn/cmd_timeline_test.go b/cmd/mstdn/cmd_timeline_test.go index b0801a8..f9b6299 100644 --- a/cmd/mstdn/cmd_timeline_test.go +++ b/cmd/mstdn/cmd_timeline_test.go @@ -14,7 +14,13 @@ func TestCmdTimeline(t *testing.T) { func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v1/timelines/home": - fmt.Fprintln(w, `[{"content": "zzz"}]`) + fmt.Fprintln(w, `[{"content": "home"}]`) + return + case "/api/v1/timelines/public": + fmt.Fprintln(w, `[{"content": "public"}]`) + return + case "/api/v1/timelines/direct": + fmt.Fprintln(w, `[{"content": "direct"}]`) return } http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) @@ -22,9 +28,25 @@ func TestCmdTimeline(t *testing.T) { }, 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"}) }, ) - if !strings.Contains(out, "zzz") { - t.Fatalf("%q should be contained in output of command: %v", "zzz", out) + 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) } } diff --git a/cmd/mstdn/main.go b/cmd/mstdn/main.go index 4386768..141a364 100644 --- a/cmd/mstdn/main.go +++ b/cmd/mstdn/main.go @@ -235,6 +235,26 @@ func makeApp() *cli.App { 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", diff --git a/status.go b/status.go index fa95ab8..7bee52a 100644 --- a/status.go +++ b/status.go @@ -295,3 +295,15 @@ func (c *Client) UploadMediaFromReader(ctx context.Context, reader io.Reader) (* } return &attachment, nil } + +// GetTimelineDirect return statuses from direct timeline. +func (c *Client) GetTimelineDirect(ctx context.Context, pg *Pagination) ([]*Status, error) { + params := url.Values{} + + var statuses []*Status + err := c.doAPI(ctx, http.MethodGet, "/api/v1/timelines/direct", params, &statuses, pg) + if err != nil { + return nil, err + } + return statuses, nil +} diff --git a/status_test.go b/status_test.go index 9fca529..d6120d0 100644 --- a/status_test.go +++ b/status_test.go @@ -370,6 +370,28 @@ func TestGetTimelinePublic(t *testing.T) { } } +func TestGetTimelineDirect(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `[{"content": "direct"}, {"content": "status"}]`) + })) + defer ts.Close() + + client := NewClient(&Config{Server: ts.URL}) + tl, err := client.GetTimelineDirect(context.Background(), nil) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if len(tl) != 2 { + t.Fatalf("result should be two: %d", len(tl)) + } + if tl[0].Content != "direct" { + t.Fatalf("want %q but %q", "foo", tl[0].Content) + } + if tl[1].Content != "status" { + t.Fatalf("want %q but %q", "bar", tl[1].Content) + } +} + func TestGetTimelineHashtag(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/timelines/tag/zzz" { From 23fc4c7953aa54328275a7bca00edd5d9fa8ca66 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sat, 22 Jun 2019 01:54:32 +0900 Subject: [PATCH 052/127] Switch to codecov --- .travis.yml | 12 +++++++++--- go.test.sh | 12 ++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100755 go.test.sh diff --git a/.travis.yml b/.travis.yml index b65ca33..63e43b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,14 @@ language: go +sudo: false go: + - 1.8.x - tip + before_install: - - go get github.com/mattn/goveralls - - go get golang.org/x/tools/cmd/cover + - go get -t -v ./... + script: - - $HOME/gopath/bin/goveralls -repotoken u2dqXvOxbIBr8eGxCjcgTkkN2JOSGx1fy + - ./go.test.sh + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/go.test.sh b/go.test.sh new file mode 100755 index 0000000..012162b --- /dev/null +++ b/go.test.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e +echo "" > coverage.txt + +for d in $(go list ./... | grep -v vendor); do + go test -race -coverprofile=profile.out -covermode=atomic "$d" + if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out + fi +done From 8826198705d04f926a5ab520d37e508b4b482c82 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sat, 22 Jun 2019 01:55:58 +0900 Subject: [PATCH 053/127] Only tip --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 63e43b8..b2904bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: go sudo: false go: - - 1.8.x - tip before_install: From 536597515d4dc319330bf5be8ab987cfeb04bcda Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sat, 22 Jun 2019 02:04:06 +0900 Subject: [PATCH 054/127] Temporary disable test with -race --- go.test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.test.sh b/go.test.sh index 012162b..a7deaca 100755 --- a/go.test.sh +++ b/go.test.sh @@ -4,7 +4,7 @@ set -e echo "" > coverage.txt for d in $(go list ./... | grep -v vendor); do - go test -race -coverprofile=profile.out -covermode=atomic "$d" + go test -coverprofile=profile.out -covermode=atomic "$d" if [ -f profile.out ]; then cat profile.out >> coverage.txt rm profile.out From 26fcedc8aa4e6a848c040df59ed2e24237ef0121 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sat, 22 Jun 2019 02:12:04 +0900 Subject: [PATCH 055/127] Update badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 59c6e5c..bd7bcb9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # go-mastodon [![Build Status](https://travis-ci.org/mattn/go-mastodon.svg?branch=master)](https://travis-ci.org/mattn/go-mastodon) -[![Coverage Status](https://coveralls.io/repos/github/mattn/go-mastodon/badge.svg?branch=master)](https://coveralls.io/github/mattn/go-mastodon?branch=master) +[![CodeCov](https://codecov.io/gh/mattn/go-mastodon/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-mastodon) [![GoDoc](https://godoc.org/github.com/mattn/go-mastodon?status.svg)](http://godoc.org/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) From 8a48862adc2958fb7f057783919b00580d26bb6c Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Thu, 8 Aug 2019 09:26:16 +0200 Subject: [PATCH 056/127] Use a slightly more aggressive backoff approach Doubling the backoff every iteration turned out to be a bit too relaxed for the common situations where you run into API throttling. This change gives the API enough room to breathe, but re-tries requests just a little more often. --- mastodon.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mastodon.go b/mastodon.go index d0a94d4..d9c7883 100644 --- a/mastodon.go +++ b/mastodon.go @@ -123,7 +123,7 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in } var resp *http.Response - backoff := 1000 * time.Millisecond + backoff := time.Second for { resp, err = c.Do(req) if err != nil { @@ -137,13 +137,14 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in if backoff > time.Hour { break } - backoff *= 2 select { case <-time.After(backoff): case <-ctx.Done(): return ctx.Err() } + + backoff = time.Duration(1.5 * float64(backoff)) continue } break From 050f1a0a87a8166234e30ad12f95dc8c74f3bbf5 Mon Sep 17 00:00:00 2001 From: mattn Date: Wed, 14 Aug 2019 15:00:40 +0900 Subject: [PATCH 057/127] Create FUNDING.yml --- .github/FUNDING.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..351ae11 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: mattn # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From 34e64bb423a3c4af901efae5fd9b390e46998cd3 Mon Sep 17 00:00:00 2001 From: dtluna Date: Tue, 20 Aug 2019 14:12:09 +0300 Subject: [PATCH 058/127] Make Client.Config public --- mastodon.go | 20 ++++++++++---------- streaming.go | 4 ++-- streaming_ws.go | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/mastodon.go b/mastodon.go index d9c7883..fa9adf7 100644 --- a/mastodon.go +++ b/mastodon.go @@ -31,12 +31,12 @@ type Config struct { // Client is a API client for mastodon. type Client struct { http.Client - config *Config + Config *Config UserAgent string } func (c *Client) doAPI(ctx context.Context, method string, uri string, params interface{}, res interface{}, pg *Pagination) error { - u, err := url.Parse(c.config.Server) + u, err := url.Parse(c.Config.Server) if err != nil { return err } @@ -114,7 +114,7 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in } } req = req.WithContext(ctx) - req.Header.Set("Authorization", "Bearer "+c.config.AccessToken) + req.Header.Set("Authorization", "Bearer "+c.Config.AccessToken) if params != nil { req.Header.Set("Content-Type", ct) } @@ -170,15 +170,15 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in func NewClient(config *Config) *Client { return &Client{ Client: *http.DefaultClient, - config: config, + Config: config, } } // Authenticate get access-token to the API. func (c *Client) Authenticate(ctx context.Context, username, password string) error { params := url.Values{ - "client_id": {c.config.ClientID}, - "client_secret": {c.config.ClientSecret}, + "client_id": {c.Config.ClientID}, + "client_secret": {c.Config.ClientSecret}, "grant_type": {"password"}, "username": {username}, "password": {password}, @@ -193,8 +193,8 @@ func (c *Client) Authenticate(ctx context.Context, username, password string) er // redirectURI should be the same as Application.RedirectURI. func (c *Client) AuthenticateToken(ctx context.Context, authCode, redirectURI string) error { params := url.Values{ - "client_id": {c.config.ClientID}, - "client_secret": {c.config.ClientSecret}, + "client_id": {c.Config.ClientID}, + "client_secret": {c.Config.ClientSecret}, "grant_type": {"authorization_code"}, "code": {authCode}, "redirect_uri": {redirectURI}, @@ -204,7 +204,7 @@ func (c *Client) AuthenticateToken(ctx context.Context, authCode, redirectURI st } func (c *Client) authenticate(ctx context.Context, params url.Values) error { - u, err := url.Parse(c.config.Server) + u, err := url.Parse(c.Config.Server) if err != nil { return err } @@ -236,7 +236,7 @@ func (c *Client) authenticate(ctx context.Context, params url.Values) error { if err != nil { return err } - c.config.AccessToken = res.AccessToken + c.Config.AccessToken = res.AccessToken return nil } diff --git a/streaming.go b/streaming.go index 2e389a1..b57f454 100644 --- a/streaming.go +++ b/streaming.go @@ -80,7 +80,7 @@ func handleReader(q chan Event, r io.Reader) error { } func (c *Client) streaming(ctx context.Context, p string, params url.Values) (chan Event, error) { - u, err := url.Parse(c.config.Server) + u, err := url.Parse(c.Config.Server) if err != nil { return nil, err } @@ -92,7 +92,7 @@ func (c *Client) streaming(ctx context.Context, p string, params url.Values) (ch return nil, err } req = req.WithContext(ctx) - req.Header.Set("Authorization", "Bearer "+c.config.AccessToken) + req.Header.Set("Authorization", "Bearer "+c.Config.AccessToken) q := make(chan Event) go func() { diff --git a/streaming_ws.go b/streaming_ws.go index 3d8927c..bd42bf9 100644 --- a/streaming_ws.go +++ b/streaming_ws.go @@ -58,13 +58,13 @@ func (c *WSClient) StreamingWSList(ctx context.Context, id ID) (chan Event, erro 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("access_token", c.client.Config.AccessToken) params.Set("stream", stream) if tag != "" { params.Set("tag", tag) } - u, err := changeWebSocketScheme(c.client.config.Server) + u, err := changeWebSocketScheme(c.client.Config.Server) if err != nil { return nil, err } From 24cdbe8c3d78c2653bb3730c1c45e0a131165425 Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Thu, 8 Aug 2019 09:24:01 +0000 Subject: [PATCH 059/127] Unlambda textContent call textContent already has the right function signature, no need to wrap it. --- cmd/mstdn/cmd_stream.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cmd/mstdn/cmd_stream.go b/cmd/mstdn/cmd_stream.go index 258a658..9626769 100644 --- a/cmd/mstdn/cmd_stream.go +++ b/cmd/mstdn/cmd_stream.go @@ -44,9 +44,7 @@ func cmdStream(c *cli.Context) error { "nl": func(s string) string { return s + "\n" }, - "text": func(s string) string { - return textContent(s) - }, + "text": textContent, }).Parse(asFormat) if err != nil { return err From 20bc690d8d07dbac139d6dc302014b65fc2d3073 Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Thu, 8 Aug 2019 09:16:55 +0000 Subject: [PATCH 060/127] Simplify code - Removed redundant returns - Implicitly declare httptest.Server --- accounts_test.go | 3 --- cmd/mstdn/cmd_stream_test.go | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/accounts_test.go b/accounts_test.go index 08907e6..4eed9ea 100644 --- a/accounts_test.go +++ b/accounts_test.go @@ -16,7 +16,6 @@ func TestGetAccount(t *testing.T) { return } fmt.Fprintln(w, `{"username": "zzz"}`) - return })) defer ts.Close() @@ -48,7 +47,6 @@ func TestGetAccountCurrentUser(t *testing.T) { return } fmt.Fprintln(w, `{"username": "zzz"}`) - return })) defer ts.Close() @@ -80,7 +78,6 @@ func TestAccountUpdate(t *testing.T) { return } fmt.Fprintln(w, `{"username": "zzz"}`) - return })) defer ts.Close() diff --git a/cmd/mstdn/cmd_stream_test.go b/cmd/mstdn/cmd_stream_test.go index 92c339b..b3b37c7 100644 --- a/cmd/mstdn/cmd_stream_test.go +++ b/cmd/mstdn/cmd_stream_test.go @@ -14,8 +14,7 @@ import ( ) func TestCmdStream(t *testing.T) { - var ts *httptest.Server - ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + 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 From 1ccf66b8b43e8163a0c5af5f60ae96a199e873d5 Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Thu, 8 Aug 2019 09:13:49 +0000 Subject: [PATCH 061/127] Fix code formatting Used goimports to fix code formatting. --- accounts.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/accounts.go b/accounts.go index 3e5bd60..a27d3f4 100644 --- a/accounts.go +++ b/accounts.go @@ -41,10 +41,10 @@ type Field struct { // AccountSource is a Mastodon account profile field. type AccountSource struct { - Privacy *string `json:"privacy"` + Privacy *string `json:"privacy"` Sensitive *bool `json:"sensitive"` - Language *string `json:"language"` - Note *string `json:"note"` + Language *string `json:"language"` + Note *string `json:"note"` Fields *[]Field `json:"fields"` } From 1b7f743892be1fa3698424e97a7e5824a234ded0 Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Thu, 8 Aug 2019 09:21:21 +0000 Subject: [PATCH 062/127] Fix ineffectual assignments - Don't assign variables we don't end up using - Added missing error check in test --- helper_test.go | 10 +++++----- lists_test.go | 8 ++++---- status_test.go | 3 +++ streaming_ws_test.go | 4 ++-- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/helper_test.go b/helper_test.go index 7da80b9..0d4b2ce 100644 --- a/helper_test.go +++ b/helper_test.go @@ -12,13 +12,13 @@ const wantBase64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAABxCAYAAA func TestBase64EncodeFileName(t *testing.T) { // Error in os.Open. - uri, err := Base64EncodeFileName("fail") + _, err := Base64EncodeFileName("fail") if err == nil { t.Fatalf("should be fail: %v", err) } // Success. - uri, err = Base64EncodeFileName("testdata/logo.png") + uri, err := Base64EncodeFileName("testdata/logo.png") if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -29,7 +29,7 @@ func TestBase64EncodeFileName(t *testing.T) { func TestBase64Encode(t *testing.T) { // Error in file.Stat. - uri, err := Base64Encode(nil) + _, err := Base64Encode(nil) if err == nil { t.Fatalf("should be fail: %v", err) } @@ -43,7 +43,7 @@ func TestBase64Encode(t *testing.T) { if err != nil { t.Fatalf("should not be fail: %v", err) } - uri, err = Base64Encode(logo) + _, err = Base64Encode(logo) if err == nil { t.Fatalf("should be fail: %v", err) } @@ -53,7 +53,7 @@ func TestBase64Encode(t *testing.T) { if err != nil { t.Fatalf("should not be fail: %v", err) } - uri, err = Base64Encode(logo) + uri, err := Base64Encode(logo) if err != nil { t.Fatalf("should not be fail: %v", err) } diff --git a/lists_test.go b/lists_test.go index ec98cc7..b6f400e 100644 --- a/lists_test.go +++ b/lists_test.go @@ -57,11 +57,11 @@ func TestGetAccountLists(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - lists, err := client.GetAccountLists(context.Background(), "2") + _, err := client.GetAccountLists(context.Background(), "2") if err == nil { t.Fatalf("should be fail: %v", err) } - lists, err = client.GetAccountLists(context.Background(), "1") + lists, err := client.GetAccountLists(context.Background(), "1") if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -93,11 +93,11 @@ func TestGetListAccounts(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - accounts, err := client.GetListAccounts(context.Background(), "2") + _, err := client.GetListAccounts(context.Background(), "2") if err == nil { t.Fatalf("should be fail: %v", err) } - accounts, err = client.GetListAccounts(context.Background(), "1") + accounts, err := client.GetListAccounts(context.Background(), "1") if err != nil { t.Fatalf("should not be fail: %v", err) } diff --git a/status_test.go b/status_test.go index d6120d0..7536ffb 100644 --- a/status_test.go +++ b/status_test.go @@ -609,6 +609,9 @@ func TestUploadMedia(t *testing.T) { t.Fatalf("want %q but %q", "123", attachment.ID) } file, err := os.Open("testdata/logo.png") + if err != nil { + t.Fatalf("could not open file: %v", err) + } defer file.Close() writerAttachment, err := client.UploadMediaFromReader(context.Background(), file) if err != nil { diff --git a/streaming_ws_test.go b/streaming_ws_test.go index cf60530..856702a 100644 --- a/streaming_ws_test.go +++ b/streaming_ws_test.go @@ -243,12 +243,12 @@ func TestDial(t *testing.T) { t.Fatalf("should be fail: %v", err) } - _, rawurl, err := client.dial("ws://" + ts.Listener.Addr().String()) + _, _, 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()) + _, rawurl, err := client.dial("ws://" + ts.Listener.Addr().String()) if err != nil { t.Fatalf("should not be fail: %v", err) } From ef1332c96bc28464cca926e33ab81477c00c38ee Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 30 Sep 2019 16:54:15 +0900 Subject: [PATCH 063/127] Fix contact_account --- instance.go | 2 +- instance_test.go | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/instance.go b/instance.go index 3217450..d74322f 100644 --- a/instance.go +++ b/instance.go @@ -16,7 +16,7 @@ type Instance struct { URLs map[string]string `json:"urls,omitempty"` Stats *InstanceStats `json:"stats,omitempty"` Languages []string `json:"languages"` - ContactAccount *Account `json:"account"` + ContactAccount *Account `json:"contact_account"` } // InstanceStats hold information for mastodon instance stats. diff --git a/instance_test.go b/instance_test.go index 4a87ae3..42f1e69 100644 --- a/instance_test.go +++ b/instance_test.go @@ -17,7 +17,7 @@ func TestGetInstance(t *testing.T) { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - fmt.Fprintln(w, `{"title": "mastodon", "uri": "http://mstdn.example.com", "description": "test mastodon", "email": "mstdn@mstdn.example.com"}`) + fmt.Fprintln(w, `{"title": "mastodon", "uri": "http://mstdn.example.com", "description": "test mastodon", "email": "mstdn@mstdn.example.com", "contact_account": {"username": "mattn"}}`) })) defer ts.Close() @@ -47,6 +47,9 @@ func TestGetInstance(t *testing.T) { if ins.EMail != "mstdn@mstdn.example.com" { t.Fatalf("want %q but %q", "mstdn@mstdn.example.com", ins.EMail) } + if ins.ContactAccount.Username != "mattn" { + t.Fatalf("want %q but %q", "mattn", ins.ContactAccount.Username) + } } func TestGetInstanceMore(t *testing.T) { From 80c1d52a0d487d0f46aeeef738807e4d9f7b3e06 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 30 Sep 2019 17:04:44 +0900 Subject: [PATCH 064/127] Fix tests --- apps_test.go | 2 +- mastodon_test.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps_test.go b/apps_test.go index f62bf15..32e28ab 100644 --- a/apps_test.go +++ b/apps_test.go @@ -92,7 +92,7 @@ func TestRegisterAppWithCancel(t *testing.T) { if err == nil { t.Fatalf("should be fail: %v", err) } - if want := "Post " + ts.URL + "/api/v1/apps: context canceled"; want != err.Error() { + if want := fmt.Sprintf("Post %q: context canceled", ts.URL+"/api/v1/apps"); want != err.Error() { t.Fatalf("want %q but %q", want, err.Error()) } } diff --git a/mastodon_test.go b/mastodon_test.go index 1379230..bfcbadc 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -138,7 +138,7 @@ func TestAuthenticateWithCancel(t *testing.T) { if err == nil { t.Fatalf("should be fail: %v", err) } - if want := "Post " + ts.URL + "/oauth/token: context canceled"; want != err.Error() { + if want := fmt.Sprintf("Post %q: context canceled", ts.URL+"/oauth/token"); want != err.Error() { t.Fatalf("want %q but %q", want, err.Error()) } } @@ -200,7 +200,7 @@ func TestPostStatusWithCancel(t *testing.T) { if err == nil { t.Fatalf("should be fail: %v", err) } - if want := "Post " + ts.URL + "/api/v1/statuses: context canceled"; want != err.Error() { + if want := fmt.Sprintf("Post %q: context canceled", ts.URL+"/api/v1/statuses"); want != err.Error() { t.Fatalf("want %q but %q", want, err.Error()) } } @@ -264,7 +264,7 @@ func TestGetTimelineHomeWithCancel(t *testing.T) { if err == nil { t.Fatalf("should be fail: %v", err) } - if want := "Get " + ts.URL + "/api/v1/timelines/home: context canceled"; want != err.Error() { + if want := fmt.Sprintf("Post %q: context canceled", ts.URL+"/api/v1/timelines/home"); want != err.Error() { t.Fatalf("want %q but %q", want, err.Error()) } } From e24991527b45a2bd5177730f2daa5725f7f5cb15 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 30 Sep 2019 17:10:21 +0900 Subject: [PATCH 065/127] Fix test --- mastodon_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mastodon_test.go b/mastodon_test.go index bfcbadc..ef70981 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -264,7 +264,7 @@ func TestGetTimelineHomeWithCancel(t *testing.T) { if err == nil { t.Fatalf("should be fail: %v", err) } - if want := fmt.Sprintf("Post %q: context canceled", ts.URL+"/api/v1/timelines/home"); want != err.Error() { + if want := fmt.Sprintf("Get %q: context canceled", ts.URL+"/api/v1/timelines/home"); want != err.Error() { t.Fatalf("want %q but %q", want, err.Error()) } } From 934e685e7a8cdbaa20dec4e8f1e69028edd8a423 Mon Sep 17 00:00:00 2001 From: Ben Lubar Date: Thu, 13 Jun 2019 22:21:37 -0500 Subject: [PATCH 066/127] Add convenience constants for post visibilities --- mastodon.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mastodon.go b/mastodon.go index fa9adf7..44dde22 100644 --- a/mastodon.go +++ b/mastodon.go @@ -240,6 +240,14 @@ func (c *Client) authenticate(ctx context.Context, params url.Values) error { return nil } +// Convenience constants for Toot.Visibility +const ( + VisibilityPublic = "public" + VisibilityUnlisted = "unlisted" + VisibilityFollowersOnly = "private" + VisibilityDirectMessage = "direct" +) + // Toot is struct to post status. type Toot struct { Status string `json:"status"` From 977e6c550ec1d4f39b825b1a8168415cea636f8b Mon Sep 17 00:00:00 2001 From: Ben Lubar Date: Thu, 13 Jun 2019 22:21:55 -0500 Subject: [PATCH 067/127] Add support for creating scheduled posts --- mastodon.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mastodon.go b/mastodon.go index 44dde22..e3ec1b6 100644 --- a/mastodon.go +++ b/mastodon.go @@ -250,12 +250,13 @@ const ( // Toot is struct to post status. type Toot struct { - Status string `json:"status"` - InReplyToID ID `json:"in_reply_to_id"` - MediaIDs []ID `json:"media_ids"` - Sensitive bool `json:"sensitive"` - SpoilerText string `json:"spoiler_text"` - Visibility string `json:"visibility"` + Status string `json:"status"` + InReplyToID ID `json:"in_reply_to_id"` + MediaIDs []ID `json:"media_ids"` + Sensitive bool `json:"sensitive"` + SpoilerText string `json:"spoiler_text"` + Visibility string `json:"visibility"` + ScheduledAt time.Time `json:"scheduled_at,omitempty"` } // Mention hold information for mention. From c9e2d23df34e7618e62b8c2fd5a92f52087d2ce8 Mon Sep 17 00:00:00 2001 From: Ben Lubar Date: Fri, 14 Jun 2019 01:39:23 -0500 Subject: [PATCH 068/127] Make ScheduledAt a pointer. --- mastodon.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mastodon.go b/mastodon.go index e3ec1b6..733a97e 100644 --- a/mastodon.go +++ b/mastodon.go @@ -250,13 +250,13 @@ const ( // Toot is struct to post status. type Toot struct { - Status string `json:"status"` - InReplyToID ID `json:"in_reply_to_id"` - MediaIDs []ID `json:"media_ids"` - Sensitive bool `json:"sensitive"` - SpoilerText string `json:"spoiler_text"` - Visibility string `json:"visibility"` - ScheduledAt time.Time `json:"scheduled_at,omitempty"` + Status string `json:"status"` + InReplyToID ID `json:"in_reply_to_id"` + MediaIDs []ID `json:"media_ids"` + Sensitive bool `json:"sensitive"` + SpoilerText string `json:"spoiler_text"` + Visibility string `json:"visibility"` + ScheduledAt *time.Time `json:"scheduled_at,omitempty"` } // Mention hold information for mention. From 25da74b864efd8eed0c7a16366adf7d9c443c16b Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Thu, 8 Aug 2019 06:35:37 +0200 Subject: [PATCH 069/127] Fix follower pagination in cmd/mstdn Make sure to reset Pagination's MinID on every iteration, as discussed in #99. --- cmd/mstdn/cmd_followers.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/mstdn/cmd_followers.go b/cmd/mstdn/cmd_followers.go index ee5a86b..bfacf8b 100644 --- a/cmd/mstdn/cmd_followers.go +++ b/cmd/mstdn/cmd_followers.go @@ -29,6 +29,7 @@ func cmdFollowers(c *cli.Context) error { break } pg.SinceID = "" + pg.MinID = "" time.Sleep(10 * time.Second) } s := newScreen(config) From 2abdb8e37c000f18c689f404f42ee3db6eda3143 Mon Sep 17 00:00:00 2001 From: buckket Date: Fri, 17 May 2019 23:47:32 +0200 Subject: [PATCH 070/127] Add support for /api/v1/push/subscription --- README.md | 4 +++ compat.go | 24 +++++++++++++ notification.go | 83 ++++++++++++++++++++++++++++++++++++++++++++ notification_test.go | 79 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+) diff --git a/README.md b/README.md index bd7bcb9..b6639a8 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,10 @@ func main() { * [x] GET /api/v1/notifications/:id * [x] POST /api/v1/notifications/dismiss * [x] POST /api/v1/notifications/clear +* [x] POST /api/v1/push/subscription +* [x] GET /api/v1/push/subscription +* [x] PUT /api/v1/push/subscription +* [x] DELETE /api/v1/push/subscription * [x] GET /api/v1/reports * [x] POST /api/v1/reports * [x] GET /api/v1/search diff --git a/compat.go b/compat.go index 0031ae4..789906d 100644 --- a/compat.go +++ b/compat.go @@ -3,6 +3,7 @@ package mastodon import ( "encoding/json" "fmt" + "strconv" ) type ID string @@ -23,3 +24,26 @@ func (id *ID) UnmarshalJSON(data []byte) error { *id = ID(fmt.Sprint(n)) return nil } + +type Sbool bool + +func (s *Sbool) UnmarshalJSON(data []byte) error { + if len(data) > 0 && data[0] == '"' && data[len(data)-1] == '"' { + var str string + if err := json.Unmarshal(data, &str); err != nil { + return err + } + b, err := strconv.ParseBool(str) + if err != nil { + return err + } + *s = Sbool(b) + return nil + } + var b bool + if err := json.Unmarshal(data, &b); err != nil { + return err + } + *s = Sbool(b) + return nil +} diff --git a/notification.go b/notification.go index c16f3be..66f8e30 100644 --- a/notification.go +++ b/notification.go @@ -2,9 +2,13 @@ package mastodon import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "encoding/base64" "fmt" "net/http" "net/url" + "strconv" "time" ) @@ -17,6 +21,20 @@ type Notification struct { Status *Status `json:"status"` } +type PushSubscription struct { + ID ID `json:"id"` + Endpoint string `json:"endpoint"` + ServerKey string `json:"server_key"` + Alerts *PushAlerts `json:"alerts"` +} + +type PushAlerts struct { + Follow *Sbool `json:"follow"` + Favourite *Sbool `json:"favourite"` + Reblog *Sbool `json:"reblog"` + Mention *Sbool `json:"mention"` +} + // GetNotifications return notifications. func (c *Client) GetNotifications(ctx context.Context, pg *Pagination) ([]*Notification, error) { var notifications []*Notification @@ -52,3 +70,68 @@ func (c *Client) DismissNotification(ctx context.Context, id ID) error { func (c *Client) ClearNotifications(ctx context.Context) error { return c.doAPI(ctx, http.MethodPost, "/api/v1/notifications/clear", nil, nil, nil) } + +// AddPushSubscription adds a new push subscription. +func (c *Client) AddPushSubscription(ctx context.Context, endpoint string, public ecdsa.PublicKey, shared []byte, alerts PushAlerts) (*PushSubscription, error) { + var subscription PushSubscription + pk := elliptic.Marshal(public.Curve, public.X, public.Y) + params := url.Values{} + params.Add("subscription[endpoint]", endpoint) + params.Add("subscription[keys][p256dh]", base64.RawURLEncoding.EncodeToString(pk)) + params.Add("subscription[keys][auth]", base64.RawURLEncoding.EncodeToString(shared)) + if alerts.Follow != nil { + params.Add("data[alerts][follow]", strconv.FormatBool(bool(*alerts.Follow))) + } + if alerts.Favourite != nil { + params.Add("data[alerts][favourite]", strconv.FormatBool(bool(*alerts.Favourite))) + } + if alerts.Reblog != nil { + params.Add("data[alerts][reblog]", strconv.FormatBool(bool(*alerts.Reblog))) + } + if alerts.Mention != nil { + params.Add("data[alerts][mention]", strconv.FormatBool(bool(*alerts.Mention))) + } + err := c.doAPI(ctx, http.MethodPost, "/api/v1/push/subscription", params, &subscription, nil) + if err != nil { + return nil, err + } + return &subscription, nil +} + +// UpdatePushSubscription updates which type of notifications are sent for the active push subscription. +func (c *Client) UpdatePushSubscription(ctx context.Context, alerts *PushAlerts) (*PushSubscription, error) { + var subscription PushSubscription + params := url.Values{} + if alerts.Follow != nil { + params.Add("data[alerts][follow]", strconv.FormatBool(bool(*alerts.Follow))) + } + if alerts.Mention != nil { + params.Add("data[alerts][favourite]", strconv.FormatBool(bool(*alerts.Favourite))) + } + if alerts.Reblog != nil { + params.Add("data[alerts][reblog]", strconv.FormatBool(bool(*alerts.Reblog))) + } + if alerts.Mention != nil { + params.Add("data[alerts][mention]", strconv.FormatBool(bool(*alerts.Mention))) + } + err := c.doAPI(ctx, http.MethodPut, "/api/v1/push/subscription", params, &subscription, nil) + if err != nil { + return nil, err + } + return &subscription, nil +} + +// RemovePushSubscription deletes the active push subscription. +func (c *Client) RemovePushSubscription(ctx context.Context) error { + return c.doAPI(ctx, http.MethodDelete, "/api/v1/push/subscription", nil, nil, nil) +} + +// GetPushSubscription retrieves information about the active push subscription. +func (c *Client) GetPushSubscription(ctx context.Context) (*PushSubscription, error) { + var subscription PushSubscription + err := c.doAPI(ctx, http.MethodGet, "/api/v1/push/subscription", nil, &subscription, nil) + if err != nil { + return nil, err + } + return &subscription, nil +} diff --git a/notification_test.go b/notification_test.go index 024e1b4..2154702 100644 --- a/notification_test.go +++ b/notification_test.go @@ -2,6 +2,9 @@ package mastodon import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" "fmt" "net/http" "net/http/httptest" @@ -64,3 +67,79 @@ func TestGetNotifications(t *testing.T) { t.Fatalf("should not be fail: %v", err) } } + +func TestPushSubscription(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v1/push/subscription": + fmt.Fprintln(w, ` {"id":1,"endpoint":"https://example.org","alerts":{"follow":"true","favourite":"true","reblog":"true","mention":"true"},"server_key":"foobar"}`) + return + } + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + + enabled := new(Sbool) + *enabled = true + alerts := PushAlerts{Follow: enabled, Favourite: enabled, Reblog: enabled, Mention: enabled} + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + + shared := make([]byte, 16) + _, err = rand.Read(shared) + if err != nil { + t.Fatal(err) + } + + testSub := func(sub *PushSubscription, err error) { + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if sub.ID != "1" { + t.Fatalf("want %v but %v", "1", sub.ID) + } + if sub.Endpoint != "https://example.org" { + t.Fatalf("want %v but %v", "https://example.org", sub.Endpoint) + } + if sub.ServerKey != "foobar" { + t.Fatalf("want %v but %v", "foobar", sub.ServerKey) + } + if *sub.Alerts.Favourite != true { + t.Fatalf("want %v but %v", true, *sub.Alerts.Favourite) + } + if *sub.Alerts.Mention != true { + t.Fatalf("want %v but %v", true, *sub.Alerts.Mention) + } + if *sub.Alerts.Reblog != true { + t.Fatalf("want %v but %v", true, *sub.Alerts.Reblog) + } + if *sub.Alerts.Follow != true { + t.Fatalf("want %v but %v", true, *sub.Alerts.Follow) + } + } + + sub, err := client.AddPushSubscription(context.Background(), "http://example.org", priv.PublicKey, shared, alerts) + testSub(sub, err) + + sub, err = client.GetPushSubscription(context.Background()) + testSub(sub, err) + + sub, err = client.UpdatePushSubscription(context.Background(), &alerts) + testSub(sub, err) + + err = client.RemovePushSubscription(context.Background()) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } +} From e43f2060a867024f8addcfcd9f90b496ea851bd6 Mon Sep 17 00:00:00 2001 From: buckket Date: Sun, 19 May 2019 21:39:22 +0200 Subject: [PATCH 071/127] Modify test fixture to increse test coverage --- notification_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notification_test.go b/notification_test.go index 2154702..124a0d7 100644 --- a/notification_test.go +++ b/notification_test.go @@ -72,7 +72,7 @@ func TestPushSubscription(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v1/push/subscription": - fmt.Fprintln(w, ` {"id":1,"endpoint":"https://example.org","alerts":{"follow":"true","favourite":"true","reblog":"true","mention":"true"},"server_key":"foobar"}`) + fmt.Fprintln(w, ` {"id":1,"endpoint":"https://example.org","alerts":{"follow":true,"favourite":"true","reblog":"true","mention":"true"},"server_key":"foobar"}`) return } http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) From 4275f0739f4305bf444827dad51a9bfd1a8b7449 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 9 Jan 2020 09:46:19 +0000 Subject: [PATCH 072/127] Add renovate.json --- renovate.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 renovate.json diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..f45d8f1 --- /dev/null +++ b/renovate.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "config:base" + ] +} From a4693067ab5e958c97a90aa82ad904ddcf0259ef Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 9 Jan 2020 09:53:13 +0000 Subject: [PATCH 073/127] Update module fatih/color to v1.9.0 --- go.mod | 5 +---- go.sum | 9 +++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index c70bdef..2268d4a 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,10 @@ go 1.12 require ( github.com/PuerkitoBio/goquery v1.5.0 - github.com/fatih/color v1.7.0 + github.com/fatih/color v1.9.0 github.com/gorilla/websocket v1.4.0 - github.com/mattn/go-colorable v0.1.1 // indirect - github.com/mattn/go-isatty v0.0.7 // indirect github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 github.com/urfave/cli v1.20.0 golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 - golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 // indirect ) diff --git a/go.sum b/go.sum index 63d5b07..bfb16bc 100644 --- a/go.sum +++ b/go.sum @@ -4,13 +4,20 @@ github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRy github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859 h1:smQbSzmT3EHl4EUwtFwFGmGIpiYgIiiPeVv1uguIQEE= github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= @@ -28,4 +35,6 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpbl golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug= golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From edf591bde6114cb8f062c36a04fc5214ac240b98 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 9 Jan 2020 09:55:36 +0000 Subject: [PATCH 074/127] Update golang.org/x/net commit hash to c0dbc17 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 2268d4a..488ba11 100644 --- a/go.mod +++ b/go.mod @@ -9,5 +9,5 @@ require ( github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 github.com/urfave/cli v1.20.0 - golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 + golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 ) diff --git a/go.sum b/go.sum index bfb16bc..ad73369 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,8 @@ golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak= golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= From 1c963e9982e1836bd53f190a24ba8dda6bd9f363 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 9 Jan 2020 10:25:15 +0000 Subject: [PATCH 075/127] Update module gorilla/websocket to v1.4.1 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 488ba11..f980974 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.12 require ( github.com/PuerkitoBio/goquery v1.5.0 github.com/fatih/color v1.9.0 - github.com/gorilla/websocket v1.4.0 + github.com/gorilla/websocket v1.4.1 github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 github.com/urfave/cli v1.20.0 diff --git a/go.sum b/go.sum index ad73369..ffefe7e 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= From 822b3dfd49f0ddce6816b03aec2826b933a854bf Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 9 Jan 2020 10:31:10 +0000 Subject: [PATCH 076/127] Update module mattn/go-tty to v0.0.3 --- go.mod | 2 +- go.sum | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index f980974..f435681 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/PuerkitoBio/goquery v1.5.0 github.com/fatih/color v1.9.0 github.com/gorilla/websocket v1.4.1 - github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859 + github.com/mattn/go-tty v0.0.3 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 github.com/urfave/cli v1.20.0 golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 diff --git a/go.sum b/go.sum index ffefe7e..79e6e62 100644 --- a/go.sum +++ b/go.sum @@ -18,10 +18,14 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 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.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859 h1:smQbSzmT3EHl4EUwtFwFGmGIpiYgIiiPeVv1uguIQEE= github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= +github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= +github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= 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.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= @@ -33,12 +37,16 @@ golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR17 golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug= golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= From 845b4bd39570f180d32c479a8c94a734351ac0ee Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 14 Jan 2020 15:56:48 +0000 Subject: [PATCH 077/127] Update golang.org/x/net commit hash to 6afb519 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index f435681..23fa799 100644 --- a/go.mod +++ b/go.mod @@ -9,5 +9,5 @@ require ( github.com/mattn/go-tty v0.0.3 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 github.com/urfave/cli v1.20.0 - golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 + golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa ) diff --git a/go.sum b/go.sum index 79e6e62..807579f 100644 --- a/go.sum +++ b/go.sum @@ -37,6 +37,8 @@ golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR17 golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= From 151613575d2bde79511815a7a0412a724a11f15b Mon Sep 17 00:00:00 2001 From: mattn Date: Tue, 28 Jan 2020 19:41:13 +0900 Subject: [PATCH 078/127] Delete renovate.json --- renovate.json | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 renovate.json diff --git a/renovate.json b/renovate.json deleted file mode 100644 index f45d8f1..0000000 --- a/renovate.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": [ - "config:base" - ] -} From fd6533a50809b3aeb5b138ced6cf5aa8769e33d9 Mon Sep 17 00:00:00 2001 From: hiromi-mi Date: Tue, 25 Feb 2020 07:05:13 +0900 Subject: [PATCH 079/127] Support conversations API Support these APIs added in Mastodon 2.6.0. - GET /api/v1/conversations - DELETE /api/v1/conversations/:id - POST /api/v1/conversations/:id/read --- README.md | 3 ++ status.go | 30 ++++++++++++++++++ status_test.go | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+) diff --git a/README.md b/README.md index b6639a8..b74f788 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,9 @@ func main() { * [x] GET /api/v1/accounts/search * [x] POST /api/v1/apps * [x] GET /api/v1/blocks +* [x] GET /api/v1/conversations +* [x] DELETE /api/v1/conversations/:id +* [x] POST /api/v1/conversations/:id/read * [x] GET /api/v1/favourites * [x] GET /api/v1/follow_requests * [x] POST /api/v1/follow_requests/:id/authorize diff --git a/status.go b/status.go index 7bee52a..ebf7a9f 100644 --- a/status.go +++ b/status.go @@ -61,6 +61,14 @@ type Card struct { Height int64 `json:"height"` } +// Conversation hold information for mastodon conversation. +type Conversation struct { + ID ID `json:"id"` + Accounts []*Account `json:"accounts"` + Unread bool `json:"unread"` + LastStatus *Status `json:"last_status"` +} + // GetFavourites return the favorite list of the current user. func (c *Client) GetFavourites(ctx context.Context, pg *Pagination) ([]*Status, error) { var statuses []*Status @@ -307,3 +315,25 @@ func (c *Client) GetTimelineDirect(ctx context.Context, pg *Pagination) ([]*Stat } return statuses, nil } + +// GetConversations return direct conversations. +func (c *Client) GetConversations(ctx context.Context, pg *Pagination) ([]*Conversation, error) { + params := url.Values{} + + var conversations []*Conversation + err := c.doAPI(ctx, http.MethodGet, "/api/v1/conversations", params, &conversations, pg) + if err != nil { + return nil, err + } + return conversations, nil +} + +// DeleteConversation delete the conversation specified by id. +func (c *Client) DeleteConversation(ctx context.Context, id ID) error { + return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/conversations/%s", id), nil, nil, nil) +} + +// MarkConversationAsRead mark the conversation as read. +func (c *Client) MarkConversationAsRead(ctx context.Context, id ID) error { + return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/conversations/%s/read", id), nil, nil, nil) +} diff --git a/status_test.go b/status_test.go index 7536ffb..85c0967 100644 --- a/status_test.go +++ b/status_test.go @@ -621,3 +621,89 @@ func TestUploadMedia(t *testing.T) { t.Fatalf("want %q but %q", "123", attachment.ID) } } + +func TestGetConversations(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/conversations" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + } + fmt.Fprintln(w, `[{"id": "4", "unread":false, "last_status" : {"content": "zzz"}}, {"id": "3", "unread":true, "last_status" : {"content": "bar"}}]`) + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + convs, err := client.GetConversations(context.Background(), nil) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if len(convs) != 2 { + t.Fatalf("result should be 2: %d", len(convs)) + } + if convs[0].ID != "4" { + t.Fatalf("want %q but %q", "4", convs[0].ID) + } + if convs[0].LastStatus.Content != "zzz" { + t.Fatalf("want %q but %q", "zzz", convs[0].LastStatus.Content) + } + if convs[1].Unread != true { + t.Fatalf("unread should be true: %t", convs[1].Unread) + } +} + +func TestDeleteConversation(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/conversations/12345678" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + if r.Method != "DELETE" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusMethodNotAllowed) + return + } + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "hoge", + }) + err = client.DeleteConversation(context.Background(), "12345678") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } +} + +func TestMarkConversationsAsRead(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/conversations/111111/read" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + if r.Method != "POST" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + err := client.MarkConversationAsRead(context.Background(), "111111") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } +} From 021f5d0019ec2294f3122ceef351ed8d877c93bf Mon Sep 17 00:00:00 2001 From: hiromi-mi Date: Tue, 25 Feb 2020 20:38:06 +0900 Subject: [PATCH 080/127] fix CI for DeleteConversation --- status_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/status_test.go b/status_test.go index 85c0967..93f46e7 100644 --- a/status_test.go +++ b/status_test.go @@ -676,7 +676,7 @@ func TestDeleteConversation(t *testing.T) { ClientSecret: "bar", AccessToken: "hoge", }) - err = client.DeleteConversation(context.Background(), "12345678") + err := client.DeleteConversation(context.Background(), "12345678") if err != nil { t.Fatalf("should not be fail: %v", err) } From 75578dd2499632a9976f3710075ebe6d17f14535 Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Sun, 1 Mar 2020 12:18:26 +0100 Subject: [PATCH 081/127] Update search to use v2 API endpoint v1 has been disabled on most instances by now. The change is minor: hash-tags are now reported as proper structs instead of a simple string-array. --- README.md | 2 +- cmd/mstdn/cmd_search_test.go | 4 ++-- mastodon.go | 2 +- status.go | 2 +- status_test.go | 8 ++++---- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b74f788..21af6af 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ func main() { * [x] DELETE /api/v1/push/subscription * [x] GET /api/v1/reports * [x] POST /api/v1/reports -* [x] GET /api/v1/search +* [x] GET /api/v2/search * [x] GET /api/v1/statuses/:id * [x] GET /api/v1/statuses/:id/context * [x] GET /api/v1/statuses/:id/card diff --git a/cmd/mstdn/cmd_search_test.go b/cmd/mstdn/cmd_search_test.go index 2d00b93..f3509cc 100644 --- a/cmd/mstdn/cmd_search_test.go +++ b/cmd/mstdn/cmd_search_test.go @@ -13,8 +13,8 @@ func TestCmdSearch(t *testing.T) { out := testWithServer( func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/api/v1/search": - fmt.Fprintln(w, `{"accounts": [{"id": 234, "acct": "zzz"}], "statuses":[{"id": 345, "content": "yyy"}], "hashtags": ["www", "わろす"]}`) + 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) diff --git a/mastodon.go b/mastodon.go index 733a97e..5c30a1c 100644 --- a/mastodon.go +++ b/mastodon.go @@ -319,7 +319,7 @@ type Emoji struct { type Results struct { Accounts []*Account `json:"accounts"` Statuses []*Status `json:"statuses"` - Hashtags []string `json:"hashtags"` + Hashtags []*Tag `json:"hashtags"` } // Pagination is a struct for specifying the get range. diff --git a/status.go b/status.go index ebf7a9f..2f6bdd7 100644 --- a/status.go +++ b/status.go @@ -277,7 +277,7 @@ func (c *Client) Search(ctx context.Context, q string, resolve bool) (*Results, params.Set("q", q) params.Set("resolve", fmt.Sprint(resolve)) var results Results - err := c.doAPI(ctx, http.MethodGet, "/api/v1/search", params, &results, nil) + err := c.doAPI(ctx, http.MethodGet, "/api/v2/search", params, &results, nil) if err != nil { return nil, err } diff --git a/status_test.go b/status_test.go index 93f46e7..929578b 100644 --- a/status_test.go +++ b/status_test.go @@ -532,11 +532,11 @@ func TestDeleteStatus(t *testing.T) { func TestSearch(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/api/v1/search" { + if r.URL.Path != "/api/v2/search" { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } - if r.RequestURI != "/api/v1/search?q=q&resolve=false" { + if r.RequestURI != "/api/v2/search?q=q&resolve=false" { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusBadRequest) return } @@ -544,7 +544,7 @@ func TestSearch(t *testing.T) { fmt.Fprintln(w, ` {"accounts":[{"username": "zzz"},{"username": "yyy"}], "statuses":[{"content": "aaa"}], - "hashtags":["tag","tag2","tag3"] + "hashtags":[{"name": "tag"},{"name": "tag2"},{"name": "tag3"}] }`) return })) @@ -575,7 +575,7 @@ func TestSearch(t *testing.T) { if len(ret.Hashtags) != 3 { t.Fatalf("Hashtags have %q entries, but %q", "3", len(ret.Hashtags)) } - if ret.Hashtags[2] != "tag3" { + if ret.Hashtags[2].Name != "tag3" { t.Fatalf("Hashtags[2] should %q , but %q", "tag3", ret.Hashtags[2]) } } From 1c0769492be35bdb567182d3eba0f14e903c197f Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Sun, 1 Mar 2020 12:25:12 +0100 Subject: [PATCH 082/127] Fix History struct members --- mastodon.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mastodon.go b/mastodon.go index 5c30a1c..bd35202 100644 --- a/mastodon.go +++ b/mastodon.go @@ -277,8 +277,8 @@ type Tag struct { // History hold information for history. type History struct { Day string `json:"day"` - Uses int64 `json:"uses"` - Accounts int64 `json:"accounts"` + Uses string `json:"uses"` + Accounts string `json:"accounts"` } // Attachment hold information for attachment. From 3e91c76504df99d2066afada755594ee8b554d1d Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Mon, 13 May 2019 01:46:11 +0200 Subject: [PATCH 083/127] Added polls entity, available since API 2.8.0 --- polls.go | 20 ++++++++++++++++++++ status.go | 1 + 2 files changed, 21 insertions(+) create mode 100644 polls.go diff --git a/polls.go b/polls.go new file mode 100644 index 0000000..841d209 --- /dev/null +++ b/polls.go @@ -0,0 +1,20 @@ +package mastodon + +import "time" + +// Poll hold information for mastodon polls. +type Poll struct { + ID ID `json:"id"` + ExpiresAt time.Time `json:"expires_at"` + Expired bool `json:"expired"` + Multiple bool `json:"multiple"` + VotesCount int64 `json:"votes_count"` + Options []PollOption `json:"options"` + Voted bool `json:"voted"` +} + +// Poll hold information for a mastodon poll option. +type PollOption struct { + Title string `json:"title"` + VotesCount int64 `json:"votes_count"` +} diff --git a/status.go b/status.go index 2f6bdd7..e0d955f 100644 --- a/status.go +++ b/status.go @@ -34,6 +34,7 @@ type Status struct { Mentions []Mention `json:"mentions"` Tags []Tag `json:"tags"` Card *Card `json:"card"` + Poll *Poll `json:"poll"` Application Application `json:"application"` Language string `json:"language"` Pinned interface{} `json:"pinned"` From 315df7d9162edb69571e2a877953097f8c431669 Mon Sep 17 00:00:00 2001 From: Masahiro Furudate <178inaba.git@gmail.com> Date: Mon, 27 Jul 2020 02:03:29 +0900 Subject: [PATCH 084/127] Add Media struct and UploadMediaFromMedia method --- mastodon.go | 48 ++++-------------------------- status.go | 84 +++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 84 insertions(+), 48 deletions(-) diff --git a/mastodon.go b/mastodon.go index bd35202..d642cf3 100644 --- a/mastodon.go +++ b/mastodon.go @@ -2,18 +2,14 @@ package mastodon import ( - "bytes" "context" "encoding/json" "errors" "fmt" "io" - "mime/multipart" "net/http" "net/url" - "os" "path" - "path/filepath" "strings" "time" @@ -58,52 +54,18 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in if err != nil { return err } - } else if file, ok := params.(string); ok { - f, err := os.Open(file) + } else if media, ok := params.(*Media); ok { + r, contentType, err := media.bodyAndContentType() if err != nil { return err } - defer f.Close() - var buf bytes.Buffer - mw := multipart.NewWriter(&buf) - part, err := mw.CreateFormFile("file", filepath.Base(file)) + req, err = http.NewRequest(method, u.String(), r) if err != nil { return err } - _, err = io.Copy(part, f) - if err != nil { - return err - } - err = mw.Close() - if err != nil { - return err - } - req, err = http.NewRequest(method, u.String(), &buf) - if err != nil { - return err - } - ct = mw.FormDataContentType() - } else if reader, ok := params.(io.Reader); ok { - var buf bytes.Buffer - mw := multipart.NewWriter(&buf) - part, err := mw.CreateFormFile("file", "upload") - if err != nil { - return err - } - _, err = io.Copy(part, reader) - if err != nil { - return err - } - err = mw.Close() - if err != nil { - return err - } - req, err = http.NewRequest(method, u.String(), &buf) - if err != nil { - return err - } - ct = mw.FormDataContentType() + + ct = contentType } else { if method == http.MethodGet && pg != nil { u.RawQuery = pg.toValues().Encode() diff --git a/status.go b/status.go index e0d955f..0ca9a97 100644 --- a/status.go +++ b/status.go @@ -1,11 +1,15 @@ package mastodon import ( + "bytes" "context" "fmt" "io" + "mime/multipart" "net/http" "net/url" + "os" + "strings" "time" ) @@ -70,6 +74,71 @@ type Conversation struct { LastStatus *Status `json:"last_status"` } +// Media is struct to hold media. +type Media struct { + File io.Reader + Thumbnail io.Reader + Description string + Focus string +} + +func (m *Media) bodyAndContentType() (io.Reader, string, error) { + var buf bytes.Buffer + mw := multipart.NewWriter(&buf) + + fileName := "upload" + if f, ok := m.File.(*os.File); ok { + fileName = f.Name() + } + file, err := mw.CreateFormFile("file", fileName) + if err != nil { + return nil, "", err + } + if _, err := io.Copy(file, m.File); err != nil { + return nil, "", err + } + + if m.Thumbnail != nil { + thumbName := "upload" + if f, ok := m.Thumbnail.(*os.File); ok { + thumbName = f.Name() + } + thumb, err := mw.CreateFormFile("thumbnail", thumbName) + if err != nil { + return nil, "", err + } + if _, err := io.Copy(thumb, m.Thumbnail); err != nil { + return nil, "", err + } + } + + if m.Description != "" { + desc, err := mw.CreateFormField("description") + if err != nil { + return nil, "", err + } + if _, err := io.Copy(desc, strings.NewReader(m.Description)); err != nil { + return nil, "", err + } + } + + if m.Focus != "" { + focus, err := mw.CreateFormField("focus") + if err != nil { + return nil, "", err + } + if _, err := io.Copy(focus, strings.NewReader(m.Focus)); err != nil { + return nil, "", err + } + } + + if err := mw.Close(); err != nil { + return nil, "", err + } + + return &buf, mw.FormDataContentType(), nil +} + // GetFavourites return the favorite list of the current user. func (c *Client) GetFavourites(ctx context.Context, pg *Pagination) ([]*Status, error) { var statuses []*Status @@ -287,19 +356,24 @@ func (c *Client) Search(ctx context.Context, q string, resolve bool) (*Results, // UploadMedia upload a media attachment from a file. func (c *Client) UploadMedia(ctx context.Context, file string) (*Attachment, error) { - var attachment Attachment - err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", file, &attachment, nil) + f, err := os.Open(file) if err != nil { return nil, err } - return &attachment, nil + defer f.Close() + + return c.UploadMediaFromMedia(ctx, &Media{File: f}) } // UploadMediaFromReader uploads a media attachment from a io.Reader. func (c *Client) UploadMediaFromReader(ctx context.Context, reader io.Reader) (*Attachment, error) { + return c.UploadMediaFromMedia(ctx, &Media{File: reader}) +} + +// UploadMediaFromMedia uploads a media attachment from a Media struct. +func (c *Client) UploadMediaFromMedia(ctx context.Context, media *Media) (*Attachment, error) { var attachment Attachment - err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", reader, &attachment, nil) - if err != nil { + if err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", media, &attachment, nil); err != nil { return nil, err } return &attachment, nil From adff0e83b95f10f842d6faed83130db8479e40f3 Mon Sep 17 00:00:00 2001 From: Ollivier Robert Date: Wed, 14 Apr 2021 13:42:14 +0200 Subject: [PATCH 085/127] Use Conversations. --- status.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/status.go b/status.go index 0ca9a97..7d910f0 100644 --- a/status.go +++ b/status.go @@ -383,11 +383,19 @@ func (c *Client) UploadMediaFromMedia(ctx context.Context, media *Media) (*Attac func (c *Client) GetTimelineDirect(ctx context.Context, pg *Pagination) ([]*Status, error) { params := url.Values{} - var statuses []*Status - err := c.doAPI(ctx, http.MethodGet, "/api/v1/timelines/direct", params, &statuses, pg) + var conversations []*Conversation + err := c.doAPI(ctx, http.MethodGet, "/api/v1/conversations", params, &conversations, pg) if err != nil { return nil, err } + + var statuses []*Status + + for _, c := range conversations { + s := c.LastStatus + statuses = append(statuses, s) + } + return statuses, nil } From 2f161cfa50427794b79e674c7b54d011cccb87f4 Mon Sep 17 00:00:00 2001 From: Ollivier Robert Date: Thu, 15 Apr 2021 12:25:43 +0200 Subject: [PATCH 086/127] Allocate space to prevent SIGSEGV. --- status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/status.go b/status.go index 7d910f0..15ae5f6 100644 --- a/status.go +++ b/status.go @@ -389,7 +389,7 @@ func (c *Client) GetTimelineDirect(ctx context.Context, pg *Pagination) ([]*Stat return nil, err } - var statuses []*Status + var statuses = make([]*Status, len(conversations)) for _, c := range conversations { s := c.LastStatus From 932595ebecd98585eb72a46a88060a58987db04f Mon Sep 17 00:00:00 2001 From: Ollivier Robert Date: Thu, 15 Apr 2021 15:02:19 +0200 Subject: [PATCH 087/127] Fix allocation. --- status.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/status.go b/status.go index 15ae5f6..28c3f0e 100644 --- a/status.go +++ b/status.go @@ -389,7 +389,7 @@ func (c *Client) GetTimelineDirect(ctx context.Context, pg *Pagination) ([]*Stat return nil, err } - var statuses = make([]*Status, len(conversations)) + var statuses = []*Status{} for _, c := range conversations { s := c.LastStatus From 2ae3a80997c78fef59206d734f87210ca87b15f3 Mon Sep 17 00:00:00 2001 From: Ollivier Robert Date: Thu, 15 Apr 2021 15:02:52 +0200 Subject: [PATCH 088/127] Fix tests to use data from Conversation, not Timelines. --- status_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/status_test.go b/status_test.go index 929578b..9b8616c 100644 --- a/status_test.go +++ b/status_test.go @@ -372,7 +372,7 @@ func TestGetTimelinePublic(t *testing.T) { func TestGetTimelineDirect(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, `[{"content": "direct"}, {"content": "status"}]`) + fmt.Fprintln(w, `[{"id": "4", "unread":false, "last_status" : {"content": "zzz"}}, {"id": "3", "unread":true, "last_status" : {"content": "bar"}}]`) })) defer ts.Close() @@ -381,13 +381,14 @@ func TestGetTimelineDirect(t *testing.T) { if err != nil { t.Fatalf("should not be fail: %v", err) } + t.Logf("%#v", tl) if len(tl) != 2 { t.Fatalf("result should be two: %d", len(tl)) } - if tl[0].Content != "direct" { + if tl[0].Content != "zzz" { t.Fatalf("want %q but %q", "foo", tl[0].Content) } - if tl[1].Content != "status" { + if tl[1].Content != "bar" { t.Fatalf("want %q but %q", "bar", tl[1].Content) } } From d6cb307605be95876b656be6fc3dc787833f6b8a Mon Sep 17 00:00:00 2001 From: Ollivier Robert Date: Thu, 15 Apr 2021 16:27:54 +0200 Subject: [PATCH 089/127] Remove leftover Logf() call. --- status_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/status_test.go b/status_test.go index 9b8616c..8bedbb2 100644 --- a/status_test.go +++ b/status_test.go @@ -381,7 +381,6 @@ func TestGetTimelineDirect(t *testing.T) { if err != nil { t.Fatalf("should not be fail: %v", err) } - t.Logf("%#v", tl) if len(tl) != 2 { t.Fatalf("result should be two: %d", len(tl)) } From 6abe72ddb09b1b1e57a9b69494e024133c88e7af Mon Sep 17 00:00:00 2001 From: Ollivier Robert Date: Thu, 15 Apr 2021 16:32:27 +0200 Subject: [PATCH 090/127] Fix TestCmdTimelone() as well. --- cmd/mstdn/cmd_timeline_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/mstdn/cmd_timeline_test.go b/cmd/mstdn/cmd_timeline_test.go index f9b6299..d08a9fc 100644 --- a/cmd/mstdn/cmd_timeline_test.go +++ b/cmd/mstdn/cmd_timeline_test.go @@ -19,8 +19,8 @@ func TestCmdTimeline(t *testing.T) { case "/api/v1/timelines/public": fmt.Fprintln(w, `[{"content": "public"}]`) return - case "/api/v1/timelines/direct": - fmt.Fprintln(w, `[{"content": "direct"}]`) + 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) From 8c434b52828c0995528401a470fb9d309aaf9504 Mon Sep 17 00:00:00 2001 From: Hanage999 Date: Mon, 3 May 2021 10:16:04 +0900 Subject: [PATCH 091/127] Replace deprecated /api/v1/notifications/dismiss with new API --- README.md | 2 +- notification.go | 8 +------- notification_test.go | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 21af6af..07ab506 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ func main() { * [x] GET /api/v1/mutes * [x] GET /api/v1/notifications * [x] GET /api/v1/notifications/:id -* [x] POST /api/v1/notifications/dismiss +* [x] POST /api/v1/notifications/:id/dismiss * [x] POST /api/v1/notifications/clear * [x] POST /api/v1/push/subscription * [x] GET /api/v1/push/subscription diff --git a/notification.go b/notification.go index 66f8e30..b24b9df 100644 --- a/notification.go +++ b/notification.go @@ -57,13 +57,7 @@ func (c *Client) GetNotification(ctx context.Context, id ID) (*Notification, err // DismissNotification deletes a single notification. func (c *Client) DismissNotification(ctx context.Context, id ID) error { - params := url.Values{} - params.Add("id", string(id)) - err := c.doAPI(ctx, http.MethodPost, "/api/v1/notifications/dismiss", params, nil, nil) - if err != nil { - return err - } - return nil + return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/notifications/%v/dismiss", id), nil, nil, nil) } // ClearNotifications clear notifications. diff --git a/notification_test.go b/notification_test.go index 124a0d7..28dc975 100644 --- a/notification_test.go +++ b/notification_test.go @@ -23,7 +23,7 @@ func TestGetNotifications(t *testing.T) { case "/api/v1/notifications/clear": fmt.Fprintln(w, `{}`) return - case "/api/v1/notifications/dismiss": + case "/api/v1/notifications/123/dismiss": fmt.Fprintln(w, `{}`) return } From eb26687c84dc2f1e9d70782215b22a9700e6eba2 Mon Sep 17 00:00:00 2001 From: WaybackBot <66856220+warcbot@users.noreply.github.com> Date: Thu, 25 Feb 2021 14:25:44 +0000 Subject: [PATCH 092/127] Update README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 07ab506..44f5066 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,11 @@ func main() { * [x] GET /api/v1/timelines/public * [x] GET /api/v1/timelines/tag/:hashtag * [x] GET /api/v1/timelines/list/:id +* [x] GET /api/v1/streaming/user +* [x] GET /api/v1/streaming/public +* [x] GET /api/v1/streaming/hashtag?tag=:hashtag +* [x] GET /api/v1/streaming/hashtag/local?tag=:hashtag +* [x] GET /api/v1/streaming/list?list=:list_id ## Installation From bf42b86b9ff81e68b57f0778512599089182b70a Mon Sep 17 00:00:00 2001 From: WaybackBot <66856220+warcbot@users.noreply.github.com> Date: Thu, 25 Feb 2021 14:26:45 +0000 Subject: [PATCH 093/127] Add streaming direct support --- README.md | 1 + streaming.go | 5 +++++ streaming_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/README.md b/README.md index 44f5066..75e8fb1 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,7 @@ func main() { * [x] GET /api/v1/streaming/hashtag?tag=:hashtag * [x] GET /api/v1/streaming/hashtag/local?tag=:hashtag * [x] GET /api/v1/streaming/list?list=:list_id +* [x] GET /api/v1/streaming/direct ## Installation diff --git a/streaming.go b/streaming.go index b57f454..f560fd8 100644 --- a/streaming.go +++ b/streaming.go @@ -164,3 +164,8 @@ func (c *Client) StreamingList(ctx context.Context, id ID) (chan Event, error) { return c.streaming(ctx, "list", params) } + +// StreamingDirect return channel to read events on a direct messages. +func (c *Client) StreamingDirect(ctx context.Context) (chan Event, error) { + return c.streaming(ctx, "direct", nil) +} diff --git a/streaming_test.go b/streaming_test.go index bc0e867..0bd5ccc 100644 --- a/streaming_test.go +++ b/streaming_test.go @@ -333,3 +333,43 @@ data: {"content": "foo"} t.Fatalf("want %q but %q", "foo", events[0].(*UpdateEvent).Status.Content) } } + +func TestStreamingDirect(t *testing.T) { + var isEnd bool + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if isEnd { + return + } else if r.URL.Path != "/api/v1/streaming/direct" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + f, _ := w.(http.Flusher) + fmt.Fprintln(w, ` +event: update +data: {"content": "foo"} + `) + f.Flush() + isEnd = true + })) + defer ts.Close() + + client := NewClient(&Config{Server: ts.URL}) + ctx, cancel := context.WithCancel(context.Background()) + time.AfterFunc(time.Second, cancel) + q, err := client.StreamingDirect(ctx) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + events := []Event{} + for e := range q { + if _, ok := e.(*ErrorEvent); !ok { + events = append(events, e) + } + } + if len(events) != 1 { + t.Fatalf("result should be one: %d", len(events)) + } + if events[0].(*UpdateEvent).Status.Content != "foo" { + t.Fatalf("want %q but %q", "foo", events[0].(*UpdateEvent).Status.Content) + } +} From 86627ec7d6359923b8d5ff3e14131d2810f6942d Mon Sep 17 00:00:00 2001 From: Masahiro Furudate <178inaba.git@gmail.com> Date: Sun, 2 Aug 2020 05:09:53 +0900 Subject: [PATCH 094/127] Fix godoc badge to godev badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 75e8fb1..dfa5c98 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://travis-ci.org/mattn/go-mastodon.svg?branch=master)](https://travis-ci.org/mattn/go-mastodon) [![CodeCov](https://codecov.io/gh/mattn/go-mastodon/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-mastodon) -[![GoDoc](https://godoc.org/github.com/mattn/go-mastodon?status.svg)](http://godoc.org/github.com/mattn/go-mastodon) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/mattn/go-mastodon)](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 From d39c10ba5e9485a540bc9c33d75ade2da98e95b7 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Tue, 29 Jun 2021 17:02:28 +0200 Subject: [PATCH 095/127] Add bookmark support --- status.go | 31 ++++++++++++++++++ status_test.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/status.go b/status.go index 28c3f0e..bd86a15 100644 --- a/status.go +++ b/status.go @@ -30,6 +30,7 @@ type Status struct { FavouritesCount int64 `json:"favourites_count"` Reblogged interface{} `json:"reblogged"` Favourited interface{} `json:"favourited"` + Bookmarked interface{} `json:"bookmarked"` Muted interface{} `json:"muted"` Sensitive bool `json:"sensitive"` SpoilerText string `json:"spoiler_text"` @@ -149,6 +150,16 @@ func (c *Client) GetFavourites(ctx context.Context, pg *Pagination) ([]*Status, return statuses, nil } +// GetBookmarks return the bookmark list of the current user. +func (c *Client) GetBookmarks(ctx context.Context, pg *Pagination) ([]*Status, error) { + var statuses []*Status + err := c.doAPI(ctx, http.MethodGet, "/api/v1/bookmarks", nil, &statuses, pg) + if err != nil { + return nil, err + } + return statuses, nil +} + // GetStatus return status specified by id. func (c *Client) GetStatus(ctx context.Context, id ID) (*Status, error) { var status Status @@ -239,6 +250,26 @@ func (c *Client) Unfavourite(ctx context.Context, id ID) (*Status, error) { return &status, nil } +// Bookmark is bookmark the toot of id and return status of the bookmark toot. +func (c *Client) Bookmark(ctx context.Context, id ID) (*Status, error) { + var status Status + err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/bookmark", id), nil, &status, nil) + if err != nil { + return nil, err + } + return &status, nil +} + +// Unbookmark is unbookmark the toot of id and return status of the unbookmark toot. +func (c *Client) Unbookmark(ctx context.Context, id ID) (*Status, error) { + var status Status + err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unbookmark", id), nil, &status, nil) + if err != nil { + return nil, err + } + return &status, nil +} + // GetTimelineHome return statuses from home timeline. func (c *Client) GetTimelineHome(ctx context.Context, pg *Pagination) ([]*Status, error) { var statuses []*Status diff --git a/status_test.go b/status_test.go index 8bedbb2..0a5231f 100644 --- a/status_test.go +++ b/status_test.go @@ -37,6 +37,34 @@ func TestGetFavourites(t *testing.T) { } } +func TestGetBookmarks(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `[{"content": "foo"}, {"content": "bar"}]`) + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + books, err := client.GetBookmarks(context.Background(), nil) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if len(books) != 2 { + t.Fatalf("result should be two: %d", len(books)) + } + if books[0].Content != "foo" { + t.Fatalf("want %q but %q", "foo", books[0].Content) + } + if books[1].Content != "bar" { + t.Fatalf("want %q but %q", "bar", books[1].Content) + } +} + func TestGetStatus(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/statuses/1234567" { @@ -340,6 +368,66 @@ func TestUnfavourite(t *testing.T) { } } +func TestBookmark(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/statuses/1234567/bookmark" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + fmt.Fprintln(w, `{"content": "zzz"}`) + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + _, err := client.Bookmark(context.Background(), "123") + if err == nil { + t.Fatalf("should be fail: %v", err) + } + status, err := client.Bookmark(context.Background(), "1234567") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if status.Content != "zzz" { + t.Fatalf("want %q but %q", "zzz", status.Content) + } +} + +func TestUnbookmark(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/statuses/1234567/unbookmark" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + fmt.Fprintln(w, `{"content": "zzz"}`) + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + _, err := client.Unbookmark(context.Background(), "123") + if err == nil { + t.Fatalf("should be fail: %v", err) + } + status, err := client.Unbookmark(context.Background(), "1234567") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if status.Content != "zzz" { + t.Fatalf("want %q but %q", "zzz", status.Content) + } +} + func TestGetTimelinePublic(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("local") == "" { From f5813a9d884a24ee21b67c173d0923f5a5736943 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 4 Nov 2021 23:04:38 +0900 Subject: [PATCH 096/127] Enable GitHub Workflows --- .github/workflows/test.yaml | 27 +++++++++++++++++++++++++++ .travis.yml | 13 ------------- 2 files changed, 27 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/test.yaml delete mode 100644 .travis.yml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..a5b62f6 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,27 @@ +name: test +on: + push: + branches: + - master + pull_request: + branches: + - master +jobs: + test: + strategy: + matrix: + os: [windows-latest, macos-latest, ubuntu-latest] + go: ["1.15", "1.16", "1.17"] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + with: + go-version: ${{ matrix.go }} + + - run: go generate ./... + - run: git diff --cached --exit-code + - run: go test ./... -v -cover -coverprofile coverage.out + - run: go test -bench . -benchmem + + - uses: codecov/codecov-action@v1 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b2904bf..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -language: go -sudo: false -go: - - tip - -before_install: - - go get -t -v ./... - -script: - - ./go.test.sh - -after_success: - - bash <(curl -s https://codecov.io/bash) From 1dd699ecee77a6750e29b7fafbd120921dfe3b5a Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 4 Nov 2021 23:05:34 +0900 Subject: [PATCH 097/127] Update README.md --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dfa5c98..dbda926 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # go-mastodon -[![Build Status](https://travis-ci.org/mattn/go-mastodon.svg?branch=master)](https://travis-ci.org/mattn/go-mastodon) -[![CodeCov](https://codecov.io/gh/mattn/go-mastodon/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-mastodon) -[![PkgGoDev](https://pkg.go.dev/badge/github.com/mattn/go-mastodon)](https://pkg.go.dev/github.com/mattn/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) +[![Codecov](https://codecov.io/gh/mattn/go-mastodon/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-mastodon) +[![GoDoc](https://godoc.org/github.com/mattn/go-mastodon?status.svg)](http://godoc.org/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 ### Application From a0db0ed8a0ad85905e2920e78692af1d63f20bf8 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 4 Nov 2021 23:06:26 +0900 Subject: [PATCH 098/127] Update dependencies --- go.mod | 13 +++++---- go.sum | 83 +++++++++++++++++++++++++++++----------------------------- 2 files changed, 49 insertions(+), 47 deletions(-) diff --git a/go.mod b/go.mod index 23fa799..0616e46 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,14 @@ module github.com/mattn/go-mastodon go 1.12 require ( - github.com/PuerkitoBio/goquery v1.5.0 - github.com/fatih/color v1.9.0 - github.com/gorilla/websocket v1.4.1 + github.com/PuerkitoBio/goquery v1.8.0 + github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect + github.com/fatih/color v1.13.0 + github.com/gorilla/websocket v1.4.2 + github.com/mattn/go-colorable v0.1.11 // indirect github.com/mattn/go-tty v0.0.3 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 - github.com/urfave/cli v1.20.0 - golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa + github.com/urfave/cli v1.22.5 + golang.org/x/net v0.0.0-20211101193420-4a448f8816b3 + golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect ) diff --git a/go.sum b/go.sum index 807579f..646b092 100644 --- a/go.sum +++ b/go.sum @@ -1,54 +1,53 @@ -github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk= -github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= -github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= -github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +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.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/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.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= +github.com/mattn/go-colorable v0.1.11/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.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +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.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859 h1:smQbSzmT3EHl4EUwtFwFGmGIpiYgIiiPeVv1uguIQEE= -github.com/mattn/go-tty v0.0.0-20190424173100-523744f04859/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= +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.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 h1:6M3SDHlHHDCx2PcQw3S4KsR170vGqDhJDOmpVd4Hjak= -golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= +github.com/urfave/cli v1.22.5/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-20211101193420-4a448f8816b3 h1:VrJZAjbekhoRn7n5FBujY31gboH+iB3pdLxn3gE9FjU= +golang.org/x/net v0.0.0-20211101193420-4a448f8816b3/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug= -golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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-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-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +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= From 58c389181352acc8cb794e4264adcaf03efd0c4e Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Thu, 4 Nov 2021 14:50:16 +0100 Subject: [PATCH 099/127] Add support to vote on polls. Add more fields to Poll --- polls.go | 50 ++++++++++++++--- polls_test.go | 145 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+), 8 deletions(-) create mode 100644 polls_test.go diff --git a/polls.go b/polls.go index 841d209..e2a45f3 100644 --- a/polls.go +++ b/polls.go @@ -1,16 +1,25 @@ package mastodon -import "time" +import ( + "context" + "fmt" + "net/http" + "net/url" + "time" +) // Poll hold information for mastodon polls. type Poll struct { - ID ID `json:"id"` - ExpiresAt time.Time `json:"expires_at"` - Expired bool `json:"expired"` - Multiple bool `json:"multiple"` - VotesCount int64 `json:"votes_count"` - Options []PollOption `json:"options"` - Voted bool `json:"voted"` + ID ID `json:"id"` + ExpiresAt time.Time `json:"expires_at"` + Expired bool `json:"expired"` + Multiple bool `json:"multiple"` + VotesCount int64 `json:"votes_count"` + VotersCount int64 `json:"voters_count"` + Options []PollOption `json:"options"` + Voted bool `json:"voted"` + OwnVotes []int `json:"own_votes"` + Emojis []Emoji `json:"emojis"` } // Poll hold information for a mastodon poll option. @@ -18,3 +27,28 @@ type PollOption struct { Title string `json:"title"` VotesCount int64 `json:"votes_count"` } + +// GetPoll return poll specified by id. +func (c *Client) GetPoll(ctx context.Context, id ID) (*Poll, error) { + var poll Poll + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/polls/%s", id), nil, &poll, nil) + if err != nil { + return nil, err + } + return &poll, nil +} + +// PollVote votes on a poll specified by id, choices is the Poll.Options index to vote on +func (c *Client) PollVote(ctx context.Context, id ID, choices ...int) (*Poll, error) { + params := url.Values{} + for _, c := range choices { + params.Add("choices[]", fmt.Sprintf("%d", c)) + } + + var poll Poll + err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/polls/%s/votes", url.PathEscape(string(id))), params, &poll, nil) + if err != nil { + return nil, err + } + return &poll, nil +} diff --git a/polls_test.go b/polls_test.go new file mode 100644 index 0000000..7ccee38 --- /dev/null +++ b/polls_test.go @@ -0,0 +1,145 @@ +package mastodon + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +func TestGetPoll(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/polls/1234567" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + fmt.Fprintln(w, `{"id": "1234567", "expires_at": "2019-12-05T04:05:08.302Z", "expired": true, "multiple": false, "votes_count": 10, "voters_count": null, "voted": true, "own_votes": [1], "options": [{"title": "accept", "votes_count": 6}, {"title": "deny", "votes_count": 4}], "emojis":[{"shortcode":"💩", "url":"http://example.com", "static_url": "http://example.com/static"}]}`) + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + _, err := client.GetPoll(context.Background(), "123") + if err == nil { + t.Fatalf("should be fail: %v", err) + } + poll, err := client.GetPoll(context.Background(), "1234567") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if poll.Expired != true { + t.Fatalf("want %t but %t", true, poll.Expired) + } + if poll.Multiple != false { + t.Fatalf("want %t but %t", true, poll.Multiple) + } + if poll.VotesCount != 10 { + t.Fatalf("want %d but %d", 10, poll.VotesCount) + } + if poll.VotersCount != 0 { + t.Fatalf("want %d but %d", 0, poll.VotersCount) + } + if poll.Voted != true { + t.Fatalf("want %t but %t", true, poll.Voted) + } + if len(poll.OwnVotes) != 1 { + t.Fatalf("should have own votes") + } + if poll.OwnVotes[0] != 1 { + t.Fatalf("want %d but %d", 1, poll.OwnVotes[0]) + } + if len(poll.Options) != 2 { + t.Fatalf("should have 2 options") + } + if poll.Options[0].Title != "accept" { + t.Fatalf("want %q but %q", "accept", poll.Options[0].Title) + } + if poll.Options[0].VotesCount != 6 { + t.Fatalf("want %q but %q", 6, poll.Options[0].VotesCount) + } + if poll.Options[1].Title != "deny" { + t.Fatalf("want %q but %q", "deny", poll.Options[1].Title) + } + if poll.Options[1].VotesCount != 4 { + t.Fatalf("want %q but %q", 4, poll.Options[1].VotesCount) + } + if len(poll.Emojis) != 1 { + t.Fatal("should have emojis") + } + if poll.Emojis[0].ShortCode != "💩" { + t.Fatalf("want %q but %q", "💩", poll.Emojis[0].ShortCode) + } + if poll.Emojis[0].URL != "http://example.com" { + t.Fatalf("want %q but %q", "https://example.com", poll.Emojis[0].URL) + } + if poll.Emojis[0].StaticURL != "http://example.com/static" { + t.Fatalf("want %q but %q", "https://example.com/static", poll.Emojis[0].StaticURL) + } +} + +func TestPollVote(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/polls/1234567/votes" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + if r.Method != "POST" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusMethodNotAllowed) + return + } + fmt.Fprintln(w, `{"id": "1234567", "expires_at": "2019-12-05T04:05:08.302Z", "expired": false, "multiple": false, "votes_count": 10, "voters_count": null, "voted": true, "own_votes": [1], "options": [{"title": "accept", "votes_count": 6}, {"title": "deny", "votes_count": 4}], "emojis":[]}`) + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + poll, err := client.PollVote(context.Background(), ID("1234567"), 1) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if poll.Expired != false { + t.Fatalf("want %t but %t", false, poll.Expired) + } + if poll.Multiple != false { + t.Fatalf("want %t but %t", true, poll.Multiple) + } + if poll.VotesCount != 10 { + t.Fatalf("want %d but %d", 10, poll.VotesCount) + } + if poll.VotersCount != 0 { + t.Fatalf("want %d but %d", 0, poll.VotersCount) + } + if poll.Voted != true { + t.Fatalf("want %t but %t", true, poll.Voted) + } + if len(poll.OwnVotes) != 1 { + t.Fatalf("should have own votes") + } + if poll.OwnVotes[0] != 1 { + t.Fatalf("want %d but %d", 1, poll.OwnVotes[0]) + } + if len(poll.Options) != 2 { + t.Fatalf("should have 2 options") + } + if poll.Options[0].Title != "accept" { + t.Fatalf("want %q but %q", "accept", poll.Options[0].Title) + } + if poll.Options[0].VotesCount != 6 { + t.Fatalf("want %q but %q", 6, poll.Options[0].VotesCount) + } + if poll.Options[1].Title != "deny" { + t.Fatalf("want %q but %q", "deny", poll.Options[1].Title) + } + if poll.Options[1].VotesCount != 4 { + t.Fatalf("want %q but %q", 4, poll.Options[1].VotesCount) + } +} From ae6cc118205477dc1fd9dee5c2a997df6e8118b2 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Fri, 5 Nov 2021 09:21:53 +0100 Subject: [PATCH 100/127] Add support for creating polls --- mastodon.go | 9 +++++++++ status.go | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/mastodon.go b/mastodon.go index d642cf3..3523713 100644 --- a/mastodon.go +++ b/mastodon.go @@ -219,6 +219,15 @@ type Toot struct { SpoilerText string `json:"spoiler_text"` Visibility string `json:"visibility"` ScheduledAt *time.Time `json:"scheduled_at,omitempty"` + Poll *TootPoll `json:"poll"` +} + +// TootPoll holds information for creating a poll in Toot. +type TootPoll struct { + Options []string `json:"options"` + ExpiresInSeconds int64 `json:"expires_in"` + Multiple bool `json:"multiple"` + HideTotals bool `json:"hide_totals"` } // Mention hold information for mention. diff --git a/status.go b/status.go index bd86a15..5cdbf22 100644 --- a/status.go +++ b/status.go @@ -349,6 +349,19 @@ func (c *Client) PostStatus(ctx context.Context, toot *Toot) (*Status, error) { params.Add("media_ids[]", string(media)) } } + // Can't use Media and Poll at the same time. + if toot.Poll != nil && toot.Poll.Options != nil && toot.MediaIDs == nil { + for _, opt := range toot.Poll.Options { + params.Add("poll[options][]", string(opt)) + } + params.Add("poll[expires_in]", fmt.Sprintf("%d", toot.Poll.ExpiresInSeconds)) + if toot.Poll.Multiple { + params.Add("poll[multiple]", "true") + } + if toot.Poll.HideTotals { + params.Add("poll[hide_totals]", "true") + } + } if toot.Visibility != "" { params.Set("visibility", fmt.Sprint(toot.Visibility)) } From 4cbbf813dc0e65993c9450b107701464a5bd5488 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Fri, 5 Nov 2021 10:47:57 +0100 Subject: [PATCH 101/127] add test for PostStatus --- mastodon_test.go | 111 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/mastodon_test.go b/mastodon_test.go index ef70981..9a8f3e3 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -2,6 +2,7 @@ package mastodon import ( "context" + "encoding/json" "fmt" "io" "net/http" @@ -204,6 +205,116 @@ func TestPostStatusWithCancel(t *testing.T) { t.Fatalf("want %q but %q", want, err.Error()) } } +func TestPostStatusParams(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/statuses" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + r.ParseForm() + if r.FormValue("media_ids[]") != "" && r.FormValue("poll[options][]") != "" { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } + s := Status{ + ID: ID("1"), + Content: fmt.Sprintf("

%s

", r.FormValue("status")), + } + if r.FormValue("in_reply_to_id") != "" { + s.InReplyToID = ID(r.FormValue("in_reply_to_id")) + } + if r.FormValue("visibility") != "" { + s.Visibility = (r.FormValue("visibility")) + } + if r.FormValue("sensitive") == "true" { + s.Sensitive = true + s.SpoilerText = fmt.Sprintf("

%s

", r.FormValue("spoiler_text")) + } + if r.FormValue("media_ids[]") != "" { + for _, id := range r.Form["media_ids[]"] { + s.MediaAttachments = append(s.MediaAttachments, + Attachment{ID: ID(id)}) + } + } + if r.FormValue("poll[options][]") != "" { + p := Poll{} + for _, opt := range r.Form["poll[options][]"] { + p.Options = append(p.Options, PollOption{ + Title: opt, + VotesCount: 0, + }) + } + if r.FormValue("poll[multiple]") == "true" { + p.Multiple = true + } + s.Poll = &p + } + json.NewEncoder(w).Encode(s) + })) + defer ts.Close() + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + s, err := client.PostStatus(context.Background(), &Toot{ + Status: "foobar", + InReplyToID: ID("2"), + Visibility: "unlisted", + Sensitive: true, + SpoilerText: "bar", + MediaIDs: []ID{"1", "2"}, + Poll: &TootPoll{ + Options: []string{"A", "B"}, + }, + }) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if len(s.MediaAttachments) > 0 && s.Poll != nil { + t.Fatal("should not fail, can't have both Media and Poll") + } + if s.Content != "

foobar

" { + t.Fatalf("want %q but %q", "

foobar

", s.Content) + } + if s.InReplyToID != "2" { + t.Fatalf("want %q but %q", "2", s.InReplyToID) + } + if s.Visibility != "unlisted" { + t.Fatalf("want %q but %q", "unlisted", s.Visibility) + } + if s.Sensitive != true { + t.Fatalf("want %t but %t", true, s.Sensitive) + } + if s.SpoilerText != "

bar

" { + t.Fatalf("want %q but %q", "

bar

", s.SpoilerText) + } + s, err = client.PostStatus(context.Background(), &Toot{ + Status: "foobar", + Poll: &TootPoll{ + Multiple: true, + Options: []string{"A", "B"}, + }, + }) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if s.Poll == nil { + t.Fatalf("poll should not be %v", s.Poll) + } + if len(s.Poll.Options) != 2 { + t.Fatalf("want %q but %q", 2, len(s.Poll.Options)) + } + if s.Poll.Options[0].Title != "A" { + t.Fatalf("want %q but %q", "A", s.Poll.Options[0].Title) + } + if s.Poll.Options[1].Title != "B" { + t.Fatalf("want %q but %q", "B", s.Poll.Options[1].Title) + } + if s.Poll.Multiple != true { + t.Fatalf("want %t but %t", true, s.Poll.Multiple) + } +} func TestGetTimelineHome(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { From 62214db600258b785d9d7748f86de799e135736f Mon Sep 17 00:00:00 2001 From: shine <4771718+shinenelson@users.noreply.github.com> Date: Thu, 1 Jul 2021 01:34:28 +0530 Subject: [PATCH 102/127] document bookmark APIs follows d39c10b which introduced the bookmark APIs. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index dbda926..a359189 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ func main() { * [x] GET /api/v1/accounts/:id/lists * [x] GET /api/v1/accounts/relationships * [x] GET /api/v1/accounts/search +* [x] GET /api/v1/bookmarks * [x] POST /api/v1/apps * [x] GET /api/v1/blocks * [x] GET /api/v1/conversations @@ -131,6 +132,8 @@ func main() { * [x] POST /api/v1/statuses/:id/unreblog * [x] POST /api/v1/statuses/:id/favourite * [x] POST /api/v1/statuses/:id/unfavourite +* [x] POST /api/v1/statuses/:id/bookmark +* [x] POST /api/v1/statuses/:id/unbookmark * [x] GET /api/v1/timelines/home * [x] GET /api/v1/timelines/public * [x] GET /api/v1/timelines/tag/:hashtag From 45d75e80853082b98248dc0534314dcac0a68532 Mon Sep 17 00:00:00 2001 From: Marian Steinbach Date: Tue, 23 Nov 2021 09:05:52 +0100 Subject: [PATCH 103/127] Update badge in README to point to pkg.go.dev --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a359189..a60f93f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Build Status](https://github.com/mattn/go-mastodon/workflows/test/badge.svg?branch=master)](https://github.com/mattn/go-mastodon/actions?query=workflow%3Atest) [![Codecov](https://codecov.io/gh/mattn/go-mastodon/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-mastodon) -[![GoDoc](https://godoc.org/github.com/mattn/go-mastodon?status.svg)](http://godoc.org/github.com/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) From 7745e19ff7872bcc549eb5e6955dae408fd21acf Mon Sep 17 00:00:00 2001 From: Marian Steinbach Date: Tue, 16 Nov 2021 14:50:42 +0100 Subject: [PATCH 104/127] Only set Authorization header when AccessToken is set --- streaming.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/streaming.go b/streaming.go index f560fd8..2148ed0 100644 --- a/streaming.go +++ b/streaming.go @@ -92,7 +92,10 @@ func (c *Client) streaming(ctx context.Context, p string, params url.Values) (ch return nil, err } req = req.WithContext(ctx) - req.Header.Set("Authorization", "Bearer "+c.Config.AccessToken) + + if c.Config.AccessToken != "" { + req.Header.Set("Authorization", "Bearer "+c.Config.AccessToken) + } q := make(chan Event) go func() { From dfa87f3a80ff69492caee49cdcc2c6237ed67361 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Thu, 10 Mar 2022 19:23:35 +0100 Subject: [PATCH 105/127] add support for filters --- README.md | 5 + filters.go | 124 ++++++++++++++++++ filters_test.go | 342 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 471 insertions(+) create mode 100644 filters.go create mode 100644 filters_test.go diff --git a/README.md b/README.md index a60f93f..d883b91 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,11 @@ func main() { * [x] DELETE /api/v1/conversations/:id * [x] POST /api/v1/conversations/:id/read * [x] GET /api/v1/favourites +* [x] GET api/v1/filters +* [x] POST api/v1/filters +* [x] GET api/v1/filters/:id +* [x] PUT api/v1/filters/:id +* [x] DELETE api/v1/filters/:id * [x] GET /api/v1/follow_requests * [x] POST /api/v1/follow_requests/:id/authorize * [x] POST /api/v1/follow_requests/:id/reject diff --git a/filters.go b/filters.go new file mode 100644 index 0000000..56f1229 --- /dev/null +++ b/filters.go @@ -0,0 +1,124 @@ +package mastodon + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/url" + "time" +) + +// Filter is metadata for a filter of users. +type Filter struct { + ID ID `json:"id"` + Phrase string `json:"phrase"` + Context []string `json:"context"` + WholeWord bool `json:"whole_word"` + ExpiresAt time.Time `json:"expires_at"` + Irreversible bool `json:"irreversible"` +} + +// GetFilters returns all the filters on the current account. +func (c *Client) GetFilters(ctx context.Context) ([]*Filter, error) { + var filters []*Filter + err := c.doAPI(ctx, http.MethodGet, "/api/v1/filters", nil, &filters, nil) + if err != nil { + return nil, err + } + return filters, nil +} + +// GetFilter retrieves a filter by ID. +func (c *Client) GetFilter(ctx context.Context, id ID) (*Filter, error) { + var filter Filter + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/filters/%s", url.PathEscape(string(id))), nil, &filter, nil) + if err != nil { + return nil, err + } + return &filter, nil +} + +// CreateFilter creates a new filter. +func (c *Client) CreateFilter(ctx context.Context, filter *Filter) (*Filter, error) { + if filter == nil { + return nil, errors.New("filter can't be nil") + } + if filter.Phrase == "" { + return nil, errors.New("phrase can't be empty") + } + if len(filter.Context) == 0 { + return nil, errors.New("context can't be empty") + } + params := url.Values{} + params.Set("phrase", filter.Phrase) + for _, c := range filter.Context { + params.Add("context[]", c) + } + if filter.WholeWord { + params.Add("whole_word", "true") + } + if filter.Irreversible { + params.Add("irreversible", "true") + } + if !filter.ExpiresAt.IsZero() { + diff := time.Until(filter.ExpiresAt) + params.Add("expires_in", fmt.Sprintf("%.0f", diff.Seconds())) + } + + var f Filter + err := c.doAPI(ctx, http.MethodPost, "/api/v1/filters", params, &f, nil) + if err != nil { + return nil, err + } + return &f, nil +} + +// UpdateFilter updates a filter. +func (c *Client) UpdateFilter(ctx context.Context, id ID, filter *Filter) (*Filter, error) { + if filter == nil { + return nil, errors.New("filter can't be nil") + } + if id == ID("") { + return nil, errors.New("ID can't be empty") + } + if filter.Phrase == "" { + return nil, errors.New("phrase can't be empty") + } + if len(filter.Context) == 0 { + return nil, errors.New("context can't be empty") + } + params := url.Values{} + params.Set("phrase", filter.Phrase) + for _, c := range filter.Context { + params.Add("context[]", c) + } + if filter.WholeWord { + params.Add("whole_word", "true") + } else { + params.Add("whole_word", "false") + } + if filter.Irreversible { + params.Add("irreversible", "true") + } else { + params.Add("irreversible", "false") + } + if !filter.ExpiresAt.IsZero() { + diff := time.Until(filter.ExpiresAt) + params.Add("expires_in", fmt.Sprintf("%.0f", diff.Seconds())) + } else { + params.Add("expires_in", "") + } + + var f Filter + err := c.doAPI(ctx, http.MethodPut, fmt.Sprintf("/api/v1/filters/%s", url.PathEscape(string(id))), params, &f, nil) + if err != nil { + return nil, err + } + return &f, nil +} + +// DeleteFilter removes a filter. +func (c *Client) DeleteFilter(ctx context.Context, id ID) error { + return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/filters/%s", url.PathEscape(string(id))), nil, nil, nil) +} diff --git a/filters_test.go b/filters_test.go new file mode 100644 index 0000000..71e440d --- /dev/null +++ b/filters_test.go @@ -0,0 +1,342 @@ +package mastodon + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "sort" + "strings" + "testing" + "time" +) + +func TestGetFilters(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `[{"id": "6191", "phrase": "rust", "context": ["home"], "whole_word": true, "expires_at": "2019-05-21T13:47:31.333Z", "irreversible": false}, {"id": "5580", "phrase": "@twitter.com", "context": ["home", "notifications", "public", "thread"], "whole_word": false, "expires_at": null, "irreversible": true}]`) + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + d, err := time.Parse(time.RFC3339Nano, "2019-05-21T13:47:31.333Z") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + tf := []Filter{ + { + ID: ID("6191"), + Phrase: "rust", + Context: []string{"home"}, + WholeWord: true, + ExpiresAt: d, + Irreversible: false, + }, + { + ID: ID("5580"), + Phrase: "@twitter.com", + Context: []string{"notifications", "home", "thread", "public"}, + WholeWord: false, + ExpiresAt: time.Time{}, + Irreversible: true, + }, + } + + filters, err := client.GetFilters(context.Background()) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if len(filters) != 2 { + t.Fatalf("result should be two: %d", len(filters)) + } + for i, f := range tf { + if filters[i].ID != f.ID { + t.Fatalf("want %q but %q", string(f.ID), filters[i].ID) + } + if filters[i].Phrase != f.Phrase { + t.Fatalf("want %q but %q", f.Phrase, filters[i].Phrase) + } + sort.Strings(filters[i].Context) + sort.Strings(f.Context) + if strings.Join(filters[i].Context, ", ") != strings.Join(f.Context, ", ") { + t.Fatalf("want %q but %q", f.Context, filters[i].Context) + } + if filters[i].ExpiresAt != f.ExpiresAt { + t.Fatalf("want %q but %q", f.ExpiresAt, filters[i].ExpiresAt) + } + if filters[i].WholeWord != f.WholeWord { + t.Fatalf("want %t but %t", f.WholeWord, filters[i].WholeWord) + } + if filters[i].Irreversible != f.Irreversible { + t.Fatalf("want %t but %t", f.Irreversible, filters[i].Irreversible) + } + } +} + +func TestGetFilter(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/filters/1" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + fmt.Fprintln(w, `{"id": "1", "phrase": "rust", "context": ["home"], "whole_word": true, "expires_at": "2019-05-21T13:47:31.333Z", "irreversible": false}`) + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + _, err := client.GetFilter(context.Background(), "2") + if err == nil { + t.Fatalf("should be fail: %v", err) + } + d, err := time.Parse(time.RFC3339Nano, "2019-05-21T13:47:31.333Z") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + tf := Filter{ + ID: ID("1"), + Phrase: "rust", + Context: []string{"home"}, + WholeWord: true, + ExpiresAt: d, + Irreversible: false, + } + filter, err := client.GetFilter(context.Background(), "1") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if filter.ID != tf.ID { + t.Fatalf("want %q but %q", string(tf.ID), filter.ID) + } + if filter.Phrase != tf.Phrase { + t.Fatalf("want %q but %q", tf.Phrase, filter.Phrase) + } + sort.Strings(filter.Context) + sort.Strings(tf.Context) + if strings.Join(filter.Context, ", ") != strings.Join(tf.Context, ", ") { + t.Fatalf("want %q but %q", tf.Context, filter.Context) + } + if filter.ExpiresAt != tf.ExpiresAt { + t.Fatalf("want %q but %q", tf.ExpiresAt, filter.ExpiresAt) + } + if filter.WholeWord != tf.WholeWord { + t.Fatalf("want %t but %t", tf.WholeWord, filter.WholeWord) + } + if filter.Irreversible != tf.Irreversible { + t.Fatalf("want %t but %t", tf.Irreversible, filter.Irreversible) + } +} + +func TestCreateFilter(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.PostFormValue("phrase") != "rust" && r.PostFormValue("phrase") != "@twitter.com" { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + if r.PostFormValue("phrase") == "rust" { + fmt.Fprintln(w, `{"id": "1", "phrase": "rust", "context": ["home"], "whole_word": true, "expires_at": "2019-05-21T13:47:31.333Z", "irreversible": true}`) + return + } else { + fmt.Fprintln(w, `{"id": "2", "phrase": "@twitter.com", "context": ["home", "notifications", "public", "thread"], "whole_word": false, "expires_at": null, "irreversible": false}`) + return + } + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + _, err := client.CreateFilter(context.Background(), nil) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + _, err = client.CreateFilter(context.Background(), &Filter{Context: []string{"home"}}) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + _, err = client.CreateFilter(context.Background(), &Filter{Phrase: "rust"}) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + _, err = client.CreateFilter(context.Background(), &Filter{Phrase: "Test", Context: []string{"home"}}) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + + d, err := time.Parse(time.RFC3339Nano, "2019-05-21T13:47:31.333Z") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + tf := []Filter{ + { + ID: ID("1"), + Phrase: "rust", + Context: []string{"home"}, + WholeWord: true, + ExpiresAt: d, + Irreversible: true, + }, + { + ID: ID("2"), + Phrase: "@twitter.com", + Context: []string{"notifications", "home", "thread", "public"}, + WholeWord: false, + ExpiresAt: time.Time{}, + Irreversible: false, + }, + } + for _, f := range tf { + filter, err := client.CreateFilter(context.Background(), &f) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if filter.ID != f.ID { + t.Fatalf("want %q but %q", string(f.ID), filter.ID) + } + if filter.Phrase != f.Phrase { + t.Fatalf("want %q but %q", f.Phrase, filter.Phrase) + } + sort.Strings(filter.Context) + sort.Strings(f.Context) + if strings.Join(filter.Context, ", ") != strings.Join(f.Context, ", ") { + t.Fatalf("want %q but %q", f.Context, filter.Context) + } + if filter.ExpiresAt != f.ExpiresAt { + t.Fatalf("want %q but %q", f.ExpiresAt, filter.ExpiresAt) + } + if filter.WholeWord != f.WholeWord { + t.Fatalf("want %t but %t", f.WholeWord, filter.WholeWord) + } + if filter.Irreversible != f.Irreversible { + t.Fatalf("want %t but %t", f.Irreversible, filter.Irreversible) + } + } +} + +func TestUpdateFilter(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/api/v1/filters/1" { + fmt.Fprintln(w, `{"id": "1", "phrase": "rust", "context": ["home"], "whole_word": true, "expires_at": "2019-05-21T13:47:31.333Z", "irreversible": true}`) + return + } else if r.URL.Path == "/api/v1/filters/2" { + fmt.Fprintln(w, `{"id": "2", "phrase": "@twitter.com", "context": ["home", "notifications", "public", "thread"], "whole_word": false, "expires_at": null, "irreversible": false}`) + return + } else { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + _, err := client.UpdateFilter(context.Background(), ID("1"), nil) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + _, err = client.UpdateFilter(context.Background(), ID(""), &Filter{Phrase: ""}) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + _, err = client.UpdateFilter(context.Background(), ID("2"), &Filter{Phrase: ""}) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + _, err = client.UpdateFilter(context.Background(), ID("2"), &Filter{Phrase: "rust"}) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + _, err = client.UpdateFilter(context.Background(), ID("3"), &Filter{Phrase: "rust", Context: []string{"home"}}) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + + d, err := time.Parse(time.RFC3339Nano, "2019-05-21T13:47:31.333Z") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + tf := []Filter{ + { + ID: ID("1"), + Phrase: "rust", + Context: []string{"home"}, + WholeWord: true, + ExpiresAt: d, + Irreversible: true, + }, + { + ID: ID("2"), + Phrase: "@twitter.com", + Context: []string{"notifications", "home", "thread", "public"}, + WholeWord: false, + ExpiresAt: time.Time{}, + Irreversible: false, + }, + } + for _, f := range tf { + filter, err := client.UpdateFilter(context.Background(), f.ID, &f) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if filter.ID != f.ID { + t.Fatalf("want %q but %q", string(f.ID), filter.ID) + } + if filter.Phrase != f.Phrase { + t.Fatalf("want %q but %q", f.Phrase, filter.Phrase) + } + sort.Strings(filter.Context) + sort.Strings(f.Context) + if strings.Join(filter.Context, ", ") != strings.Join(f.Context, ", ") { + t.Fatalf("want %q but %q", f.Context, filter.Context) + } + if filter.ExpiresAt != f.ExpiresAt { + t.Fatalf("want %q but %q", f.ExpiresAt, filter.ExpiresAt) + } + if filter.WholeWord != f.WholeWord { + t.Fatalf("want %t but %t", f.WholeWord, filter.WholeWord) + } + if filter.Irreversible != f.Irreversible { + t.Fatalf("want %t but %t", f.Irreversible, filter.Irreversible) + } + } +} + +func TestDeleteFilter(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/filters/1" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + err := client.DeleteFilter(context.Background(), "2") + if err == nil { + t.Fatalf("should be fail: %v", err) + } + err = client.DeleteFilter(context.Background(), "1") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } +} From f436c5397c7df701ce232daa29e961aa78619645 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Sat, 12 Mar 2022 11:59:12 +0100 Subject: [PATCH 106/127] fix slash --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d883b91..3d8f494 100644 --- a/README.md +++ b/README.md @@ -93,11 +93,11 @@ func main() { * [x] DELETE /api/v1/conversations/:id * [x] POST /api/v1/conversations/:id/read * [x] GET /api/v1/favourites -* [x] GET api/v1/filters -* [x] POST api/v1/filters -* [x] GET api/v1/filters/:id -* [x] PUT api/v1/filters/:id -* [x] DELETE api/v1/filters/:id +* [x] GET /api/v1/filters +* [x] POST /api/v1/filters +* [x] GET /api/v1/filters/:id +* [x] PUT /api/v1/filters/:id +* [x] DELETE /api/v1/filters/:id * [x] GET /api/v1/follow_requests * [x] POST /api/v1/follow_requests/:id/authorize * [x] POST /api/v1/follow_requests/:id/reject From 87278bda2ea5c5c316659bbc9da906110a1cf74b Mon Sep 17 00:00:00 2001 From: Alexander Bakker Date: Sun, 1 May 2022 14:15:40 +0200 Subject: [PATCH 107/127] Use bufio.Reader instead of bufio.Scanner when streaming I occasionally run into "bufio.Scanner: token too long" while streaming. This change should prevent that from happening. --- streaming.go | 28 +++++++++++++++++++++++----- streaming_test.go | 27 ++++++++++++++++++--------- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/streaming.go b/streaming.go index 2148ed0..615efec 100644 --- a/streaming.go +++ b/streaming.go @@ -2,8 +2,10 @@ package mastodon import ( "bufio" + "bytes" "context" "encoding/json" + "errors" "io" "net/http" "net/url" @@ -43,10 +45,27 @@ type Event interface { func handleReader(q chan Event, r io.Reader) error { var name string - s := bufio.NewScanner(r) - for s.Scan() { - line := s.Text() - token := strings.SplitN(line, ":", 2) + var lineBuf bytes.Buffer + br := bufio.NewReader(r) + for { + line, isPrefix, err := br.ReadLine() + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + return err + } + if isPrefix { + lineBuf.Write(line) + continue + } + if lineBuf.Len() > 0 { + lineBuf.Write(line) + line = lineBuf.Bytes() + lineBuf.Reset() + } + + token := strings.SplitN(string(line), ":", 2) if len(token) != 2 { continue } @@ -76,7 +95,6 @@ func handleReader(q chan Event, r io.Reader) error { } } } - return s.Err() } func (c *Client) streaming(ctx context.Context, p string, params url.Values) (chan Event, error) { diff --git a/streaming_test.go b/streaming_test.go index 0bd5ccc..fd61f9c 100644 --- a/streaming_test.go +++ b/streaming_test.go @@ -1,6 +1,7 @@ package mastodon import ( + "bufio" "context" "fmt" "net/http" @@ -11,18 +12,23 @@ import ( ) func TestHandleReader(t *testing.T) { + large := "large" + largeContent := strings.Repeat(large, 2*(bufio.MaxScanTokenSize/len(large))) + q := make(chan Event) - r := strings.NewReader(` + r := strings.NewReader(fmt.Sprintf(` event: update data: {content: error} event: update data: {"content": "foo"} +event: update +data: {"content": "%s"} event: notification data: {"type": "mention"} event: delete data: 1234567 :thump - `) + `, largeContent)) go func() { defer close(q) err := handleReader(q, r) @@ -30,13 +36,16 @@ data: 1234567 t.Fatalf("should not be fail: %v", err) } }() - var passUpdate, passNotification, passDelete, passError bool + var passUpdate, passUpdateLarge, passNotification, passDelete, passError bool for e := range q { switch event := e.(type) { case *UpdateEvent: - passUpdate = true - if event.Status.Content != "foo" { - t.Fatalf("want %q but %q", "foo", event.Status.Content) + if event.Status.Content == "foo" { + passUpdate = true + } else if event.Status.Content == largeContent { + passUpdateLarge = true + } else { + t.Fatalf("bad update content: %q", event.Status.Content) } case *NotificationEvent: passNotification = true @@ -55,10 +64,10 @@ data: 1234567 } } } - if !passUpdate || !passNotification || !passDelete || !passError { + if !passUpdate || !passUpdateLarge || !passNotification || !passDelete || !passError { t.Fatalf("have not passed through somewhere: "+ - "update %t, notification %t, delete %t, error %t", - passUpdate, passNotification, passDelete, passError) + "update: %t, update (large): %t, notification: %t, delete: %t, error: %t", + passUpdate, passUpdateLarge, passNotification, passDelete, passError) } } From c6a292132ebc313b9429f938554dac28fd23c6a8 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Tue, 17 May 2022 19:35:39 +0200 Subject: [PATCH 108/127] add Discoverable and Source on Account --- accounts.go | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/accounts.go b/accounts.go index a27d3f4..6b481fb 100644 --- a/accounts.go +++ b/accounts.go @@ -11,25 +11,27 @@ import ( // Account hold information for mastodon account. type Account struct { - ID ID `json:"id"` - Username string `json:"username"` - Acct string `json:"acct"` - DisplayName string `json:"display_name"` - Locked bool `json:"locked"` - CreatedAt time.Time `json:"created_at"` - FollowersCount int64 `json:"followers_count"` - FollowingCount int64 `json:"following_count"` - StatusesCount int64 `json:"statuses_count"` - Note string `json:"note"` - URL string `json:"url"` - Avatar string `json:"avatar"` - AvatarStatic string `json:"avatar_static"` - Header string `json:"header"` - HeaderStatic string `json:"header_static"` - Emojis []Emoji `json:"emojis"` - Moved *Account `json:"moved"` - Fields []Field `json:"fields"` - Bot bool `json:"bot"` + ID ID `json:"id"` + Username string `json:"username"` + Acct string `json:"acct"` + DisplayName string `json:"display_name"` + Locked bool `json:"locked"` + CreatedAt time.Time `json:"created_at"` + FollowersCount int64 `json:"followers_count"` + FollowingCount int64 `json:"following_count"` + StatusesCount int64 `json:"statuses_count"` + Note string `json:"note"` + URL string `json:"url"` + Avatar string `json:"avatar"` + AvatarStatic string `json:"avatar_static"` + Header string `json:"header"` + HeaderStatic string `json:"header_static"` + Emojis []Emoji `json:"emojis"` + Moved *Account `json:"moved"` + Fields []Field `json:"fields"` + Bot bool `json:"bot"` + Discoverable bool `json:"discoverable"` + Source *AccountSource `json:"source"` } // Field is a Mastodon account profile field. From b2204e0d6ae82b5bf18f54b8bc951c11771193e3 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Sat, 4 Jun 2022 15:44:49 +0200 Subject: [PATCH 109/127] add support for pinned posts --- accounts.go | 12 ++++++++++++ accounts_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/accounts.go b/accounts.go index 6b481fb..a5e6823 100644 --- a/accounts.go +++ b/accounts.go @@ -139,6 +139,18 @@ func (c *Client) GetAccountStatuses(ctx context.Context, id ID, pg *Pagination) return statuses, nil } +// GetAccountPinnedStatuses return statuses pinned by specified accuont. +func (c *Client) GetAccountPinnedStatuses(ctx context.Context, id ID) ([]*Status, error) { + var statuses []*Status + params := url.Values{} + params.Set("pinned", "true") + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/statuses", url.PathEscape(string(id))), params, &statuses, nil) + if err != nil { + return nil, err + } + return statuses, nil +} + // GetAccountFollowers return followers list. func (c *Client) GetAccountFollowers(ctx context.Context, id ID, pg *Pagination) ([]*Account, error) { var accounts []*Account diff --git a/accounts_test.go b/accounts_test.go index 4eed9ea..73f59c2 100644 --- a/accounts_test.go +++ b/accounts_test.go @@ -144,6 +144,44 @@ func TestGetAccountStatuses(t *testing.T) { } } +func TestGetAccountPinnedStatuses(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/accounts/1234567/statuses" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + pinned := r.URL.Query().Get("pinned") + if pinned != "true" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + fmt.Fprintln(w, `[{"content": "foo"}, {"content": "bar"}]`) + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + _, err := client.GetAccountPinnedStatuses(context.Background(), "123") + if err == nil { + t.Fatalf("should be fail: %v", err) + } + ss, err := client.GetAccountPinnedStatuses(context.Background(), "1234567") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if ss[0].Content != "foo" { + t.Fatalf("want %q but %q", "foo", ss[0].Content) + } + if ss[1].Content != "bar" { + t.Fatalf("want %q but %q", "bar", ss[1].Content) + } +} + func TestGetAccountFollowers(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/accounts/1234567/followers" { From d272534ac7b0b546e42057a38974e18ff1db75f4 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sat, 4 Jun 2022 23:01:37 +0900 Subject: [PATCH 110/127] Separate go.mod --- cmd/mstdn/go.mod | 14 ++++++++++++ cmd/mstdn/go.sum | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 12 ++--------- go.sum | 53 ++-------------------------------------------- 4 files changed, 73 insertions(+), 61 deletions(-) create mode 100644 cmd/mstdn/go.mod create mode 100644 cmd/mstdn/go.sum diff --git a/cmd/mstdn/go.mod b/cmd/mstdn/go.mod new file mode 100644 index 0000000..729c262 --- /dev/null +++ b/cmd/mstdn/go.mod @@ -0,0 +1,14 @@ +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 +) diff --git a/cmd/mstdn/go.sum b/cmd/mstdn/go.sum new file mode 100644 index 0000000..2006e2f --- /dev/null +++ b/cmd/mstdn/go.sum @@ -0,0 +1,55 @@ +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= diff --git a/go.mod b/go.mod index 0616e46..5ab4e81 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,8 @@ module github.com/mattn/go-mastodon -go 1.12 +go 1.16 require ( - github.com/PuerkitoBio/goquery v1.8.0 - github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect - github.com/fatih/color v1.13.0 - github.com/gorilla/websocket v1.4.2 - github.com/mattn/go-colorable v0.1.11 // indirect - github.com/mattn/go-tty v0.0.3 + github.com/gorilla/websocket v1.5.0 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 - github.com/urfave/cli v1.22.5 - golang.org/x/net v0.0.0-20211101193420-4a448f8816b3 - golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect ) diff --git a/go.sum b/go.sum index 646b092..34589e8 100644 --- a/go.sum +++ b/go.sum @@ -1,53 +1,4 @@ -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.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= -github.com/cpuguy83/go-md2man/v2 v2.0.1/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.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= -github.com/gorilla/websocket v1.4.2/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.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= -github.com/mattn/go-colorable v0.1.11/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.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= -github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= -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/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= -github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= -github.com/urfave/cli v1.22.5/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-20211101193420-4a448f8816b3 h1:VrJZAjbekhoRn7n5FBujY31gboH+iB3pdLxn3gE9FjU= -golang.org/x/net v0.0.0-20211101193420-4a448f8816b3/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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-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-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4= -golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -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= From 2a3ac1d1d5479cee4421afd2657906671ca26c4d Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Fri, 26 Aug 2022 16:33:24 -0700 Subject: [PATCH 111/127] Client Credentials --- apps.go | 17 +++++++++++++++++ apps_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ mastodon.go | 12 ++++++++++++ mastodon_test.go | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+) diff --git a/apps.go b/apps.go index b03f9b1..d113909 100644 --- a/apps.go +++ b/apps.go @@ -94,3 +94,20 @@ func RegisterApp(ctx context.Context, appConfig *AppConfig) (*Application, error return &app, nil } + +// ApplicationVerification is mastodon application. +type ApplicationVerification struct { + Name string `json:"name"` + Website string `json:"website"` + VapidKey string `json:"vapid_key"` +} + +// VerifyAppCredentials returns the mastodon application. +func (c *Client) VerifyAppCredentials(ctx context.Context) (*ApplicationVerification, error) { + var application ApplicationVerification + err := c.doAPI(ctx, http.MethodGet, "/api/v1/apps/verify_credentials", nil, &application, nil) + if err != nil { + return nil, err + } + return &application, nil +} diff --git a/apps_test.go b/apps_test.go index 32e28ab..8734234 100644 --- a/apps_test.go +++ b/apps_test.go @@ -96,3 +96,49 @@ func TestRegisterAppWithCancel(t *testing.T) { t.Fatalf("want %q but %q", want, err.Error()) } } + +func TestVerifyAppCredentials(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Bearer zoo" { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + if r.URL.Path != "/api/v1/apps/verify_credentials" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + fmt.Fprintln(w, `{"name":"zzz","website":"yyy","vapid_key":"xxx"}`) + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zip", + }) + _, err := client.VerifyAppCredentials(context.Background()) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + + client = NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + a, err := client.VerifyAppCredentials(context.Background()) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if a.Name != "zzz" { + t.Fatalf("want %q but %q", "zzz", a.Name) + } + if a.Website != "yyy" { + t.Fatalf("want %q but %q", "yyy", a.Name) + } + if a.VapidKey != "xxx" { + t.Fatalf("want %q but %q", "xxx", a.Name) + } +} diff --git a/mastodon.go b/mastodon.go index 3523713..5b43308 100644 --- a/mastodon.go +++ b/mastodon.go @@ -150,6 +150,18 @@ func (c *Client) Authenticate(ctx context.Context, username, password string) er return c.authenticate(ctx, params) } +// AuthenticateApp logs in using client credentials. +func (c *Client) AuthenticateApp(ctx context.Context) error { + params := url.Values{ + "client_id": {c.Config.ClientID}, + "client_secret": {c.Config.ClientSecret}, + "grant_type": {"client_credentials"}, + "redirect_uri": {"urn:ietf:wg:oauth:2.0:oob"}, + } + + return c.authenticate(ctx, params) +} + // AuthenticateToken logs in using a grant token returned by Application.AuthURI. // // redirectURI should be the same as Application.RedirectURI. diff --git a/mastodon_test.go b/mastodon_test.go index 9a8f3e3..527dd89 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -144,6 +144,38 @@ func TestAuthenticateWithCancel(t *testing.T) { } } +func TestAuthenticateApp(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.FormValue("client_id") != "foo" || r.FormValue("client_secret") != "bar" { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + fmt.Fprintln(w, `{"name":"zzz","website":"yyy","vapid_key":"xxx"}`) + return + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bat", + }) + err := client.AuthenticateApp(context.Background()) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + + client = NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + }) + err = client.AuthenticateApp(context.Background()) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } +} + func TestPostStatus(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Bearer zoo" { From 7dfe81e233c663eed07ee8b22d73b4b870dc87b6 Mon Sep 17 00:00:00 2001 From: Tyr Mactire Date: Fri, 26 Aug 2022 20:39:44 -0700 Subject: [PATCH 112/127] docs --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3d8f494..23607a6 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,7 @@ func main() { * [x] GET /api/v1/accounts/:id/lists * [x] GET /api/v1/accounts/relationships * [x] GET /api/v1/accounts/search +* [x] GET /api/v1/apps/verify_credentials * [x] GET /api/v1/bookmarks * [x] POST /api/v1/apps * [x] GET /api/v1/blocks From 114537dcc024aaead8dff8f9f7627e87daceb7d4 Mon Sep 17 00:00:00 2001 From: Mark Ayers Date: Sat, 12 Nov 2022 07:39:47 -0500 Subject: [PATCH 113/127] Update install instruction --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 23607a6..989d6e4 100644 --- a/README.md +++ b/README.md @@ -153,8 +153,8 @@ func main() { ## Installation -``` -$ go get github.com/mattn/go-mastodon +```shell +go install github.com/mattn/go-mastodon@latest ``` ## License From 309dce6ff3f1b04b74c282c01de7256bb140bdde Mon Sep 17 00:00:00 2001 From: Darren O'Connor Date: Sun, 13 Nov 2022 21:14:40 +0000 Subject: [PATCH 114/127] minor spelling fixes --- accounts.go | 40 ++++++++++++++++++++-------------------- apps.go | 2 +- instance.go | 12 ++++++------ mastodon.go | 6 +++--- notification.go | 8 ++++---- polls.go | 6 +++--- report.go | 4 ++-- status.go | 26 +++++++++++++------------- streaming.go | 20 ++++++++++---------- 9 files changed, 62 insertions(+), 62 deletions(-) diff --git a/accounts.go b/accounts.go index a5e6823..0b43e4c 100644 --- a/accounts.go +++ b/accounts.go @@ -9,7 +9,7 @@ import ( "time" ) -// Account hold information for mastodon account. +// Account holds information for a mastodon account. type Account struct { ID ID `json:"id"` Username string `json:"username"` @@ -60,7 +60,7 @@ func (c *Client) GetAccount(ctx context.Context, id ID) (*Account, error) { return &account, nil } -// GetAccountCurrentUser return Account of current user. +// GetAccountCurrentUser returns the Account of current user. func (c *Client) GetAccountCurrentUser(ctx context.Context) (*Account, error) { var account Account err := c.doAPI(ctx, http.MethodGet, "/api/v1/accounts/verify_credentials", nil, &account, nil) @@ -129,7 +129,7 @@ func (c *Client) AccountUpdate(ctx context.Context, profile *Profile) (*Account, return &account, nil } -// GetAccountStatuses return statuses by specified accuont. +// GetAccountStatuses return statuses by specified account. func (c *Client) GetAccountStatuses(ctx context.Context, id ID, pg *Pagination) ([]*Status, error) { var statuses []*Status err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/statuses", url.PathEscape(string(id))), nil, &statuses, pg) @@ -139,7 +139,7 @@ func (c *Client) GetAccountStatuses(ctx context.Context, id ID, pg *Pagination) return statuses, nil } -// GetAccountPinnedStatuses return statuses pinned by specified accuont. +// GetAccountPinnedStatuses returns statuses pinned by specified accuont. func (c *Client) GetAccountPinnedStatuses(ctx context.Context, id ID) ([]*Status, error) { var statuses []*Status params := url.Values{} @@ -151,7 +151,7 @@ func (c *Client) GetAccountPinnedStatuses(ctx context.Context, id ID) ([]*Status return statuses, nil } -// GetAccountFollowers return followers list. +// GetAccountFollowers returns followers list. func (c *Client) GetAccountFollowers(ctx context.Context, id ID, pg *Pagination) ([]*Account, error) { var accounts []*Account err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/followers", url.PathEscape(string(id))), nil, &accounts, pg) @@ -161,7 +161,7 @@ func (c *Client) GetAccountFollowers(ctx context.Context, id ID, pg *Pagination) return accounts, nil } -// GetAccountFollowing return following list. +// GetAccountFollowing returns following list. func (c *Client) GetAccountFollowing(ctx context.Context, id ID, pg *Pagination) ([]*Account, error) { var accounts []*Account err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/following", url.PathEscape(string(id))), nil, &accounts, pg) @@ -171,7 +171,7 @@ func (c *Client) GetAccountFollowing(ctx context.Context, id ID, pg *Pagination) return accounts, nil } -// GetBlocks return block list. +// GetBlocks returns block list. func (c *Client) GetBlocks(ctx context.Context, pg *Pagination) ([]*Account, error) { var accounts []*Account err := c.doAPI(ctx, http.MethodGet, "/api/v1/blocks", nil, &accounts, pg) @@ -181,7 +181,7 @@ func (c *Client) GetBlocks(ctx context.Context, pg *Pagination) ([]*Account, err return accounts, nil } -// Relationship hold information for relation-ship to the account. +// Relationship holds information for relationship to the account. type Relationship struct { ID ID `json:"id"` Following bool `json:"following"` @@ -195,7 +195,7 @@ type Relationship struct { Endorsed bool `json:"endorsed"` } -// AccountFollow follow the account. +// AccountFollow follows the account. func (c *Client) AccountFollow(ctx context.Context, id ID) (*Relationship, error) { var relationship Relationship err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/follow", url.PathEscape(string(id))), nil, &relationship, nil) @@ -205,7 +205,7 @@ func (c *Client) AccountFollow(ctx context.Context, id ID) (*Relationship, error return &relationship, nil } -// AccountUnfollow unfollow the account. +// AccountUnfollow unfollows the account. func (c *Client) AccountUnfollow(ctx context.Context, id ID) (*Relationship, error) { var relationship Relationship err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/unfollow", url.PathEscape(string(id))), nil, &relationship, nil) @@ -215,7 +215,7 @@ func (c *Client) AccountUnfollow(ctx context.Context, id ID) (*Relationship, err return &relationship, nil } -// AccountBlock block the account. +// AccountBlock blocks the account. func (c *Client) AccountBlock(ctx context.Context, id ID) (*Relationship, error) { var relationship Relationship err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/block", url.PathEscape(string(id))), nil, &relationship, nil) @@ -225,7 +225,7 @@ func (c *Client) AccountBlock(ctx context.Context, id ID) (*Relationship, error) return &relationship, nil } -// AccountUnblock unblock the account. +// AccountUnblock unblocks the account. func (c *Client) AccountUnblock(ctx context.Context, id ID) (*Relationship, error) { var relationship Relationship err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/unblock", url.PathEscape(string(id))), nil, &relationship, nil) @@ -235,7 +235,7 @@ func (c *Client) AccountUnblock(ctx context.Context, id ID) (*Relationship, erro return &relationship, nil } -// AccountMute mute the account. +// AccountMute mutes the account. func (c *Client) AccountMute(ctx context.Context, id ID) (*Relationship, error) { var relationship Relationship err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/mute", url.PathEscape(string(id))), nil, &relationship, nil) @@ -245,7 +245,7 @@ func (c *Client) AccountMute(ctx context.Context, id ID) (*Relationship, error) return &relationship, nil } -// AccountUnmute unmute the account. +// AccountUnmute unmutes the account. func (c *Client) AccountUnmute(ctx context.Context, id ID) (*Relationship, error) { var relationship Relationship err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/unmute", url.PathEscape(string(id))), nil, &relationship, nil) @@ -255,7 +255,7 @@ func (c *Client) AccountUnmute(ctx context.Context, id ID) (*Relationship, error return &relationship, nil } -// GetAccountRelationships return relationship for the account. +// GetAccountRelationships returns relationship for the account. func (c *Client) GetAccountRelationships(ctx context.Context, ids []string) ([]*Relationship, error) { params := url.Values{} for _, id := range ids { @@ -270,7 +270,7 @@ func (c *Client) GetAccountRelationships(ctx context.Context, ids []string) ([]* return relationships, nil } -// AccountsSearch search accounts by query. +// AccountsSearch searches accounts by query. func (c *Client) AccountsSearch(ctx context.Context, q string, limit int64) ([]*Account, error) { params := url.Values{} params.Set("q", q) @@ -284,7 +284,7 @@ func (c *Client) AccountsSearch(ctx context.Context, q string, limit int64) ([]* return accounts, nil } -// FollowRemoteUser send follow-request. +// FollowRemoteUser sends follow-request. func (c *Client) FollowRemoteUser(ctx context.Context, uri string) (*Account, error) { params := url.Values{} params.Set("uri", uri) @@ -297,7 +297,7 @@ func (c *Client) FollowRemoteUser(ctx context.Context, uri string) (*Account, er return &account, nil } -// GetFollowRequests return follow-requests. +// GetFollowRequests returns follow requests. func (c *Client) GetFollowRequests(ctx context.Context, pg *Pagination) ([]*Account, error) { var accounts []*Account err := c.doAPI(ctx, http.MethodGet, "/api/v1/follow_requests", nil, &accounts, pg) @@ -307,12 +307,12 @@ func (c *Client) GetFollowRequests(ctx context.Context, pg *Pagination) ([]*Acco return accounts, nil } -// FollowRequestAuthorize is authorize the follow request of user with id. +// FollowRequestAuthorize authorizes the follow request of user with id. func (c *Client) FollowRequestAuthorize(ctx context.Context, id ID) error { return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/follow_requests/%s/authorize", url.PathEscape(string(id))), nil, nil, nil) } -// FollowRequestReject is rejects the follow request of user with id. +// FollowRequestReject rejects the follow request of user with id. func (c *Client) FollowRequestReject(ctx context.Context, id ID) error { return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/follow_requests/%s/reject", url.PathEscape(string(id))), nil, nil, nil) } diff --git a/apps.go b/apps.go index d113909..c885ec9 100644 --- a/apps.go +++ b/apps.go @@ -27,7 +27,7 @@ type AppConfig struct { Website string } -// Application is mastodon application. +// Application is a mastodon application. type Application struct { ID ID `json:"id"` RedirectURI string `json:"redirect_uri"` diff --git a/instance.go b/instance.go index d74322f..672120c 100644 --- a/instance.go +++ b/instance.go @@ -5,7 +5,7 @@ import ( "net/http" ) -// Instance hold information for mastodon instance. +// Instance holds information for a mastodon instance. type Instance struct { URI string `json:"uri"` Title string `json:"title"` @@ -19,14 +19,14 @@ type Instance struct { ContactAccount *Account `json:"contact_account"` } -// InstanceStats hold information for mastodon instance stats. +// InstanceStats holds information for mastodon instance stats. type InstanceStats struct { UserCount int64 `json:"user_count"` StatusCount int64 `json:"status_count"` DomainCount int64 `json:"domain_count"` } -// GetInstance return Instance. +// GetInstance returns Instance. func (c *Client) GetInstance(ctx context.Context) (*Instance, error) { var instance Instance err := c.doAPI(ctx, http.MethodGet, "/api/v1/instance", nil, &instance, nil) @@ -36,7 +36,7 @@ func (c *Client) GetInstance(ctx context.Context) (*Instance, error) { return &instance, nil } -// WeeklyActivity hold information for mastodon weekly activity. +// WeeklyActivity holds information for mastodon weekly activity. type WeeklyActivity struct { Week Unixtime `json:"week"` Statuses int64 `json:"statuses,string"` @@ -44,7 +44,7 @@ type WeeklyActivity struct { Registrations int64 `json:"registrations,string"` } -// GetInstanceActivity return instance activity. +// GetInstanceActivity returns instance activity. func (c *Client) GetInstanceActivity(ctx context.Context) ([]*WeeklyActivity, error) { var activity []*WeeklyActivity err := c.doAPI(ctx, http.MethodGet, "/api/v1/instance/activity", nil, &activity, nil) @@ -54,7 +54,7 @@ func (c *Client) GetInstanceActivity(ctx context.Context) ([]*WeeklyActivity, er return activity, nil } -// GetInstancePeers return instance peers. +// GetInstancePeers returns instance peers. func (c *Client) GetInstancePeers(ctx context.Context) ([]string, error) { var peers []string err := c.doAPI(ctx, http.MethodGet, "/api/v1/instance/peers", nil, &peers, nil) diff --git a/mastodon.go b/mastodon.go index 5b43308..0706981 100644 --- a/mastodon.go +++ b/mastodon.go @@ -128,7 +128,7 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in return json.NewDecoder(resp.Body).Decode(&res) } -// NewClient return new mastodon API client. +// NewClient returns a new mastodon API client. func NewClient(config *Config) *Client { return &Client{ Client: *http.DefaultClient, @@ -136,7 +136,7 @@ func NewClient(config *Config) *Client { } } -// Authenticate get access-token to the API. +// Authenticate gets access-token to the API. func (c *Client) Authenticate(ctx context.Context, username, password string) error { params := url.Values{ "client_id": {c.Config.ClientID}, @@ -222,7 +222,7 @@ const ( VisibilityDirectMessage = "direct" ) -// Toot is struct to post status. +// Toot is a struct to post status. type Toot struct { Status string `json:"status"` InReplyToID ID `json:"in_reply_to_id"` diff --git a/notification.go b/notification.go index b24b9df..fa0cab9 100644 --- a/notification.go +++ b/notification.go @@ -12,7 +12,7 @@ import ( "time" ) -// Notification hold information for mastodon notification. +// Notification holds information for a mastodon notification. type Notification struct { ID ID `json:"id"` Type string `json:"type"` @@ -35,7 +35,7 @@ type PushAlerts struct { Mention *Sbool `json:"mention"` } -// GetNotifications return notifications. +// GetNotifications returns notifications. func (c *Client) GetNotifications(ctx context.Context, pg *Pagination) ([]*Notification, error) { var notifications []*Notification err := c.doAPI(ctx, http.MethodGet, "/api/v1/notifications", nil, ¬ifications, pg) @@ -45,7 +45,7 @@ func (c *Client) GetNotifications(ctx context.Context, pg *Pagination) ([]*Notif return notifications, nil } -// GetNotification return notification. +// GetNotification returns notification. func (c *Client) GetNotification(ctx context.Context, id ID) (*Notification, error) { var notification Notification err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/notifications/%v", id), nil, ¬ification, nil) @@ -60,7 +60,7 @@ func (c *Client) DismissNotification(ctx context.Context, id ID) error { return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/notifications/%v/dismiss", id), nil, nil, nil) } -// ClearNotifications clear notifications. +// ClearNotifications clears notifications. func (c *Client) ClearNotifications(ctx context.Context) error { return c.doAPI(ctx, http.MethodPost, "/api/v1/notifications/clear", nil, nil, nil) } diff --git a/polls.go b/polls.go index e2a45f3..16be421 100644 --- a/polls.go +++ b/polls.go @@ -8,7 +8,7 @@ import ( "time" ) -// Poll hold information for mastodon polls. +// Poll holds information for mastodon polls. type Poll struct { ID ID `json:"id"` ExpiresAt time.Time `json:"expires_at"` @@ -22,13 +22,13 @@ type Poll struct { Emojis []Emoji `json:"emojis"` } -// Poll hold information for a mastodon poll option. +// Poll holds information for a mastodon poll option. type PollOption struct { Title string `json:"title"` VotesCount int64 `json:"votes_count"` } -// GetPoll return poll specified by id. +// GetPoll returns poll specified by id. func (c *Client) GetPoll(ctx context.Context, id ID) (*Poll, error) { var poll Poll err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/polls/%s", id), nil, &poll, nil) diff --git a/report.go b/report.go index 94ae0dc..662c16a 100644 --- a/report.go +++ b/report.go @@ -6,13 +6,13 @@ import ( "net/url" ) -// Report hold information for mastodon report. +// Report holds information for a mastodon report. type Report struct { ID int64 `json:"id"` ActionTaken bool `json:"action_taken"` } -// GetReports return report of the current user. +// GetReports returns report of the current user. func (c *Client) GetReports(ctx context.Context) ([]*Report, error) { var reports []*Report err := c.doAPI(ctx, http.MethodGet, "/api/v1/reports", nil, &reports, nil) diff --git a/status.go b/status.go index 5cdbf22..1e8f590 100644 --- a/status.go +++ b/status.go @@ -45,13 +45,13 @@ type Status struct { Pinned interface{} `json:"pinned"` } -// Context hold information for mastodon context. +// Context holds information for a mastodon context. type Context struct { Ancestors []*Status `json:"ancestors"` Descendants []*Status `json:"descendants"` } -// Card hold information for mastodon card. +// Card holds information for a mastodon card. type Card struct { URL string `json:"url"` Title string `json:"title"` @@ -67,7 +67,7 @@ type Card struct { Height int64 `json:"height"` } -// Conversation hold information for mastodon conversation. +// Conversation holds information for a mastodon conversation. type Conversation struct { ID ID `json:"id"` Accounts []*Account `json:"accounts"` @@ -140,7 +140,7 @@ func (m *Media) bodyAndContentType() (io.Reader, string, error) { return &buf, mw.FormDataContentType(), nil } -// GetFavourites return the favorite list of the current user. +// GetFavourites returns the favorite list of the current user. func (c *Client) GetFavourites(ctx context.Context, pg *Pagination) ([]*Status, error) { var statuses []*Status err := c.doAPI(ctx, http.MethodGet, "/api/v1/favourites", nil, &statuses, pg) @@ -150,7 +150,7 @@ func (c *Client) GetFavourites(ctx context.Context, pg *Pagination) ([]*Status, return statuses, nil } -// GetBookmarks return the bookmark list of the current user. +// GetBookmarks returns the bookmark list of the current user. func (c *Client) GetBookmarks(ctx context.Context, pg *Pagination) ([]*Status, error) { var statuses []*Status err := c.doAPI(ctx, http.MethodGet, "/api/v1/bookmarks", nil, &statuses, pg) @@ -160,7 +160,7 @@ func (c *Client) GetBookmarks(ctx context.Context, pg *Pagination) ([]*Status, e return statuses, nil } -// GetStatus return status specified by id. +// GetStatus returns status specified by id. func (c *Client) GetStatus(ctx context.Context, id ID) (*Status, error) { var status Status err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s", id), nil, &status, nil) @@ -170,7 +170,7 @@ func (c *Client) GetStatus(ctx context.Context, id ID) (*Status, error) { return &status, nil } -// GetStatusContext return status specified by id. +// GetStatusContext returns status specified by id. func (c *Client) GetStatusContext(ctx context.Context, id ID) (*Context, error) { var context Context err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/context", id), nil, &context, nil) @@ -180,7 +180,7 @@ func (c *Client) GetStatusContext(ctx context.Context, id ID) (*Context, error) return &context, nil } -// GetStatusCard return status specified by id. +// GetStatusCard returns status specified by id. func (c *Client) GetStatusCard(ctx context.Context, id ID) (*Card, error) { var card Card err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/card", id), nil, &card, nil) @@ -210,7 +210,7 @@ func (c *Client) GetFavouritedBy(ctx context.Context, id ID, pg *Pagination) ([] return accounts, nil } -// Reblog is reblog the toot of id and return status of reblog. +// Reblog reblogs the toot of id and returns status of reblog. func (c *Client) Reblog(ctx context.Context, id ID) (*Status, error) { var status Status err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/reblog", id), nil, &status, nil) @@ -220,7 +220,7 @@ func (c *Client) Reblog(ctx context.Context, id ID) (*Status, error) { return &status, nil } -// Unreblog is unreblog the toot of id and return status of the original toot. +// Unreblog unreblogs the toot of id and returns status of the original toot. func (c *Client) Unreblog(ctx context.Context, id ID) (*Status, error) { var status Status err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unreblog", id), nil, &status, nil) @@ -230,7 +230,7 @@ func (c *Client) Unreblog(ctx context.Context, id ID) (*Status, error) { return &status, nil } -// Favourite is favourite the toot of id and return status of the favourite toot. +// Favourite favourites the toot of id and returns status of the favourite toot. func (c *Client) Favourite(ctx context.Context, id ID) (*Status, error) { var status Status err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/favourite", id), nil, &status, nil) @@ -240,7 +240,7 @@ func (c *Client) Favourite(ctx context.Context, id ID) (*Status, error) { return &status, nil } -// Unfavourite is unfavourite the toot of id and return status of the unfavourite toot. +// Unfavourite unfavourites the toot of id and returns status of the unfavourite toot. func (c *Client) Unfavourite(ctx context.Context, id ID) (*Status, error) { var status Status err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unfavourite", id), nil, &status, nil) @@ -250,7 +250,7 @@ func (c *Client) Unfavourite(ctx context.Context, id ID) (*Status, error) { return &status, nil } -// Bookmark is bookmark the toot of id and return status of the bookmark toot. +// Bookmark bookmarks the toot of id and returns status of the bookmark toot. func (c *Client) Bookmark(ctx context.Context, id ID) (*Status, error) { var status Status err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/bookmark", id), nil, &status, nil) diff --git a/streaming.go b/streaming.go index 615efec..2439d67 100644 --- a/streaming.go +++ b/streaming.go @@ -13,32 +13,32 @@ import ( "strings" ) -// UpdateEvent is struct for passing status event to app. +// UpdateEvent is a struct for passing status event to app. type UpdateEvent struct { Status *Status `json:"status"` } func (e *UpdateEvent) event() {} -// NotificationEvent is struct for passing notification event to app. +// NotificationEvent is a struct for passing notification event to app. type NotificationEvent struct { Notification *Notification `json:"notification"` } func (e *NotificationEvent) event() {} -// DeleteEvent is struct for passing deletion event to app. +// DeleteEvent is a struct for passing deletion event to app. type DeleteEvent struct{ ID ID } func (e *DeleteEvent) event() {} -// ErrorEvent is struct for passing errors to app. +// ErrorEvent is a struct for passing errors to app. type ErrorEvent struct{ err error } func (e *ErrorEvent) event() {} func (e *ErrorEvent) Error() string { return e.err.Error() } -// Event is interface passing events to app. +// Event is an interface passing events to app. type Event interface { event() } @@ -150,12 +150,12 @@ func (c *Client) doStreaming(req *http.Request, q chan Event) { } } -// StreamingUser return channel to read events on home. +// StreamingUser returns a channel to read events on home. func (c *Client) StreamingUser(ctx context.Context) (chan Event, error) { return c.streaming(ctx, "user", nil) } -// StreamingPublic return channel to read events on public. +// StreamingPublic returns a channel to read events on public. func (c *Client) StreamingPublic(ctx context.Context, isLocal bool) (chan Event, error) { p := "public" if isLocal { @@ -165,7 +165,7 @@ func (c *Client) StreamingPublic(ctx context.Context, isLocal bool) (chan Event, return c.streaming(ctx, p, nil) } -// StreamingHashtag return channel to read events on tagged timeline. +// StreamingHashtag returns a channel to read events on tagged timeline. func (c *Client) StreamingHashtag(ctx context.Context, tag string, isLocal bool) (chan Event, error) { params := url.Values{} params.Set("tag", tag) @@ -178,7 +178,7 @@ func (c *Client) StreamingHashtag(ctx context.Context, tag string, isLocal bool) return c.streaming(ctx, p, params) } -// StreamingList return channel to read events on a list. +// StreamingList returns a channel to read events on a list. func (c *Client) StreamingList(ctx context.Context, id ID) (chan Event, error) { params := url.Values{} params.Set("list", string(id)) @@ -186,7 +186,7 @@ func (c *Client) StreamingList(ctx context.Context, id ID) (chan Event, error) { return c.streaming(ctx, "list", params) } -// StreamingDirect return channel to read events on a direct messages. +// StreamingDirect returns a channel to read events on a direct messages. func (c *Client) StreamingDirect(ctx context.Context) (chan Event, error) { return c.streaming(ctx, "direct", nil) } From e5c082de35cc4cb5a0837572ccce5578c64f32e9 Mon Sep 17 00:00:00 2001 From: Darren O'Connor Date: Tue, 15 Nov 2022 02:54:56 +0000 Subject: [PATCH 115/127] Add UploadMediaFromBytes function --- status.go | 7 ++++++- status_test.go | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/status.go b/status.go index 1e8f590..e616366 100644 --- a/status.go +++ b/status.go @@ -409,7 +409,12 @@ func (c *Client) UploadMedia(ctx context.Context, file string) (*Attachment, err return c.UploadMediaFromMedia(ctx, &Media{File: f}) } -// UploadMediaFromReader uploads a media attachment from a io.Reader. +// UploadMediaFromBytes uploads a media attachment from a byte slice. +func (c *Client) UploadMediaFromBytes(ctx context.Context, b []byte) (*Attachment, error) { + return c.UploadMediaFromReader(ctx, bytes.NewReader(b)) +} + +// UploadMediaFromReader uploads a media attachment from an io.Reader. func (c *Client) UploadMediaFromReader(ctx context.Context, reader io.Reader) (*Attachment, error) { return c.UploadMediaFromMedia(ctx, &Media{File: reader}) } diff --git a/status_test.go b/status_test.go index 0a5231f..617a687 100644 --- a/status_test.go +++ b/status_test.go @@ -708,6 +708,18 @@ func TestUploadMedia(t *testing.T) { if writerAttachment.ID != "123" { t.Fatalf("want %q but %q", "123", attachment.ID) } + bytes, err := os.ReadFile("testdata/logo.png") + if err != nil { + t.Fatalf("could not open file: %v", err) + } + byteAttachment, err := client.UploadMediaFromBytes(context.Background(), bytes) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if byteAttachment.ID != "123" { + t.Fatalf("want %q but got %q", "123", attachment.ID) + } + } func TestGetConversations(t *testing.T) { From f76d33a68c7fe66d244bd156ed27f3860b8938f2 Mon Sep 17 00:00:00 2001 From: Darren O'Connor Date: Tue, 15 Nov 2022 03:00:46 +0000 Subject: [PATCH 116/127] switch os.ReadFile to ioutil.ReadFile due to Ubuntu 15 test failing --- status_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/status_test.go b/status_test.go index 617a687..0047617 100644 --- a/status_test.go +++ b/status_test.go @@ -3,6 +3,7 @@ package mastodon import ( "context" "fmt" + "io/ioutil" "net/http" "net/http/httptest" "os" @@ -708,7 +709,7 @@ func TestUploadMedia(t *testing.T) { if writerAttachment.ID != "123" { t.Fatalf("want %q but %q", "123", attachment.ID) } - bytes, err := os.ReadFile("testdata/logo.png") + bytes, err := ioutil.ReadFile("testdata/logo.png") if err != nil { t.Fatalf("could not open file: %v", err) } From 9e1af56ceb351f24f23bad9ba2b7889cd1c270cc Mon Sep 17 00:00:00 2001 From: Darren O'Connor Date: Wed, 16 Nov 2022 01:43:29 +0000 Subject: [PATCH 117/127] remove redundent return statements --- accounts_test.go | 15 --------------- apps_test.go | 2 -- lists_test.go | 9 --------- mastodon_test.go | 7 ------- notification_test.go | 2 -- report_test.go | 2 -- status_test.go | 22 ---------------------- 7 files changed, 59 deletions(-) diff --git a/accounts_test.go b/accounts_test.go index 73f59c2..473e9b6 100644 --- a/accounts_test.go +++ b/accounts_test.go @@ -118,7 +118,6 @@ func TestGetAccountStatuses(t *testing.T) { return } fmt.Fprintln(w, `[{"content": "foo"}, {"content": "bar"}]`) - return })) defer ts.Close() @@ -156,7 +155,6 @@ func TestGetAccountPinnedStatuses(t *testing.T) { return } fmt.Fprintln(w, `[{"content": "foo"}, {"content": "bar"}]`) - return })) defer ts.Close() @@ -189,7 +187,6 @@ func TestGetAccountFollowers(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) - return })) defer ts.Close() @@ -225,7 +222,6 @@ func TestGetAccountFollowing(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) - return })) defer ts.Close() @@ -263,7 +259,6 @@ func TestGetBlocks(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) - return })) defer ts.Close() @@ -299,7 +294,6 @@ func TestAccountFollow(t *testing.T) { return } fmt.Fprintln(w, `{"id":1234567,"following":true}`) - return })) defer ts.Close() @@ -332,7 +326,6 @@ func TestAccountUnfollow(t *testing.T) { return } fmt.Fprintln(w, `{"id":1234567,"following":false}`) - return })) defer ts.Close() @@ -365,7 +358,6 @@ func TestAccountBlock(t *testing.T) { return } fmt.Fprintln(w, `{"id":1234567,"blocking":true}`) - return })) defer ts.Close() @@ -398,7 +390,6 @@ func TestAccountUnblock(t *testing.T) { return } fmt.Fprintln(w, `{"id":1234567,"blocking":false}`) - return })) defer ts.Close() @@ -431,7 +422,6 @@ func TestAccountMute(t *testing.T) { return } fmt.Fprintln(w, `{"id":1234567,"muting":true}`) - return })) defer ts.Close() @@ -464,7 +454,6 @@ func TestAccountUnmute(t *testing.T) { return } fmt.Fprintln(w, `{"id":1234567,"muting":false}`) - return })) defer ts.Close() @@ -530,7 +519,6 @@ func TestAccountsSearch(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foobar"}, {"username": "barfoo"}]`) - return })) defer ts.Close() @@ -566,7 +554,6 @@ func TestFollowRemoteUser(t *testing.T) { return } fmt.Fprintln(w, `{"username": "zzz"}`) - return })) defer ts.Close() @@ -598,7 +585,6 @@ func TestGetFollowRequests(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) - return })) defer ts.Close() @@ -684,7 +670,6 @@ func TestGetMutes(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) - return })) defer ts.Close() diff --git a/apps_test.go b/apps_test.go index 8734234..b8403e8 100644 --- a/apps_test.go +++ b/apps_test.go @@ -27,7 +27,6 @@ func TestRegisterApp(t *testing.T) { return } fmt.Fprintln(w, `{"id": 123, "client_id": "foo", "client_secret": "bar"}`) - return })) defer ts.Close() @@ -79,7 +78,6 @@ func TestRegisterAppWithCancel(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(3 * time.Second) fmt.Fprintln(w, `{"client_id": "foo", "client_secret": "bar"}`) - return })) defer ts.Close() diff --git a/lists_test.go b/lists_test.go index b6f400e..5cca304 100644 --- a/lists_test.go +++ b/lists_test.go @@ -15,7 +15,6 @@ func TestGetLists(t *testing.T) { return } fmt.Fprintln(w, `[{"id": "1", "title": "foo"}, {"id": "2", "title": "bar"}]`) - return })) defer ts.Close() @@ -47,7 +46,6 @@ func TestGetAccountLists(t *testing.T) { return } fmt.Fprintln(w, `[{"id": "1", "title": "foo"}, {"id": "2", "title": "bar"}]`) - return })) defer ts.Close() @@ -83,7 +81,6 @@ func TestGetListAccounts(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) - return })) defer ts.Close() @@ -119,7 +116,6 @@ func TestGetList(t *testing.T) { return } fmt.Fprintln(w, `{"id": "1", "title": "foo"}`) - return })) defer ts.Close() @@ -149,7 +145,6 @@ func TestCreateList(t *testing.T) { return } fmt.Fprintln(w, `{"id": "1", "title": "foo"}`) - return })) defer ts.Close() @@ -183,7 +178,6 @@ func TestRenameList(t *testing.T) { return } fmt.Fprintln(w, `{"id": "1", "title": "bar"}`) - return })) defer ts.Close() @@ -216,7 +210,6 @@ func TestDeleteList(t *testing.T) { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusMethodNotAllowed) return } - return })) defer ts.Close() @@ -246,7 +239,6 @@ func TestAddToList(t *testing.T) { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - return })) defer ts.Close() @@ -272,7 +264,6 @@ func TestRemoveFromList(t *testing.T) { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusMethodNotAllowed) return } - return })) defer ts.Close() diff --git a/mastodon_test.go b/mastodon_test.go index 527dd89..ff3fc5c 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -96,7 +96,6 @@ func TestAuthenticate(t *testing.T) { return } fmt.Fprintln(w, `{"access_token": "zoo"}`) - return })) defer ts.Close() @@ -124,7 +123,6 @@ func TestAuthenticate(t *testing.T) { func TestAuthenticateWithCancel(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(3 * time.Second) - return })) defer ts.Close() @@ -151,7 +149,6 @@ func TestAuthenticateApp(t *testing.T) { return } fmt.Fprintln(w, `{"name":"zzz","website":"yyy","vapid_key":"xxx"}`) - return })) defer ts.Close() @@ -183,7 +180,6 @@ func TestPostStatus(t *testing.T) { return } fmt.Fprintln(w, `{"access_token": "zoo"}`) - return })) defer ts.Close() @@ -216,7 +212,6 @@ func TestPostStatus(t *testing.T) { func TestPostStatusWithCancel(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(3 * time.Second) - return })) defer ts.Close() @@ -351,7 +346,6 @@ func TestPostStatusParams(t *testing.T) { func TestGetTimelineHome(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `[{"content": "foo"}, {"content": "bar"}]`) - return })) defer ts.Close() @@ -391,7 +385,6 @@ func TestGetTimelineHome(t *testing.T) { func TestGetTimelineHomeWithCancel(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(3 * time.Second) - return })) defer ts.Close() diff --git a/notification_test.go b/notification_test.go index 28dc975..78cdbdd 100644 --- a/notification_test.go +++ b/notification_test.go @@ -28,7 +28,6 @@ func TestGetNotifications(t *testing.T) { return } http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - return })) defer ts.Close() @@ -76,7 +75,6 @@ func TestPushSubscription(t *testing.T) { return } http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) - return })) defer ts.Close() diff --git a/report_test.go b/report_test.go index db7fadb..374f490 100644 --- a/report_test.go +++ b/report_test.go @@ -15,7 +15,6 @@ func TestGetReports(t *testing.T) { return } fmt.Fprintln(w, `[{"id": 122, "action_taken": false}, {"id": 123, "action_taken": true}]`) - return })) defer ts.Close() @@ -55,7 +54,6 @@ func TestReport(t *testing.T) { } else { fmt.Fprintln(w, `{"id": 1234, "action_taken": true}`) } - return })) defer ts.Close() diff --git a/status_test.go b/status_test.go index 0047617..5209668 100644 --- a/status_test.go +++ b/status_test.go @@ -13,7 +13,6 @@ import ( func TestGetFavourites(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `[{"content": "foo"}, {"content": "bar"}]`) - return })) defer ts.Close() @@ -41,7 +40,6 @@ func TestGetFavourites(t *testing.T) { func TestGetBookmarks(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, `[{"content": "foo"}, {"content": "bar"}]`) - return })) defer ts.Close() @@ -73,7 +71,6 @@ func TestGetStatus(t *testing.T) { return } fmt.Fprintln(w, `{"content": "zzz", "emojis":[{"shortcode":"💩", "url":"http://example.com", "static_url": "http://example.com/static"}]}`) - return })) defer ts.Close() @@ -115,7 +112,6 @@ func TestGetStatusCard(t *testing.T) { return } fmt.Fprintln(w, `{"title": "zzz"}`) - return })) defer ts.Close() @@ -145,7 +141,6 @@ func TestGetStatusContext(t *testing.T) { return } fmt.Fprintln(w, `{"ancestors": [{"content": "zzz"},{"content": "bbb"}]}`) - return })) defer ts.Close() @@ -184,7 +179,6 @@ func TestGetRebloggedBy(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) - return })) defer ts.Close() @@ -220,7 +214,6 @@ func TestGetFavouritedBy(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) - return })) defer ts.Close() @@ -256,7 +249,6 @@ func TestReblog(t *testing.T) { return } fmt.Fprintln(w, `{"content": "zzz"}`) - return })) defer ts.Close() @@ -286,7 +278,6 @@ func TestUnreblog(t *testing.T) { return } fmt.Fprintln(w, `{"content": "zzz"}`) - return })) defer ts.Close() @@ -316,7 +307,6 @@ func TestFavourite(t *testing.T) { return } fmt.Fprintln(w, `{"content": "zzz"}`) - return })) defer ts.Close() @@ -346,7 +336,6 @@ func TestUnfavourite(t *testing.T) { return } fmt.Fprintln(w, `{"content": "zzz"}`) - return })) defer ts.Close() @@ -376,7 +365,6 @@ func TestBookmark(t *testing.T) { return } fmt.Fprintln(w, `{"content": "zzz"}`) - return })) defer ts.Close() @@ -406,7 +394,6 @@ func TestUnbookmark(t *testing.T) { return } fmt.Fprintln(w, `{"content": "zzz"}`) - return })) defer ts.Close() @@ -488,7 +475,6 @@ func TestGetTimelineHashtag(t *testing.T) { return } fmt.Fprintln(w, `[{"content": "zzz"},{"content": "yyy"}]`) - return })) defer ts.Close() @@ -524,7 +510,6 @@ func TestGetTimelineList(t *testing.T) { return } fmt.Fprintln(w, `[{"content": "zzz"},{"content": "yyy"}]`) - return })) defer ts.Close() @@ -560,7 +545,6 @@ func TestGetTimelineMedia(t *testing.T) { return } fmt.Fprintln(w, `[{"content": "zzz"},{"content": "yyy"}]`) - return })) defer ts.Close() @@ -599,7 +583,6 @@ func TestDeleteStatus(t *testing.T) { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusMethodNotAllowed) return } - return })) defer ts.Close() @@ -635,7 +618,6 @@ func TestSearch(t *testing.T) { "statuses":[{"content": "aaa"}], "hashtags":[{"name": "tag"},{"name": "tag2"},{"name": "tag3"}] }`) - return })) defer ts.Close() @@ -680,7 +662,6 @@ func TestUploadMedia(t *testing.T) { return } fmt.Fprintln(w, `{"id": 123}`) - return })) defer ts.Close() @@ -729,7 +710,6 @@ func TestGetConversations(t *testing.T) { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) } fmt.Fprintln(w, `[{"id": "4", "unread":false, "last_status" : {"content": "zzz"}}, {"id": "3", "unread":true, "last_status" : {"content": "bar"}}]`) - return })) defer ts.Close() @@ -767,7 +747,6 @@ func TestDeleteConversation(t *testing.T) { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusMethodNotAllowed) return } - return })) defer ts.Close() @@ -793,7 +772,6 @@ func TestMarkConversationsAsRead(t *testing.T) { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } - return })) defer ts.Close() From be6120570837cb461e6c52349d7917154af8af87 Mon Sep 17 00:00:00 2001 From: Darren O'Connor Date: Wed, 16 Nov 2022 01:57:22 +0000 Subject: [PATCH 118/127] Remove unused variables --- accounts_test.go | 8 ++++---- lists_test.go | 12 ++++++------ report_test.go | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/accounts_test.go b/accounts_test.go index 473e9b6..47e0310 100644 --- a/accounts_test.go +++ b/accounts_test.go @@ -303,11 +303,11 @@ func TestAccountFollow(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - rel, err := client.AccountFollow(context.Background(), "123") + _, err := client.AccountFollow(context.Background(), "123") if err == nil { t.Fatalf("should be fail: %v", err) } - rel, err = client.AccountFollow(context.Background(), "1234567") + rel, err := client.AccountFollow(context.Background(), "1234567") if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -335,11 +335,11 @@ func TestAccountUnfollow(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - rel, err := client.AccountUnfollow(context.Background(), "123") + _, err := client.AccountUnfollow(context.Background(), "123") if err == nil { t.Fatalf("should be fail: %v", err) } - rel, err = client.AccountUnfollow(context.Background(), "1234567") + rel, err := client.AccountUnfollow(context.Background(), "1234567") if err != nil { t.Fatalf("should not be fail: %v", err) } diff --git a/lists_test.go b/lists_test.go index 5cca304..b1c9e3b 100644 --- a/lists_test.go +++ b/lists_test.go @@ -125,11 +125,11 @@ func TestGetList(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - list, err := client.GetList(context.Background(), "2") + _, err := client.GetList(context.Background(), "2") if err == nil { t.Fatalf("should be fail: %v", err) } - list, err = client.GetList(context.Background(), "1") + list, err := client.GetList(context.Background(), "1") if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -154,11 +154,11 @@ func TestCreateList(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - list, err := client.CreateList(context.Background(), "") + _, err := client.CreateList(context.Background(), "") if err == nil { t.Fatalf("should be fail: %v", err) } - list, err = client.CreateList(context.Background(), "foo") + list, err := client.CreateList(context.Background(), "foo") if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -187,11 +187,11 @@ func TestRenameList(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - list, err := client.RenameList(context.Background(), "2", "bar") + _, err := client.RenameList(context.Background(), "2", "bar") if err == nil { t.Fatalf("should be fail: %v", err) } - list, err = client.RenameList(context.Background(), "1", "bar") + list, err := client.RenameList(context.Background(), "1", "bar") if err != nil { t.Fatalf("should not be fail: %v", err) } diff --git a/report_test.go b/report_test.go index 374f490..f25f0b5 100644 --- a/report_test.go +++ b/report_test.go @@ -63,11 +63,11 @@ func TestReport(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - rp, err := client.Report(context.Background(), "121", nil, "") + _, err := client.Report(context.Background(), "121", nil, "") if err == nil { t.Fatalf("should be fail: %v", err) } - rp, err = client.Report(context.Background(), "122", nil, "") + rp, err := client.Report(context.Background(), "122", nil, "") if err != nil { t.Fatalf("should not be fail: %v", err) } From 5f0c9a21c2b068961ba897a628adb5897eec01d1 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Wed, 16 Nov 2022 20:20:18 +0100 Subject: [PATCH 119/127] add support for language --- mastodon.go | 1 + mastodon_test.go | 7 +++++++ status.go | 3 +++ 3 files changed, 11 insertions(+) diff --git a/mastodon.go b/mastodon.go index 0706981..c704e10 100644 --- a/mastodon.go +++ b/mastodon.go @@ -230,6 +230,7 @@ type Toot struct { Sensitive bool `json:"sensitive"` SpoilerText string `json:"spoiler_text"` Visibility string `json:"visibility"` + Language string `json:"language"` ScheduledAt *time.Time `json:"scheduled_at,omitempty"` Poll *TootPoll `json:"poll"` } diff --git a/mastodon_test.go b/mastodon_test.go index ff3fc5c..0ef9d07 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -252,6 +252,9 @@ func TestPostStatusParams(t *testing.T) { if r.FormValue("visibility") != "" { s.Visibility = (r.FormValue("visibility")) } + if r.FormValue("language") != "" { + s.Language = (r.FormValue("language")) + } if r.FormValue("sensitive") == "true" { s.Sensitive = true s.SpoilerText = fmt.Sprintf("

%s

", r.FormValue("spoiler_text")) @@ -288,6 +291,7 @@ func TestPostStatusParams(t *testing.T) { Status: "foobar", InReplyToID: ID("2"), Visibility: "unlisted", + Language: "sv", Sensitive: true, SpoilerText: "bar", MediaIDs: []ID{"1", "2"}, @@ -310,6 +314,9 @@ func TestPostStatusParams(t *testing.T) { if s.Visibility != "unlisted" { t.Fatalf("want %q but %q", "unlisted", s.Visibility) } + if s.Language != "sv" { + t.Fatalf("want %q but %q", "sv", s.Language) + } if s.Sensitive != true { t.Fatalf("want %t but %t", true, s.Sensitive) } diff --git a/status.go b/status.go index e616366..f5cc451 100644 --- a/status.go +++ b/status.go @@ -365,6 +365,9 @@ func (c *Client) PostStatus(ctx context.Context, toot *Toot) (*Status, error) { if toot.Visibility != "" { params.Set("visibility", fmt.Sprint(toot.Visibility)) } + if toot.Language != "" { + params.Set("language", fmt.Sprint(toot.Language)) + } if toot.Sensitive { params.Set("sensitive", "true") } From b597f437a9fd1514466f320235d236f7d468989a Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Sun, 27 Nov 2022 13:24:42 +0900 Subject: [PATCH 120/127] use urfave/cli/v2 --- cmd/mstdn/cmd_account.go | 2 +- cmd/mstdn/cmd_account_test.go | 2 +- cmd/mstdn/cmd_delete.go | 2 +- cmd/mstdn/cmd_delete_test.go | 2 +- cmd/mstdn/cmd_follow.go | 2 +- cmd/mstdn/cmd_follow_test.go | 2 +- cmd/mstdn/cmd_followers.go | 2 +- cmd/mstdn/cmd_followers_test.go | 2 +- cmd/mstdn/cmd_instance.go | 2 +- cmd/mstdn/cmd_instance_activity.go | 2 +- cmd/mstdn/cmd_instance_peers.go | 2 +- cmd/mstdn/cmd_instance_test.go | 2 +- cmd/mstdn/cmd_mikami.go | 2 +- cmd/mstdn/cmd_mikami_test.go | 2 +- cmd/mstdn/cmd_notification.go | 2 +- cmd/mstdn/cmd_notification_test.go | 2 +- cmd/mstdn/cmd_search.go | 2 +- cmd/mstdn/cmd_search_test.go | 2 +- cmd/mstdn/cmd_stream.go | 2 +- cmd/mstdn/cmd_test.go | 2 +- cmd/mstdn/cmd_timeline.go | 2 +- cmd/mstdn/cmd_timeline_test.go | 2 +- cmd/mstdn/cmd_toot.go | 2 +- cmd/mstdn/cmd_toot_test.go | 2 +- cmd/mstdn/cmd_upload.go | 2 +- cmd/mstdn/cmd_upload_test.go | 2 +- cmd/mstdn/cmd_xsearch.go | 2 +- cmd/mstdn/cmd_xsearch_test.go | 2 +- cmd/mstdn/go.mod | 3 ++- cmd/mstdn/go.sum | 20 +++++++++----------- cmd/mstdn/main.go | 18 +++++++++--------- cmd/mstdn/main_test.go | 2 +- 32 files changed, 49 insertions(+), 50 deletions(-) diff --git a/cmd/mstdn/cmd_account.go b/cmd/mstdn/cmd_account.go index 23083c2..6501769 100644 --- a/cmd/mstdn/cmd_account.go +++ b/cmd/mstdn/cmd_account.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/mattn/go-mastodon" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func cmdAccount(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_account_test.go b/cmd/mstdn/cmd_account_test.go index 8894451..d35a088 100644 --- a/cmd/mstdn/cmd_account_test.go +++ b/cmd/mstdn/cmd_account_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func TestCmdAccount(t *testing.T) { diff --git a/cmd/mstdn/cmd_delete.go b/cmd/mstdn/cmd_delete.go index 092bad6..acc8a36 100644 --- a/cmd/mstdn/cmd_delete.go +++ b/cmd/mstdn/cmd_delete.go @@ -5,7 +5,7 @@ import ( "errors" "github.com/mattn/go-mastodon" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func cmdDelete(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_delete_test.go b/cmd/mstdn/cmd_delete_test.go index e37442d..4e202ca 100644 --- a/cmd/mstdn/cmd_delete_test.go +++ b/cmd/mstdn/cmd_delete_test.go @@ -5,7 +5,7 @@ import ( "net/http" "testing" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func TestCmdDelete(t *testing.T) { diff --git a/cmd/mstdn/cmd_follow.go b/cmd/mstdn/cmd_follow.go index e064f03..ca87963 100644 --- a/cmd/mstdn/cmd_follow.go +++ b/cmd/mstdn/cmd_follow.go @@ -5,7 +5,7 @@ import ( "errors" "github.com/mattn/go-mastodon" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func cmdFollow(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_follow_test.go b/cmd/mstdn/cmd_follow_test.go index e4f43c9..ce2cfce 100644 --- a/cmd/mstdn/cmd_follow_test.go +++ b/cmd/mstdn/cmd_follow_test.go @@ -5,7 +5,7 @@ import ( "net/http" "testing" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func TestCmdFollow(t *testing.T) { diff --git a/cmd/mstdn/cmd_followers.go b/cmd/mstdn/cmd_followers.go index bfacf8b..9d9f10f 100644 --- a/cmd/mstdn/cmd_followers.go +++ b/cmd/mstdn/cmd_followers.go @@ -6,7 +6,7 @@ import ( "time" "github.com/mattn/go-mastodon" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func cmdFollowers(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_followers_test.go b/cmd/mstdn/cmd_followers_test.go index f8c5080..17b7f4c 100644 --- a/cmd/mstdn/cmd_followers_test.go +++ b/cmd/mstdn/cmd_followers_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func TestCmdFollowers(t *testing.T) { diff --git a/cmd/mstdn/cmd_instance.go b/cmd/mstdn/cmd_instance.go index 5932c68..84042dd 100644 --- a/cmd/mstdn/cmd_instance.go +++ b/cmd/mstdn/cmd_instance.go @@ -6,7 +6,7 @@ import ( "sort" "github.com/mattn/go-mastodon" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func cmdInstance(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_instance_activity.go b/cmd/mstdn/cmd_instance_activity.go index 199ba99..fa133c3 100644 --- a/cmd/mstdn/cmd_instance_activity.go +++ b/cmd/mstdn/cmd_instance_activity.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/mattn/go-mastodon" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func cmdInstanceActivity(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_instance_peers.go b/cmd/mstdn/cmd_instance_peers.go index 7a73d1d..86d8915 100644 --- a/cmd/mstdn/cmd_instance_peers.go +++ b/cmd/mstdn/cmd_instance_peers.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/mattn/go-mastodon" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func cmdInstancePeers(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_instance_test.go b/cmd/mstdn/cmd_instance_test.go index f5a6279..a8435ad 100644 --- a/cmd/mstdn/cmd_instance_test.go +++ b/cmd/mstdn/cmd_instance_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func TestCmdInstance(t *testing.T) { diff --git a/cmd/mstdn/cmd_mikami.go b/cmd/mstdn/cmd_mikami.go index abbfc18..069dd44 100644 --- a/cmd/mstdn/cmd_mikami.go +++ b/cmd/mstdn/cmd_mikami.go @@ -1,7 +1,7 @@ package main import ( - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func cmdMikami(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_mikami_test.go b/cmd/mstdn/cmd_mikami_test.go index 3fe9eae..3d61bf7 100644 --- a/cmd/mstdn/cmd_mikami_test.go +++ b/cmd/mstdn/cmd_mikami_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func TestCmdMikami(t *testing.T) { diff --git a/cmd/mstdn/cmd_notification.go b/cmd/mstdn/cmd_notification.go index b32ba4e..177494f 100644 --- a/cmd/mstdn/cmd_notification.go +++ b/cmd/mstdn/cmd_notification.go @@ -6,7 +6,7 @@ import ( "github.com/fatih/color" "github.com/mattn/go-mastodon" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func cmdNotification(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_notification_test.go b/cmd/mstdn/cmd_notification_test.go index 25a9429..aac67f3 100644 --- a/cmd/mstdn/cmd_notification_test.go +++ b/cmd/mstdn/cmd_notification_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func TestCmdNotification(t *testing.T) { diff --git a/cmd/mstdn/cmd_search.go b/cmd/mstdn/cmd_search.go index 6d7e81b..c6bdd87 100644 --- a/cmd/mstdn/cmd_search.go +++ b/cmd/mstdn/cmd_search.go @@ -6,7 +6,7 @@ import ( "fmt" "github.com/mattn/go-mastodon" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func cmdSearch(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_search_test.go b/cmd/mstdn/cmd_search_test.go index f3509cc..fd5e740 100644 --- a/cmd/mstdn/cmd_search_test.go +++ b/cmd/mstdn/cmd_search_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func TestCmdSearch(t *testing.T) { diff --git a/cmd/mstdn/cmd_stream.go b/cmd/mstdn/cmd_stream.go index 9626769..a0caa41 100644 --- a/cmd/mstdn/cmd_stream.go +++ b/cmd/mstdn/cmd_stream.go @@ -10,7 +10,7 @@ import ( "text/template" "github.com/mattn/go-mastodon" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) // SimpleJSON is a struct for output JSON for data to be simple used diff --git a/cmd/mstdn/cmd_test.go b/cmd/mstdn/cmd_test.go index 14043c3..ecb841f 100644 --- a/cmd/mstdn/cmd_test.go +++ b/cmd/mstdn/cmd_test.go @@ -6,7 +6,7 @@ import ( "net/http/httptest" "github.com/mattn/go-mastodon" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func testWithServer(h http.HandlerFunc, testFuncs ...func(*cli.App)) string { diff --git a/cmd/mstdn/cmd_timeline.go b/cmd/mstdn/cmd_timeline.go index c610966..987a5c4 100644 --- a/cmd/mstdn/cmd_timeline.go +++ b/cmd/mstdn/cmd_timeline.go @@ -4,7 +4,7 @@ import ( "context" "github.com/mattn/go-mastodon" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func cmdTimeline(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_timeline_test.go b/cmd/mstdn/cmd_timeline_test.go index d08a9fc..e24ba14 100644 --- a/cmd/mstdn/cmd_timeline_test.go +++ b/cmd/mstdn/cmd_timeline_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func TestCmdTimeline(t *testing.T) { diff --git a/cmd/mstdn/cmd_toot.go b/cmd/mstdn/cmd_toot.go index 7c2ee63..b777a65 100644 --- a/cmd/mstdn/cmd_toot.go +++ b/cmd/mstdn/cmd_toot.go @@ -6,7 +6,7 @@ import ( "fmt" "github.com/mattn/go-mastodon" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func cmdToot(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_toot_test.go b/cmd/mstdn/cmd_toot_test.go index 4f997f0..bc083be 100644 --- a/cmd/mstdn/cmd_toot_test.go +++ b/cmd/mstdn/cmd_toot_test.go @@ -5,7 +5,7 @@ import ( "net/http" "testing" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func TestCmdToot(t *testing.T) { diff --git a/cmd/mstdn/cmd_upload.go b/cmd/mstdn/cmd_upload.go index 15f912e..68d8b70 100644 --- a/cmd/mstdn/cmd_upload.go +++ b/cmd/mstdn/cmd_upload.go @@ -6,7 +6,7 @@ import ( "fmt" "github.com/mattn/go-mastodon" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func cmdUpload(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_upload_test.go b/cmd/mstdn/cmd_upload_test.go index 585d943..0e76bd1 100644 --- a/cmd/mstdn/cmd_upload_test.go +++ b/cmd/mstdn/cmd_upload_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func TestCmdUpload(t *testing.T) { diff --git a/cmd/mstdn/cmd_xsearch.go b/cmd/mstdn/cmd_xsearch.go index 2c7950b..223d9fa 100644 --- a/cmd/mstdn/cmd_xsearch.go +++ b/cmd/mstdn/cmd_xsearch.go @@ -6,7 +6,7 @@ import ( "net/url" "github.com/PuerkitoBio/goquery" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func cmdXSearch(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_xsearch_test.go b/cmd/mstdn/cmd_xsearch_test.go index f3dc105..43c2baa 100644 --- a/cmd/mstdn/cmd_xsearch_test.go +++ b/cmd/mstdn/cmd_xsearch_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func TestCmdXSearch(t *testing.T) { diff --git a/cmd/mstdn/go.mod b/cmd/mstdn/go.mod index 729c262..19d310d 100644 --- a/cmd/mstdn/go.mod +++ b/cmd/mstdn/go.mod @@ -9,6 +9,7 @@ require ( 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 + github.com/urfave/cli v1.13.0 + github.com/urfave/cli/v2 v2.23.5 // indirect golang.org/x/net v0.0.0-20220531201128-c960675eff93 ) diff --git a/cmd/mstdn/go.sum b/cmd/mstdn/go.sum index 2006e2f..65758c5 100644 --- a/cmd/mstdn/go.sum +++ b/cmd/mstdn/go.sum @@ -1,9 +1,8 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 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= @@ -11,9 +10,8 @@ github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYF 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 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= 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= @@ -22,15 +20,16 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k 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= +github.com/urfave/cli v1.13.0 h1:kkpCmfxnnnWIie2rCljcvaVrNYmsFq1ynTJH5kn1Ip4= +github.com/urfave/cli v1.13.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw= +github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= 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= @@ -43,7 +42,6 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w 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= @@ -52,4 +50,4 @@ 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= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/mstdn/main.go b/cmd/mstdn/main.go index 141a364..db17144 100644 --- a/cmd/mstdn/main.go +++ b/cmd/mstdn/main.go @@ -17,7 +17,7 @@ import ( "github.com/fatih/color" "github.com/mattn/go-mastodon" "github.com/mattn/go-tty" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" "golang.org/x/net/html" ) @@ -183,23 +183,23 @@ func makeApp() *cli.App { app.Usage = "mastodon client" app.Version = "0.0.1" app.Flags = []cli.Flag{ - cli.StringFlag{ + &cli.StringFlag{ Name: "profile", Usage: "profile name", Value: "", }, } - app.Commands = []cli.Command{ + app.Commands = []*cli.Command{ { Name: "toot", Usage: "post toot", Flags: []cli.Flag{ - cli.StringFlag{ + &cli.StringFlag{ Name: "ff", Usage: "post utf-8 string from a file(\"-\" means STDIN)", Value: "", }, - cli.StringFlag{ + &cli.StringFlag{ Name: "i", Usage: "in-reply-to", Value: "", @@ -211,19 +211,19 @@ func makeApp() *cli.App { Name: "stream", Usage: "stream statuses", Flags: []cli.Flag{ - cli.StringFlag{ + &cli.StringFlag{ Name: "type", Usage: "stream type (public,public/local,user:NAME,hashtag:TAG)", }, - cli.BoolFlag{ + &cli.BoolFlag{ Name: "json", Usage: "output JSON", }, - cli.BoolFlag{ + &cli.BoolFlag{ Name: "simplejson", Usage: "output simple JSON", }, - cli.StringFlag{ + &cli.StringFlag{ Name: "template", Usage: "output with tamplate format", }, diff --git a/cmd/mstdn/main_test.go b/cmd/mstdn/main_test.go index 19e6ab1..123ab88 100644 --- a/cmd/mstdn/main_test.go +++ b/cmd/mstdn/main_test.go @@ -7,7 +7,7 @@ import ( "os" "testing" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) func TestReadFileFile(t *testing.T) { From 51f9d7f99979ecfd6fd6e0d85b6123acb4675260 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Sun, 20 Nov 2022 20:18:10 +0100 Subject: [PATCH 121/127] add source, history for statuses and option to update a status --- README.md | 3 + mastodon_test.go | 180 ++++++++++++++++++++++++++++++++++++++++++++++- status.go | 54 +++++++++++++- status_test.go | 82 +++++++++++++++++++++ 4 files changed, 317 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 989d6e4..8440744 100644 --- a/README.md +++ b/README.md @@ -130,9 +130,12 @@ func main() { * [x] GET /api/v1/statuses/:id * [x] GET /api/v1/statuses/:id/context * [x] GET /api/v1/statuses/:id/card +* [x] GET /api/v1/statuses/:id/history * [x] GET /api/v1/statuses/:id/reblogged_by +* [x] GET /api/v1/statuses/:id/source * [x] GET /api/v1/statuses/:id/favourited_by * [x] POST /api/v1/statuses +* [x] PUT /api/v1/statuses/:id * [x] DELETE /api/v1/statuses/:id * [x] POST /api/v1/statuses/:id/reblog * [x] POST /api/v1/statuses/:id/unreblog diff --git a/mastodon_test.go b/mastodon_test.go index 0ef9d07..a8978b5 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -324,12 +324,190 @@ func TestPostStatusParams(t *testing.T) { t.Fatalf("want %q but %q", "

bar

", s.SpoilerText) } s, err = client.PostStatus(context.Background(), &Toot{ + Status: "foobar", + Poll: &TootPoll{ + Multiple: true, + Options: []string{"A", "B"}, + HideTotals: true, + }, + }) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if s.Poll == nil { + t.Fatalf("poll should not be %v", s.Poll) + } + if len(s.Poll.Options) != 2 { + t.Fatalf("want %q but %q", 2, len(s.Poll.Options)) + } + if s.Poll.Options[0].Title != "A" { + t.Fatalf("want %q but %q", "A", s.Poll.Options[0].Title) + } + if s.Poll.Options[1].Title != "B" { + t.Fatalf("want %q but %q", "B", s.Poll.Options[1].Title) + } + if s.Poll.Multiple != true { + t.Fatalf("want %t but %t", true, s.Poll.Multiple) + } +} + +func TestUpdateStatus(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != "Bearer zoo" { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + fmt.Fprintln(w, `{"access_token": "zoo"}`) + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + }) + _, err := client.UpdateStatus(context.Background(), &Toot{ + Status: "foobar", + }, ID("1")) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + + client = NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + _, err = client.UpdateStatus(context.Background(), &Toot{ + Status: "foobar", + }, ID("1")) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } +} + +func TestUpdateStatusWithCancel(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(3 * time.Second) + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + }) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + _, err := client.UpdateStatus(ctx, &Toot{ + Status: "foobar", + }, ID("1")) + if err == nil { + t.Fatalf("should be fail: %v", err) + } + if want := fmt.Sprintf("Put %q: context canceled", ts.URL+"/api/v1/statuses/1"); want != err.Error() { + t.Fatalf("want %q but %q", want, err.Error()) + } +} +func TestUpdateStatusParams(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/statuses/1" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + r.ParseForm() + if r.FormValue("media_ids[]") != "" && r.FormValue("poll[options][]") != "" { + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + } + s := Status{ + ID: ID("1"), + Content: fmt.Sprintf("

%s

", r.FormValue("status")), + } + if r.FormValue("in_reply_to_id") != "" { + s.InReplyToID = ID(r.FormValue("in_reply_to_id")) + } + if r.FormValue("visibility") != "" { + s.Visibility = (r.FormValue("visibility")) + } + if r.FormValue("language") != "" { + s.Language = (r.FormValue("language")) + } + if r.FormValue("sensitive") == "true" { + s.Sensitive = true + s.SpoilerText = fmt.Sprintf("

%s

", r.FormValue("spoiler_text")) + } + if r.FormValue("media_ids[]") != "" { + for _, id := range r.Form["media_ids[]"] { + s.MediaAttachments = append(s.MediaAttachments, + Attachment{ID: ID(id)}) + } + } + if r.FormValue("poll[options][]") != "" { + p := Poll{} + for _, opt := range r.Form["poll[options][]"] { + p.Options = append(p.Options, PollOption{ + Title: opt, + VotesCount: 0, + }) + } + if r.FormValue("poll[multiple]") == "true" { + p.Multiple = true + } + s.Poll = &p + } + json.NewEncoder(w).Encode(s) + })) + defer ts.Close() + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + s, err := client.UpdateStatus(context.Background(), &Toot{ + Status: "foobar", + InReplyToID: ID("2"), + Visibility: "unlisted", + Language: "sv", + Sensitive: true, + SpoilerText: "bar", + MediaIDs: []ID{"1", "2"}, + Poll: &TootPoll{ + Options: []string{"A", "B"}, + }, + }, ID("1")) + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if len(s.MediaAttachments) > 0 && s.Poll != nil { + t.Fatal("should not fail, can't have both Media and Poll") + } + if s.Content != "

foobar

" { + t.Fatalf("want %q but %q", "

foobar

", s.Content) + } + if s.InReplyToID != "2" { + t.Fatalf("want %q but %q", "2", s.InReplyToID) + } + if s.Visibility != "unlisted" { + t.Fatalf("want %q but %q", "unlisted", s.Visibility) + } + if s.Language != "sv" { + t.Fatalf("want %q but %q", "sv", s.Language) + } + if s.Sensitive != true { + t.Fatalf("want %t but %t", true, s.Sensitive) + } + if s.SpoilerText != "

bar

" { + t.Fatalf("want %q but %q", "

bar

", s.SpoilerText) + } + s, err = client.UpdateStatus(context.Background(), &Toot{ Status: "foobar", Poll: &TootPoll{ Multiple: true, Options: []string{"A", "B"}, }, - }) + }, ID("1")) if err != nil { t.Fatalf("should not be fail: %v", err) } diff --git a/status.go b/status.go index f5cc451..1319452 100644 --- a/status.go +++ b/status.go @@ -24,6 +24,7 @@ type Status struct { Reblog *Status `json:"reblog"` Content string `json:"content"` CreatedAt time.Time `json:"created_at"` + EditedAt time.Time `json:"edited_at"` Emojis []Emoji `json:"emojis"` RepliesCount int64 `json:"replies_count"` ReblogsCount int64 `json:"reblogs_count"` @@ -45,6 +46,17 @@ type Status struct { Pinned interface{} `json:"pinned"` } +// StatusHistory is a struct to hold status history data. +type StatusHistory struct { + Content string `json:"content"` + SpoilerText string `json:"spoiler_text"` + Account Account `json:"account"` + Sensitive bool `json:"sensitive"` + CreatedAt time.Time `json:"created_at"` + Emojis []Emoji `json:"emojis"` + MediaAttachments []Attachment `json:"media_attachments"` +} + // Context holds information for a mastodon context. type Context struct { Ancestors []*Status `json:"ancestors"` @@ -67,6 +79,13 @@ type Card struct { Height int64 `json:"height"` } +// Source holds source properties so a status can be edited. +type Source struct { + ID ID `json:"id"` + Text string `json:"text"` + SpoilerText string `json:"spoiler_text"` +} + // Conversation holds information for a mastodon conversation. type Conversation struct { ID ID `json:"id"` @@ -190,6 +209,25 @@ func (c *Client) GetStatusCard(ctx context.Context, id ID) (*Card, error) { return &card, nil } +func (c *Client) GetStatusSource(ctx context.Context, id ID) (*Source, error) { + var source Source + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/source", id), nil, &source, nil) + if err != nil { + return nil, err + } + return &source, nil +} + +// GetStatusHistory returns the status history specified by id. +func (c *Client) GetStatusHistory(ctx context.Context, id ID) ([]*StatusHistory, error) { + var statuses []*StatusHistory + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/history", id), nil, &statuses, nil) + if err != nil { + return nil, err + } + return statuses, nil +} + // GetRebloggedBy returns the account list of the user who reblogged the toot of id. func (c *Client) GetRebloggedBy(ctx context.Context, id ID, pg *Pagination) ([]*Account, error) { var accounts []*Account @@ -339,6 +377,15 @@ func (c *Client) GetTimelineMedia(ctx context.Context, isLocal bool, pg *Paginat // PostStatus post the toot. func (c *Client) PostStatus(ctx context.Context, toot *Toot) (*Status, error) { + return c.postStatus(ctx, toot, false, ID("none")) +} + +// UpdateStatus updates the toot. +func (c *Client) UpdateStatus(ctx context.Context, toot *Toot, id ID) (*Status, error) { + return c.postStatus(ctx, toot, true, id) +} + +func (c *Client) postStatus(ctx context.Context, toot *Toot, update bool, updateID ID) (*Status, error) { params := url.Values{} params.Set("status", toot.Status) if toot.InReplyToID != "" { @@ -376,7 +423,12 @@ func (c *Client) PostStatus(ctx context.Context, toot *Toot) (*Status, error) { } var status Status - err := c.doAPI(ctx, http.MethodPost, "/api/v1/statuses", params, &status, nil) + var err error + if !update { + err = c.doAPI(ctx, http.MethodPost, "/api/v1/statuses", params, &status, nil) + } else { + err = c.doAPI(ctx, http.MethodPut, fmt.Sprintf("/api/v1/statuses/%s", updateID), params, &status, nil) + } if err != nil { return nil, err } diff --git a/status_test.go b/status_test.go index 5209668..2cac4b8 100644 --- a/status_test.go +++ b/status_test.go @@ -172,6 +172,88 @@ func TestGetStatusContext(t *testing.T) { } } +func TestGetStatusSource(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/statuses/1234567/source" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + fmt.Fprintln(w, `{"id":"1234567","text":"Foo","spoiler_text":"Bar"}%`) + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + _, err := client.GetStatusSource(context.Background(), "123") + if err == nil { + t.Fatalf("should be fail: %v", err) + } + source, err := client.GetStatusSource(context.Background(), "1234567") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if source.ID != ID("1234567") { + t.Fatalf("want %q but %q", "1234567", source.ID) + } + if source.Text != "Foo" { + t.Fatalf("want %q but %q", "Foo", source.Text) + } + if source.SpoilerText != "Bar" { + t.Fatalf("want %q but %q", "Bar", source.SpoilerText) + } +} + +func TestGetStatusHistory(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/api/v1/statuses/1234567/history" { + http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return + } + fmt.Fprintln(w, `[{"content": "foo", "emojis":[{"shortcode":"💩", "url":"http://example.com", "static_url": "http://example.com/static"}]}, {"content": "bar", "emojis":[{"shortcode":"💩", "url":"http://example.com", "static_url": "http://example.com/static"}]}]`) + })) + defer ts.Close() + + client := NewClient(&Config{ + Server: ts.URL, + ClientID: "foo", + ClientSecret: "bar", + AccessToken: "zoo", + }) + _, err := client.GetStatusHistory(context.Background(), "123") + if err == nil { + t.Fatalf("should be fail: %v", err) + } + statuses, err := client.GetStatusHistory(context.Background(), "1234567") + if err != nil { + t.Fatalf("should not be fail: %v", err) + } + if len(statuses) != 2 { + t.Fatalf("want len %q but got %q", "2", len(statuses)) + } + if statuses[0].Content != "foo" { + t.Fatalf("want %q but %q", "bar", statuses[0].Content) + } + if statuses[1].Content != "bar" { + t.Fatalf("want %q but %q", "bar", statuses[1].Content) + } + if len(statuses[0].Emojis) != 1 { + t.Fatal("should have emojis") + } + if statuses[0].Emojis[0].ShortCode != "💩" { + t.Fatalf("want %q but %q", "💩", statuses[0].Emojis[0].ShortCode) + } + if statuses[0].Emojis[0].URL != "http://example.com" { + t.Fatalf("want %q but %q", "https://example.com", statuses[0].Emojis[0].URL) + } + if statuses[0].Emojis[0].StaticURL != "http://example.com/static" { + t.Fatalf("want %q but %q", "https://example.com/static", statuses[0].Emojis[0].StaticURL) + } +} + func TestGetRebloggedBy(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/v1/statuses/1234567/reblogged_by" { From 98f591c5e2f1b41c8a4752671e9d2e86acfb06ad Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Sun, 20 Nov 2022 20:25:15 +0100 Subject: [PATCH 122/127] add comment for GetStatusSource --- status.go | 1 + 1 file changed, 1 insertion(+) diff --git a/status.go b/status.go index 1319452..f12e4eb 100644 --- a/status.go +++ b/status.go @@ -209,6 +209,7 @@ func (c *Client) GetStatusCard(ctx context.Context, id ID) (*Card, error) { return &card, nil } +// GetStatusSource returns source data specified by id. func (c *Client) GetStatusSource(ctx context.Context, id ID) (*Source, error) { var source Source err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/source", id), nil, &source, nil) From ae970802cfcf0789d5d17c952b4f07cbe9769399 Mon Sep 17 00:00:00 2001 From: Raffaele Sena Date: Tue, 22 Nov 2022 11:23:26 -0800 Subject: [PATCH 123/127] Add command timeline-tag to search for statuses matching the tag. Since `search` only return the list of tags matching the search, this is helpful to actually see the statuses for a particular tag. --- cmd/mstdn/cmd_timeline.go | 22 ++++++++++++++++++++++ cmd/mstdn/main.go | 11 +++++++++++ 2 files changed, 33 insertions(+) diff --git a/cmd/mstdn/cmd_timeline.go b/cmd/mstdn/cmd_timeline.go index 987a5c4..6272af4 100644 --- a/cmd/mstdn/cmd_timeline.go +++ b/cmd/mstdn/cmd_timeline.go @@ -2,6 +2,8 @@ package main import ( "context" + "errors" + "strings" "github.com/mattn/go-mastodon" "github.com/urfave/cli/v2" @@ -66,3 +68,23 @@ func cmdTimelineDirect(c *cli.Context) error { } return nil } + +func cmdTimelineHashtag(c *cli.Context) error { + if !c.Args().Present() { + return errors.New("arguments required") + } + local := c.Bool("local") + tag := strings.TrimLeft(argstr(c), "#") + + client := c.App.Metadata["client"].(*mastodon.Client) + config := c.App.Metadata["config"].(*mastodon.Config) + timeline, err := client.GetTimelineHashtag(context.Background(), tag, local, 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 +} diff --git a/cmd/mstdn/main.go b/cmd/mstdn/main.go index db17144..a52cf10 100644 --- a/cmd/mstdn/main.go +++ b/cmd/mstdn/main.go @@ -255,6 +255,17 @@ func makeApp() *cli.App { Usage: "show timeline direct", Action: cmdTimelineDirect, }, + { + Name: "timeline-tag", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "local", + Usage: "local tags only", + }, + }, + Usage: "show tagged timeline", + Action: cmdTimelineHashtag, + }, { Name: "notification", Usage: "show notification", From 29bb16009bce9e626f7125a125fbfeb7e9d87afa Mon Sep 17 00:00:00 2001 From: Michal Vyskocil Date: Mon, 12 Dec 2022 22:27:50 +0100 Subject: [PATCH 124/127] fix go vet warnings: call to (*T).Fatalf from a non-test goroutine The goroutine started from test must not call t.Fatal, but t.Error. Adds a sync.WaitGroup to make sure all goroutines have a chance to report an error before test stops. --- streaming_test.go | 13 +++++++++++-- streaming_ws_test.go | 22 ++++++++++++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/streaming_test.go b/streaming_test.go index fd61f9c..8bfe835 100644 --- a/streaming_test.go +++ b/streaming_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "strings" + "sync" "testing" "time" ) @@ -29,11 +30,14 @@ event: delete data: 1234567 :thump `, largeContent)) + var wg sync.WaitGroup + wg.Add(1) go func() { + defer wg.Done() defer close(q) err := handleReader(q, r) if err != nil { - t.Fatalf("should not be fail: %v", err) + t.Errorf("should not be fail: %v", err) } }() var passUpdate, passUpdateLarge, passNotification, passDelete, passError bool @@ -69,6 +73,7 @@ data: 1234567 "update: %t, update (large): %t, notification: %t, delete: %t, error: %t", passUpdate, passUpdateLarge, passNotification, passDelete, passError) } + wg.Wait() } func TestStreaming(t *testing.T) { @@ -148,11 +153,14 @@ func TestDoStreaming(t *testing.T) { req = req.WithContext(ctx) q := make(chan Event) + var wg sync.WaitGroup + wg.Add(1) go func() { + defer wg.Done() defer close(q) c.doStreaming(req, q) if err != nil { - t.Fatalf("should not be fail: %v", err) + t.Errorf("should not be fail: %v", err) } }() var passError bool @@ -167,6 +175,7 @@ func TestDoStreaming(t *testing.T) { if !passError { t.Fatalf("have not passed through: error %t", passError) } + wg.Wait() } func TestStreamingUser(t *testing.T) { diff --git a/streaming_ws_test.go b/streaming_ws_test.go index 856702a..41fa1d5 100644 --- a/streaming_ws_test.go +++ b/streaming_ws_test.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/http/httptest" + "sync" "testing" "time" @@ -151,12 +152,16 @@ func TestStreamingWS(t *testing.T) { if err != nil { t.Fatalf("should not be fail: %v", err) } + var wg sync.WaitGroup + wg.Add(1) go func() { + defer wg.Done() e := <-q if errorEvent, ok := e.(*ErrorEvent); !ok { - t.Fatalf("should be fail: %v", errorEvent.err) + t.Errorf("should be fail: %v", errorEvent.err) } }() + wg.Wait() } func TestHandleWS(t *testing.T) { @@ -183,10 +188,13 @@ func TestHandleWS(t *testing.T) { q := make(chan Event) client := NewClient(&Config{}).NewWSClient() + var wg sync.WaitGroup + wg.Add(1) go func() { + defer wg.Done() e := <-q if errorEvent, ok := e.(*ErrorEvent); !ok { - t.Fatalf("should be fail: %v", errorEvent.err) + t.Errorf("should be fail: %v", errorEvent.err) } }() err := client.handleWS(context.Background(), ":", q) @@ -196,10 +204,12 @@ func TestHandleWS(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() + wg.Add(1) go func() { + defer wg.Done() e := <-q if errorEvent, ok := e.(*ErrorEvent); !ok { - t.Fatalf("should be fail: %v", errorEvent.err) + t.Errorf("should be fail: %v", errorEvent.err) } }() err = client.handleWS(ctx, "ws://"+ts.Listener.Addr().String(), q) @@ -207,13 +217,17 @@ func TestHandleWS(t *testing.T) { t.Fatalf("should be fail: %v", err) } + wg.Add(1) go func() { + defer wg.Done() e := <-q if errorEvent, ok := e.(*ErrorEvent); !ok { - t.Fatalf("should be fail: %v", errorEvent.err) + t.Errorf("should be fail: %v", errorEvent.err) } }() client.handleWS(context.Background(), "ws://"+ts.Listener.Addr().String(), q) + + wg.Wait() } func TestDialRedirect(t *testing.T) { From 3203150fd3e1a423e99a7ea965794788ae2d02f6 Mon Sep 17 00:00:00 2001 From: till Date: Sun, 18 Dec 2022 14:44:28 +0100 Subject: [PATCH 125/127] Update: support configuration This exposes settings for clients, so it's nice to be able to access it directly. --- instance.go | 16 ++++++++++++++++ instance_test.go | 13 +++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/instance.go b/instance.go index 672120c..a2aa7f4 100644 --- a/instance.go +++ b/instance.go @@ -17,6 +17,17 @@ type Instance struct { Stats *InstanceStats `json:"stats,omitempty"` Languages []string `json:"languages"` ContactAccount *Account `json:"contact_account"` + Configuration *InstanceConfig `json:"configuration"` +} + +type InstanceConfigMap map[string]int + +// InstanceConfig holds configuration accessible for clients. +type InstanceConfig struct { + Accounts *InstanceConfigMap `json:"accounts"` + Statuses *InstanceConfigMap `json:"statuses"` + MediaAttachments map[string]interface{} `json:"media_attachments"` + Polls *InstanceConfigMap `json:"polls"` } // InstanceStats holds information for mastodon instance stats. @@ -36,6 +47,11 @@ func (c *Client) GetInstance(ctx context.Context) (*Instance, error) { return &instance, nil } +// GetConfig returns InstanceConfig. +func (c *Instance) GetConfig() *InstanceConfig { + return c.Configuration +} + // WeeklyActivity holds information for mastodon weekly activity. type WeeklyActivity struct { Week Unixtime `json:"week"` diff --git a/instance_test.go b/instance_test.go index 42f1e69..fb73c0d 100644 --- a/instance_test.go +++ b/instance_test.go @@ -60,7 +60,7 @@ func TestGetInstanceMore(t *testing.T) { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - fmt.Fprintln(w, `{"title": "mastodon", "uri": "http://mstdn.example.com", "description": "test mastodon", "email": "mstdn@mstdn.example.com", "version": "0.0.1", "urls":{"foo":"http://stream1.example.com", "bar": "http://stream2.example.com"}, "thumbnail": "http://mstdn.example.com/logo.png", "stats":{"user_count":1, "status_count":2, "domain_count":3}}}`) + fmt.Fprintln(w, `{"title": "mastodon", "uri": "http://mstdn.example.com", "description": "test mastodon", "email": "mstdn@mstdn.example.com", "version": "0.0.1", "urls":{"foo":"http://stream1.example.com", "bar": "http://stream2.example.com"}, "thumbnail": "http://mstdn.example.com/logo.png", "configuration":{"accounts": {"max_featured_tags": 10}, "statuses": {"max_characters": 500}}, "stats":{"user_count":1, "status_count":2, "domain_count":3}}}`) })) defer ts.Close() @@ -103,7 +103,7 @@ func TestGetInstanceMore(t *testing.T) { t.Fatalf("want %q but %q", "http://mstdn.example.com/logo.png", ins.Thumbnail) } if ins.Stats == nil { - t.Fatal("status should be nil") + t.Fatal("stats should not be nil") } if ins.Stats.UserCount != 1 { t.Fatalf("want %v but %v", 1, ins.Stats.UserCount) @@ -114,6 +114,15 @@ func TestGetInstanceMore(t *testing.T) { if ins.Stats.DomainCount != 3 { t.Fatalf("want %v but %v", 3, ins.Stats.DomainCount) } + + cfg := ins.GetConfig() + if cfg.Accounts == nil { + t.Error("expected accounts to be non nil") + } + if cfg.Statuses == nil { + t.Error("expected statuses to be non nil") + } + } func TestGetInstanceActivity(t *testing.T) { From 6e810f25fa797d36757a28716e73b57ffa839c14 Mon Sep 17 00:00:00 2001 From: till Date: Sun, 18 Dec 2022 14:49:46 +0100 Subject: [PATCH 126/127] Chore: update actions and go versions Resolves: #157 --- .github/workflows/test.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a5b62f6..0d5395e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,11 +11,11 @@ jobs: strategy: matrix: os: [windows-latest, macos-latest, ubuntu-latest] - go: ["1.15", "1.16", "1.17"] + go: ["1.16", "1.17", "1.18", "1.19"] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} @@ -24,4 +24,4 @@ jobs: - run: go test ./... -v -cover -coverprofile coverage.out - run: go test -bench . -benchmem - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v2 From 9faaa4f0dc23d9001ccd1010a9a51f56ba8d2f9f Mon Sep 17 00:00:00 2001 From: "Brint E. Kriebel" Date: Tue, 20 Dec 2022 22:37:57 -0800 Subject: [PATCH 127/127] Add support for edited statuses ActivityPub supports "status.update" for editing statuses. These should be made available for streams. --- cmd/mstdn/cmd_stream.go | 13 ++++++++++++- mastodon_test.go | 1 + streaming.go | 13 +++++++++++++ streaming_test.go | 16 ++++++++++++++++ streaming_ws.go | 6 ++++++ streaming_ws_test.go | 26 ++++++++++++++++++-------- 6 files changed, 66 insertions(+), 9 deletions(-) diff --git a/cmd/mstdn/cmd_stream.go b/cmd/mstdn/cmd_stream.go index a0caa41..03995a3 100644 --- a/cmd/mstdn/cmd_stream.go +++ b/cmd/mstdn/cmd_stream.go @@ -87,7 +87,16 @@ func cmdStream(c *cli.Context) error { if asJSON { json.NewEncoder(c.App.Writer).Encode(e) } else if asSimpleJSON { - if t, ok := e.(*mastodon.UpdateEvent); ok { + switch t := e.(type) { + case *mastodon.UpdateEvent: + 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), + }) + case *mastodon.UpdateEditEvent: json.NewEncoder(c.App.Writer).Encode(&SimpleJSON{ ID: t.Status.ID, Username: t.Status.Account.Username, @@ -102,6 +111,8 @@ func cmdStream(c *cli.Context) error { switch t := e.(type) { case *mastodon.UpdateEvent: s.displayStatus(c.App.Writer, t.Status) + case *mastodon.UpdateEditEvent: + s.displayStatus(c.App.Writer, t.Status) case *mastodon.NotificationEvent: // TODO s.displayStatus(c.App.Writer, t.Notification.Status) case *mastodon.ErrorEvent: diff --git a/mastodon_test.go b/mastodon_test.go index a8978b5..5d14a44 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -592,6 +592,7 @@ func TestGetTimelineHomeWithCancel(t *testing.T) { func TestForTheCoverages(t *testing.T) { (*UpdateEvent)(nil).event() + (*UpdateEditEvent)(nil).event() (*NotificationEvent)(nil).event() (*DeleteEvent)(nil).event() (*ErrorEvent)(nil).event() diff --git a/streaming.go b/streaming.go index 2439d67..b109f6c 100644 --- a/streaming.go +++ b/streaming.go @@ -20,6 +20,13 @@ type UpdateEvent struct { func (e *UpdateEvent) event() {} +// UpdateEditEvent is a struct for passing status edit event to app. +type UpdateEditEvent struct { + Status *Status `json:"status"` +} + +func (e *UpdateEditEvent) event() {} + // NotificationEvent is a struct for passing notification event to app. type NotificationEvent struct { Notification *Notification `json:"notification"` @@ -81,6 +88,12 @@ func handleReader(q chan Event, r io.Reader) error { if err == nil { q <- &UpdateEvent{&status} } + case "status.update": + var status Status + err = json.Unmarshal([]byte(token[1]), &status) + if err == nil { + q <- &UpdateEditEvent{&status} + } case "notification": var notification Notification err = json.Unmarshal([]byte(token[1]), ¬ification) diff --git a/streaming_test.go b/streaming_test.go index 8bfe835..a9e90d9 100644 --- a/streaming_test.go +++ b/streaming_test.go @@ -28,6 +28,8 @@ event: notification data: {"type": "mention"} event: delete data: 1234567 +event: status.update +data: {"content": "foo"} :thump `, largeContent)) var wg sync.WaitGroup @@ -51,6 +53,14 @@ data: 1234567 } else { t.Fatalf("bad update content: %q", event.Status.Content) } + case *UpdateEditEvent: + if event.Status.Content == "foo" { + passUpdate = true + } else if event.Status.Content == largeContent { + passUpdateLarge = true + } else { + t.Fatalf("bad update content: %q", event.Status.Content) + } case *NotificationEvent: passNotification = true if event.Notification.Type != "mention" { @@ -125,6 +135,12 @@ data: {"content": "foo"} if event.Status.Content != "foo" { t.Fatalf("want %q but %q", "foo", event.Status.Content) } + case *UpdateEditEvent: + cnt++ + passUpdate = true + if event.Status.Content != "foo" { + t.Fatalf("want %q but %q", "foo", event.Status.Content) + } } } if cnt != 1 { diff --git a/streaming_ws.go b/streaming_ws.go index bd42bf9..5658bbf 100644 --- a/streaming_ws.go +++ b/streaming_ws.go @@ -127,6 +127,12 @@ func (c *WSClient) handleWS(ctx context.Context, rawurl string, q chan Event) er if err == nil { q <- &UpdateEvent{Status: &status} } + case "status.update": + var status Status + err = json.Unmarshal([]byte(s.Payload.(string)), &status) + if err == nil { + q <- &UpdateEditEvent{Status: &status} + } case "notification": var notification Notification err = json.Unmarshal([]byte(s.Payload.(string)), ¬ification) diff --git a/streaming_ws_test.go b/streaming_ws_test.go index 41fa1d5..7e78890 100644 --- a/streaming_ws_test.go +++ b/streaming_ws_test.go @@ -80,6 +80,13 @@ func wsMock(w http.ResponseWriter, r *http.Request) { return } + err = conn.WriteMessage(websocket.TextMessage, + []byte(`{"event":"status.update","payload":"{\"content\":\"bar\"}"}`)) + 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 { @@ -112,20 +119,20 @@ func wsTest(t *testing.T, q chan Event, cancel func()) { for e := range q { events = append(events, e) } - if len(events) != 6 { - t.Fatalf("result should be four: %d", len(events)) + if len(events) != 7 { + t.Fatalf("result should be seven: %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[1].(*UpdateEditEvent).Status.Content != "bar" { + t.Fatalf("want %q but %q", "bar", events[1].(*UpdateEditEvent).Status.Content) } - if events[2].(*DeleteEvent).ID != "1234567" { - t.Fatalf("want %q but %q", "1234567", events[2].(*DeleteEvent).ID) + if events[2].(*NotificationEvent).Notification.ID != "123" { + t.Fatalf("want %q but %q", "123", events[2].(*NotificationEvent).Notification.ID) } - if errorEvent, ok := events[3].(*ErrorEvent); !ok { - t.Fatalf("should be fail: %v", errorEvent.err) + if events[3].(*DeleteEvent).ID != "1234567" { + t.Fatalf("want %q but %q", "1234567", events[3].(*DeleteEvent).ID) } if errorEvent, ok := events[4].(*ErrorEvent); !ok { t.Fatalf("should be fail: %v", errorEvent.err) @@ -133,6 +140,9 @@ func wsTest(t *testing.T, q chan Event, cancel func()) { if errorEvent, ok := events[5].(*ErrorEvent); !ok { t.Fatalf("should be fail: %v", errorEvent.err) } + if errorEvent, ok := events[6].(*ErrorEvent); !ok { + t.Fatalf("should be fail: %v", errorEvent.err) + } } func TestStreamingWS(t *testing.T) {