Compare commits

..

No commits in common. "master" and "fix-66" have entirely different histories.

67 changed files with 332 additions and 3552 deletions

12
.github/FUNDING.yml vendored
View File

@ -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']

View File

@ -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

8
.travis.yml 100644
View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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()

42
apps.go
View File

@ -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
}

View File

@ -26,7 +26,8 @@ func TestRegisterApp(t *testing.T) {
fmt.Fprintln(w, `<html><head><title>Apps</title></head></html>`)
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)
}
}

View File

@ -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 {

View File

@ -6,7 +6,7 @@ import (
"strings"
"testing"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
func TestCmdAccount(t *testing.T) {

View File

@ -5,7 +5,7 @@ import (
"errors"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
func cmdDelete(c *cli.Context) error {

View File

@ -5,7 +5,7 @@ import (
"net/http"
"testing"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
func TestCmdDelete(t *testing.T) {

View File

@ -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 {

View File

@ -5,7 +5,7 @@ import (
"net/http"
"testing"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
func TestCmdFollow(t *testing.T) {

View File

@ -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 {
@ -28,8 +28,6 @@ func cmdFollowers(c *cli.Context) error {
if pg.MaxID == "" {
break
}
pg.SinceID = ""
pg.MinID = ""
time.Sleep(10 * time.Second)
}
s := newScreen(config)

View File

@ -6,7 +6,7 @@ import (
"strings"
"testing"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
func TestCmdFollowers(t *testing.T) {

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -6,7 +6,7 @@ import (
"strings"
"testing"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
func TestCmdInstance(t *testing.T) {

View File

@ -1,7 +1,7 @@
package main
import (
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
func cmdMikami(c *cli.Context) error {

View File

@ -7,7 +7,7 @@ import (
"strings"
"testing"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
func TestCmdMikami(t *testing.T) {

View File

@ -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 {

View File

@ -6,7 +6,7 @@ import (
"strings"
"testing"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
func TestCmdNotification(t *testing.T) {

View File

@ -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 {

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -6,7 +6,7 @@ import (
"fmt"
"github.com/mattn/go-mastodon"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
func cmdToot(c *cli.Context) error {

View File

@ -5,7 +5,7 @@ import (
"net/http"
"testing"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
func TestCmdToot(t *testing.T) {

View File

@ -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 {

View File

@ -6,7 +6,7 @@ import (
"strings"
"testing"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
func TestCmdUpload(t *testing.T) {

View File

@ -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 {

View File

@ -8,7 +8,7 @@ import (
"strings"
"testing"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
func TestCmdXSearch(t *testing.T) {

View File

@ -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
)

View File

@ -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=

View File

@ -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,23 +183,23 @@ func makeApp() *cli.App {
app.Usage = "mastodon client"
app.Version = "0.0.1"
app.Flags = []cli.Flag{
&cli.StringFlag{
cli.StringFlag{
Name: "profile",
Usage: "profile name",
Value: "",
},
}
app.Commands = []*cli.Command{
app.Commands = []cli.Command{
{
Name: "toot",
Usage: "post toot",
Flags: []cli.Flag{
&cli.StringFlag{
cli.StringFlag{
Name: "ff",
Usage: "post utf-8 string from a file(\"-\" means STDIN)",
Value: "",
},
&cli.StringFlag{
cli.StringFlag{
Name: "i",
Usage: "in-reply-to",
Value: "",
@ -211,19 +211,19 @@ func makeApp() *cli.App {
Name: "stream",
Usage: "stream statuses",
Flags: []cli.Flag{
&cli.StringFlag{
cli.StringFlag{
Name: "type",
Usage: "stream type (public,public/local,user:NAME,hashtag:TAG)",
},
&cli.BoolFlag{
cli.BoolFlag{
Name: "json",
Usage: "output JSON",
},
&cli.BoolFlag{
cli.BoolFlag{
Name: "simplejson",
Usage: "output simple JSON",
},
&cli.StringFlag{
cli.StringFlag{
Name: "template",
Usage: "output with tamplate format",
},
@ -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,

View File

@ -7,7 +7,7 @@ import (
"os"
"testing"
"github.com/urfave/cli/v2"
"github.com/urfave/cli"
)
func TestReadFileFile(t *testing.T) {

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}
}

8
go.mod
View File

@ -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
)

4
go.sum
View File

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

View File

@ -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

View File

@ -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)
}

View File

@ -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
}

View File

@ -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])
}
}

107
lists.go
View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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 ID `json:"in_reply_to_id"`
MediaIDs []ID `json:"media_ids"`
Sensitive bool `json:"sensitive"`
SpoilerText string `json:"spoiler_text"`
Visibility string `json:"visibility"`
}
// Mention hold information for mention.
@ -253,64 +191,38 @@ type Mention struct {
// 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"`
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"`
}
// 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"`
ShortCode string `json:"shortcode"`
URL string `json:"url"`
StaticURL string `json:"static_url"`
}
// Results hold information for search result.
type Results struct {
Accounts []*Account `json:"accounts"`
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
Limit int64
}
@ -334,12 +246,6 @@ 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
}
}
@ -352,7 +258,12 @@ func getPaginationID(rawurl, key string) (ID, error) {
return "", err
}
return ID(u.Query().Get(key)), nil
id, err := strconv.ParseInt(u.Query().Get(key), 10, 64)
if err != nil {
return "", err
}
return ID(fmt.Sprint(id)), nil
}
func (p *Pagination) toValues() url.Values {
@ -362,13 +273,9 @@ 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 != "" {
} else if p.SinceID != "" {
params.Set("since_id", string(p.SinceID))
}
if p.MinID != "" {
params.Set("min_id", string(p.MinID))
}
if p.Limit > 0 {
params.Set("limit", fmt.Sprint(p.Limit))
}

View File

@ -2,7 +2,6 @@ package mastodon
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
@ -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("<p>%s</p>", 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("<p>%s</p>", 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 != "<p>foobar</p>" {
t.Fatalf("want %q but %q", "<p>foobar</p>", 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 != "<p>bar</p>" {
t.Fatalf("want %q but %q", "<p>bar</p>", 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("<p>%s</p>", 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("<p>%s</p>", 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 != "<p>foobar</p>" {
t.Fatalf("want %q but %q", "<p>foobar</p>", 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 != "<p>bar</p>" {
t.Fatalf("want %q but %q", "<p>bar</p>", 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,11 +293,6 @@ func TestNewPagination(t *testing.T) {
t.Fatalf("should be fail: %v", err)
}
_, err = newPagination(`<http://example.com?min_id=abc>; rel="prev"`)
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
pg, err := newPagination(`<http://example.com?max_id=123>; rel="next", <http://example.com?since_id=789>; rel="prev"`)
if err != nil {
t.Fatalf("should not be fail: %v", err)
@ -639,8 +312,8 @@ 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")
@ -655,8 +328,7 @@ func TestGetPaginationID(t *testing.T) {
func TestPaginationSetValues(t *testing.T) {
p := &Pagination{
MaxID: "123",
SinceID: "456",
MinID: "789",
SinceID: "789",
Limit: 10,
}
before := url.Values{"key": {"value"}}
@ -667,11 +339,8 @@ 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"))
@ -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"))
}
}

View File

@ -2,17 +2,12 @@ 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"`
Type string `json:"type"`
@ -21,21 +16,7 @@ type Notification struct {
Status *Status `json:"status"`
}
type PushSubscription struct {
ID ID `json:"id"`
Endpoint string `json:"endpoint"`
ServerKey string `json:"server_key"`
Alerts *PushAlerts `json:"alerts"`
}
type PushAlerts struct {
Follow *Sbool `json:"follow"`
Favourite *Sbool `json:"favourite"`
Reblog *Sbool `json:"reblog"`
Mention *Sbool `json:"mention"`
}
// GetNotifications 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, &notifications, pg)
@ -45,7 +26,7 @@ func (c *Client) GetNotifications(ctx context.Context, pg *Pagination) ([]*Notif
return notifications, nil
}
// GetNotification returns notification.
// GetNotification return notification.
func (c *Client) GetNotification(ctx context.Context, id ID) (*Notification, error) {
var notification Notification
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/notifications/%v", id), nil, &notification, nil)
@ -55,77 +36,7 @@ func (c *Client) GetNotification(ctx context.Context, id ID) (*Notification, err
return &notification, 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
}

View File

@ -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()
@ -61,83 +56,4 @@ func TestGetNotifications(t *testing.T) {
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
err = client.DismissNotification(context.Background(), "123")
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
}
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)
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -6,13 +6,13 @@ import (
"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)

View File

@ -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,11 +65,11 @@ 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)
}

328
status.go
View File

@ -1,165 +1,53 @@
package mastodon
import (
"bytes"
"context"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"strings"
"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"`
Emojis []Emoji `json:"emojis"`
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,17 +57,7 @@ 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.
// GetStatus return status specified by id.
func (c *Client) GetStatus(ctx context.Context, id ID) (*Status, error) {
var status Status
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s", id), nil, &status, nil)
@ -189,7 +67,7 @@ func (c *Client) GetStatus(ctx context.Context, id ID) (*Status, error) {
return &status, nil
}
// GetStatusContext returns status specified by id.
// GetStatusContext return status specified by id.
func (c *Client) GetStatusContext(ctx context.Context, id ID) (*Context, error) {
var context Context
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/context", id), nil, &context, nil)
@ -199,7 +77,7 @@ func (c *Client) GetStatusContext(ctx context.Context, id ID) (*Context, error)
return &context, nil
}
// GetStatusCard returns status specified by id.
// GetStatusCard return status specified by id.
func (c *Client) GetStatusCard(ctx context.Context, id ID) (*Card, error) {
var card Card
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/card", id), nil, &card, nil)
@ -209,26 +87,6 @@ func (c *Client) GetStatusCard(ctx context.Context, id ID) (*Card, error) {
return &card, nil
}
// GetStatusSource returns source data specified by id.
func (c *Client) GetStatusSource(ctx context.Context, id ID) (*Source, error) {
var source Source
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/source", id), nil, &source, nil)
if err != nil {
return nil, err
}
return &source, nil
}
// GetStatusHistory returns the status history specified by id.
func (c *Client) GetStatusHistory(ctx context.Context, id ID) ([]*StatusHistory, error) {
var statuses []*StatusHistory
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/history", id), nil, &statuses, nil)
if err != nil {
return nil, err
}
return statuses, nil
}
// GetRebloggedBy returns the account list of the user who reblogged the toot of id.
func (c *Client) GetRebloggedBy(ctx context.Context, id ID, pg *Pagination) ([]*Account, error) {
var accounts []*Account
@ -249,7 +107,7 @@ func (c *Client) GetFavouritedBy(ctx context.Context, id ID, pg *Pagination) ([]
return accounts, nil
}
// Reblog reblogs the toot of id and returns status of reblog.
// Reblog is reblog the toot of id and return status of reblog.
func (c *Client) Reblog(ctx context.Context, id ID) (*Status, error) {
var status Status
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/reblog", id), nil, &status, nil)
@ -259,7 +117,7 @@ func (c *Client) Reblog(ctx context.Context, id ID) (*Status, error) {
return &status, nil
}
// Unreblog unreblogs the toot of id and returns status of the original toot.
// Unreblog is unreblog the toot of id and return status of the original toot.
func (c *Client) Unreblog(ctx context.Context, id ID) (*Status, error) {
var status Status
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unreblog", id), nil, &status, nil)
@ -269,7 +127,7 @@ func (c *Client) Unreblog(ctx context.Context, id ID) (*Status, error) {
return &status, nil
}
// Favourite favourites the toot of id and returns status of the favourite toot.
// Favourite is favourite the toot of id and return status of the favourite toot.
func (c *Client) Favourite(ctx context.Context, id ID) (*Status, error) {
var status Status
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/favourite", id), nil, &status, nil)
@ -279,7 +137,7 @@ func (c *Client) Favourite(ctx context.Context, id ID) (*Status, error) {
return &status, nil
}
// Unfavourite unfavourites the toot of id and returns status of the unfavourite toot.
// Unfavourite is unfavourite the toot of id and return status of the unfavourite toot.
func (c *Client) Unfavourite(ctx context.Context, id ID) (*Status, error) {
var status Status
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unfavourite", id), nil, &status, nil)
@ -289,26 +147,6 @@ func (c *Client) Unfavourite(ctx context.Context, id ID) (*Status, error) {
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)
if err != nil {
return nil, err
}
return &status, nil
}
// GetTimelineHome return statuses from home timeline.
func (c *Client) GetTimelineHome(ctx context.Context, pg *Pagination) ([]*Status, error) {
var statuses []*Status
@ -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,15 +206,6 @@ 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 != "" {
@ -397,39 +216,18 @@ func (c *Client) postStatus(ctx context.Context, toot *Toot, update bool, update
params.Add("media_ids[]", string(media))
}
}
// Can't use Media and Poll at the same time.
if toot.Poll != nil && toot.Poll.Options != nil && toot.MediaIDs == nil {
for _, opt := range toot.Poll.Options {
params.Add("poll[options][]", string(opt))
}
params.Add("poll[expires_in]", fmt.Sprintf("%d", toot.Poll.ExpiresInSeconds))
if toot.Poll.Multiple {
params.Add("poll[multiple]", "true")
}
if toot.Poll.HideTotals {
params.Add("poll[hide_totals]", "true")
}
}
if toot.Visibility != "" {
params.Set("visibility", fmt.Sprint(toot.Visibility))
}
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
}
@ -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)
}

View File

@ -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,33 +36,6 @@ func TestGetFavourites(t *testing.T) {
}
}
func TestGetBookmarks(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, `[{"content": "foo"}, {"content": "bar"}]`)
}))
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" {
@ -71,6 +43,7 @@ func TestGetStatus(t *testing.T) {
return
}
fmt.Fprintln(w, `{"content": "zzz", "emojis":[{"shortcode":"💩", "url":"http://example.com", "static_url": "http://example.com/static"}]}`)
return
}))
defer ts.Close()
@ -95,7 +68,7 @@ func TestGetStatus(t *testing.T) {
t.Fatal("should have emojis")
}
if status.Emojis[0].ShortCode != "💩" {
t.Fatalf("want %q but %q", "💩", status.Emojis[0].ShortCode)
t.Fatalf("want %q but %q", "💩", status.Emojis[0])
}
if status.Emojis[0].URL != "http://example.com" {
t.Fatalf("want %q but %q", "https://example.com", status.Emojis[0].URL)
@ -112,6 +85,7 @@ func TestGetStatusCard(t *testing.T) {
return
}
fmt.Fprintln(w, `{"title": "zzz"}`)
return
}))
defer ts.Close()
@ -141,6 +115,7 @@ func TestGetStatusContext(t *testing.T) {
return
}
fmt.Fprintln(w, `{"ancestors": [{"content": "zzz"},{"content": "bbb"}]}`)
return
}))
defer ts.Close()
@ -172,88 +147,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 +154,7 @@ func TestGetRebloggedBy(t *testing.T) {
return
}
fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`)
return
}))
defer ts.Close()
@ -296,6 +190,7 @@ func TestGetFavouritedBy(t *testing.T) {
return
}
fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`)
return
}))
defer ts.Close()
@ -331,6 +226,7 @@ func TestReblog(t *testing.T) {
return
}
fmt.Fprintln(w, `{"content": "zzz"}`)
return
}))
defer ts.Close()
@ -360,6 +256,7 @@ func TestUnreblog(t *testing.T) {
return
}
fmt.Fprintln(w, `{"content": "zzz"}`)
return
}))
defer ts.Close()
@ -389,6 +286,7 @@ func TestFavourite(t *testing.T) {
return
}
fmt.Fprintln(w, `{"content": "zzz"}`)
return
}))
defer ts.Close()
@ -418,6 +316,7 @@ func TestUnfavourite(t *testing.T) {
return
}
fmt.Fprintln(w, `{"content": "zzz"}`)
return
}))
defer ts.Close()
@ -440,64 +339,6 @@ func TestUnfavourite(t *testing.T) {
}
}
func TestBookmark(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/api/v1/statuses/1234567/bookmark" {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
fmt.Fprintln(w, `{"content": "zzz"}`)
}))
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")
if err != nil {
t.Fatalf("should not be fail: %v", err)
}
if status.Content != "zzz" {
t.Fatalf("want %q but %q", "zzz", status.Content)
}
}
func TestGetTimelinePublic(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Get("local") == "" {
@ -528,28 +369,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 +376,7 @@ func TestGetTimelineHashtag(t *testing.T) {
return
}
fmt.Fprintln(w, `[{"content": "zzz"},{"content": "yyy"}]`)
return
}))
defer ts.Close()
@ -585,41 +405,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 +412,7 @@ func TestGetTimelineMedia(t *testing.T) {
return
}
fmt.Fprintln(w, `[{"content": "zzz"},{"content": "yyy"}]`)
return
}))
defer ts.Close()
@ -665,6 +451,7 @@ func TestDeleteStatus(t *testing.T) {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusMethodNotAllowed)
return
}
return
}))
defer ts.Close()
@ -686,11 +473,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 +485,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 +516,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 +532,7 @@ func TestUploadMedia(t *testing.T) {
return
}
fmt.Fprintln(w, `{"id": 123}`)
return
}))
defer ts.Close()
@ -760,111 +549,4 @@ func TestUploadMedia(t *testing.T) {
if attachment.ID != "123" {
t.Fatalf("want %q but %q", "123", attachment.ID)
}
file, err := os.Open("testdata/logo.png")
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)
}
}

View File

@ -2,10 +2,8 @@ package mastodon
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/url"
@ -13,66 +11,42 @@ import (
"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.
// DeleteEvent is struct for passing deletion event to app.
type DeleteEvent struct{ ID ID }
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 +62,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]), &notification)
@ -108,10 +76,11 @@ func handleReader(q chan Event, r io.Reader) error {
}
}
}
return s.Err()
}
func (c *Client) streaming(ctx context.Context, p string, params url.Values) (chan Event, error) {
u, err := url.Parse(c.Config.Server)
u, err := url.Parse(c.config.Server)
if err != nil {
return nil, err
}
@ -123,10 +92,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 +129,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 +144,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 +156,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)
}

View File

@ -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
@ -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)
}
}

View File

@ -51,20 +51,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 +122,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)), &notification)

View File

@ -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 %q but %q", "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 %q but %q", "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)
}

View File

@ -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
}