diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 351ae11..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,12 +0,0 @@ -# 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'] diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index 0d5395e..0000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: test -on: - push: - branches: - - master - pull_request: - branches: - - master -jobs: - test: - strategy: - matrix: - os: [windows-latest, macos-latest, ubuntu-latest] - go: ["1.16", "1.17", "1.18", "1.19"] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v3 - 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@v2 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b65ca33 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: go +go: + - tip +before_install: + - go get github.com/mattn/goveralls + - go get golang.org/x/tools/cmd/cover +script: + - $HOME/gopath/bin/goveralls -repotoken u2dqXvOxbIBr8eGxCjcgTkkN2JOSGx1fy diff --git a/README.md b/README.md index 8440744..a66ccb7 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # 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) -[![Go Reference](https://pkg.go.dev/badge/github.com/mattn/go-mastodon.svg)](https://pkg.go.dev/github.com/mattn/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) +[![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 @@ -83,81 +82,43 @@ func main() { * [x] GET /api/v1/accounts/:id/unblock * [x] GET /api/v1/accounts/:id/mute * [x] GET /api/v1/accounts/:id/unmute -* [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 -* [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/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 * [x] POST /api/v1/follows * [x] GET /api/v1/instance -* [x] GET /api/v1/instance/activity -* [x] GET /api/v1/instance/peers -* [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 * [x] GET /api/v1/notifications/:id -* [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 -* [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/v2/search +* [x] GET /api/v1/search * [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 * [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 -* [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 -* [x] GET /api/v1/streaming/direct ## Installation -```shell -go install github.com/mattn/go-mastodon@latest +``` +$ go get github.com/mattn/go-mastodon ``` ## License diff --git a/accounts.go b/accounts.go index 0b43e4c..1e9abb3 100644 --- a/accounts.go +++ b/accounts.go @@ -5,49 +5,26 @@ import ( "fmt" "net/http" "net/url" - "strconv" "time" ) -// Account holds information for a mastodon account. +// 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"` - Discoverable bool `json:"discoverable"` - Source *AccountSource `json:"source"` -} - -// Field is a Mastodon account profile field. -type Field struct { - Name string `json:"name"` - Value string `json:"value"` - VerifiedAt time.Time `json:"verified_at"` -} - -// AccountSource is a Mastodon account profile field. -type AccountSource struct { - Privacy *string `json:"privacy"` - Sensitive *bool `json:"sensitive"` - Language *string `json:"language"` - Note *string `json:"note"` - Fields *[]Field `json:"fields"` + 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"` } // GetAccount return Account. @@ -60,7 +37,7 @@ func (c *Client) GetAccount(ctx context.Context, id ID) (*Account, error) { return &account, nil } -// GetAccountCurrentUser returns the Account of current user. +// GetAccountCurrentUser return 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) @@ -76,9 +53,6 @@ type Profile struct { // If it is empty, update it with empty. DisplayName *string Note *string - Locked *bool - Fields *[]Field - Source *AccountSource // Set the base64 encoded character string of the image. Avatar string @@ -94,26 +68,6 @@ 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 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 profile.Source.Language != nil { - params.Set("source[language]", *profile.Source.Language) - } - } if profile.Avatar != "" { params.Set("avatar", profile.Avatar) } @@ -129,7 +83,7 @@ func (c *Client) AccountUpdate(ctx context.Context, profile *Profile) (*Account, return &account, nil } -// GetAccountStatuses return statuses by specified account. +// GetAccountStatuses return statuses by specified accuont. 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,19 +93,7 @@ func (c *Client) GetAccountStatuses(ctx context.Context, id ID, pg *Pagination) return statuses, nil } -// GetAccountPinnedStatuses returns 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 returns followers list. +// GetAccountFollowers return 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 +103,7 @@ func (c *Client) GetAccountFollowers(ctx context.Context, id ID, pg *Pagination) return accounts, nil } -// GetAccountFollowing returns following list. +// GetAccountFollowing return 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 +113,7 @@ func (c *Client) GetAccountFollowing(ctx context.Context, id ID, pg *Pagination) return accounts, nil } -// GetBlocks returns block list. +// GetBlocks return 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,21 +123,17 @@ func (c *Client) GetBlocks(ctx context.Context, pg *Pagination) ([]*Account, err return accounts, nil } -// Relationship holds information for relationship to the account. +// 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"` - MutingNotifications bool `json:"muting_notifications"` - Requested bool `json:"requested"` - DomainBlocking bool `json:"domain_blocking"` - ShowingReblogs bool `json:"showing_reblogs"` - Endorsed bool `json:"endorsed"` + 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"` } -// AccountFollow follows the account. +// AccountFollow follow 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 +143,7 @@ func (c *Client) AccountFollow(ctx context.Context, id ID) (*Relationship, error return &relationship, nil } -// AccountUnfollow unfollows the account. +// AccountUnfollow unfollow 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 +153,7 @@ func (c *Client) AccountUnfollow(ctx context.Context, id ID) (*Relationship, err return &relationship, nil } -// AccountBlock blocks the account. +// AccountBlock block 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 +163,7 @@ func (c *Client) AccountBlock(ctx context.Context, id ID) (*Relationship, error) return &relationship, nil } -// AccountUnblock unblocks the account. +// AccountUnblock unblock 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 +173,7 @@ func (c *Client) AccountUnblock(ctx context.Context, id ID) (*Relationship, erro return &relationship, nil } -// AccountMute mutes the account. +// AccountMute mute 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 +183,7 @@ func (c *Client) AccountMute(ctx context.Context, id ID) (*Relationship, error) return &relationship, nil } -// AccountUnmute unmutes the account. +// AccountUnmute unmute 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 +193,7 @@ func (c *Client) AccountUnmute(ctx context.Context, id ID) (*Relationship, error return &relationship, nil } -// GetAccountRelationships returns relationship for the account. +// GetAccountRelationships return relationship for the account. func (c *Client) GetAccountRelationships(ctx context.Context, ids []string) ([]*Relationship, error) { params := url.Values{} for _, id := range ids { @@ -270,7 +208,7 @@ func (c *Client) GetAccountRelationships(ctx context.Context, ids []string) ([]* return relationships, nil } -// AccountsSearch searches accounts by query. +// AccountsSearch search 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 +222,7 @@ func (c *Client) AccountsSearch(ctx context.Context, q string, limit int64) ([]* return accounts, nil } -// FollowRemoteUser sends follow-request. +// FollowRemoteUser send follow-request. func (c *Client) FollowRemoteUser(ctx context.Context, uri string) (*Account, error) { params := url.Values{} params.Set("uri", uri) @@ -297,7 +235,7 @@ func (c *Client) FollowRemoteUser(ctx context.Context, uri string) (*Account, er return &account, nil } -// GetFollowRequests returns follow requests. +// GetFollowRequests return 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 +245,12 @@ func (c *Client) GetFollowRequests(ctx context.Context, pg *Pagination) ([]*Acco return accounts, nil } -// FollowRequestAuthorize authorizes the follow request of user with id. +// FollowRequestAuthorize is authorize 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 rejects the follow request of user with id. +// FollowRequestReject is 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/accounts_test.go b/accounts_test.go index 47e0310..7603cc6 100644 --- a/accounts_test.go +++ b/accounts_test.go @@ -6,7 +6,6 @@ import ( "net/http" "net/http/httptest" "testing" - "time" ) func TestGetAccount(t *testing.T) { @@ -16,6 +15,7 @@ func TestGetAccount(t *testing.T) { return } fmt.Fprintln(w, `{"username": "zzz"}`) + return })) defer ts.Close() @@ -47,6 +47,7 @@ func TestGetAccountCurrentUser(t *testing.T) { return } fmt.Fprintln(w, `{"username": "zzz"}`) + return })) defer ts.Close() @@ -78,6 +79,7 @@ func TestAccountUpdate(t *testing.T) { return } fmt.Fprintln(w, `{"username": "zzz"}`) + return })) defer ts.Close() @@ -91,15 +93,9 @@ 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 := AccountSource{Language: String("de"), Privacy: String("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...", }) @@ -118,6 +114,7 @@ func TestGetAccountStatuses(t *testing.T) { return } fmt.Fprintln(w, `[{"content": "foo"}, {"content": "bar"}]`) + return })) defer ts.Close() @@ -143,43 +140,6 @@ 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"}]`) - })) - 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" { @@ -187,6 +147,7 @@ func TestGetAccountFollowers(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) + return })) defer ts.Close() @@ -222,6 +183,7 @@ func TestGetAccountFollowing(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) + return })) defer ts.Close() @@ -259,6 +221,7 @@ func TestGetBlocks(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) + return })) defer ts.Close() @@ -294,6 +257,7 @@ func TestAccountFollow(t *testing.T) { return } fmt.Fprintln(w, `{"id":1234567,"following":true}`) + return })) defer ts.Close() @@ -303,11 +267,11 @@ func TestAccountFollow(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - _, err := client.AccountFollow(context.Background(), "123") + rel, 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) } @@ -326,6 +290,7 @@ func TestAccountUnfollow(t *testing.T) { return } fmt.Fprintln(w, `{"id":1234567,"following":false}`) + return })) defer ts.Close() @@ -335,11 +300,11 @@ func TestAccountUnfollow(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - _, err := client.AccountUnfollow(context.Background(), "123") + rel, 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) } @@ -358,6 +323,7 @@ func TestAccountBlock(t *testing.T) { return } fmt.Fprintln(w, `{"id":1234567,"blocking":true}`) + return })) defer ts.Close() @@ -390,6 +356,7 @@ func TestAccountUnblock(t *testing.T) { return } fmt.Fprintln(w, `{"id":1234567,"blocking":false}`) + return })) defer ts.Close() @@ -422,6 +389,7 @@ func TestAccountMute(t *testing.T) { return } fmt.Fprintln(w, `{"id":1234567,"muting":true}`) + return })) defer ts.Close() @@ -454,6 +422,7 @@ func TestAccountUnmute(t *testing.T) { return } fmt.Fprintln(w, `{"id":1234567,"muting":false}`) + return })) defer ts.Close() @@ -519,6 +488,7 @@ func TestAccountsSearch(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foobar"}, {"username": "barfoo"}]`) + return })) defer ts.Close() @@ -554,6 +524,7 @@ func TestFollowRemoteUser(t *testing.T) { return } fmt.Fprintln(w, `{"username": "zzz"}`) + return })) defer ts.Close() @@ -585,6 +556,7 @@ func TestGetFollowRequests(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) + return })) defer ts.Close() @@ -670,6 +642,7 @@ func TestGetMutes(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) + return })) defer ts.Close() diff --git a/apps.go b/apps.go index c885ec9..6d7552f 100644 --- a/apps.go +++ b/apps.go @@ -18,24 +18,19 @@ 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 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". + // This can be a space-separated list of the following items: "read", "write" and "follow". Scopes string // Optional. Website string } -// Application is a mastodon application. +// Application is mastodon application. type Application struct { - ID ID `json:"id"` + ID int64 `json:"id"` 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. @@ -78,36 +73,5 @@ 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 } - -// 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 b8403e8..bab20bf 100644 --- a/apps_test.go +++ b/apps_test.go @@ -26,7 +26,8 @@ func TestRegisterApp(t *testing.T) { fmt.Fprintln(w, `Apps`) return } - fmt.Fprintln(w, `{"id": 123, "client_id": "foo", "client_secret": "bar"}`) + fmt.Fprintln(w, `{"client_id": "foo", "client_secret": "bar"}`) + return })) defer ts.Close() @@ -63,9 +64,6 @@ 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) } @@ -78,6 +76,7 @@ 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() @@ -90,53 +89,7 @@ func TestRegisterAppWithCancel(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/apps"); want != err.Error() { + if want := "Post " + ts.URL + "/api/v1/apps: context canceled"; want != err.Error() { 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/cmd/mstdn/cmd_account.go b/cmd/mstdn/cmd_account.go index 6501769..23083c2 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/v2" + "github.com/urfave/cli" ) func cmdAccount(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_account_test.go b/cmd/mstdn/cmd_account_test.go index d35a088..8894451 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/v2" + "github.com/urfave/cli" ) func TestCmdAccount(t *testing.T) { diff --git a/cmd/mstdn/cmd_delete.go b/cmd/mstdn/cmd_delete.go index acc8a36..83755c5 100644 --- a/cmd/mstdn/cmd_delete.go +++ b/cmd/mstdn/cmd_delete.go @@ -3,9 +3,10 @@ package main import ( "context" "errors" + "strconv" "github.com/mattn/go-mastodon" - "github.com/urfave/cli/v2" + "github.com/urfave/cli" ) func cmdDelete(c *cli.Context) error { @@ -14,7 +15,11 @@ func cmdDelete(c *cli.Context) error { return errors.New("arguments required") } for i := 0; i < c.NArg(); i++ { - err := client.DeleteStatus(context.Background(), mastodon.ID(c.Args().Get(i))) + id, err := strconv.ParseInt(c.Args().Get(i), 10, 64) + if err != nil { + return err + } + err = client.DeleteStatus(context.Background(), id) if err != nil { return err } diff --git a/cmd/mstdn/cmd_delete_test.go b/cmd/mstdn/cmd_delete_test.go index 4e202ca..e37442d 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/v2" + "github.com/urfave/cli" ) func TestCmdDelete(t *testing.T) { diff --git a/cmd/mstdn/cmd_follow.go b/cmd/mstdn/cmd_follow.go index ca87963..e064f03 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/v2" + "github.com/urfave/cli" ) func cmdFollow(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_follow_test.go b/cmd/mstdn/cmd_follow_test.go index ce2cfce..e4f43c9 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/v2" + "github.com/urfave/cli" ) func TestCmdFollow(t *testing.T) { diff --git a/cmd/mstdn/cmd_followers.go b/cmd/mstdn/cmd_followers.go index 9d9f10f..ecb4a79 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/v2" + "github.com/urfave/cli" ) func cmdFollowers(c *cli.Context) error { @@ -25,11 +25,9 @@ func cmdFollowers(c *cli.Context) error { return err } followers = append(followers, fs...) - if pg.MaxID == "" { + if pg.MaxID == 0 { break } - pg.SinceID = "" - pg.MinID = "" time.Sleep(10 * time.Second) } s := newScreen(config) diff --git a/cmd/mstdn/cmd_followers_test.go b/cmd/mstdn/cmd_followers_test.go index 17b7f4c..f8c5080 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/v2" + "github.com/urfave/cli" ) func TestCmdFollowers(t *testing.T) { diff --git a/cmd/mstdn/cmd_instance.go b/cmd/mstdn/cmd_instance.go index 84042dd..b287efc 100644 --- a/cmd/mstdn/cmd_instance.go +++ b/cmd/mstdn/cmd_instance.go @@ -3,10 +3,9 @@ package main import ( "context" "fmt" - "sort" "github.com/mattn/go-mastodon" - "github.com/urfave/cli/v2" + "github.com/urfave/cli" ) func cmdInstance(c *cli.Context) error { @@ -19,26 +18,5 @@ 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 } diff --git a/cmd/mstdn/cmd_instance_activity.go b/cmd/mstdn/cmd_instance_activity.go deleted file mode 100644 index fa133c3..0000000 --- a/cmd/mstdn/cmd_instance_activity.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import ( - "context" - "fmt" - - "github.com/mattn/go-mastodon" - "github.com/urfave/cli/v2" -) - -func cmdInstanceActivity(c *cli.Context) error { - client := c.App.Metadata["client"].(*mastodon.Client) - activities, err := client.GetInstanceActivity(context.Background()) - if err != nil { - return err - } - for _, activity := range activities { - fmt.Fprintf(c.App.Writer, "Logins : %v\n", activity.Logins) - fmt.Fprintf(c.App.Writer, "Registrations : %v\n", activity.Registrations) - fmt.Fprintf(c.App.Writer, "Statuses : %v\n", activity.Statuses) - fmt.Fprintf(c.App.Writer, "Week : %v\n", activity.Week) - } - return nil -} diff --git a/cmd/mstdn/cmd_instance_peers.go b/cmd/mstdn/cmd_instance_peers.go deleted file mode 100644 index 86d8915..0000000 --- a/cmd/mstdn/cmd_instance_peers.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "context" - "fmt" - - "github.com/mattn/go-mastodon" - "github.com/urfave/cli/v2" -) - -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/cmd_instance_test.go b/cmd/mstdn/cmd_instance_test.go index a8435ad..f5a6279 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/v2" + "github.com/urfave/cli" ) func TestCmdInstance(t *testing.T) { diff --git a/cmd/mstdn/cmd_mikami.go b/cmd/mstdn/cmd_mikami.go index 069dd44..abbfc18 100644 --- a/cmd/mstdn/cmd_mikami.go +++ b/cmd/mstdn/cmd_mikami.go @@ -1,7 +1,7 @@ package main import ( - "github.com/urfave/cli/v2" + "github.com/urfave/cli" ) func cmdMikami(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_mikami_test.go b/cmd/mstdn/cmd_mikami_test.go index 3d61bf7..3fe9eae 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/v2" + "github.com/urfave/cli" ) func TestCmdMikami(t *testing.T) { diff --git a/cmd/mstdn/cmd_notification.go b/cmd/mstdn/cmd_notification.go index 177494f..b32ba4e 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/v2" + "github.com/urfave/cli" ) func cmdNotification(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_notification_test.go b/cmd/mstdn/cmd_notification_test.go index aac67f3..25a9429 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/v2" + "github.com/urfave/cli" ) func TestCmdNotification(t *testing.T) { diff --git a/cmd/mstdn/cmd_search.go b/cmd/mstdn/cmd_search.go index c6bdd87..6d7e81b 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/v2" + "github.com/urfave/cli" ) func cmdSearch(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_search_test.go b/cmd/mstdn/cmd_search_test.go index fd5e740..2d00b93 100644 --- a/cmd/mstdn/cmd_search_test.go +++ b/cmd/mstdn/cmd_search_test.go @@ -6,15 +6,15 @@ import ( "strings" "testing" - "github.com/urfave/cli/v2" + "github.com/urfave/cli" ) func TestCmdSearch(t *testing.T) { out := testWithServer( func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/api/v2/search": - fmt.Fprintln(w, `{"accounts": [{"id": 234, "acct": "zzz"}], "statuses":[{"id": 345, "content": "yyy"}], "hashtags": [{"name": "www"}, {"name": "わろす"}]}`) + case "/api/v1/search": + fmt.Fprintln(w, `{"accounts": [{"id": 234, "acct": "zzz"}], "statuses":[{"id": 345, "content": "yyy"}], "hashtags": ["www", "わろす"]}`) return } http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) diff --git a/cmd/mstdn/cmd_stream.go b/cmd/mstdn/cmd_stream.go index 03995a3..258a658 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/v2" + "github.com/urfave/cli" ) // SimpleJSON is a struct for output JSON for data to be simple used @@ -44,7 +44,9 @@ func cmdStream(c *cli.Context) error { "nl": func(s string) string { return s + "\n" }, - "text": textContent, + "text": func(s string) string { + return textContent(s) + }, }).Parse(asFormat) if err != nil { return err @@ -87,16 +89,7 @@ func cmdStream(c *cli.Context) error { if asJSON { json.NewEncoder(c.App.Writer).Encode(e) } else if asSimpleJSON { - 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: + if t, ok := e.(*mastodon.UpdateEvent); ok { json.NewEncoder(c.App.Writer).Encode(&SimpleJSON{ ID: t.Status.ID, Username: t.Status.Account.Username, @@ -111,8 +104,6 @@ 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/cmd/mstdn/cmd_stream_test.go b/cmd/mstdn/cmd_stream_test.go index b3b37c7..92c339b 100644 --- a/cmd/mstdn/cmd_stream_test.go +++ b/cmd/mstdn/cmd_stream_test.go @@ -14,7 +14,8 @@ import ( ) func TestCmdStream(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var ts *httptest.Server + 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 diff --git a/cmd/mstdn/cmd_test.go b/cmd/mstdn/cmd_test.go index ecb841f..14043c3 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/v2" + "github.com/urfave/cli" ) 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 6272af4..d9a0024 100644 --- a/cmd/mstdn/cmd_timeline.go +++ b/cmd/mstdn/cmd_timeline.go @@ -2,11 +2,9 @@ package main import ( "context" - "errors" - "strings" "github.com/mattn/go-mastodon" - "github.com/urfave/cli/v2" + "github.com/urfave/cli" ) func cmdTimeline(c *cli.Context) error { @@ -22,69 +20,3 @@ 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 -} - -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/cmd_timeline_test.go b/cmd/mstdn/cmd_timeline_test.go index e24ba14..b0801a8 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/v2" + "github.com/urfave/cli" ) func TestCmdTimeline(t *testing.T) { @@ -14,13 +14,7 @@ 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": "home"}]`) - return - case "/api/v1/timelines/public": - fmt.Fprintln(w, `[{"content": "public"}]`) - return - case "/api/v1/conversations": - fmt.Fprintln(w, `[{"id": "4", "unread":false, "last_status" : {"content": "direct"}}]`) + fmt.Fprintln(w, `[{"content": "zzz"}]`) return } http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) @@ -28,25 +22,9 @@ 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"}) }, ) - 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) + if !strings.Contains(out, "zzz") { + t.Fatalf("%q should be contained in output of command: %v", "zzz", out) } } diff --git a/cmd/mstdn/cmd_toot.go b/cmd/mstdn/cmd_toot.go index b777a65..29b6f4c 100644 --- a/cmd/mstdn/cmd_toot.go +++ b/cmd/mstdn/cmd_toot.go @@ -3,10 +3,9 @@ package main import ( "context" "errors" - "fmt" "github.com/mattn/go-mastodon" - "github.com/urfave/cli/v2" + "github.com/urfave/cli" ) func cmdToot(c *cli.Context) error { @@ -27,7 +26,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.String("i"))), + InReplyToID: c.Int64("i"), }) return err } diff --git a/cmd/mstdn/cmd_toot_test.go b/cmd/mstdn/cmd_toot_test.go index bc083be..4f997f0 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/v2" + "github.com/urfave/cli" ) func TestCmdToot(t *testing.T) { diff --git a/cmd/mstdn/cmd_upload.go b/cmd/mstdn/cmd_upload.go index 68d8b70..15f912e 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/v2" + "github.com/urfave/cli" ) func cmdUpload(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_upload_test.go b/cmd/mstdn/cmd_upload_test.go index 0e76bd1..585d943 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/v2" + "github.com/urfave/cli" ) func TestCmdUpload(t *testing.T) { diff --git a/cmd/mstdn/cmd_xsearch.go b/cmd/mstdn/cmd_xsearch.go index 223d9fa..2c7950b 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/v2" + "github.com/urfave/cli" ) func cmdXSearch(c *cli.Context) error { diff --git a/cmd/mstdn/cmd_xsearch_test.go b/cmd/mstdn/cmd_xsearch_test.go index 43c2baa..f3dc105 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/v2" + "github.com/urfave/cli" ) func TestCmdXSearch(t *testing.T) { diff --git a/cmd/mstdn/go.mod b/cmd/mstdn/go.mod deleted file mode 100644 index 19d310d..0000000 --- a/cmd/mstdn/go.mod +++ /dev/null @@ -1,15 +0,0 @@ -module github.com/mattn/go-mastodon/cmd/mstdn - -go 1.16 - -replace github.com/mattn/go-mastodon => ../.. - -require ( - github.com/PuerkitoBio/goquery v1.8.0 - github.com/fatih/color v1.13.0 - github.com/mattn/go-mastodon v0.0.4 - github.com/mattn/go-tty v0.0.4 - github.com/urfave/cli v1.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 deleted file mode 100644 index 65758c5..0000000 --- a/cmd/mstdn/go.sum +++ /dev/null @@ -1,53 +0,0 @@ -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.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 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -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/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/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.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= -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-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.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/mstdn/main.go b/cmd/mstdn/main.go index a52cf10..392f748 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/v2" + "github.com/urfave/cli" "golang.org/x/net/html" ) @@ -183,26 +183,26 @@ 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.IntFlag{ Name: "i", Usage: "in-reply-to", - Value: "", + Value: 0, }, }, Action: cmdToot, @@ -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", }, @@ -235,37 +235,6 @@ 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: "timeline-tag", - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "local", - Usage: "local tags only", - }, - }, - Usage: "show tagged timeline", - Action: cmdTimelineHashtag, - }, { Name: "notification", Usage: "show notification", @@ -276,16 +245,6 @@ 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", @@ -401,7 +360,6 @@ func run() int { } client := mastodon.NewClient(config) - client.UserAgent = "mstdn" app.Metadata = map[string]interface{}{ "client": client, "config": config, diff --git a/cmd/mstdn/main_test.go b/cmd/mstdn/main_test.go index 123ab88..19e6ab1 100644 --- a/cmd/mstdn/main_test.go +++ b/cmd/mstdn/main_test.go @@ -7,7 +7,7 @@ import ( "os" "testing" - "github.com/urfave/cli/v2" + "github.com/urfave/cli" ) func TestReadFileFile(t *testing.T) { diff --git a/compat.go b/compat.go index 789906d..0031ae4 100644 --- a/compat.go +++ b/compat.go @@ -3,7 +3,6 @@ package mastodon import ( "encoding/json" "fmt" - "strconv" ) type ID string @@ -24,26 +23,3 @@ 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/example_test.go b/example_test.go index a25a3ca..18716a2 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 == "" { + if pg.MaxID == 0 { break } time.Sleep(10 * time.Second) diff --git a/filters.go b/filters.go deleted file mode 100644 index 56f1229..0000000 --- a/filters.go +++ /dev/null @@ -1,124 +0,0 @@ -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 deleted file mode 100644 index 71e440d..0000000 --- a/filters_test.go +++ /dev/null @@ -1,342 +0,0 @@ -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) - } -} diff --git a/go.mod b/go.mod deleted file mode 100644 index 5ab4e81..0000000 --- a/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module github.com/mattn/go-mastodon - -go 1.16 - -require ( - github.com/gorilla/websocket v1.5.0 - github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 34589e8..0000000 --- a/go.sum +++ /dev/null @@ -1,4 +0,0 @@ -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= -github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= diff --git a/go.test.sh b/go.test.sh deleted file mode 100755 index a7deaca..0000000 --- a/go.test.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -e -echo "" > coverage.txt - -for d in $(go list ./... | grep -v vendor); do - go test -coverprofile=profile.out -covermode=atomic "$d" - if [ -f profile.out ]; then - cat profile.out >> coverage.txt - rm profile.out - fi -done diff --git a/helper_test.go b/helper_test.go index 0d4b2ce..7da80b9 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. - _, err := Base64EncodeFileName("fail") + uri, 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. - _, err := Base64Encode(nil) + uri, 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) } - _, err = Base64Encode(logo) + uri, 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/instance.go b/instance.go index a2aa7f4..9b45c25 100644 --- a/instance.go +++ b/instance.go @@ -5,39 +5,15 @@ import ( "net/http" ) -// Instance holds information for a mastodon instance. +// 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"` - Thumbnail string `json:"thumbnail,omitempty"` - URLs map[string]string `json:"urls,omitempty"` - Stats *InstanceStats `json:"stats,omitempty"` - Languages []string `json:"languages"` - ContactAccount *Account `json:"contact_account"` - Configuration *InstanceConfig `json:"configuration"` + URI string `json:"uri"` + Title string `json:"title"` + Description string `json:"description"` + EMail string `json:"email"` } -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. -type InstanceStats struct { - UserCount int64 `json:"user_count"` - StatusCount int64 `json:"status_count"` - DomainCount int64 `json:"domain_count"` -} - -// GetInstance returns Instance. +// GetInstance return 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) @@ -46,36 +22,3 @@ 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"` - Statuses int64 `json:"statuses,string"` - Logins int64 `json:"logins,string"` - Registrations int64 `json:"registrations,string"` -} - -// 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) - if err != nil { - return nil, err - } - return activity, nil -} - -// 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) - if err != nil { - return nil, err - } - return peers, nil -} diff --git a/instance_test.go b/instance_test.go index fb73c0d..6724b53 100644 --- a/instance_test.go +++ b/instance_test.go @@ -6,7 +6,6 @@ import ( "net/http" "net/http/httptest" "testing" - "time" ) func TestGetInstance(t *testing.T) { @@ -17,7 +16,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", "contact_account": {"username": "mattn"}}`) + fmt.Fprintln(w, `{"title": "mastodon"}`) })) defer ts.Close() @@ -38,151 +37,4 @@ 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) - } - if ins.ContactAccount.Username != "mattn" { - t.Fatalf("want %q but %q", "mattn", ins.ContactAccount.Username) - } -} - -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", "configuration":{"accounts": {"max_featured_tags": 10}, "statuses": {"max_characters": 500}}, "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("stats should not 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) - } - - 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) { - 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 %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) - } -} - -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/lists.go b/lists.go deleted file mode 100644 index 59610cc..0000000 --- a/lists.go +++ /dev/null @@ -1,107 +0,0 @@ -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) -} diff --git a/lists_test.go b/lists_test.go deleted file mode 100644 index b1c9e3b..0000000 --- a/lists_test.go +++ /dev/null @@ -1,280 +0,0 @@ -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"}]`) - })) - 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"}]`) - })) - defer ts.Close() - - client := NewClient(&Config{ - Server: ts.URL, - ClientID: "foo", - ClientSecret: "bar", - AccessToken: "zoo", - }) - _, 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"}]`) - })) - defer ts.Close() - - client := NewClient(&Config{ - Server: ts.URL, - ClientID: "foo", - ClientSecret: "bar", - AccessToken: "zoo", - }) - _, 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"}`) - })) - defer ts.Close() - - client := NewClient(&Config{ - Server: ts.URL, - ClientID: "foo", - ClientSecret: "bar", - AccessToken: "zoo", - }) - _, 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"}`) - })) - defer ts.Close() - - client := NewClient(&Config{ - Server: ts.URL, - ClientID: "foo", - ClientSecret: "bar", - AccessToken: "zoo", - }) - _, 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"}`) - })) - defer ts.Close() - - client := NewClient(&Config{ - Server: ts.URL, - ClientID: "foo", - ClientSecret: "bar", - AccessToken: "zoo", - }) - _, 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 - } - })) - 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 - } - })) - 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 - } - })) - 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/mastodon.go b/mastodon.go index c704e10..a81d9bd 100644 --- a/mastodon.go +++ b/mastodon.go @@ -2,16 +2,20 @@ package mastodon import ( + "bytes" "context" "encoding/json" "errors" "fmt" "io" + "mime/multipart" "net/http" "net/url" + "os" "path" + "path/filepath" + "strconv" "strings" - "time" "github.com/tomnomnom/linkheader" ) @@ -27,12 +31,11 @@ type Config struct { // Client is a API client for mastodon. type Client struct { http.Client - Config *Config - UserAgent string + config *Config } 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 } @@ -54,18 +57,32 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in if err != nil { return err } - } else if media, ok := params.(*Media); ok { - r, contentType, err := media.bodyAndContentType() + } else if file, ok := params.(string); ok { + f, err := os.Open(file) if err != nil { return err } + defer f.Close() - req, err = http.NewRequest(method, u.String(), r) + var buf bytes.Buffer + mw := multipart.NewWriter(&buf) + part, err := mw.CreateFormFile("file", filepath.Base(file)) if err != nil { return err } - - ct = contentType + _, 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 method == http.MethodGet && pg != nil { u.RawQuery = pg.toValues().Encode() @@ -76,41 +93,16 @@ 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) } - if c.UserAgent != "" { - req.Header.Set("User-Agent", c.UserAgent) - } - - var resp *http.Response - backoff := time.Second - 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 - } - - select { - case <-time.After(backoff): - case <-ctx.Done(): - return ctx.Err() - } - - backoff = time.Duration(1.5 * float64(backoff)) - continue - } - break + + resp, err := c.Do(req) + if err != nil { + return err } + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return parseAPIError("bad request", resp) @@ -128,57 +120,25 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in return json.NewDecoder(resp.Body).Decode(&res) } -// NewClient returns a new mastodon API client. +// NewClient return new mastodon API client. func NewClient(config *Config) *Client { return &Client{ Client: *http.DefaultClient, - Config: config, + config: config, } } -// Authenticate gets access-token to the API. +// 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}, - "grant_type": {"password"}, - "username": {username}, - "password": {password}, - "scope": {"read write follow"}, - } + 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") - 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. -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) + u, err := url.Parse(c.config.Server) if err != nil { return err } @@ -190,9 +150,6 @@ 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 @@ -203,44 +160,25 @@ func (c *Client) authenticate(ctx context.Context, params url.Values) error { return parseAPIError("bad authorization", resp) } - var res struct { + res := struct { AccessToken string `json:"access_token"` - } + }{} err = json.NewDecoder(resp.Body).Decode(&res) if err != nil { return err } - c.Config.AccessToken = res.AccessToken + c.config.AccessToken = res.AccessToken return nil } -// Convenience constants for Toot.Visibility -const ( - VisibilityPublic = "public" - VisibilityUnlisted = "unlisted" - VisibilityFollowersOnly = "private" - VisibilityDirectMessage = "direct" -) - -// Toot is a struct to post status. +// 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"` - Language string `json:"language"` - 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"` + 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"` } // Mention hold information for mention. @@ -248,69 +186,36 @@ type Mention struct { URL string `json:"url"` Username string `json:"username"` Acct string `json:"acct"` - ID ID `json:"id"` + ID int64 `json:"id"` } // Tag hold information for tag. type Tag struct { - 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 string `json:"uses"` - Accounts string `json:"accounts"` + Name string `json:"name"` + URL string `json:"url"` } // 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"` - Meta AttachmentMeta `json:"meta"` -} - -// AttachmentMeta holds information for attachment metadata. -type AttachmentMeta struct { - Original AttachmentSize `json:"original"` - Small AttachmentSize `json:"small"` -} - -// AttachmentSize holds information for attatchment size. -type AttachmentSize struct { - Width int64 `json:"width"` - Height int64 `json:"height"` - Size string `json:"size"` - Aspect float64 `json:"aspect"` -} - -// Emoji hold information for CustomEmoji. -type Emoji struct { - ShortCode string `json:"shortcode"` - StaticURL string `json:"static_url"` - URL string `json:"url"` - VisibleInPicker bool `json:"visible_in_picker"` + ID int64 `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"` } // Results hold information for search result. type Results struct { Accounts []*Account `json:"accounts"` Statuses []*Status `json:"statuses"` - Hashtags []*Tag `json:"hashtags"` + Hashtags []string `json:"hashtags"` } // Pagination is a struct for specifying the get range. type Pagination struct { - MaxID ID - SinceID ID - MinID ID + MaxID int64 + SinceID int64 Limit int64 } @@ -334,25 +239,24 @@ 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 } } return p, nil } -func getPaginationID(rawurl, key string) (ID, error) { +func getPaginationID(rawurl, key string) (int64, error) { u, err := url.Parse(rawurl) if err != nil { - return "", err + return 0, err } - return ID(u.Query().Get(key)), nil + id, err := strconv.ParseInt(u.Query().Get(key), 10, 64) + if err != nil { + return 0, err + } + + return id, nil } func (p *Pagination) toValues() url.Values { @@ -360,14 +264,10 @@ 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)) - } - if p.SinceID != "" { - params.Set("since_id", string(p.SinceID)) - } - if p.MinID != "" { - params.Set("min_id", string(p.MinID)) + 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.Limit > 0 { params.Set("limit", fmt.Sprint(p.Limit)) diff --git a/mastodon_test.go b/mastodon_test.go index 5d14a44..eb6c476 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -2,7 +2,6 @@ package mastodon import ( "context" - "encoding/json" "fmt" "io" "net/http" @@ -26,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 %q but %q", "234", pg.MaxID) + if pg.MaxID != 234 { + t.Fatalf("want %d but %d", 234, pg.MaxID) } - if pg.SinceID != "890" { - t.Fatalf("want %q but %q", "890", pg.SinceID) + if pg.SinceID != 890 { + t.Fatalf("want %d but %d", 890, pg.SinceID) } if accounts[0].Username != "foo" { t.Fatalf("want %q but %q", "foo", accounts[0].Username) @@ -55,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 %q but %q", "234", pg.MaxID) + if pg.MaxID != 234 { + t.Fatalf("want %d but %d", 234, pg.MaxID) } - if pg.SinceID != "890" { - t.Fatalf("want %q but %q", "890", pg.SinceID) + if pg.SinceID != 890 { + t.Fatalf("want %d but %d", 890, pg.SinceID) } if accounts[0].Username != "foo" { t.Fatalf("want %q but %q", "foo", accounts[0].Username) @@ -96,6 +95,7 @@ func TestAuthenticate(t *testing.T) { return } fmt.Fprintln(w, `{"access_token": "zoo"}`) + return })) defer ts.Close() @@ -123,6 +123,7 @@ 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() @@ -137,42 +138,11 @@ func TestAuthenticateWithCancel(t *testing.T) { if err == nil { t.Fatalf("should be fail: %v", err) } - if want := fmt.Sprintf("Post %q: context canceled", ts.URL+"/oauth/token"); want != err.Error() { + if want := "Post " + ts.URL + "/oauth/token: context canceled"; want != err.Error() { t.Fatalf("want %q but %q", want, err.Error()) } } -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"}`) - })) - 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" { @@ -180,6 +150,7 @@ func TestPostStatus(t *testing.T) { return } fmt.Fprintln(w, `{"access_token": "zoo"}`) + return })) defer ts.Close() @@ -212,6 +183,7 @@ 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() @@ -228,309 +200,15 @@ func TestPostStatusWithCancel(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/statuses"); want != err.Error() { + if want := "Post " + ts.URL + "/api/v1/statuses: context canceled"; want != err.Error() { 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("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.PostStatus(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"}, - }, - }) - 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.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) - } - 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) { fmt.Fprintln(w, `[{"content": "foo"}, {"content": "bar"}]`) + return })) defer ts.Close() @@ -570,6 +248,7 @@ 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() @@ -585,14 +264,13 @@ func TestGetTimelineHomeWithCancel(t *testing.T) { if err == nil { t.Fatalf("should be fail: %v", err) } - if want := fmt.Sprintf("Get %q: context canceled", ts.URL+"/api/v1/timelines/home"); want != err.Error() { + if want := "Get " + ts.URL + "/api/v1/timelines/home: context canceled"; want != err.Error() { t.Fatalf("want %q but %q", want, err.Error()) } } func TestForTheCoverages(t *testing.T) { (*UpdateEvent)(nil).event() - (*UpdateEditEvent)(nil).event() (*NotificationEvent)(nil).event() (*DeleteEvent)(nil).event() (*ErrorEvent)(nil).event() @@ -615,20 +293,15 @@ func TestNewPagination(t *testing.T) { t.Fatalf("should be fail: %v", err) } - _, err = newPagination(`; rel="prev"`) - if err != nil { - t.Fatalf("should not be fail: %v", err) - } - pg, err := newPagination(`; rel="next", ; rel="prev"`) if err != nil { t.Fatalf("should not be fail: %v", err) } - if pg.MaxID != "123" { - t.Fatalf("want %q but %q", "123", pg.MaxID) + if pg.MaxID != 123 { + t.Fatalf("want %d but %d", 123, pg.MaxID) } - if pg.SinceID != "789" { - t.Fatalf("want %q but %q", "789", pg.SinceID) + if pg.SinceID != 789 { + t.Fatalf("want %d but %d", 789, pg.SinceID) } } @@ -639,24 +312,23 @@ func TestGetPaginationID(t *testing.T) { } _, err = getPaginationID("http://example.com?max_id=abc", "max_id") - if err != nil { - t.Fatalf("should not be fail: %v", err) + if err == nil { + t.Fatalf("should be fail: %v", err) } id, err := getPaginationID("http://example.com?max_id=123", "max_id") if err != nil { t.Fatalf("should not be fail: %v", err) } - if id != "123" { - t.Fatalf("want %q but %q", "123", id) + if id != 123 { + t.Fatalf("want %d but %d", 123, id) } } func TestPaginationSetValues(t *testing.T) { p := &Pagination{ - MaxID: "123", - SinceID: "456", - MinID: "789", + MaxID: 123, + SinceID: 789, Limit: 10, } before := url.Values{"key": {"value"}} @@ -667,19 +339,16 @@ 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") != "456" { - t.Fatalf("want %q but %q", "456", after.Get("since_id")) - } - if after.Get("min_id") != "789" { - t.Fatalf("want %q but %q", "789", after.Get("min_id")) + if after.Get("since_id") != "" { + t.Fatalf("result should be empty string: %q", after.Get("since_id")) } if after.Get("limit") != "10" { t.Fatalf("want %q but %q", "10", after.Get("limit")) } p = &Pagination{ - MaxID: "", - SinceID: "789", + MaxID: 0, + SinceID: 789, } before = url.Values{} after = p.setValues(before) @@ -689,7 +358,4 @@ 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")) - } } diff --git a/notification.go b/notification.go index fa0cab9..15e6cf7 100644 --- a/notification.go +++ b/notification.go @@ -2,40 +2,21 @@ package mastodon import ( "context" - "crypto/ecdsa" - "crypto/elliptic" - "encoding/base64" "fmt" "net/http" - "net/url" - "strconv" "time" ) -// Notification holds information for a mastodon notification. +// Notification hold information for mastodon notification. type Notification struct { - ID ID `json:"id"` + ID int64 `json:"id"` Type string `json:"type"` CreatedAt time.Time `json:"created_at"` Account Account `json:"account"` 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 returns notifications. +// GetNotifications return 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,87 +26,17 @@ func (c *Client) GetNotifications(ctx context.Context, pg *Pagination) ([]*Notif return notifications, nil } -// GetNotification returns notification. -func (c *Client) GetNotification(ctx context.Context, id ID) (*Notification, error) { +// GetNotification return notification. +func (c *Client) GetNotification(ctx context.Context, id int64) (*Notification, error) { var notification Notification - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/notifications/%v", id), nil, ¬ification, nil) + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/notifications/%d", id), nil, ¬ification, nil) if err != nil { return nil, err } return ¬ification, nil } -// DismissNotification deletes a single notification. -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 clears notifications. +// ClearNotifications clear notifications. 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 78cdbdd..740dec9 100644 --- a/notification_test.go +++ b/notification_test.go @@ -2,9 +2,6 @@ package mastodon import ( "context" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" "fmt" "net/http" "net/http/httptest" @@ -23,11 +20,9 @@ func TestGetNotifications(t *testing.T) { case "/api/v1/notifications/clear": fmt.Fprintln(w, `{}`) return - case "/api/v1/notifications/123/dismiss": - fmt.Fprintln(w, `{}`) - return } http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) + return })) defer ts.Close() @@ -44,100 +39,21 @@ func TestGetNotifications(t *testing.T) { if len(ns) != 2 { t.Fatalf("result should be two: %d", len(ns)) } - if ns[0].ID != "122" { - t.Fatalf("want %v but %v", "122", ns[0].ID) + if ns[0].ID != 122 { + 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) + if ns[1].ID != 123 { + 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) + if n.ID != 123 { + t.Fatalf("want %v but %v", 123, n.ID) } err = client.ClearNotifications(context.Background()) 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) - } -} - -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) - })) - 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) - } } diff --git a/polls.go b/polls.go deleted file mode 100644 index 16be421..0000000 --- a/polls.go +++ /dev/null @@ -1,54 +0,0 @@ -package mastodon - -import ( - "context" - "fmt" - "net/http" - "net/url" - "time" -) - -// Poll holds 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"` - VotersCount int64 `json:"voters_count"` - Options []PollOption `json:"options"` - Voted bool `json:"voted"` - OwnVotes []int `json:"own_votes"` - Emojis []Emoji `json:"emojis"` -} - -// Poll holds information for a mastodon poll option. -type PollOption struct { - Title string `json:"title"` - VotesCount int64 `json:"votes_count"` -} - -// 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) - 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 deleted file mode 100644 index 7ccee38..0000000 --- a/polls_test.go +++ /dev/null @@ -1,145 +0,0 @@ -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) - } -} diff --git a/report.go b/report.go index 662c16a..1e6debf 100644 --- a/report.go +++ b/report.go @@ -2,17 +2,18 @@ package mastodon import ( "context" + "fmt" "net/http" "net/url" ) -// Report holds information for a mastodon report. +// Report hold information for mastodon report. type Report struct { ID int64 `json:"id"` ActionTaken bool `json:"action_taken"` } -// GetReports returns report of the current user. +// GetReports return 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) @@ -23,11 +24,11 @@ func (c *Client) GetReports(ctx context.Context) ([]*Report, error) { } // Report reports the report -func (c *Client) Report(ctx context.Context, accountID ID, ids []ID, comment string) (*Report, error) { +func (c *Client) Report(ctx context.Context, accountID int64, ids []int64, comment string) (*Report, error) { params := url.Values{} - params.Set("account_id", string(accountID)) + params.Set("account_id", fmt.Sprint(accountID)) for _, id := range ids { - params.Add("status_ids[]", string(id)) + params.Add("status_ids[]", fmt.Sprint(id)) } params.Set("comment", comment) var report Report diff --git a/report_test.go b/report_test.go index f25f0b5..2d78bcf 100644 --- a/report_test.go +++ b/report_test.go @@ -15,6 +15,7 @@ func TestGetReports(t *testing.T) { return } fmt.Fprintln(w, `[{"id": 122, "action_taken": false}, {"id": 123, "action_taken": true}]`) + return })) defer ts.Close() @@ -54,6 +55,7 @@ func TestReport(t *testing.T) { } else { fmt.Fprintln(w, `{"id": 1234, "action_taken": true}`) } + return })) defer ts.Close() @@ -63,26 +65,26 @@ func TestReport(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - _, 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 %q but %q", "1234", rp.ID) + t.Fatalf("want %v but %v", 1234, rp.ID) } if rp.ActionTaken { t.Fatalf("want %v but %v", true, rp.ActionTaken) } - rp, err = client.Report(context.Background(), "123", []ID{"567"}, "") + rp, err = client.Report(context.Background(), 123, []int64{567}, "") if err != nil { t.Fatalf("should not be fail: %v", err) } if rp.ID != 1234 { - t.Fatalf("want %q but %q", "1234", rp.ID) + t.Fatalf("want %v but %v", 1234, rp.ID) } if !rp.ActionTaken { t.Fatalf("want %v but %v", false, rp.ActionTaken) diff --git a/status.go b/status.go index f12e4eb..38ea606 100644 --- a/status.go +++ b/status.go @@ -1,165 +1,53 @@ package mastodon import ( - "bytes" "context" "fmt" - "io" - "mime/multipart" "net/http" "net/url" - "os" - "strings" + "strconv" "time" ) // Status is struct to hold status. type Status struct { ID ID `json:"id"` - URI string `json:"uri"` - URL string `json:"url"` - Account Account `json:"account"` + CreatedAt time.Time `json:"created_at"` 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"` - EditedAt time.Time `json:"edited_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"` - Bookmarked interface{} `json:"bookmarked"` - 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"` 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"` + 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"` } -// 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. +// Context hold information for mastodon context. type Context struct { Ancestors []*Status `json:"ancestors"` Descendants []*Status `json:"descendants"` } -// Card holds information for a mastodon card. +// 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"` - 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"` + URL string `json:"url"` + Title string `json:"title"` + Description string `json:"description"` + Image string `json:"image"` } -// 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"` - Accounts []*Account `json:"accounts"` - Unread bool `json:"unread"` - 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 returns the favorite list of the current user. +// GetFavourites return 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) @@ -169,70 +57,40 @@ func (c *Client) GetFavourites(ctx context.Context, pg *Pagination) ([]*Status, return statuses, nil } -// 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) - if err != nil { - return nil, err - } - return statuses, nil -} - -// GetStatus returns status specified by id. -func (c *Client) GetStatus(ctx context.Context, id ID) (*Status, error) { +// GetStatus return status specified by id. +func (c *Client) GetStatus(ctx context.Context, id int64) (*Status, error) { var status Status - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s", id), nil, &status, nil) + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%d", id), nil, &status, nil) if err != nil { return nil, err } return &status, nil } -// GetStatusContext returns status specified by id. -func (c *Client) GetStatusContext(ctx context.Context, id ID) (*Context, error) { +// GetStatusContext return status specified by id. +func (c *Client) GetStatusContext(ctx context.Context, id int64) (*Context, error) { var context Context - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/context", id), nil, &context, nil) + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%d/context", id), nil, &context, nil) if err != nil { return nil, err } return &context, nil } -// GetStatusCard returns status specified by id. -func (c *Client) GetStatusCard(ctx context.Context, id ID) (*Card, error) { +// GetStatusCard return status specified by id. +func (c *Client) GetStatusCard(ctx context.Context, id int64) (*Card, error) { var card Card - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/card", id), nil, &card, nil) + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%d/card", id), nil, &card, nil) if err != nil { return nil, err } 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) - 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) { +func (c *Client) GetRebloggedBy(ctx context.Context, id int64, pg *Pagination) ([]*Account, error) { var accounts []*Account - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/reblogged_by", id), nil, &accounts, pg) + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%d/reblogged_by", id), nil, &accounts, pg) if err != nil { return nil, err } @@ -240,69 +98,49 @@ func (c *Client) GetRebloggedBy(ctx context.Context, id ID, pg *Pagination) ([]* } // GetFavouritedBy returns the account list of the user who liked the toot of id. -func (c *Client) GetFavouritedBy(ctx context.Context, id ID, pg *Pagination) ([]*Account, error) { +func (c *Client) GetFavouritedBy(ctx context.Context, id int64, pg *Pagination) ([]*Account, error) { var accounts []*Account - err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/favourited_by", id), nil, &accounts, pg) + err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%d/favourited_by", id), nil, &accounts, pg) if err != nil { return nil, err } return accounts, nil } -// Reblog reblogs the toot of id and returns status of reblog. -func (c *Client) Reblog(ctx context.Context, id ID) (*Status, error) { +// Reblog is reblog the toot of id and return status of reblog. +func (c *Client) Reblog(ctx context.Context, id int64) (*Status, error) { var status Status - err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/reblog", id), nil, &status, nil) + err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%d/reblog", id), nil, &status, nil) if err != nil { return nil, err } return &status, nil } -// Unreblog unreblogs the toot of id and returns status of the original toot. -func (c *Client) Unreblog(ctx context.Context, id ID) (*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) { var status Status - err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unreblog", id), nil, &status, nil) + err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%d/unreblog", id), nil, &status, nil) if err != nil { return nil, err } return &status, nil } -// Favourite favourites the toot of id and returns status of the favourite toot. -func (c *Client) Favourite(ctx context.Context, id ID) (*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) { var status Status - err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/favourite", id), nil, &status, nil) + err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%d/favourite", id), nil, &status, nil) if err != nil { return nil, err } return &status, nil } -// Unfavourite unfavourites the toot of id and returns status of the unfavourite toot. -func (c *Client) Unfavourite(ctx context.Context, id ID) (*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) { var status Status - err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unfavourite", id), nil, &status, nil) - if err != nil { - return nil, err - } - return &status, nil -} - -// 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) - 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) + err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%d/unfavourite", id), nil, &status, nil) if err != nil { return nil, err } @@ -349,16 +187,6 @@ 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) { @@ -378,58 +206,28 @@ 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 != "" { - params.Set("in_reply_to_id", string(toot.InReplyToID)) + if toot.InReplyToID > 0 { + params.Set("in_reply_to_id", fmt.Sprint(toot.InReplyToID)) } if toot.MediaIDs != nil { for _, media := range toot.MediaIDs { - 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") + params.Add("media_ids[]", strconv.FormatInt(media, 10)) } } 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") + params.Set("senstitive", "true") } if toot.SpoilerText != "" { params.Set("spoiler_text", toot.SpoilerText) } var status Status - 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) - } + err := c.doAPI(ctx, http.MethodPost, "/api/v1/statuses", params, &status, nil) if err != nil { return nil, err } @@ -437,8 +235,8 @@ func (c *Client) postStatus(ctx context.Context, toot *Toot, update bool, update } // DeleteStatus delete the toot. -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) +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) } // Search search content with query. @@ -447,81 +245,19 @@ 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/v2/search", params, &results, nil) + err := c.doAPI(ctx, http.MethodGet, "/api/v1/search", params, &results, nil) if err != nil { return nil, err } return &results, nil } -// UploadMedia upload a media attachment from a file. +// UploadMedia upload a media attachment. func (c *Client) UploadMedia(ctx context.Context, file string) (*Attachment, error) { - f, err := os.Open(file) - if err != nil { - return nil, err - } - defer f.Close() - - return c.UploadMediaFromMedia(ctx, &Media{File: f}) -} - -// 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}) -} - -// UploadMediaFromMedia uploads a media attachment from a Media struct. -func (c *Client) UploadMediaFromMedia(ctx context.Context, media *Media) (*Attachment, error) { var attachment Attachment - if err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", media, &attachment, nil); err != nil { + err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", file, &attachment, nil) + if err != nil { return nil, err } return &attachment, nil } - -// GetTimelineDirect return statuses from direct timeline. -func (c *Client) GetTimelineDirect(ctx context.Context, pg *Pagination) ([]*Status, 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 - } - - var statuses = []*Status{} - - for _, c := range conversations { - s := c.LastStatus - statuses = append(statuses, s) - } - - 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 2cac4b8..1359bb9 100644 --- a/status_test.go +++ b/status_test.go @@ -3,16 +3,15 @@ package mastodon import ( "context" "fmt" - "io/ioutil" "net/http" "net/http/httptest" - "os" "testing" ) 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() @@ -37,40 +36,14 @@ 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"}]`) - })) - 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" { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } - fmt.Fprintln(w, `{"content": "zzz", "emojis":[{"shortcode":"💩", "url":"http://example.com", "static_url": "http://example.com/static"}]}`) + fmt.Fprintln(w, `{"content": "zzz"}`) + return })) defer ts.Close() @@ -80,29 +53,17 @@ 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) } 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].ShortCode) - } - if status.Emojis[0].URL != "http://example.com" { - 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", status.Emojis[0].StaticURL) - } } func TestGetStatusCard(t *testing.T) { @@ -112,6 +73,7 @@ func TestGetStatusCard(t *testing.T) { return } fmt.Fprintln(w, `{"title": "zzz"}`) + return })) defer ts.Close() @@ -121,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) } @@ -141,6 +103,7 @@ func TestGetStatusContext(t *testing.T) { return } fmt.Fprintln(w, `{"ancestors": [{"content": "zzz"},{"content": "bbb"}]}`) + return })) defer ts.Close() @@ -150,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) } @@ -172,88 +135,6 @@ 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" { @@ -261,6 +142,7 @@ func TestGetRebloggedBy(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) + return })) defer ts.Close() @@ -270,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) } @@ -296,6 +178,7 @@ func TestGetFavouritedBy(t *testing.T) { return } fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`) + return })) defer ts.Close() @@ -305,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) } @@ -331,6 +214,7 @@ func TestReblog(t *testing.T) { return } fmt.Fprintln(w, `{"content": "zzz"}`) + return })) defer ts.Close() @@ -340,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) } @@ -360,6 +244,7 @@ func TestUnreblog(t *testing.T) { return } fmt.Fprintln(w, `{"content": "zzz"}`) + return })) defer ts.Close() @@ -369,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) } @@ -389,6 +274,7 @@ func TestFavourite(t *testing.T) { return } fmt.Fprintln(w, `{"content": "zzz"}`) + return })) defer ts.Close() @@ -398,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) } @@ -418,6 +304,7 @@ func TestUnfavourite(t *testing.T) { return } fmt.Fprintln(w, `{"content": "zzz"}`) + return })) defer ts.Close() @@ -427,69 +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") - 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 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"}`) - })) - 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"}`) - })) - 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") + status, err := client.Unfavourite(context.Background(), 1234567) if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -528,28 +357,6 @@ 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, `[{"id": "4", "unread":false, "last_status" : {"content": "zzz"}}, {"id": "3", "unread":true, "last_status" : {"content": "bar"}}]`) - })) - 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 != "zzz" { - t.Fatalf("want %q but %q", "foo", tl[0].Content) - } - if tl[1].Content != "bar" { - 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" { @@ -557,6 +364,7 @@ func TestGetTimelineHashtag(t *testing.T) { return } fmt.Fprintln(w, `[{"content": "zzz"},{"content": "yyy"}]`) + return })) defer ts.Close() @@ -585,41 +393,6 @@ 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"}]`) - })) - 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") == "" { @@ -627,6 +400,7 @@ func TestGetTimelineMedia(t *testing.T) { return } fmt.Fprintln(w, `[{"content": "zzz"},{"content": "yyy"}]`) + return })) defer ts.Close() @@ -665,6 +439,7 @@ func TestDeleteStatus(t *testing.T) { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusMethodNotAllowed) return } + return })) defer ts.Close() @@ -674,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) } @@ -686,11 +461,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/v2/search" { + if r.URL.Path != "/api/v1/search" { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } - if r.RequestURI != "/api/v2/search?q=q&resolve=false" { + if r.RequestURI != "/api/v1/search?q=q&resolve=false" { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusBadRequest) return } @@ -698,8 +473,9 @@ func TestSearch(t *testing.T) { fmt.Fprintln(w, ` {"accounts":[{"username": "zzz"},{"username": "yyy"}], "statuses":[{"content": "aaa"}], - "hashtags":[{"name": "tag"},{"name": "tag2"},{"name": "tag3"}] + "hashtags":["tag","tag2","tag3"] }`) + return })) defer ts.Close() @@ -728,7 +504,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].Name != "tag3" { + if ret.Hashtags[2] != "tag3" { t.Fatalf("Hashtags[2] should %q , but %q", "tag3", ret.Hashtags[2]) } } @@ -744,6 +520,7 @@ func TestUploadMedia(t *testing.T) { return } fmt.Fprintln(w, `{"id": 123}`) + return })) defer ts.Close() @@ -757,114 +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) - } - 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 { - t.Fatalf("should not be fail: %v", err) - } - if writerAttachment.ID != "123" { - t.Fatalf("want %q but %q", "123", attachment.ID) - } - bytes, err := ioutil.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) { - 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"}}]`) - })) - 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 - } - })) - 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 - } - })) - 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) + if attachment.ID != 123 { + t.Fatalf("want %q but %q", 123, attachment.ID) } } diff --git a/streaming.go b/streaming.go index b109f6c..0d3e559 100644 --- a/streaming.go +++ b/streaming.go @@ -2,77 +2,52 @@ package mastodon import ( "bufio" - "bytes" "context" "encoding/json" - "errors" "io" "net/http" "net/url" "path" + "strconv" "strings" ) -// UpdateEvent is a struct for passing status event to app. +// UpdateEvent is struct for passing status event to app. type UpdateEvent struct { Status *Status `json:"status"` } 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. +// NotificationEvent is struct for passing notification event to app. type NotificationEvent struct { Notification *Notification `json:"notification"` } func (e *NotificationEvent) event() {} -// DeleteEvent is a struct for passing deletion event to app. -type DeleteEvent struct{ ID ID } +// DeleteEvent is struct for passing deletion event to app. +type DeleteEvent struct{ ID int64 } func (e *DeleteEvent) event() {} -// ErrorEvent is a struct for passing errors to app. +// ErrorEvent is 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 an interface passing events to app. +// Event is interface passing events to app. type Event interface { event() } func handleReader(q chan Event, r io.Reader) error { var name string - 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) + s := bufio.NewScanner(r) + for s.Scan() { + line := s.Text() + token := strings.SplitN(line, ":", 2) if len(token) != 2 { continue } @@ -88,12 +63,6 @@ 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) @@ -101,17 +70,22 @@ func handleReader(q chan Event, r io.Reader) error { q <- &NotificationEvent{¬ification} } case "delete": - q <- &DeleteEvent{ID: ID(strings.TrimSpace(token[1]))} + var id int64 + id, err = strconv.ParseInt(strings.TrimSpace(token[1]), 10, 64) + if err == nil { + q <- &DeleteEvent{id} + } } if err != nil { q <- &ErrorEvent{err} } } } + return s.Err() } 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 } @@ -123,10 +97,7 @@ func (c *Client) streaming(ctx context.Context, p string, params url.Values) (ch return nil, err } req = req.WithContext(ctx) - - if c.Config.AccessToken != "" { - req.Header.Set("Authorization", "Bearer "+c.Config.AccessToken) - } + req.Header.Set("Authorization", "Bearer "+c.config.AccessToken) q := make(chan Event) go func() { @@ -163,12 +134,12 @@ func (c *Client) doStreaming(req *http.Request, q chan Event) { } } -// StreamingUser returns a channel to read events on home. +// StreamingUser return channel to read events on home. func (c *Client) StreamingUser(ctx context.Context) (chan Event, error) { return c.streaming(ctx, "user", nil) } -// StreamingPublic returns a channel to read events on public. +// StreamingPublic return channel to read events on public. func (c *Client) StreamingPublic(ctx context.Context, isLocal bool) (chan Event, error) { p := "public" if isLocal { @@ -178,7 +149,7 @@ func (c *Client) StreamingPublic(ctx context.Context, isLocal bool) (chan Event, return c.streaming(ctx, p, nil) } -// StreamingHashtag returns a channel to read events on tagged timeline. +// StreamingHashtag return 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) @@ -190,16 +161,3 @@ func (c *Client) StreamingHashtag(ctx context.Context, tag string, isLocal bool) return c.streaming(ctx, p, params) } - -// 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)) - - return c.streaming(ctx, "list", params) -} - -// 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) -} diff --git a/streaming_test.go b/streaming_test.go index a9e90d9..9f5b715 100644 --- a/streaming_test.go +++ b/streaming_test.go @@ -1,65 +1,42 @@ package mastodon import ( - "bufio" "context" "fmt" "net/http" "net/http/httptest" "strings" - "sync" "testing" "time" ) func TestHandleReader(t *testing.T) { - large := "large" - largeContent := strings.Repeat(large, 2*(bufio.MaxScanTokenSize/len(large))) - q := make(chan Event) - r := strings.NewReader(fmt.Sprintf(` + r := strings.NewReader(` event: update data: {content: error} event: update data: {"content": "foo"} -event: update -data: {"content": "%s"} event: notification data: {"type": "mention"} event: delete data: 1234567 -event: status.update -data: {"content": "foo"} :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.Errorf("should not be fail: %v", err) + t.Fatalf("should not be fail: %v", err) } }() - var passUpdate, passUpdateLarge, passNotification, passDelete, passError bool + var passUpdate, passNotification, passDelete, passError bool for e := range q { switch event := e.(type) { case *UpdateEvent: - 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 *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) + passUpdate = true + if event.Status.Content != "foo" { + t.Fatalf("want %q but %q", "foo", event.Status.Content) } case *NotificationEvent: passNotification = true @@ -68,8 +45,8 @@ data: {"content": "foo"} } case *DeleteEvent: passDelete = true - if event.ID != "1234567" { - t.Fatalf("want %q but %q", "1234567", event.ID) + if event.ID != 1234567 { + t.Fatalf("want %d but %d", 1234567, event.ID) } case *ErrorEvent: passError = true @@ -78,12 +55,11 @@ data: {"content": "foo"} } } } - if !passUpdate || !passUpdateLarge || !passNotification || !passDelete || !passError { + if !passUpdate || !passNotification || !passDelete || !passError { t.Fatalf("have not passed through somewhere: "+ - "update: %t, update (large): %t, notification: %t, delete: %t, error: %t", - passUpdate, passUpdateLarge, passNotification, passDelete, passError) + "update %t, notification %t, delete %t, error %t", + passUpdate, passNotification, passDelete, passError) } - wg.Wait() } func TestStreaming(t *testing.T) { @@ -135,12 +111,6 @@ 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 { @@ -169,14 +139,11 @@ 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.Errorf("should not be fail: %v", err) + t.Fatalf("should not be fail: %v", err) } }() var passError bool @@ -191,7 +158,6 @@ func TestDoStreaming(t *testing.T) { if !passError { t.Fatalf("have not passed through: error %t", passError) } - wg.Wait() } func TestStreamingUser(t *testing.T) { @@ -327,83 +293,3 @@ 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) - } -} - -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) - } -} diff --git a/streaming_ws.go b/streaming_ws.go index 5658bbf..5553a25 100644 --- a/streaming_ws.go +++ b/streaming_ws.go @@ -3,10 +3,8 @@ package mastodon import ( "context" "encoding/json" - "fmt" "net/url" "path" - "strings" "github.com/gorilla/websocket" ) @@ -51,20 +49,15 @@ 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) + 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 } @@ -127,12 +120,6 @@ 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) @@ -140,11 +127,7 @@ func (c *WSClient) handleWS(ctx context.Context, rawurl string, q chan Event) er q <- &NotificationEvent{Notification: ¬ification} } case "delete": - if f, ok := s.Payload.(float64); ok { - q <- &DeleteEvent{ID: ID(fmt.Sprint(int64(f)))} - } else { - q <- &DeleteEvent{ID: ID(strings.TrimSpace(s.Payload.(string)))} - } + q <- &DeleteEvent{ID: int64(s.Payload.(float64))} } if err != nil { q <- &ErrorEvent{err} diff --git a/streaming_ws_test.go b/streaming_ws_test.go index 7e78890..f52477c 100644 --- a/streaming_ws_test.go +++ b/streaming_ws_test.go @@ -4,7 +4,6 @@ import ( "context" "net/http" "net/http/httptest" - "sync" "testing" "time" @@ -80,13 +79,6 @@ 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 { @@ -119,20 +111,20 @@ func wsTest(t *testing.T, q chan Event, cancel func()) { for e := range q { events = append(events, e) } - if len(events) != 7 { - t.Fatalf("result should be seven: %d", len(events)) + if len(events) != 6 { + t.Fatalf("result should be four: %d", len(events)) } if events[0].(*UpdateEvent).Status.Content != "foo" { t.Fatalf("want %q but %q", "foo", events[0].(*UpdateEvent).Status.Content) } - if events[1].(*UpdateEditEvent).Status.Content != "bar" { - t.Fatalf("want %q but %q", "bar", events[1].(*UpdateEditEvent).Status.Content) + if events[1].(*NotificationEvent).Notification.ID != 123 { + t.Fatalf("want %d but %d", 123, events[1].(*NotificationEvent).Notification.ID) } - if events[2].(*NotificationEvent).Notification.ID != "123" { - t.Fatalf("want %q but %q", "123", events[2].(*NotificationEvent).Notification.ID) + if events[2].(*DeleteEvent).ID != 1234567 { + t.Fatalf("want %d but %d", 1234567, events[2].(*DeleteEvent).ID) } - if events[3].(*DeleteEvent).ID != "1234567" { - t.Fatalf("want %q but %q", "1234567", events[3].(*DeleteEvent).ID) + if errorEvent, ok := events[3].(*ErrorEvent); !ok { + t.Fatalf("should be fail: %v", errorEvent.err) } if errorEvent, ok := events[4].(*ErrorEvent); !ok { t.Fatalf("should be fail: %v", errorEvent.err) @@ -140,9 +132,6 @@ 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) { @@ -162,16 +151,12 @@ 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.Errorf("should be fail: %v", errorEvent.err) + t.Fatalf("should be fail: %v", errorEvent.err) } }() - wg.Wait() } func TestHandleWS(t *testing.T) { @@ -198,13 +183,10 @@ 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.Errorf("should be fail: %v", errorEvent.err) + t.Fatalf("should be fail: %v", errorEvent.err) } }() err := client.handleWS(context.Background(), ":", q) @@ -214,12 +196,10 @@ 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.Errorf("should be fail: %v", errorEvent.err) + t.Fatalf("should be fail: %v", errorEvent.err) } }() err = client.handleWS(ctx, "ws://"+ts.Listener.Addr().String(), q) @@ -227,17 +207,13 @@ 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.Errorf("should be fail: %v", errorEvent.err) + t.Fatalf("should be fail: %v", errorEvent.err) } }() client.handleWS(context.Background(), "ws://"+ts.Listener.Addr().String(), q) - - wg.Wait() } func TestDialRedirect(t *testing.T) { @@ -267,12 +243,12 @@ func TestDial(t *testing.T) { t.Fatalf("should be fail: %v", err) } - _, _, err = client.dial("ws://" + ts.Listener.Addr().String()) + _, rawurl, 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) } diff --git a/unixtime.go b/unixtime.go deleted file mode 100644 index a935a9e..0000000 --- a/unixtime.go +++ /dev/null @@ -1,20 +0,0 @@ -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 -}