Compare commits
133 Commits
fix-typeof
...
master
Author | SHA1 | Date |
---|---|---|
Brint E. Kriebel | 9faaa4f0dc | |
till | 6e810f25fa | |
till | 3203150fd3 | |
Michal Vyskocil | 29bb16009b | |
Raffaele Sena | ae970802cf | |
Rasmus Lindroth | 98f591c5e2 | |
Rasmus Lindroth | 51f9d7f999 | |
Yasuhiro Matsumoto | b597f437a9 | |
Rasmus Lindroth | 5f0c9a21c2 | |
Darren O'Connor | be61205708 | |
Darren O'Connor | 9e1af56ceb | |
Darren O'Connor | f76d33a68c | |
Darren O'Connor | e5c082de35 | |
Darren O'Connor | 309dce6ff3 | |
Mark Ayers | 114537dcc0 | |
Tyr Mactire | 7dfe81e233 | |
Tyr Mactire | 2a3ac1d1d5 | |
Yasuhiro Matsumoto | d272534ac7 | |
Rasmus Lindroth | b2204e0d6a | |
Rasmus Lindroth | c6a292132e | |
Alexander Bakker | 87278bda2e | |
Rasmus Lindroth | f436c5397c | |
Rasmus Lindroth | dfa87f3a80 | |
Marian Steinbach | 7745e19ff7 | |
Marian Steinbach | 45d75e8085 | |
shine | 62214db600 | |
Rasmus Lindroth | 4cbbf813dc | |
Rasmus Lindroth | ae6cc11820 | |
Rasmus Lindroth | 58c3891813 | |
Yasuhiro Matsumoto | a0db0ed8a0 | |
Yasuhiro Matsumoto | 1dd699ecee | |
Yasuhiro Matsumoto | f5813a9d88 | |
Rasmus Lindroth | d39c10ba5e | |
Masahiro Furudate | 86627ec7d6 | |
WaybackBot | bf42b86b9f | |
WaybackBot | eb26687c84 | |
Hanage999 | 8c434b5282 | |
Ollivier Robert | 6abe72ddb0 | |
Ollivier Robert | d6cb307605 | |
Ollivier Robert | 2ae3a80997 | |
Ollivier Robert | 932595ebec | |
Ollivier Robert | 2f161cfa50 | |
Ollivier Robert | adff0e83b9 | |
Masahiro Furudate | 315df7d916 | |
Christian Muehlhaeuser | 3e91c76504 | |
Christian Muehlhaeuser | 1c0769492b | |
Christian Muehlhaeuser | 75578dd249 | |
hiromi-mi | 021f5d0019 | |
hiromi-mi | fd6533a508 | |
mattn | 151613575d | |
Renovate Bot | 845b4bd395 | |
Renovate Bot | 822b3dfd49 | |
Renovate Bot | 1c963e9982 | |
Renovate Bot | edf591bde6 | |
Renovate Bot | a4693067ab | |
Renovate Bot | 4275f0739f | |
buckket | e43f2060a8 | |
buckket | 2abdb8e37c | |
Christian Muehlhaeuser | 25da74b864 | |
Ben Lubar | c9e2d23df3 | |
Ben Lubar | 977e6c550e | |
Ben Lubar | 934e685e7a | |
Yasuhiro Matsumoto | e24991527b | |
Yasuhiro Matsumoto | 80c1d52a0d | |
Yasuhiro Matsumoto | ef1332c96b | |
Christian Muehlhaeuser | 1b7f743892 | |
Christian Muehlhaeuser | 1ccf66b8b4 | |
Christian Muehlhaeuser | 20bc690d8d | |
Christian Muehlhaeuser | 24cdbe8c3d | |
dtluna | 34e64bb423 | |
mattn | 050f1a0a87 | |
Christian Muehlhaeuser | 8a48862adc | |
Yasuhiro Matsumoto | 26fcedc8aa | |
Yasuhiro Matsumoto | 536597515d | |
Yasuhiro Matsumoto | 8826198705 | |
Yasuhiro Matsumoto | 23fc4c7953 | |
Yasuhiro Matsumoto | 559ed99cdf | |
Yasuhiro Matsumoto | 3268207afe | |
buckket | 8f6192e26b | |
Christian Muehlhaeuser | c09198f7c9 | |
178inaba | f51571807d | |
buckket | e71411ef96 | |
buckket | 636b33ad1c | |
buckket | 68ca31fccd | |
Christian Muehlhaeuser | bb2662b33c | |
Christian Muehlhaeuser | e725c81450 | |
Ben Lubar | 9427a55316 | |
Ben Lubar | 3e2bdc63c7 | |
Ben Lubar | 6f05c48bf6 | |
Brian C. Lindner | e804ee7eb2 | |
178inaba | efa05aa949 | |
178inaba | 460b971f54 | |
178inaba | 1fcdf9f501 | |
178inaba | 5fd7d16157 | |
178inaba | b8bb5ae68c | |
Jessica Paczuski | 6bf95fc751 | |
Jessica Paczuski | 814e71920d | |
Christian Muehlhaeuser | 4def10a243 | |
Christian Muehlhaeuser | 3daf61de23 | |
Ben Lubar | c5945152ec | |
Strubbl | 2ebf34adae | |
Ben Lubar | 61705d1f2b | |
Spotlight | 48920165ef | |
Yasuhiro Matsumoto | 2ccbcfe14d | |
Yasuhiro Matsumoto | 5d863ccf79 | |
Yamagishi Kazutoshi | f0445dd4da | |
Yamagishi Kazutoshi | faab8cdc27 | |
Yasuhiro Matsumoto | b9e51b2916 | |
Yasuhiro Matsumoto | 0022a53649 | |
mattn | 84ffd85539 | |
Yasuhiro Matsumoto | f505a4f6ae | |
mattn | e946e2a506 | |
Yamagishi Kazutoshi | e0de6af209 | |
mattn | 0d8819ecaf | |
Yasuhiro Matsumoto | a98b28c817 | |
Yasuhiro Matsumoto | 83242d96ca | |
mattn | 0a6f156537 | |
Yasuhiro Matsumoto | 81259792b7 | |
Yasuhiro Matsumoto | 3002812c02 | |
Yasuhiro Matsumoto | bd72aa305d | |
Yasuhiro Matsumoto | 2866ebbdc5 | |
Yasuhiro Matsumoto | a4d531815f | |
utam0k | 1d165763fa | |
mattn | 689663979a | |
Kaoru HAYAMA | aaeb9f1de2 | |
Yasuhiro Matsumoto | d38a3e6dfc | |
Yasuhiro Matsumoto | a7e10ddba4 | |
Yasuhiro Matsumoto | aef736e991 | |
mattn | e0cf1e0650 | |
Kaoru HAYAMA | fe913e01e5 | |
Kaoru HAYAMA | 74f003d4a4 | |
Kaoru HAYAMA | abf6939992 | |
Yasuhiro Matsumoto | 730317321a |
|
@ -0,0 +1,12 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: mattn # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
@ -0,0 +1,27 @@
|
|||
name: test
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, macos-latest, ubuntu-latest]
|
||||
go: ["1.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
|
|
@ -1,8 +0,0 @@
|
|||
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
|
51
README.md
51
README.md
|
@ -1,10 +1,11 @@
|
|||
# go-mastodon
|
||||
|
||||
[![Build Status](https://travis-ci.org/mattn/go-mastodon.svg?branch=master)](https://travis-ci.org/mattn/go-mastodon)
|
||||
[![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)
|
||||
[![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)
|
||||
[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-mastodon)](https://goreportcard.com/report/github.com/mattn/go-mastodon)
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### Application
|
||||
|
@ -82,43 +83,81 @@ 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/v1/search
|
||||
* [x] GET /api/v2/search
|
||||
* [x] GET /api/v1/statuses/:id
|
||||
* [x] GET /api/v1/statuses/:id/context
|
||||
* [x] GET /api/v1/statuses/:id/card
|
||||
* [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
|
||||
|
||||
```
|
||||
$ go get github.com/mattn/go-mastodon
|
||||
```shell
|
||||
go install github.com/mattn/go-mastodon@latest
|
||||
```
|
||||
|
||||
## License
|
||||
|
|
142
accounts.go
142
accounts.go
|
@ -5,26 +5,49 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Account hold information for mastodon account.
|
||||
// Account holds information for a mastodon account.
|
||||
type Account struct {
|
||||
ID ID `json:"id"`
|
||||
Username string `json:"username"`
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// GetAccount return Account.
|
||||
|
@ -37,7 +60,7 @@ func (c *Client) GetAccount(ctx context.Context, id ID) (*Account, error) {
|
|||
return &account, nil
|
||||
}
|
||||
|
||||
// GetAccountCurrentUser return Account of current user.
|
||||
// GetAccountCurrentUser returns the Account of current user.
|
||||
func (c *Client) GetAccountCurrentUser(ctx context.Context) (*Account, error) {
|
||||
var account Account
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/accounts/verify_credentials", nil, &account, nil)
|
||||
|
@ -53,6 +76,9 @@ 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
|
||||
|
@ -68,6 +94,26 @@ func (c *Client) AccountUpdate(ctx context.Context, profile *Profile) (*Account,
|
|||
if profile.Note != nil {
|
||||
params.Set("note", *profile.Note)
|
||||
}
|
||||
if profile.Locked != nil {
|
||||
params.Set("locked", strconv.FormatBool(*profile.Locked))
|
||||
}
|
||||
if profile.Fields != nil {
|
||||
for idx, field := range *profile.Fields {
|
||||
params.Set(fmt.Sprintf("fields_attributes[%d][name]", idx), field.Name)
|
||||
params.Set(fmt.Sprintf("fields_attributes[%d][value]", idx), field.Value)
|
||||
}
|
||||
}
|
||||
if profile.Source != nil {
|
||||
if 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)
|
||||
}
|
||||
|
@ -83,7 +129,7 @@ func (c *Client) AccountUpdate(ctx context.Context, profile *Profile) (*Account,
|
|||
return &account, nil
|
||||
}
|
||||
|
||||
// GetAccountStatuses return statuses by specified accuont.
|
||||
// GetAccountStatuses return statuses by specified account.
|
||||
func (c *Client) GetAccountStatuses(ctx context.Context, id ID, pg *Pagination) ([]*Status, error) {
|
||||
var statuses []*Status
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/statuses", url.PathEscape(string(id))), nil, &statuses, pg)
|
||||
|
@ -93,7 +139,19 @@ func (c *Client) GetAccountStatuses(ctx context.Context, id ID, pg *Pagination)
|
|||
return statuses, nil
|
||||
}
|
||||
|
||||
// GetAccountFollowers return followers list.
|
||||
// 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.
|
||||
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)
|
||||
|
@ -103,7 +161,7 @@ func (c *Client) GetAccountFollowers(ctx context.Context, id ID, pg *Pagination)
|
|||
return accounts, nil
|
||||
}
|
||||
|
||||
// GetAccountFollowing return following list.
|
||||
// GetAccountFollowing returns following list.
|
||||
func (c *Client) GetAccountFollowing(ctx context.Context, id ID, pg *Pagination) ([]*Account, error) {
|
||||
var accounts []*Account
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/following", url.PathEscape(string(id))), nil, &accounts, pg)
|
||||
|
@ -113,7 +171,7 @@ func (c *Client) GetAccountFollowing(ctx context.Context, id ID, pg *Pagination)
|
|||
return accounts, nil
|
||||
}
|
||||
|
||||
// GetBlocks return block list.
|
||||
// GetBlocks returns block list.
|
||||
func (c *Client) GetBlocks(ctx context.Context, pg *Pagination) ([]*Account, error) {
|
||||
var accounts []*Account
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/blocks", nil, &accounts, pg)
|
||||
|
@ -123,17 +181,21 @@ func (c *Client) GetBlocks(ctx context.Context, pg *Pagination) ([]*Account, err
|
|||
return accounts, nil
|
||||
}
|
||||
|
||||
// Relationship hold information for relation-ship to the account.
|
||||
// Relationship holds information for relationship to the account.
|
||||
type Relationship struct {
|
||||
ID ID `json:"id"`
|
||||
Following bool `json:"following"`
|
||||
FollowedBy bool `json:"followed_by"`
|
||||
Blocking bool `json:"blocking"`
|
||||
Muting bool `json:"muting"`
|
||||
Requested bool `json:"requested"`
|
||||
ID ID `json:"id"`
|
||||
Following bool `json:"following"`
|
||||
FollowedBy bool `json:"followed_by"`
|
||||
Blocking bool `json:"blocking"`
|
||||
Muting bool `json:"muting"`
|
||||
MutingNotifications bool `json:"muting_notifications"`
|
||||
Requested bool `json:"requested"`
|
||||
DomainBlocking bool `json:"domain_blocking"`
|
||||
ShowingReblogs bool `json:"showing_reblogs"`
|
||||
Endorsed bool `json:"endorsed"`
|
||||
}
|
||||
|
||||
// AccountFollow follow the account.
|
||||
// AccountFollow follows the account.
|
||||
func (c *Client) AccountFollow(ctx context.Context, id ID) (*Relationship, error) {
|
||||
var relationship Relationship
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/follow", url.PathEscape(string(id))), nil, &relationship, nil)
|
||||
|
@ -143,7 +205,7 @@ func (c *Client) AccountFollow(ctx context.Context, id ID) (*Relationship, error
|
|||
return &relationship, nil
|
||||
}
|
||||
|
||||
// AccountUnfollow unfollow the account.
|
||||
// AccountUnfollow unfollows the account.
|
||||
func (c *Client) AccountUnfollow(ctx context.Context, id ID) (*Relationship, error) {
|
||||
var relationship Relationship
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/unfollow", url.PathEscape(string(id))), nil, &relationship, nil)
|
||||
|
@ -153,7 +215,7 @@ func (c *Client) AccountUnfollow(ctx context.Context, id ID) (*Relationship, err
|
|||
return &relationship, nil
|
||||
}
|
||||
|
||||
// AccountBlock block the account.
|
||||
// AccountBlock blocks the account.
|
||||
func (c *Client) AccountBlock(ctx context.Context, id ID) (*Relationship, error) {
|
||||
var relationship Relationship
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/block", url.PathEscape(string(id))), nil, &relationship, nil)
|
||||
|
@ -163,7 +225,7 @@ func (c *Client) AccountBlock(ctx context.Context, id ID) (*Relationship, error)
|
|||
return &relationship, nil
|
||||
}
|
||||
|
||||
// AccountUnblock unblock the account.
|
||||
// AccountUnblock unblocks the account.
|
||||
func (c *Client) AccountUnblock(ctx context.Context, id ID) (*Relationship, error) {
|
||||
var relationship Relationship
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/unblock", url.PathEscape(string(id))), nil, &relationship, nil)
|
||||
|
@ -173,7 +235,7 @@ func (c *Client) AccountUnblock(ctx context.Context, id ID) (*Relationship, erro
|
|||
return &relationship, nil
|
||||
}
|
||||
|
||||
// AccountMute mute the account.
|
||||
// AccountMute mutes the account.
|
||||
func (c *Client) AccountMute(ctx context.Context, id ID) (*Relationship, error) {
|
||||
var relationship Relationship
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/mute", url.PathEscape(string(id))), nil, &relationship, nil)
|
||||
|
@ -183,7 +245,7 @@ func (c *Client) AccountMute(ctx context.Context, id ID) (*Relationship, error)
|
|||
return &relationship, nil
|
||||
}
|
||||
|
||||
// AccountUnmute unmute the account.
|
||||
// AccountUnmute unmutes the account.
|
||||
func (c *Client) AccountUnmute(ctx context.Context, id ID) (*Relationship, error) {
|
||||
var relationship Relationship
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/accounts/%s/unmute", url.PathEscape(string(id))), nil, &relationship, nil)
|
||||
|
@ -193,7 +255,7 @@ func (c *Client) AccountUnmute(ctx context.Context, id ID) (*Relationship, error
|
|||
return &relationship, nil
|
||||
}
|
||||
|
||||
// GetAccountRelationships return relationship for the account.
|
||||
// GetAccountRelationships returns relationship for the account.
|
||||
func (c *Client) GetAccountRelationships(ctx context.Context, ids []string) ([]*Relationship, error) {
|
||||
params := url.Values{}
|
||||
for _, id := range ids {
|
||||
|
@ -208,7 +270,7 @@ func (c *Client) GetAccountRelationships(ctx context.Context, ids []string) ([]*
|
|||
return relationships, nil
|
||||
}
|
||||
|
||||
// AccountsSearch search accounts by query.
|
||||
// AccountsSearch searches accounts by query.
|
||||
func (c *Client) AccountsSearch(ctx context.Context, q string, limit int64) ([]*Account, error) {
|
||||
params := url.Values{}
|
||||
params.Set("q", q)
|
||||
|
@ -222,7 +284,7 @@ func (c *Client) AccountsSearch(ctx context.Context, q string, limit int64) ([]*
|
|||
return accounts, nil
|
||||
}
|
||||
|
||||
// FollowRemoteUser send follow-request.
|
||||
// FollowRemoteUser sends follow-request.
|
||||
func (c *Client) FollowRemoteUser(ctx context.Context, uri string) (*Account, error) {
|
||||
params := url.Values{}
|
||||
params.Set("uri", uri)
|
||||
|
@ -235,7 +297,7 @@ func (c *Client) FollowRemoteUser(ctx context.Context, uri string) (*Account, er
|
|||
return &account, nil
|
||||
}
|
||||
|
||||
// GetFollowRequests return follow-requests.
|
||||
// GetFollowRequests returns follow requests.
|
||||
func (c *Client) GetFollowRequests(ctx context.Context, pg *Pagination) ([]*Account, error) {
|
||||
var accounts []*Account
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/follow_requests", nil, &accounts, pg)
|
||||
|
@ -245,12 +307,12 @@ func (c *Client) GetFollowRequests(ctx context.Context, pg *Pagination) ([]*Acco
|
|||
return accounts, nil
|
||||
}
|
||||
|
||||
// FollowRequestAuthorize is authorize the follow request of user with id.
|
||||
// FollowRequestAuthorize authorizes the follow request of user with id.
|
||||
func (c *Client) FollowRequestAuthorize(ctx context.Context, id ID) error {
|
||||
return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/follow_requests/%s/authorize", url.PathEscape(string(id))), nil, nil, nil)
|
||||
}
|
||||
|
||||
// FollowRequestReject is rejects the follow request of user with id.
|
||||
// FollowRequestReject rejects the follow request of user with id.
|
||||
func (c *Client) FollowRequestReject(ctx context.Context, id ID) error {
|
||||
return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/follow_requests/%s/reject", url.PathEscape(string(id))), nil, nil, nil)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetAccount(t *testing.T) {
|
||||
|
@ -15,7 +16,6 @@ func TestGetAccount(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"username": "zzz"}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -47,7 +47,6 @@ func TestGetAccountCurrentUser(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"username": "zzz"}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -79,7 +78,6 @@ func TestAccountUpdate(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"username": "zzz"}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -93,9 +91,15 @@ func TestAccountUpdate(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
tbool := true
|
||||
fields := []Field{{"foo", "bar", time.Time{}}, {"dum", "baz", time.Time{}}}
|
||||
source := 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...",
|
||||
})
|
||||
|
@ -114,7 +118,6 @@ func TestGetAccountStatuses(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `[{"content": "foo"}, {"content": "bar"}]`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -140,6 +143,43 @@ 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" {
|
||||
|
@ -147,7 +187,6 @@ func TestGetAccountFollowers(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -183,7 +222,6 @@ func TestGetAccountFollowing(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -221,7 +259,6 @@ func TestGetBlocks(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -257,7 +294,6 @@ func TestAccountFollow(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"id":1234567,"following":true}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -267,11 +303,11 @@ func TestAccountFollow(t *testing.T) {
|
|||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
rel, err := client.AccountFollow(context.Background(), "123")
|
||||
_, err := client.AccountFollow(context.Background(), "123")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
rel, err = client.AccountFollow(context.Background(), "1234567")
|
||||
rel, err := client.AccountFollow(context.Background(), "1234567")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
|
@ -290,7 +326,6 @@ func TestAccountUnfollow(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"id":1234567,"following":false}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -300,11 +335,11 @@ func TestAccountUnfollow(t *testing.T) {
|
|||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
rel, err := client.AccountUnfollow(context.Background(), "123")
|
||||
_, err := client.AccountUnfollow(context.Background(), "123")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
rel, err = client.AccountUnfollow(context.Background(), "1234567")
|
||||
rel, err := client.AccountUnfollow(context.Background(), "1234567")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
|
@ -323,7 +358,6 @@ func TestAccountBlock(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"id":1234567,"blocking":true}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -356,7 +390,6 @@ func TestAccountUnblock(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"id":1234567,"blocking":false}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -389,7 +422,6 @@ func TestAccountMute(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"id":1234567,"muting":true}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -422,7 +454,6 @@ func TestAccountUnmute(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"id":1234567,"muting":false}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -488,7 +519,6 @@ func TestAccountsSearch(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `[{"username": "foobar"}, {"username": "barfoo"}]`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -524,7 +554,6 @@ func TestFollowRemoteUser(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"username": "zzz"}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -556,7 +585,6 @@ func TestGetFollowRequests(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -642,7 +670,6 @@ func TestGetMutes(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
|
42
apps.go
42
apps.go
|
@ -18,19 +18,24 @@ type AppConfig struct {
|
|||
// Where the user should be redirected after authorization (for no redirect, use urn:ietf:wg:oauth:2.0:oob)
|
||||
RedirectURIs string
|
||||
|
||||
// This can be a space-separated list of the following items: "read", "write" and "follow".
|
||||
// This can be a space-separated list of items listed on the /settings/applications/new page of any Mastodon
|
||||
// instance. "read", "write", and "follow" are top-level scopes that include all the permissions of the more
|
||||
// specific scopes like "read:favourites", "write:statuses", and "write:follows".
|
||||
Scopes string
|
||||
|
||||
// Optional.
|
||||
Website string
|
||||
}
|
||||
|
||||
// Application is mastodon application.
|
||||
// Application is a mastodon application.
|
||||
type Application struct {
|
||||
ID int64 `json:"id"`
|
||||
ID ID `json:"id"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
|
||||
// AuthURI is not part of the Mastodon API; it is generated by go-mastodon.
|
||||
AuthURI string `json:"auth_uri,omitempty"`
|
||||
}
|
||||
|
||||
// RegisterApp returns the mastodon application.
|
||||
|
@ -73,5 +78,36 @@ 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
|
||||
}
|
||||
|
|
55
apps_test.go
55
apps_test.go
|
@ -26,8 +26,7 @@ func TestRegisterApp(t *testing.T) {
|
|||
fmt.Fprintln(w, `<html><head><title>Apps</title></head></html>`)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"client_id": "foo", "client_secret": "bar"}`)
|
||||
return
|
||||
fmt.Fprintln(w, `{"id": 123, "client_id": "foo", "client_secret": "bar"}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -64,6 +63,9 @@ func TestRegisterApp(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if string(app.ID) != "123" {
|
||||
t.Fatalf("want %q but %q", "bar", app.ClientSecret)
|
||||
}
|
||||
if app.ClientID != "foo" {
|
||||
t.Fatalf("want %q but %q", "foo", app.ClientID)
|
||||
}
|
||||
|
@ -76,7 +78,6 @@ func TestRegisterAppWithCancel(t *testing.T) {
|
|||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(3 * time.Second)
|
||||
fmt.Fprintln(w, `{"client_id": "foo", "client_secret": "bar"}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -89,7 +90,53 @@ func TestRegisterAppWithCancel(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
if want := "Post " + ts.URL + "/api/v1/apps: context canceled"; want != err.Error() {
|
||||
if want := fmt.Sprintf("Post %q: context canceled", ts.URL+"/api/v1/apps"); want != err.Error() {
|
||||
t.Fatalf("want %q but %q", want, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/mattn/go-mastodon"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func cmdAccount(c *cli.Context) error {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCmdAccount(t *testing.T) {
|
||||
|
|
|
@ -3,10 +3,9 @@ package main
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/mattn/go-mastodon"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func cmdDelete(c *cli.Context) error {
|
||||
|
@ -15,11 +14,7 @@ func cmdDelete(c *cli.Context) error {
|
|||
return errors.New("arguments required")
|
||||
}
|
||||
for i := 0; i < c.NArg(); i++ {
|
||||
id, err := strconv.ParseInt(c.Args().Get(i), 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = client.DeleteStatus(context.Background(), id)
|
||||
err := client.DeleteStatus(context.Background(), mastodon.ID(c.Args().Get(i)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCmdDelete(t *testing.T) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"errors"
|
||||
|
||||
"github.com/mattn/go-mastodon"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func cmdFollow(c *cli.Context) error {
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCmdFollow(t *testing.T) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/mattn/go-mastodon"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func cmdFollowers(c *cli.Context) error {
|
||||
|
@ -25,9 +25,11 @@ func cmdFollowers(c *cli.Context) error {
|
|||
return err
|
||||
}
|
||||
followers = append(followers, fs...)
|
||||
if pg.MaxID == 0 {
|
||||
if pg.MaxID == "" {
|
||||
break
|
||||
}
|
||||
pg.SinceID = ""
|
||||
pg.MinID = ""
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
s := newScreen(config)
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCmdFollowers(t *testing.T) {
|
||||
|
|
|
@ -3,9 +3,10 @@ package main
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/mattn/go-mastodon"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func cmdInstance(c *cli.Context) error {
|
||||
|
@ -18,5 +19,26 @@ func cmdInstance(c *cli.Context) error {
|
|||
fmt.Fprintf(c.App.Writer, "Title : %s\n", instance.Title)
|
||||
fmt.Fprintf(c.App.Writer, "Description: %s\n", instance.Description)
|
||||
fmt.Fprintf(c.App.Writer, "EMail : %s\n", instance.EMail)
|
||||
if instance.Version != "" {
|
||||
fmt.Fprintf(c.App.Writer, "Version : %s\n", instance.Version)
|
||||
}
|
||||
if instance.Thumbnail != "" {
|
||||
fmt.Fprintf(c.App.Writer, "Thumbnail : %s\n", instance.Thumbnail)
|
||||
}
|
||||
if instance.URLs != nil {
|
||||
var keys []string
|
||||
for _, k := range instance.URLs {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(c.App.Writer, "%s: %s\n", k, instance.URLs[k])
|
||||
}
|
||||
}
|
||||
if instance.Stats != nil {
|
||||
fmt.Fprintf(c.App.Writer, "User Count : %v\n", instance.Stats.UserCount)
|
||||
fmt.Fprintf(c.App.Writer, "Status Count : %v\n", instance.Stats.StatusCount)
|
||||
fmt.Fprintf(c.App.Writer, "Domain Count : %v\n", instance.Stats.DomainCount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
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
|
||||
}
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCmdInstance(t *testing.T) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func cmdMikami(c *cli.Context) error {
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCmdMikami(t *testing.T) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
|
||||
"github.com/fatih/color"
|
||||
"github.com/mattn/go-mastodon"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func cmdNotification(c *cli.Context) error {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCmdNotification(t *testing.T) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/mattn/go-mastodon"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func cmdSearch(c *cli.Context) error {
|
||||
|
|
|
@ -6,15 +6,15 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCmdSearch(t *testing.T) {
|
||||
out := testWithServer(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/search":
|
||||
fmt.Fprintln(w, `{"accounts": [{"id": 234, "acct": "zzz"}], "statuses":[{"id": 345, "content": "yyy"}], "hashtags": ["www", "わろす"]}`)
|
||||
case "/api/v2/search":
|
||||
fmt.Fprintln(w, `{"accounts": [{"id": 234, "acct": "zzz"}], "statuses":[{"id": 345, "content": "yyy"}], "hashtags": [{"name": "www"}, {"name": "わろす"}]}`)
|
||||
return
|
||||
}
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
"text/template"
|
||||
|
||||
"github.com/mattn/go-mastodon"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// SimpleJSON is a struct for output JSON for data to be simple used
|
||||
|
@ -44,9 +44,7 @@ func cmdStream(c *cli.Context) error {
|
|||
"nl": func(s string) string {
|
||||
return s + "\n"
|
||||
},
|
||||
"text": func(s string) string {
|
||||
return textContent(s)
|
||||
},
|
||||
"text": textContent,
|
||||
}).Parse(asFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -89,7 +87,16 @@ func cmdStream(c *cli.Context) error {
|
|||
if asJSON {
|
||||
json.NewEncoder(c.App.Writer).Encode(e)
|
||||
} else if asSimpleJSON {
|
||||
if t, ok := e.(*mastodon.UpdateEvent); ok {
|
||||
switch t := e.(type) {
|
||||
case *mastodon.UpdateEvent:
|
||||
json.NewEncoder(c.App.Writer).Encode(&SimpleJSON{
|
||||
ID: t.Status.ID,
|
||||
Username: t.Status.Account.Username,
|
||||
Acct: t.Status.Account.Acct,
|
||||
Avatar: t.Status.Account.AvatarStatic,
|
||||
Content: textContent(t.Status.Content),
|
||||
})
|
||||
case *mastodon.UpdateEditEvent:
|
||||
json.NewEncoder(c.App.Writer).Encode(&SimpleJSON{
|
||||
ID: t.Status.ID,
|
||||
Username: t.Status.Account.Username,
|
||||
|
@ -104,6 +111,8 @@ func cmdStream(c *cli.Context) error {
|
|||
switch t := e.(type) {
|
||||
case *mastodon.UpdateEvent:
|
||||
s.displayStatus(c.App.Writer, t.Status)
|
||||
case *mastodon.UpdateEditEvent:
|
||||
s.displayStatus(c.App.Writer, t.Status)
|
||||
case *mastodon.NotificationEvent:
|
||||
// TODO s.displayStatus(c.App.Writer, t.Notification.Status)
|
||||
case *mastodon.ErrorEvent:
|
||||
|
|
|
@ -14,8 +14,7 @@ import (
|
|||
)
|
||||
|
||||
func TestCmdStream(t *testing.T) {
|
||||
var ts *httptest.Server
|
||||
ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/streaming/public/local" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"net/http/httptest"
|
||||
|
||||
"github.com/mattn/go-mastodon"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func testWithServer(h http.HandlerFunc, testFuncs ...func(*cli.App)) string {
|
||||
|
|
|
@ -2,9 +2,11 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/mattn/go-mastodon"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func cmdTimeline(c *cli.Context) error {
|
||||
|
@ -20,3 +22,69 @@ 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
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCmdTimeline(t *testing.T) {
|
||||
|
@ -14,7 +14,13 @@ func TestCmdTimeline(t *testing.T) {
|
|||
func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/timelines/home":
|
||||
fmt.Fprintln(w, `[{"content": "zzz"}]`)
|
||||
fmt.Fprintln(w, `[{"content": "home"}]`)
|
||||
return
|
||||
case "/api/v1/timelines/public":
|
||||
fmt.Fprintln(w, `[{"content": "public"}]`)
|
||||
return
|
||||
case "/api/v1/conversations":
|
||||
fmt.Fprintln(w, `[{"id": "4", "unread":false, "last_status" : {"content": "direct"}}]`)
|
||||
return
|
||||
}
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
|
@ -22,9 +28,25 @@ func TestCmdTimeline(t *testing.T) {
|
|||
},
|
||||
func(app *cli.App) {
|
||||
app.Run([]string{"mstdn", "timeline"})
|
||||
app.Run([]string{"mstdn", "timeline-home"})
|
||||
app.Run([]string{"mstdn", "timeline-public"})
|
||||
app.Run([]string{"mstdn", "timeline-local"})
|
||||
app.Run([]string{"mstdn", "timeline-direct"})
|
||||
},
|
||||
)
|
||||
if !strings.Contains(out, "zzz") {
|
||||
t.Fatalf("%q should be contained in output of command: %v", "zzz", out)
|
||||
want := strings.Join([]string{
|
||||
"@example.com",
|
||||
"home",
|
||||
"@example.com",
|
||||
"home",
|
||||
"@example.com",
|
||||
"public",
|
||||
"@example.com",
|
||||
"public",
|
||||
"@example.com",
|
||||
"direct",
|
||||
}, "\n") + "\n"
|
||||
if !strings.Contains(out, want) {
|
||||
t.Fatalf("%q should be contained in output of command: %v", want, out)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,10 @@ package main
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/mattn/go-mastodon"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func cmdToot(c *cli.Context) error {
|
||||
|
@ -26,7 +27,7 @@ func cmdToot(c *cli.Context) error {
|
|||
client := c.App.Metadata["client"].(*mastodon.Client)
|
||||
_, err := client.PostStatus(context.Background(), &mastodon.Toot{
|
||||
Status: toot,
|
||||
InReplyToID: c.Int64("i"),
|
||||
InReplyToID: mastodon.ID(fmt.Sprint(c.String("i"))),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import (
|
|||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCmdToot(t *testing.T) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/mattn/go-mastodon"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func cmdUpload(c *cli.Context) error {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCmdUpload(t *testing.T) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"net/url"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func cmdXSearch(c *cli.Context) error {
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestCmdXSearch(t *testing.T) {
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
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
|
||||
)
|
|
@ -0,0 +1,53 @@
|
|||
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=
|
|
@ -17,7 +17,7 @@ import (
|
|||
"github.com/fatih/color"
|
||||
"github.com/mattn/go-mastodon"
|
||||
"github.com/mattn/go-tty"
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
|
@ -183,26 +183,26 @@ func makeApp() *cli.App {
|
|||
app.Usage = "mastodon client"
|
||||
app.Version = "0.0.1"
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
&cli.StringFlag{
|
||||
Name: "profile",
|
||||
Usage: "profile name",
|
||||
Value: "",
|
||||
},
|
||||
}
|
||||
app.Commands = []cli.Command{
|
||||
app.Commands = []*cli.Command{
|
||||
{
|
||||
Name: "toot",
|
||||
Usage: "post toot",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
&cli.StringFlag{
|
||||
Name: "ff",
|
||||
Usage: "post utf-8 string from a file(\"-\" means STDIN)",
|
||||
Value: "",
|
||||
},
|
||||
cli.IntFlag{
|
||||
&cli.StringFlag{
|
||||
Name: "i",
|
||||
Usage: "in-reply-to",
|
||||
Value: 0,
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
Action: cmdToot,
|
||||
|
@ -211,19 +211,19 @@ func makeApp() *cli.App {
|
|||
Name: "stream",
|
||||
Usage: "stream statuses",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
&cli.StringFlag{
|
||||
Name: "type",
|
||||
Usage: "stream type (public,public/local,user:NAME,hashtag:TAG)",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
&cli.BoolFlag{
|
||||
Name: "json",
|
||||
Usage: "output JSON",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
&cli.BoolFlag{
|
||||
Name: "simplejson",
|
||||
Usage: "output simple JSON",
|
||||
},
|
||||
cli.StringFlag{
|
||||
&cli.StringFlag{
|
||||
Name: "template",
|
||||
Usage: "output with tamplate format",
|
||||
},
|
||||
|
@ -235,6 +235,37 @@ 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",
|
||||
|
@ -245,6 +276,16 @@ func makeApp() *cli.App {
|
|||
Usage: "show instance information",
|
||||
Action: cmdInstance,
|
||||
},
|
||||
{
|
||||
Name: "instance_activity",
|
||||
Usage: "show instance activity information",
|
||||
Action: cmdInstanceActivity,
|
||||
},
|
||||
{
|
||||
Name: "instance_peers",
|
||||
Usage: "show instance peers information",
|
||||
Action: cmdInstancePeers,
|
||||
},
|
||||
{
|
||||
Name: "account",
|
||||
Usage: "show account information",
|
||||
|
@ -360,6 +401,7 @@ func run() int {
|
|||
}
|
||||
|
||||
client := mastodon.NewClient(config)
|
||||
client.UserAgent = "mstdn"
|
||||
app.Metadata = map[string]interface{}{
|
||||
"client": client,
|
||||
"config": config,
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestReadFileFile(t *testing.T) {
|
||||
|
|
24
compat.go
24
compat.go
|
@ -3,6 +3,7 @@ package mastodon
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ID string
|
||||
|
@ -23,3 +24,26 @@ func (id *ID) UnmarshalJSON(data []byte) error {
|
|||
*id = ID(fmt.Sprint(n))
|
||||
return nil
|
||||
}
|
||||
|
||||
type Sbool bool
|
||||
|
||||
func (s *Sbool) UnmarshalJSON(data []byte) error {
|
||||
if len(data) > 0 && data[0] == '"' && data[len(data)-1] == '"' {
|
||||
var str string
|
||||
if err := json.Unmarshal(data, &str); err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := strconv.ParseBool(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*s = Sbool(b)
|
||||
return nil
|
||||
}
|
||||
var b bool
|
||||
if err := json.Unmarshal(data, &b); err != nil {
|
||||
return err
|
||||
}
|
||||
*s = Sbool(b)
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ func ExamplePagination() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
followers = append(followers, fs...)
|
||||
if pg.MaxID == 0 {
|
||||
if pg.MaxID == "" {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Second)
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
package mastodon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Filter is metadata for a filter of users.
|
||||
type Filter struct {
|
||||
ID ID `json:"id"`
|
||||
Phrase string `json:"phrase"`
|
||||
Context []string `json:"context"`
|
||||
WholeWord bool `json:"whole_word"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
Irreversible bool `json:"irreversible"`
|
||||
}
|
||||
|
||||
// GetFilters returns all the filters on the current account.
|
||||
func (c *Client) GetFilters(ctx context.Context) ([]*Filter, error) {
|
||||
var filters []*Filter
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/filters", nil, &filters, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return filters, nil
|
||||
}
|
||||
|
||||
// GetFilter retrieves a filter by ID.
|
||||
func (c *Client) GetFilter(ctx context.Context, id ID) (*Filter, error) {
|
||||
var filter Filter
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/filters/%s", url.PathEscape(string(id))), nil, &filter, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &filter, nil
|
||||
}
|
||||
|
||||
// CreateFilter creates a new filter.
|
||||
func (c *Client) CreateFilter(ctx context.Context, filter *Filter) (*Filter, error) {
|
||||
if filter == nil {
|
||||
return nil, errors.New("filter can't be nil")
|
||||
}
|
||||
if filter.Phrase == "" {
|
||||
return nil, errors.New("phrase can't be empty")
|
||||
}
|
||||
if len(filter.Context) == 0 {
|
||||
return nil, errors.New("context can't be empty")
|
||||
}
|
||||
params := url.Values{}
|
||||
params.Set("phrase", filter.Phrase)
|
||||
for _, c := range filter.Context {
|
||||
params.Add("context[]", c)
|
||||
}
|
||||
if filter.WholeWord {
|
||||
params.Add("whole_word", "true")
|
||||
}
|
||||
if filter.Irreversible {
|
||||
params.Add("irreversible", "true")
|
||||
}
|
||||
if !filter.ExpiresAt.IsZero() {
|
||||
diff := time.Until(filter.ExpiresAt)
|
||||
params.Add("expires_in", fmt.Sprintf("%.0f", diff.Seconds()))
|
||||
}
|
||||
|
||||
var f Filter
|
||||
err := c.doAPI(ctx, http.MethodPost, "/api/v1/filters", params, &f, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
// UpdateFilter updates a filter.
|
||||
func (c *Client) UpdateFilter(ctx context.Context, id ID, filter *Filter) (*Filter, error) {
|
||||
if filter == nil {
|
||||
return nil, errors.New("filter can't be nil")
|
||||
}
|
||||
if id == ID("") {
|
||||
return nil, errors.New("ID can't be empty")
|
||||
}
|
||||
if filter.Phrase == "" {
|
||||
return nil, errors.New("phrase can't be empty")
|
||||
}
|
||||
if len(filter.Context) == 0 {
|
||||
return nil, errors.New("context can't be empty")
|
||||
}
|
||||
params := url.Values{}
|
||||
params.Set("phrase", filter.Phrase)
|
||||
for _, c := range filter.Context {
|
||||
params.Add("context[]", c)
|
||||
}
|
||||
if filter.WholeWord {
|
||||
params.Add("whole_word", "true")
|
||||
} else {
|
||||
params.Add("whole_word", "false")
|
||||
}
|
||||
if filter.Irreversible {
|
||||
params.Add("irreversible", "true")
|
||||
} else {
|
||||
params.Add("irreversible", "false")
|
||||
}
|
||||
if !filter.ExpiresAt.IsZero() {
|
||||
diff := time.Until(filter.ExpiresAt)
|
||||
params.Add("expires_in", fmt.Sprintf("%.0f", diff.Seconds()))
|
||||
} else {
|
||||
params.Add("expires_in", "")
|
||||
}
|
||||
|
||||
var f Filter
|
||||
err := c.doAPI(ctx, http.MethodPut, fmt.Sprintf("/api/v1/filters/%s", url.PathEscape(string(id))), params, &f, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &f, nil
|
||||
}
|
||||
|
||||
// DeleteFilter removes a filter.
|
||||
func (c *Client) DeleteFilter(ctx context.Context, id ID) error {
|
||||
return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/filters/%s", url.PathEscape(string(id))), nil, nil, nil)
|
||||
}
|
|
@ -0,0 +1,342 @@
|
|||
package mastodon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetFilters(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, `[{"id": "6191", "phrase": "rust", "context": ["home"], "whole_word": true, "expires_at": "2019-05-21T13:47:31.333Z", "irreversible": false}, {"id": "5580", "phrase": "@twitter.com", "context": ["home", "notifications", "public", "thread"], "whole_word": false, "expires_at": null, "irreversible": true}]`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
d, err := time.Parse(time.RFC3339Nano, "2019-05-21T13:47:31.333Z")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
tf := []Filter{
|
||||
{
|
||||
ID: ID("6191"),
|
||||
Phrase: "rust",
|
||||
Context: []string{"home"},
|
||||
WholeWord: true,
|
||||
ExpiresAt: d,
|
||||
Irreversible: false,
|
||||
},
|
||||
{
|
||||
ID: ID("5580"),
|
||||
Phrase: "@twitter.com",
|
||||
Context: []string{"notifications", "home", "thread", "public"},
|
||||
WholeWord: false,
|
||||
ExpiresAt: time.Time{},
|
||||
Irreversible: true,
|
||||
},
|
||||
}
|
||||
|
||||
filters, err := client.GetFilters(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if len(filters) != 2 {
|
||||
t.Fatalf("result should be two: %d", len(filters))
|
||||
}
|
||||
for i, f := range tf {
|
||||
if filters[i].ID != f.ID {
|
||||
t.Fatalf("want %q but %q", string(f.ID), filters[i].ID)
|
||||
}
|
||||
if filters[i].Phrase != f.Phrase {
|
||||
t.Fatalf("want %q but %q", f.Phrase, filters[i].Phrase)
|
||||
}
|
||||
sort.Strings(filters[i].Context)
|
||||
sort.Strings(f.Context)
|
||||
if strings.Join(filters[i].Context, ", ") != strings.Join(f.Context, ", ") {
|
||||
t.Fatalf("want %q but %q", f.Context, filters[i].Context)
|
||||
}
|
||||
if filters[i].ExpiresAt != f.ExpiresAt {
|
||||
t.Fatalf("want %q but %q", f.ExpiresAt, filters[i].ExpiresAt)
|
||||
}
|
||||
if filters[i].WholeWord != f.WholeWord {
|
||||
t.Fatalf("want %t but %t", f.WholeWord, filters[i].WholeWord)
|
||||
}
|
||||
if filters[i].Irreversible != f.Irreversible {
|
||||
t.Fatalf("want %t but %t", f.Irreversible, filters[i].Irreversible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFilter(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/filters/1" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"id": "1", "phrase": "rust", "context": ["home"], "whole_word": true, "expires_at": "2019-05-21T13:47:31.333Z", "irreversible": false}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.GetFilter(context.Background(), "2")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
d, err := time.Parse(time.RFC3339Nano, "2019-05-21T13:47:31.333Z")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
tf := Filter{
|
||||
ID: ID("1"),
|
||||
Phrase: "rust",
|
||||
Context: []string{"home"},
|
||||
WholeWord: true,
|
||||
ExpiresAt: d,
|
||||
Irreversible: false,
|
||||
}
|
||||
filter, err := client.GetFilter(context.Background(), "1")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if filter.ID != tf.ID {
|
||||
t.Fatalf("want %q but %q", string(tf.ID), filter.ID)
|
||||
}
|
||||
if filter.Phrase != tf.Phrase {
|
||||
t.Fatalf("want %q but %q", tf.Phrase, filter.Phrase)
|
||||
}
|
||||
sort.Strings(filter.Context)
|
||||
sort.Strings(tf.Context)
|
||||
if strings.Join(filter.Context, ", ") != strings.Join(tf.Context, ", ") {
|
||||
t.Fatalf("want %q but %q", tf.Context, filter.Context)
|
||||
}
|
||||
if filter.ExpiresAt != tf.ExpiresAt {
|
||||
t.Fatalf("want %q but %q", tf.ExpiresAt, filter.ExpiresAt)
|
||||
}
|
||||
if filter.WholeWord != tf.WholeWord {
|
||||
t.Fatalf("want %t but %t", tf.WholeWord, filter.WholeWord)
|
||||
}
|
||||
if filter.Irreversible != tf.Irreversible {
|
||||
t.Fatalf("want %t but %t", tf.Irreversible, filter.Irreversible)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateFilter(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.PostFormValue("phrase") != "rust" && r.PostFormValue("phrase") != "@twitter.com" {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if r.PostFormValue("phrase") == "rust" {
|
||||
fmt.Fprintln(w, `{"id": "1", "phrase": "rust", "context": ["home"], "whole_word": true, "expires_at": "2019-05-21T13:47:31.333Z", "irreversible": true}`)
|
||||
return
|
||||
} else {
|
||||
fmt.Fprintln(w, `{"id": "2", "phrase": "@twitter.com", "context": ["home", "notifications", "public", "thread"], "whole_word": false, "expires_at": null, "irreversible": false}`)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.CreateFilter(context.Background(), nil)
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
_, err = client.CreateFilter(context.Background(), &Filter{Context: []string{"home"}})
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
_, err = client.CreateFilter(context.Background(), &Filter{Phrase: "rust"})
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
_, err = client.CreateFilter(context.Background(), &Filter{Phrase: "Test", Context: []string{"home"}})
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
|
||||
d, err := time.Parse(time.RFC3339Nano, "2019-05-21T13:47:31.333Z")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
tf := []Filter{
|
||||
{
|
||||
ID: ID("1"),
|
||||
Phrase: "rust",
|
||||
Context: []string{"home"},
|
||||
WholeWord: true,
|
||||
ExpiresAt: d,
|
||||
Irreversible: true,
|
||||
},
|
||||
{
|
||||
ID: ID("2"),
|
||||
Phrase: "@twitter.com",
|
||||
Context: []string{"notifications", "home", "thread", "public"},
|
||||
WholeWord: false,
|
||||
ExpiresAt: time.Time{},
|
||||
Irreversible: false,
|
||||
},
|
||||
}
|
||||
for _, f := range tf {
|
||||
filter, err := client.CreateFilter(context.Background(), &f)
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if filter.ID != f.ID {
|
||||
t.Fatalf("want %q but %q", string(f.ID), filter.ID)
|
||||
}
|
||||
if filter.Phrase != f.Phrase {
|
||||
t.Fatalf("want %q but %q", f.Phrase, filter.Phrase)
|
||||
}
|
||||
sort.Strings(filter.Context)
|
||||
sort.Strings(f.Context)
|
||||
if strings.Join(filter.Context, ", ") != strings.Join(f.Context, ", ") {
|
||||
t.Fatalf("want %q but %q", f.Context, filter.Context)
|
||||
}
|
||||
if filter.ExpiresAt != f.ExpiresAt {
|
||||
t.Fatalf("want %q but %q", f.ExpiresAt, filter.ExpiresAt)
|
||||
}
|
||||
if filter.WholeWord != f.WholeWord {
|
||||
t.Fatalf("want %t but %t", f.WholeWord, filter.WholeWord)
|
||||
}
|
||||
if filter.Irreversible != f.Irreversible {
|
||||
t.Fatalf("want %t but %t", f.Irreversible, filter.Irreversible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateFilter(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/api/v1/filters/1" {
|
||||
fmt.Fprintln(w, `{"id": "1", "phrase": "rust", "context": ["home"], "whole_word": true, "expires_at": "2019-05-21T13:47:31.333Z", "irreversible": true}`)
|
||||
return
|
||||
} else if r.URL.Path == "/api/v1/filters/2" {
|
||||
fmt.Fprintln(w, `{"id": "2", "phrase": "@twitter.com", "context": ["home", "notifications", "public", "thread"], "whole_word": false, "expires_at": null, "irreversible": false}`)
|
||||
return
|
||||
} else {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.UpdateFilter(context.Background(), ID("1"), nil)
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
_, err = client.UpdateFilter(context.Background(), ID(""), &Filter{Phrase: ""})
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
_, err = client.UpdateFilter(context.Background(), ID("2"), &Filter{Phrase: ""})
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
_, err = client.UpdateFilter(context.Background(), ID("2"), &Filter{Phrase: "rust"})
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
_, err = client.UpdateFilter(context.Background(), ID("3"), &Filter{Phrase: "rust", Context: []string{"home"}})
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
|
||||
d, err := time.Parse(time.RFC3339Nano, "2019-05-21T13:47:31.333Z")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
tf := []Filter{
|
||||
{
|
||||
ID: ID("1"),
|
||||
Phrase: "rust",
|
||||
Context: []string{"home"},
|
||||
WholeWord: true,
|
||||
ExpiresAt: d,
|
||||
Irreversible: true,
|
||||
},
|
||||
{
|
||||
ID: ID("2"),
|
||||
Phrase: "@twitter.com",
|
||||
Context: []string{"notifications", "home", "thread", "public"},
|
||||
WholeWord: false,
|
||||
ExpiresAt: time.Time{},
|
||||
Irreversible: false,
|
||||
},
|
||||
}
|
||||
for _, f := range tf {
|
||||
filter, err := client.UpdateFilter(context.Background(), f.ID, &f)
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if filter.ID != f.ID {
|
||||
t.Fatalf("want %q but %q", string(f.ID), filter.ID)
|
||||
}
|
||||
if filter.Phrase != f.Phrase {
|
||||
t.Fatalf("want %q but %q", f.Phrase, filter.Phrase)
|
||||
}
|
||||
sort.Strings(filter.Context)
|
||||
sort.Strings(f.Context)
|
||||
if strings.Join(filter.Context, ", ") != strings.Join(f.Context, ", ") {
|
||||
t.Fatalf("want %q but %q", f.Context, filter.Context)
|
||||
}
|
||||
if filter.ExpiresAt != f.ExpiresAt {
|
||||
t.Fatalf("want %q but %q", f.ExpiresAt, filter.ExpiresAt)
|
||||
}
|
||||
if filter.WholeWord != f.WholeWord {
|
||||
t.Fatalf("want %t but %t", f.WholeWord, filter.WholeWord)
|
||||
}
|
||||
if filter.Irreversible != f.Irreversible {
|
||||
t.Fatalf("want %t but %t", f.Irreversible, filter.Irreversible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteFilter(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/filters/1" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
err := client.DeleteFilter(context.Background(), "2")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
err = client.DeleteFilter(context.Background(), "1")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
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
|
||||
)
|
|
@ -0,0 +1,4 @@
|
|||
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=
|
|
@ -0,0 +1,12 @@
|
|||
#!/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
|
|
@ -12,13 +12,13 @@ const wantBase64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAABxCAYAAA
|
|||
|
||||
func TestBase64EncodeFileName(t *testing.T) {
|
||||
// Error in os.Open.
|
||||
uri, err := Base64EncodeFileName("fail")
|
||||
_, err := Base64EncodeFileName("fail")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
|
||||
// Success.
|
||||
uri, err = Base64EncodeFileName("testdata/logo.png")
|
||||
uri, err := Base64EncodeFileName("testdata/logo.png")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ func TestBase64EncodeFileName(t *testing.T) {
|
|||
|
||||
func TestBase64Encode(t *testing.T) {
|
||||
// Error in file.Stat.
|
||||
uri, err := Base64Encode(nil)
|
||||
_, err := Base64Encode(nil)
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ func TestBase64Encode(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
uri, err = Base64Encode(logo)
|
||||
_, err = Base64Encode(logo)
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ func TestBase64Encode(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
uri, err = Base64Encode(logo)
|
||||
uri, err := Base64Encode(logo)
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
|
|
69
instance.go
69
instance.go
|
@ -5,15 +5,39 @@ import (
|
|||
"net/http"
|
||||
)
|
||||
|
||||
// Instance hold information for mastodon instance.
|
||||
// Instance holds information for a mastodon instance.
|
||||
type Instance struct {
|
||||
URI string `json:"uri"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
EMail string `json:"email"`
|
||||
URI string `json:"uri"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
EMail string `json:"email"`
|
||||
Version string `json:"version,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// GetInstance return Instance.
|
||||
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.
|
||||
func (c *Client) GetInstance(ctx context.Context) (*Instance, error) {
|
||||
var instance Instance
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/instance", nil, &instance, nil)
|
||||
|
@ -22,3 +46,36 @@ 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
|
||||
}
|
||||
|
|
150
instance_test.go
150
instance_test.go
|
@ -6,6 +6,7 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestGetInstance(t *testing.T) {
|
||||
|
@ -16,7 +17,7 @@ func TestGetInstance(t *testing.T) {
|
|||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"title": "mastodon"}`)
|
||||
fmt.Fprintln(w, `{"title": "mastodon", "uri": "http://mstdn.example.com", "description": "test mastodon", "email": "mstdn@mstdn.example.com", "contact_account": {"username": "mattn"}}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -37,4 +38,151 @@ 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])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
package mastodon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// List is metadata for a list of users.
|
||||
type List struct {
|
||||
ID ID `json:"id"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
// GetLists returns all the lists on the current account.
|
||||
func (c *Client) GetLists(ctx context.Context) ([]*List, error) {
|
||||
var lists []*List
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/lists", nil, &lists, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lists, nil
|
||||
}
|
||||
|
||||
// GetAccountLists returns the lists containing a given account.
|
||||
func (c *Client) GetAccountLists(ctx context.Context, id ID) ([]*List, error) {
|
||||
var lists []*List
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/accounts/%s/lists", url.PathEscape(string(id))), nil, &lists, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return lists, nil
|
||||
}
|
||||
|
||||
// GetListAccounts returns the accounts in a given list.
|
||||
func (c *Client) GetListAccounts(ctx context.Context, id ID) ([]*Account, error) {
|
||||
var accounts []*Account
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/lists/%s/accounts", url.PathEscape(string(id))), url.Values{"limit": {"0"}}, &accounts, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// GetList retrieves a list by ID.
|
||||
func (c *Client) GetList(ctx context.Context, id ID) (*List, error) {
|
||||
var list List
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/lists/%s", url.PathEscape(string(id))), nil, &list, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &list, nil
|
||||
}
|
||||
|
||||
// CreateList creates a new list with a given title.
|
||||
func (c *Client) CreateList(ctx context.Context, title string) (*List, error) {
|
||||
params := url.Values{}
|
||||
params.Set("title", title)
|
||||
|
||||
var list List
|
||||
err := c.doAPI(ctx, http.MethodPost, "/api/v1/lists", params, &list, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &list, nil
|
||||
}
|
||||
|
||||
// RenameList assigns a new title to a list.
|
||||
func (c *Client) RenameList(ctx context.Context, id ID, title string) (*List, error) {
|
||||
params := url.Values{}
|
||||
params.Set("title", title)
|
||||
|
||||
var list List
|
||||
err := c.doAPI(ctx, http.MethodPut, fmt.Sprintf("/api/v1/lists/%s", url.PathEscape(string(id))), params, &list, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &list, nil
|
||||
}
|
||||
|
||||
// DeleteList removes a list.
|
||||
func (c *Client) DeleteList(ctx context.Context, id ID) error {
|
||||
return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/lists/%s", url.PathEscape(string(id))), nil, nil, nil)
|
||||
}
|
||||
|
||||
// AddToList adds accounts to a list.
|
||||
//
|
||||
// Only accounts already followed by the user can be added to a list.
|
||||
func (c *Client) AddToList(ctx context.Context, list ID, accounts ...ID) error {
|
||||
params := url.Values{}
|
||||
for _, acct := range accounts {
|
||||
params.Add("account_ids", string(acct))
|
||||
}
|
||||
|
||||
return c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/lists/%s/accounts", url.PathEscape(string(list))), params, nil, nil)
|
||||
}
|
||||
|
||||
// RemoveFromList removes accounts from a list.
|
||||
func (c *Client) RemoveFromList(ctx context.Context, list ID, accounts ...ID) error {
|
||||
params := url.Values{}
|
||||
for _, acct := range accounts {
|
||||
params.Add("account_ids", string(acct))
|
||||
}
|
||||
|
||||
return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/lists/%s/accounts", url.PathEscape(string(list))), params, nil, nil)
|
||||
}
|
|
@ -0,0 +1,280 @@
|
|||
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)
|
||||
}
|
||||
}
|
254
mastodon.go
254
mastodon.go
|
@ -2,20 +2,16 @@
|
|||
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"
|
||||
)
|
||||
|
@ -31,11 +27,12 @@ type Config struct {
|
|||
// Client is a API client for mastodon.
|
||||
type Client struct {
|
||||
http.Client
|
||||
config *Config
|
||||
Config *Config
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
func (c *Client) doAPI(ctx context.Context, method string, uri string, params interface{}, res interface{}, pg *Pagination) error {
|
||||
u, err := url.Parse(c.config.Server)
|
||||
u, err := url.Parse(c.Config.Server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -57,32 +54,18 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if file, ok := params.(string); ok {
|
||||
f, err := os.Open(file)
|
||||
} else if media, ok := params.(*Media); ok {
|
||||
r, contentType, err := media.bodyAndContentType()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
part, err := mw.CreateFormFile("file", filepath.Base(file))
|
||||
req, err = http.NewRequest(method, u.String(), r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(part, f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = mw.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err = http.NewRequest(method, u.String(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ct = mw.FormDataContentType()
|
||||
|
||||
ct = contentType
|
||||
} else {
|
||||
if method == http.MethodGet && pg != nil {
|
||||
u.RawQuery = pg.toValues().Encode()
|
||||
|
@ -93,16 +76,41 @@ 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)
|
||||
}
|
||||
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
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
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return parseAPIError("bad request", resp)
|
||||
|
@ -120,25 +128,57 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in
|
|||
return json.NewDecoder(resp.Body).Decode(&res)
|
||||
}
|
||||
|
||||
// NewClient return new mastodon API client.
|
||||
// NewClient returns a new mastodon API client.
|
||||
func NewClient(config *Config) *Client {
|
||||
return &Client{
|
||||
Client: *http.DefaultClient,
|
||||
config: config,
|
||||
Config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate get access-token to the API.
|
||||
// Authenticate gets access-token to the API.
|
||||
func (c *Client) Authenticate(ctx context.Context, username, password string) error {
|
||||
params := url.Values{}
|
||||
params.Set("client_id", c.config.ClientID)
|
||||
params.Set("client_secret", c.config.ClientSecret)
|
||||
params.Set("grant_type", "password")
|
||||
params.Set("username", username)
|
||||
params.Set("password", password)
|
||||
params.Set("scope", "read write follow")
|
||||
params := url.Values{
|
||||
"client_id": {c.Config.ClientID},
|
||||
"client_secret": {c.Config.ClientSecret},
|
||||
"grant_type": {"password"},
|
||||
"username": {username},
|
||||
"password": {password},
|
||||
"scope": {"read write follow"},
|
||||
}
|
||||
|
||||
u, err := url.Parse(c.config.Server)
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -150,6 +190,9 @@ func (c *Client) Authenticate(ctx context.Context, username, password string) er
|
|||
}
|
||||
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
|
||||
|
@ -160,25 +203,44 @@ func (c *Client) Authenticate(ctx context.Context, username, password string) er
|
|||
return parseAPIError("bad authorization", resp)
|
||||
}
|
||||
|
||||
res := struct {
|
||||
var res struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
}{}
|
||||
}
|
||||
err = json.NewDecoder(resp.Body).Decode(&res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.config.AccessToken = res.AccessToken
|
||||
c.Config.AccessToken = res.AccessToken
|
||||
return nil
|
||||
}
|
||||
|
||||
// Toot is struct to post status.
|
||||
// Convenience constants for Toot.Visibility
|
||||
const (
|
||||
VisibilityPublic = "public"
|
||||
VisibilityUnlisted = "unlisted"
|
||||
VisibilityFollowersOnly = "private"
|
||||
VisibilityDirectMessage = "direct"
|
||||
)
|
||||
|
||||
// Toot is a struct to post status.
|
||||
type Toot struct {
|
||||
Status string `json:"status"`
|
||||
InReplyToID int64 `json:"in_reply_to_id"`
|
||||
MediaIDs []int64 `json:"media_ids"`
|
||||
Sensitive bool `json:"sensitive"`
|
||||
SpoilerText string `json:"spoiler_text"`
|
||||
Visibility string `json:"visibility"`
|
||||
Status string `json:"status"`
|
||||
InReplyToID ID `json:"in_reply_to_id"`
|
||||
MediaIDs []ID `json:"media_ids"`
|
||||
Sensitive bool `json:"sensitive"`
|
||||
SpoilerText string `json:"spoiler_text"`
|
||||
Visibility string `json:"visibility"`
|
||||
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"`
|
||||
}
|
||||
|
||||
// Mention hold information for mention.
|
||||
|
@ -186,36 +248,69 @@ type Mention struct {
|
|||
URL string `json:"url"`
|
||||
Username string `json:"username"`
|
||||
Acct string `json:"acct"`
|
||||
ID int64 `json:"id"`
|
||||
ID ID `json:"id"`
|
||||
}
|
||||
|
||||
// Tag hold information for tag.
|
||||
type Tag struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
History []History `json:"history"`
|
||||
}
|
||||
|
||||
// History hold information for history.
|
||||
type History struct {
|
||||
Day string `json:"day"`
|
||||
Uses string `json:"uses"`
|
||||
Accounts string `json:"accounts"`
|
||||
}
|
||||
|
||||
// Attachment hold information for attachment.
|
||||
type Attachment struct {
|
||||
ID int64 `json:"id"`
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
RemoteURL string `json:"remote_url"`
|
||||
PreviewURL string `json:"preview_url"`
|
||||
TextURL string `json:"text_url"`
|
||||
ID ID `json:"id"`
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
RemoteURL string `json:"remote_url"`
|
||||
PreviewURL string `json:"preview_url"`
|
||||
TextURL string `json:"text_url"`
|
||||
Description string `json:"description"`
|
||||
Meta AttachmentMeta `json:"meta"`
|
||||
}
|
||||
|
||||
// AttachmentMeta holds information for attachment metadata.
|
||||
type AttachmentMeta struct {
|
||||
Original AttachmentSize `json:"original"`
|
||||
Small AttachmentSize `json:"small"`
|
||||
}
|
||||
|
||||
// AttachmentSize holds information for attatchment size.
|
||||
type AttachmentSize struct {
|
||||
Width int64 `json:"width"`
|
||||
Height int64 `json:"height"`
|
||||
Size string `json:"size"`
|
||||
Aspect float64 `json:"aspect"`
|
||||
}
|
||||
|
||||
// Emoji hold information for CustomEmoji.
|
||||
type Emoji struct {
|
||||
ShortCode string `json:"shortcode"`
|
||||
StaticURL string `json:"static_url"`
|
||||
URL string `json:"url"`
|
||||
VisibleInPicker bool `json:"visible_in_picker"`
|
||||
}
|
||||
|
||||
// Results hold information for search result.
|
||||
type Results struct {
|
||||
Accounts []*Account `json:"accounts"`
|
||||
Statuses []*Status `json:"statuses"`
|
||||
Hashtags []string `json:"hashtags"`
|
||||
Hashtags []*Tag `json:"hashtags"`
|
||||
}
|
||||
|
||||
// Pagination is a struct for specifying the get range.
|
||||
type Pagination struct {
|
||||
MaxID int64
|
||||
SinceID int64
|
||||
MaxID ID
|
||||
SinceID ID
|
||||
MinID ID
|
||||
Limit int64
|
||||
}
|
||||
|
||||
|
@ -239,24 +334,25 @@ func newPagination(rawlink string) (*Pagination, error) {
|
|||
return nil, err
|
||||
}
|
||||
p.SinceID = sinceID
|
||||
|
||||
minID, err := getPaginationID(link.URL, "min_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.MinID = minID
|
||||
}
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func getPaginationID(rawurl, key string) (int64, error) {
|
||||
func getPaginationID(rawurl, key string) (ID, error) {
|
||||
u, err := url.Parse(rawurl)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(u.Query().Get(key), 10, 64)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
return ID(u.Query().Get(key)), nil
|
||||
}
|
||||
|
||||
func (p *Pagination) toValues() url.Values {
|
||||
|
@ -264,10 +360,14 @@ func (p *Pagination) toValues() url.Values {
|
|||
}
|
||||
|
||||
func (p *Pagination) setValues(params url.Values) url.Values {
|
||||
if p.MaxID > 0 {
|
||||
params.Set("max_id", fmt.Sprint(p.MaxID))
|
||||
} else if p.SinceID > 0 {
|
||||
params.Set("since_id", fmt.Sprint(p.SinceID))
|
||||
if p.MaxID != "" {
|
||||
params.Set("max_id", string(p.MaxID))
|
||||
}
|
||||
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))
|
||||
|
|
406
mastodon_test.go
406
mastodon_test.go
|
@ -2,6 +2,7 @@ package mastodon
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
@ -25,26 +26,26 @@ func TestDoAPI(t *testing.T) {
|
|||
c := NewClient(&Config{Server: ts.URL})
|
||||
var accounts []Account
|
||||
err := c.doAPI(context.Background(), http.MethodGet, "/", nil, &accounts, &Pagination{
|
||||
MaxID: 999,
|
||||
MaxID: "999",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
|
||||
pg := &Pagination{
|
||||
MaxID: 123,
|
||||
SinceID: 789,
|
||||
MaxID: "123",
|
||||
SinceID: "789",
|
||||
Limit: 10,
|
||||
}
|
||||
err = c.doAPI(context.Background(), http.MethodGet, "/", url.Values{}, &accounts, pg)
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if pg.MaxID != 234 {
|
||||
t.Fatalf("want %d but %d", 234, pg.MaxID)
|
||||
if pg.MaxID != "234" {
|
||||
t.Fatalf("want %q but %q", "234", pg.MaxID)
|
||||
}
|
||||
if pg.SinceID != 890 {
|
||||
t.Fatalf("want %d but %d", 890, pg.SinceID)
|
||||
if pg.SinceID != "890" {
|
||||
t.Fatalf("want %q but %q", "890", pg.SinceID)
|
||||
}
|
||||
if accounts[0].Username != "foo" {
|
||||
t.Fatalf("want %q but %q", "foo", accounts[0].Username)
|
||||
|
@ -54,19 +55,19 @@ func TestDoAPI(t *testing.T) {
|
|||
}
|
||||
|
||||
pg = &Pagination{
|
||||
MaxID: 123,
|
||||
SinceID: 789,
|
||||
MaxID: "123",
|
||||
SinceID: "789",
|
||||
Limit: 10,
|
||||
}
|
||||
err = c.doAPI(context.Background(), http.MethodGet, "/", nil, &accounts, pg)
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if pg.MaxID != 234 {
|
||||
t.Fatalf("want %d but %d", 234, pg.MaxID)
|
||||
if pg.MaxID != "234" {
|
||||
t.Fatalf("want %q but %q", "234", pg.MaxID)
|
||||
}
|
||||
if pg.SinceID != 890 {
|
||||
t.Fatalf("want %d but %d", 890, pg.SinceID)
|
||||
if pg.SinceID != "890" {
|
||||
t.Fatalf("want %q but %q", "890", pg.SinceID)
|
||||
}
|
||||
if accounts[0].Username != "foo" {
|
||||
t.Fatalf("want %q but %q", "foo", accounts[0].Username)
|
||||
|
@ -95,7 +96,6 @@ func TestAuthenticate(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"access_token": "zoo"}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -123,7 +123,6 @@ func TestAuthenticate(t *testing.T) {
|
|||
func TestAuthenticateWithCancel(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(3 * time.Second)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -138,11 +137,42 @@ func TestAuthenticateWithCancel(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
if want := "Post " + ts.URL + "/oauth/token: context canceled"; want != err.Error() {
|
||||
if want := fmt.Sprintf("Post %q: context canceled", ts.URL+"/oauth/token"); want != err.Error() {
|
||||
t.Fatalf("want %q but %q", want, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
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" {
|
||||
|
@ -150,7 +180,6 @@ func TestPostStatus(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"access_token": "zoo"}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -183,7 +212,6 @@ func TestPostStatus(t *testing.T) {
|
|||
func TestPostStatusWithCancel(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(3 * time.Second)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -200,15 +228,309 @@ func TestPostStatusWithCancel(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
if want := "Post " + ts.URL + "/api/v1/statuses: context canceled"; want != err.Error() {
|
||||
if want := fmt.Sprintf("Post %q: context canceled", ts.URL+"/api/v1/statuses"); want != err.Error() {
|
||||
t.Fatalf("want %q but %q", want, err.Error())
|
||||
}
|
||||
}
|
||||
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()
|
||||
|
||||
|
@ -248,7 +570,6 @@ func TestGetTimelineHome(t *testing.T) {
|
|||
func TestGetTimelineHomeWithCancel(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(3 * time.Second)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -264,13 +585,14 @@ func TestGetTimelineHomeWithCancel(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
if want := "Get " + ts.URL + "/api/v1/timelines/home: context canceled"; want != err.Error() {
|
||||
if want := fmt.Sprintf("Get %q: context canceled", ts.URL+"/api/v1/timelines/home"); 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()
|
||||
|
@ -293,15 +615,20 @@ 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)
|
||||
}
|
||||
if pg.MaxID != 123 {
|
||||
t.Fatalf("want %d but %d", 123, pg.MaxID)
|
||||
if pg.MaxID != "123" {
|
||||
t.Fatalf("want %q but %q", "123", pg.MaxID)
|
||||
}
|
||||
if pg.SinceID != 789 {
|
||||
t.Fatalf("want %d but %d", 789, pg.SinceID)
|
||||
if pg.SinceID != "789" {
|
||||
t.Fatalf("want %q but %q", "789", pg.SinceID)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -312,23 +639,24 @@ func TestGetPaginationID(t *testing.T) {
|
|||
}
|
||||
|
||||
_, err = getPaginationID("http://example.com?max_id=abc", "max_id")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
|
||||
id, err := getPaginationID("http://example.com?max_id=123", "max_id")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if id != 123 {
|
||||
t.Fatalf("want %d but %d", 123, id)
|
||||
if id != "123" {
|
||||
t.Fatalf("want %q but %q", "123", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaginationSetValues(t *testing.T) {
|
||||
p := &Pagination{
|
||||
MaxID: 123,
|
||||
SinceID: 789,
|
||||
MaxID: "123",
|
||||
SinceID: "456",
|
||||
MinID: "789",
|
||||
Limit: 10,
|
||||
}
|
||||
before := url.Values{"key": {"value"}}
|
||||
|
@ -339,16 +667,19 @@ 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") != "" {
|
||||
t.Fatalf("result should be empty string: %q", after.Get("since_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("limit") != "10" {
|
||||
t.Fatalf("want %q but %q", "10", after.Get("limit"))
|
||||
}
|
||||
|
||||
p = &Pagination{
|
||||
MaxID: 0,
|
||||
SinceID: 789,
|
||||
MaxID: "",
|
||||
SinceID: "789",
|
||||
}
|
||||
before = url.Values{}
|
||||
after = p.setValues(before)
|
||||
|
@ -358,4 +689,7 @@ func TestPaginationSetValues(t *testing.T) {
|
|||
if after.Get("since_id") != "789" {
|
||||
t.Fatalf("want %q but %q", "789", after.Get("since_id"))
|
||||
}
|
||||
if after.Get("min_id") != "" {
|
||||
t.Fatalf("result should be empty string: %q", after.Get("min_id"))
|
||||
}
|
||||
}
|
||||
|
|
103
notification.go
103
notification.go
|
@ -2,21 +2,40 @@ package mastodon
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Notification hold information for mastodon notification.
|
||||
// Notification holds information for a mastodon notification.
|
||||
type Notification struct {
|
||||
ID int64 `json:"id"`
|
||||
ID ID `json:"id"`
|
||||
Type string `json:"type"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Account Account `json:"account"`
|
||||
Status *Status `json:"status"`
|
||||
}
|
||||
|
||||
// GetNotifications return notifications.
|
||||
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.
|
||||
func (c *Client) GetNotifications(ctx context.Context, pg *Pagination) ([]*Notification, error) {
|
||||
var notifications []*Notification
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/notifications", nil, ¬ifications, pg)
|
||||
|
@ -26,17 +45,87 @@ func (c *Client) GetNotifications(ctx context.Context, pg *Pagination) ([]*Notif
|
|||
return notifications, nil
|
||||
}
|
||||
|
||||
// GetNotification return notification.
|
||||
func (c *Client) GetNotification(ctx context.Context, id int64) (*Notification, error) {
|
||||
// GetNotification returns notification.
|
||||
func (c *Client) GetNotification(ctx context.Context, id ID) (*Notification, error) {
|
||||
var notification Notification
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/notifications/%d", id), nil, ¬ification, nil)
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/notifications/%v", id), nil, ¬ification, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ¬ification, nil
|
||||
}
|
||||
|
||||
// ClearNotifications clear notifications.
|
||||
// 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.
|
||||
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
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@ package mastodon
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
@ -20,9 +23,11 @@ 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()
|
||||
|
||||
|
@ -39,21 +44,100 @@ func TestGetNotifications(t *testing.T) {
|
|||
if len(ns) != 2 {
|
||||
t.Fatalf("result should be two: %d", len(ns))
|
||||
}
|
||||
if ns[0].ID != 122 {
|
||||
t.Fatalf("want %v but %v", 122, ns[0].ID)
|
||||
if ns[0].ID != "122" {
|
||||
t.Fatalf("want %v but %v", "122", ns[0].ID)
|
||||
}
|
||||
if ns[1].ID != 123 {
|
||||
t.Fatalf("want %v but %v", 123, ns[1].ID)
|
||||
if ns[1].ID != "123" {
|
||||
t.Fatalf("want %v but %v", "123", ns[1].ID)
|
||||
}
|
||||
n, err := client.GetNotification(context.Background(), 123)
|
||||
n, err := client.GetNotification(context.Background(), "123")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if n.ID != 123 {
|
||||
t.Fatalf("want %v but %v", 123, n.ID)
|
||||
if n.ID != "123" {
|
||||
t.Fatalf("want %v but %v", "123", n.ID)
|
||||
}
|
||||
err = client.ClearNotifications(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
err = client.DismissNotification(context.Background(), "123")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPushSubscription(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/push/subscription":
|
||||
fmt.Fprintln(w, ` {"id":1,"endpoint":"https://example.org","alerts":{"follow":true,"favourite":"true","reblog":"true","mention":"true"},"server_key":"foobar"}`)
|
||||
return
|
||||
}
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
|
||||
enabled := new(Sbool)
|
||||
*enabled = true
|
||||
alerts := PushAlerts{Follow: enabled, Favourite: enabled, Reblog: enabled, Mention: enabled}
|
||||
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
shared := make([]byte, 16)
|
||||
_, err = rand.Read(shared)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testSub := func(sub *PushSubscription, err error) {
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if sub.ID != "1" {
|
||||
t.Fatalf("want %v but %v", "1", sub.ID)
|
||||
}
|
||||
if sub.Endpoint != "https://example.org" {
|
||||
t.Fatalf("want %v but %v", "https://example.org", sub.Endpoint)
|
||||
}
|
||||
if sub.ServerKey != "foobar" {
|
||||
t.Fatalf("want %v but %v", "foobar", sub.ServerKey)
|
||||
}
|
||||
if *sub.Alerts.Favourite != true {
|
||||
t.Fatalf("want %v but %v", true, *sub.Alerts.Favourite)
|
||||
}
|
||||
if *sub.Alerts.Mention != true {
|
||||
t.Fatalf("want %v but %v", true, *sub.Alerts.Mention)
|
||||
}
|
||||
if *sub.Alerts.Reblog != true {
|
||||
t.Fatalf("want %v but %v", true, *sub.Alerts.Reblog)
|
||||
}
|
||||
if *sub.Alerts.Follow != true {
|
||||
t.Fatalf("want %v but %v", true, *sub.Alerts.Follow)
|
||||
}
|
||||
}
|
||||
|
||||
sub, err := client.AddPushSubscription(context.Background(), "http://example.org", priv.PublicKey, shared, alerts)
|
||||
testSub(sub, err)
|
||||
|
||||
sub, err = client.GetPushSubscription(context.Background())
|
||||
testSub(sub, err)
|
||||
|
||||
sub, err = client.UpdatePushSubscription(context.Background(), &alerts)
|
||||
testSub(sub, err)
|
||||
|
||||
err = client.RemovePushSubscription(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
package mastodon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetPoll(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/polls/1234567" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"id": "1234567", "expires_at": "2019-12-05T04:05:08.302Z", "expired": true, "multiple": false, "votes_count": 10, "voters_count": null, "voted": true, "own_votes": [1], "options": [{"title": "accept", "votes_count": 6}, {"title": "deny", "votes_count": 4}], "emojis":[{"shortcode":"💩", "url":"http://example.com", "static_url": "http://example.com/static"}]}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.GetPoll(context.Background(), "123")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
poll, err := client.GetPoll(context.Background(), "1234567")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if poll.Expired != true {
|
||||
t.Fatalf("want %t but %t", true, poll.Expired)
|
||||
}
|
||||
if poll.Multiple != false {
|
||||
t.Fatalf("want %t but %t", true, poll.Multiple)
|
||||
}
|
||||
if poll.VotesCount != 10 {
|
||||
t.Fatalf("want %d but %d", 10, poll.VotesCount)
|
||||
}
|
||||
if poll.VotersCount != 0 {
|
||||
t.Fatalf("want %d but %d", 0, poll.VotersCount)
|
||||
}
|
||||
if poll.Voted != true {
|
||||
t.Fatalf("want %t but %t", true, poll.Voted)
|
||||
}
|
||||
if len(poll.OwnVotes) != 1 {
|
||||
t.Fatalf("should have own votes")
|
||||
}
|
||||
if poll.OwnVotes[0] != 1 {
|
||||
t.Fatalf("want %d but %d", 1, poll.OwnVotes[0])
|
||||
}
|
||||
if len(poll.Options) != 2 {
|
||||
t.Fatalf("should have 2 options")
|
||||
}
|
||||
if poll.Options[0].Title != "accept" {
|
||||
t.Fatalf("want %q but %q", "accept", poll.Options[0].Title)
|
||||
}
|
||||
if poll.Options[0].VotesCount != 6 {
|
||||
t.Fatalf("want %q but %q", 6, poll.Options[0].VotesCount)
|
||||
}
|
||||
if poll.Options[1].Title != "deny" {
|
||||
t.Fatalf("want %q but %q", "deny", poll.Options[1].Title)
|
||||
}
|
||||
if poll.Options[1].VotesCount != 4 {
|
||||
t.Fatalf("want %q but %q", 4, poll.Options[1].VotesCount)
|
||||
}
|
||||
if len(poll.Emojis) != 1 {
|
||||
t.Fatal("should have emojis")
|
||||
}
|
||||
if poll.Emojis[0].ShortCode != "💩" {
|
||||
t.Fatalf("want %q but %q", "💩", poll.Emojis[0].ShortCode)
|
||||
}
|
||||
if poll.Emojis[0].URL != "http://example.com" {
|
||||
t.Fatalf("want %q but %q", "https://example.com", poll.Emojis[0].URL)
|
||||
}
|
||||
if poll.Emojis[0].StaticURL != "http://example.com/static" {
|
||||
t.Fatalf("want %q but %q", "https://example.com/static", poll.Emojis[0].StaticURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPollVote(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/polls/1234567/votes" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"id": "1234567", "expires_at": "2019-12-05T04:05:08.302Z", "expired": false, "multiple": false, "votes_count": 10, "voters_count": null, "voted": true, "own_votes": [1], "options": [{"title": "accept", "votes_count": 6}, {"title": "deny", "votes_count": 4}], "emojis":[]}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
poll, err := client.PollVote(context.Background(), ID("1234567"), 1)
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if poll.Expired != false {
|
||||
t.Fatalf("want %t but %t", false, poll.Expired)
|
||||
}
|
||||
if poll.Multiple != false {
|
||||
t.Fatalf("want %t but %t", true, poll.Multiple)
|
||||
}
|
||||
if poll.VotesCount != 10 {
|
||||
t.Fatalf("want %d but %d", 10, poll.VotesCount)
|
||||
}
|
||||
if poll.VotersCount != 0 {
|
||||
t.Fatalf("want %d but %d", 0, poll.VotersCount)
|
||||
}
|
||||
if poll.Voted != true {
|
||||
t.Fatalf("want %t but %t", true, poll.Voted)
|
||||
}
|
||||
if len(poll.OwnVotes) != 1 {
|
||||
t.Fatalf("should have own votes")
|
||||
}
|
||||
if poll.OwnVotes[0] != 1 {
|
||||
t.Fatalf("want %d but %d", 1, poll.OwnVotes[0])
|
||||
}
|
||||
if len(poll.Options) != 2 {
|
||||
t.Fatalf("should have 2 options")
|
||||
}
|
||||
if poll.Options[0].Title != "accept" {
|
||||
t.Fatalf("want %q but %q", "accept", poll.Options[0].Title)
|
||||
}
|
||||
if poll.Options[0].VotesCount != 6 {
|
||||
t.Fatalf("want %q but %q", 6, poll.Options[0].VotesCount)
|
||||
}
|
||||
if poll.Options[1].Title != "deny" {
|
||||
t.Fatalf("want %q but %q", "deny", poll.Options[1].Title)
|
||||
}
|
||||
if poll.Options[1].VotesCount != 4 {
|
||||
t.Fatalf("want %q but %q", 4, poll.Options[1].VotesCount)
|
||||
}
|
||||
}
|
11
report.go
11
report.go
|
@ -2,18 +2,17 @@ package mastodon
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Report hold information for mastodon report.
|
||||
// Report holds information for a mastodon report.
|
||||
type Report struct {
|
||||
ID int64 `json:"id"`
|
||||
ActionTaken bool `json:"action_taken"`
|
||||
}
|
||||
|
||||
// GetReports return report of the current user.
|
||||
// GetReports returns report of the current user.
|
||||
func (c *Client) GetReports(ctx context.Context) ([]*Report, error) {
|
||||
var reports []*Report
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/reports", nil, &reports, nil)
|
||||
|
@ -24,11 +23,11 @@ func (c *Client) GetReports(ctx context.Context) ([]*Report, error) {
|
|||
}
|
||||
|
||||
// Report reports the report
|
||||
func (c *Client) Report(ctx context.Context, accountID int64, ids []int64, comment string) (*Report, error) {
|
||||
func (c *Client) Report(ctx context.Context, accountID ID, ids []ID, comment string) (*Report, error) {
|
||||
params := url.Values{}
|
||||
params.Set("account_id", fmt.Sprint(accountID))
|
||||
params.Set("account_id", string(accountID))
|
||||
for _, id := range ids {
|
||||
params.Add("status_ids[]", fmt.Sprint(id))
|
||||
params.Add("status_ids[]", string(id))
|
||||
}
|
||||
params.Set("comment", comment)
|
||||
var report Report
|
||||
|
|
|
@ -15,7 +15,6 @@ func TestGetReports(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `[{"id": 122, "action_taken": false}, {"id": 123, "action_taken": true}]`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -55,7 +54,6 @@ func TestReport(t *testing.T) {
|
|||
} else {
|
||||
fmt.Fprintln(w, `{"id": 1234, "action_taken": true}`)
|
||||
}
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -65,26 +63,26 @@ func TestReport(t *testing.T) {
|
|||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
rp, err := client.Report(context.Background(), 121, nil, "")
|
||||
_, err := client.Report(context.Background(), "121", nil, "")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
rp, err = client.Report(context.Background(), 122, nil, "")
|
||||
rp, err := client.Report(context.Background(), "122", nil, "")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if rp.ID != 1234 {
|
||||
t.Fatalf("want %v but %v", 1234, rp.ID)
|
||||
t.Fatalf("want %q but %q", "1234", rp.ID)
|
||||
}
|
||||
if rp.ActionTaken {
|
||||
t.Fatalf("want %v but %v", true, rp.ActionTaken)
|
||||
}
|
||||
rp, err = client.Report(context.Background(), 123, []int64{567}, "")
|
||||
rp, err = client.Report(context.Background(), "123", []ID{"567"}, "")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if rp.ID != 1234 {
|
||||
t.Fatalf("want %v but %v", 1234, rp.ID)
|
||||
t.Fatalf("want %q but %q", "1234", rp.ID)
|
||||
}
|
||||
if !rp.ActionTaken {
|
||||
t.Fatalf("want %v but %v", false, rp.ActionTaken)
|
||||
|
|
374
status.go
374
status.go
|
@ -1,53 +1,165 @@
|
|||
package mastodon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Status is struct to hold status.
|
||||
type Status struct {
|
||||
ID ID `json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
URI string `json:"uri"`
|
||||
URL string `json:"url"`
|
||||
Account Account `json:"account"`
|
||||
InReplyToID interface{} `json:"in_reply_to_id"`
|
||||
InReplyToAccountID interface{} `json:"in_reply_to_account_id"`
|
||||
Reblog *Status `json:"reblog"`
|
||||
Content string `json:"content"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
EditedAt time.Time `json:"edited_at"`
|
||||
Emojis []Emoji `json:"emojis"`
|
||||
RepliesCount int64 `json:"replies_count"`
|
||||
ReblogsCount int64 `json:"reblogs_count"`
|
||||
FavouritesCount int64 `json:"favourites_count"`
|
||||
Reblogged interface{} `json:"reblogged"`
|
||||
Favourited interface{} `json:"favourited"`
|
||||
Bookmarked interface{} `json:"bookmarked"`
|
||||
Muted interface{} `json:"muted"`
|
||||
Sensitive bool `json:"sensitive"`
|
||||
SpoilerText string `json:"spoiler_text"`
|
||||
Visibility string `json:"visibility"`
|
||||
Application Application `json:"application"`
|
||||
Account Account `json:"account"`
|
||||
MediaAttachments []Attachment `json:"media_attachments"`
|
||||
Mentions []Mention `json:"mentions"`
|
||||
Tags []Tag `json:"tags"`
|
||||
URI string `json:"uri"`
|
||||
Content string `json:"content"`
|
||||
URL string `json:"url"`
|
||||
ReblogsCount int64 `json:"reblogs_count"`
|
||||
FavouritesCount int64 `json:"favourites_count"`
|
||||
Reblog *Status `json:"reblog"`
|
||||
Favourited interface{} `json:"favourited"`
|
||||
Reblogged interface{} `json:"reblogged"`
|
||||
Card *Card `json:"card"`
|
||||
Poll *Poll `json:"poll"`
|
||||
Application Application `json:"application"`
|
||||
Language string `json:"language"`
|
||||
Pinned interface{} `json:"pinned"`
|
||||
}
|
||||
|
||||
// Context hold information for mastodon context.
|
||||
// StatusHistory is a struct to hold status history data.
|
||||
type StatusHistory struct {
|
||||
Content string `json:"content"`
|
||||
SpoilerText string `json:"spoiler_text"`
|
||||
Account Account `json:"account"`
|
||||
Sensitive bool `json:"sensitive"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Emojis []Emoji `json:"emojis"`
|
||||
MediaAttachments []Attachment `json:"media_attachments"`
|
||||
}
|
||||
|
||||
// Context holds information for a mastodon context.
|
||||
type Context struct {
|
||||
Ancestors []*Status `json:"ancestors"`
|
||||
Descendants []*Status `json:"descendants"`
|
||||
}
|
||||
|
||||
// Card hold information for mastodon card.
|
||||
// Card holds information for a mastodon card.
|
||||
type Card struct {
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
Type string `json:"type"`
|
||||
AuthorName string `json:"author_name"`
|
||||
AuthorURL string `json:"author_url"`
|
||||
ProviderName string `json:"provider_name"`
|
||||
ProviderURL string `json:"provider_url"`
|
||||
HTML string `json:"html"`
|
||||
Width int64 `json:"width"`
|
||||
Height int64 `json:"height"`
|
||||
}
|
||||
|
||||
// GetFavourites return the favorite list of the current user.
|
||||
// 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.
|
||||
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)
|
||||
|
@ -57,40 +169,70 @@ func (c *Client) GetFavourites(ctx context.Context, pg *Pagination) ([]*Status,
|
|||
return statuses, nil
|
||||
}
|
||||
|
||||
// GetStatus return status specified by id.
|
||||
func (c *Client) GetStatus(ctx context.Context, id int64) (*Status, error) {
|
||||
// GetBookmarks returns the bookmark list of the current user.
|
||||
func (c *Client) GetBookmarks(ctx context.Context, pg *Pagination) ([]*Status, error) {
|
||||
var statuses []*Status
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/bookmarks", nil, &statuses, pg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
// GetStatus returns status specified by id.
|
||||
func (c *Client) GetStatus(ctx context.Context, id ID) (*Status, error) {
|
||||
var status Status
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%d", id), nil, &status, nil)
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s", id), nil, &status, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
// GetStatusContext return status specified by id.
|
||||
func (c *Client) GetStatusContext(ctx context.Context, id int64) (*Context, error) {
|
||||
// GetStatusContext returns status specified by id.
|
||||
func (c *Client) GetStatusContext(ctx context.Context, id ID) (*Context, error) {
|
||||
var context Context
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%d/context", id), nil, &context, nil)
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/context", id), nil, &context, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &context, nil
|
||||
}
|
||||
|
||||
// GetStatusCard return status specified by id.
|
||||
func (c *Client) GetStatusCard(ctx context.Context, id int64) (*Card, error) {
|
||||
// GetStatusCard returns status specified by id.
|
||||
func (c *Client) GetStatusCard(ctx context.Context, id ID) (*Card, error) {
|
||||
var card Card
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%d/card", id), nil, &card, nil)
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/card", id), nil, &card, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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 int64, pg *Pagination) ([]*Account, error) {
|
||||
func (c *Client) GetRebloggedBy(ctx context.Context, id ID, pg *Pagination) ([]*Account, error) {
|
||||
var accounts []*Account
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%d/reblogged_by", id), nil, &accounts, pg)
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/reblogged_by", id), nil, &accounts, pg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -98,49 +240,69 @@ func (c *Client) GetRebloggedBy(ctx context.Context, id int64, pg *Pagination) (
|
|||
}
|
||||
|
||||
// GetFavouritedBy returns the account list of the user who liked the toot of id.
|
||||
func (c *Client) GetFavouritedBy(ctx context.Context, id int64, pg *Pagination) ([]*Account, error) {
|
||||
func (c *Client) GetFavouritedBy(ctx context.Context, id ID, pg *Pagination) ([]*Account, error) {
|
||||
var accounts []*Account
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%d/favourited_by", id), nil, &accounts, pg)
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/statuses/%s/favourited_by", id), nil, &accounts, pg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// Reblog is reblog the toot of id and return status of reblog.
|
||||
func (c *Client) Reblog(ctx context.Context, id int64) (*Status, error) {
|
||||
// Reblog reblogs the toot of id and returns status of reblog.
|
||||
func (c *Client) Reblog(ctx context.Context, id ID) (*Status, error) {
|
||||
var status Status
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%d/reblog", id), nil, &status, nil)
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/reblog", id), nil, &status, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
// Unreblog is unreblog the toot of id and return status of the original toot.
|
||||
func (c *Client) Unreblog(ctx context.Context, id int64) (*Status, error) {
|
||||
// Unreblog unreblogs the toot of id and returns status of the original toot.
|
||||
func (c *Client) Unreblog(ctx context.Context, id ID) (*Status, error) {
|
||||
var status Status
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%d/unreblog", id), nil, &status, nil)
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unreblog", id), nil, &status, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
// Favourite is favourite the toot of id and return status of the favourite toot.
|
||||
func (c *Client) Favourite(ctx context.Context, id int64) (*Status, error) {
|
||||
// Favourite favourites the toot of id and returns status of the favourite toot.
|
||||
func (c *Client) Favourite(ctx context.Context, id ID) (*Status, error) {
|
||||
var status Status
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%d/favourite", id), nil, &status, nil)
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/favourite", id), nil, &status, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &status, nil
|
||||
}
|
||||
|
||||
// Unfavourite is unfavourite the toot of id and return status of the unfavourite toot.
|
||||
func (c *Client) Unfavourite(ctx context.Context, id int64) (*Status, error) {
|
||||
// Unfavourite unfavourites the toot of id and returns status of the unfavourite toot.
|
||||
func (c *Client) Unfavourite(ctx context.Context, id ID) (*Status, error) {
|
||||
var status Status
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%d/unfavourite", id), nil, &status, nil)
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unfavourite", id), nil, &status, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -187,6 +349,16 @@ func (c *Client) GetTimelineHashtag(ctx context.Context, tag string, isLocal boo
|
|||
return statuses, nil
|
||||
}
|
||||
|
||||
// GetTimelineList return statuses from a list timeline.
|
||||
func (c *Client) GetTimelineList(ctx context.Context, id ID, pg *Pagination) ([]*Status, error) {
|
||||
var statuses []*Status
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/timelines/list/%s", url.PathEscape(string(id))), nil, &statuses, pg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
// GetTimelineMedia return statuses from media timeline.
|
||||
// NOTE: This is an experimental feature of pawoo.net.
|
||||
func (c *Client) GetTimelineMedia(ctx context.Context, isLocal bool, pg *Pagination) ([]*Status, error) {
|
||||
|
@ -206,28 +378,58 @@ 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 > 0 {
|
||||
params.Set("in_reply_to_id", fmt.Sprint(toot.InReplyToID))
|
||||
if toot.InReplyToID != "" {
|
||||
params.Set("in_reply_to_id", string(toot.InReplyToID))
|
||||
}
|
||||
if toot.MediaIDs != nil {
|
||||
for _, media := range toot.MediaIDs {
|
||||
params.Add("media_ids[]", strconv.FormatInt(media, 10))
|
||||
params.Add("media_ids[]", string(media))
|
||||
}
|
||||
}
|
||||
// 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("senstitive", "true")
|
||||
params.Set("sensitive", "true")
|
||||
}
|
||||
if toot.SpoilerText != "" {
|
||||
params.Set("spoiler_text", toot.SpoilerText)
|
||||
}
|
||||
|
||||
var status Status
|
||||
err := c.doAPI(ctx, http.MethodPost, "/api/v1/statuses", params, &status, nil)
|
||||
var err error
|
||||
if !update {
|
||||
err = c.doAPI(ctx, http.MethodPost, "/api/v1/statuses", params, &status, nil)
|
||||
} else {
|
||||
err = c.doAPI(ctx, http.MethodPut, fmt.Sprintf("/api/v1/statuses/%s", updateID), params, &status, nil)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -235,8 +437,8 @@ func (c *Client) PostStatus(ctx context.Context, toot *Toot) (*Status, error) {
|
|||
}
|
||||
|
||||
// DeleteStatus delete the toot.
|
||||
func (c *Client) DeleteStatus(ctx context.Context, id int64) error {
|
||||
return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/statuses/%d", id), nil, nil, nil)
|
||||
func (c *Client) DeleteStatus(ctx context.Context, id ID) error {
|
||||
return c.doAPI(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/statuses/%s", id), nil, nil, nil)
|
||||
}
|
||||
|
||||
// Search search content with query.
|
||||
|
@ -245,19 +447,81 @@ func (c *Client) Search(ctx context.Context, q string, resolve bool) (*Results,
|
|||
params.Set("q", q)
|
||||
params.Set("resolve", fmt.Sprint(resolve))
|
||||
var results Results
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/search", params, &results, nil)
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v2/search", params, &results, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &results, nil
|
||||
}
|
||||
|
||||
// UploadMedia upload a media attachment.
|
||||
// UploadMedia upload a media attachment from a file.
|
||||
func (c *Client) UploadMedia(ctx context.Context, file string) (*Attachment, error) {
|
||||
var attachment Attachment
|
||||
err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", file, &attachment, nil)
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
|
430
status_test.go
430
status_test.go
|
@ -3,15 +3,16 @@ 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()
|
||||
|
||||
|
@ -36,14 +37,9 @@ func TestGetFavourites(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetStatus(t *testing.T) {
|
||||
func TestGetBookmarks(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/statuses/1234567" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"content": "zzz"}`)
|
||||
return
|
||||
fmt.Fprintln(w, `[{"content": "foo"}, {"content": "bar"}]`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -53,17 +49,60 @@ func TestGetStatus(t *testing.T) {
|
|||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.GetStatus(context.Background(), 123)
|
||||
books, err := client.GetBookmarks(context.Background(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if len(books) != 2 {
|
||||
t.Fatalf("result should be two: %d", len(books))
|
||||
}
|
||||
if books[0].Content != "foo" {
|
||||
t.Fatalf("want %q but %q", "foo", books[0].Content)
|
||||
}
|
||||
if books[1].Content != "bar" {
|
||||
t.Fatalf("want %q but %q", "bar", books[1].Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStatus(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/statuses/1234567" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"content": "zzz", "emojis":[{"shortcode":"💩", "url":"http://example.com", "static_url": "http://example.com/static"}]}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.GetStatus(context.Background(), "123")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
status, err := client.GetStatus(context.Background(), 1234567)
|
||||
status, err := client.GetStatus(context.Background(), "1234567")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if status.Content != "zzz" {
|
||||
t.Fatalf("want %q but %q", "zzz", status.Content)
|
||||
}
|
||||
if len(status.Emojis) != 1 {
|
||||
t.Fatal("should have emojis")
|
||||
}
|
||||
if status.Emojis[0].ShortCode != "💩" {
|
||||
t.Fatalf("want %q but %q", "💩", status.Emojis[0].ShortCode)
|
||||
}
|
||||
if status.Emojis[0].URL != "http://example.com" {
|
||||
t.Fatalf("want %q but %q", "https://example.com", status.Emojis[0].URL)
|
||||
}
|
||||
if status.Emojis[0].StaticURL != "http://example.com/static" {
|
||||
t.Fatalf("want %q but %q", "https://example.com/static", status.Emojis[0].StaticURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStatusCard(t *testing.T) {
|
||||
|
@ -73,7 +112,6 @@ func TestGetStatusCard(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"title": "zzz"}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -83,11 +121,11 @@ func TestGetStatusCard(t *testing.T) {
|
|||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.GetStatusCard(context.Background(), 123)
|
||||
_, err := client.GetStatusCard(context.Background(), "123")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
card, err := client.GetStatusCard(context.Background(), 1234567)
|
||||
card, err := client.GetStatusCard(context.Background(), "1234567")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
|
@ -103,7 +141,6 @@ func TestGetStatusContext(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"ancestors": [{"content": "zzz"},{"content": "bbb"}]}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -113,11 +150,11 @@ func TestGetStatusContext(t *testing.T) {
|
|||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.GetStatusContext(context.Background(), 123)
|
||||
_, err := client.GetStatusContext(context.Background(), "123")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
context, err := client.GetStatusContext(context.Background(), 1234567)
|
||||
context, err := client.GetStatusContext(context.Background(), "1234567")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
|
@ -135,14 +172,13 @@ func TestGetStatusContext(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetRebloggedBy(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/reblogged_by" {
|
||||
if r.URL.Path != "/api/v1/statuses/1234567/source" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`)
|
||||
return
|
||||
fmt.Fprintln(w, `{"id":"1234567","text":"Foo","spoiler_text":"Bar"}%`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -152,11 +188,93 @@ func TestGetRebloggedBy(t *testing.T) {
|
|||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.GetRebloggedBy(context.Background(), 123, nil)
|
||||
_, err := client.GetStatusSource(context.Background(), "123")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
rbs, err := client.GetRebloggedBy(context.Background(), 1234567, nil)
|
||||
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" {
|
||||
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.GetRebloggedBy(context.Background(), "123", nil)
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
rbs, err := client.GetRebloggedBy(context.Background(), "1234567", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
|
@ -178,7 +296,6 @@ func TestGetFavouritedBy(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `[{"username": "foo"}, {"username": "bar"}]`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -188,11 +305,11 @@ func TestGetFavouritedBy(t *testing.T) {
|
|||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.GetFavouritedBy(context.Background(), 123, nil)
|
||||
_, err := client.GetFavouritedBy(context.Background(), "123", nil)
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
fbs, err := client.GetFavouritedBy(context.Background(), 1234567, nil)
|
||||
fbs, err := client.GetFavouritedBy(context.Background(), "1234567", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
|
@ -214,7 +331,6 @@ func TestReblog(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"content": "zzz"}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -224,11 +340,11 @@ func TestReblog(t *testing.T) {
|
|||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.Reblog(context.Background(), 123)
|
||||
_, err := client.Reblog(context.Background(), "123")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
status, err := client.Reblog(context.Background(), 1234567)
|
||||
status, err := client.Reblog(context.Background(), "1234567")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
|
@ -244,7 +360,6 @@ func TestUnreblog(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"content": "zzz"}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -254,11 +369,11 @@ func TestUnreblog(t *testing.T) {
|
|||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.Unreblog(context.Background(), 123)
|
||||
_, err := client.Unreblog(context.Background(), "123")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
status, err := client.Unreblog(context.Background(), 1234567)
|
||||
status, err := client.Unreblog(context.Background(), "1234567")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
|
@ -274,7 +389,6 @@ func TestFavourite(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"content": "zzz"}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -284,11 +398,11 @@ func TestFavourite(t *testing.T) {
|
|||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.Favourite(context.Background(), 123)
|
||||
_, err := client.Favourite(context.Background(), "123")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
status, err := client.Favourite(context.Background(), 1234567)
|
||||
status, err := client.Favourite(context.Background(), "1234567")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
|
@ -304,7 +418,6 @@ func TestUnfavourite(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"content": "zzz"}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -314,11 +427,69 @@ func TestUnfavourite(t *testing.T) {
|
|||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.Unfavourite(context.Background(), 123)
|
||||
_, err := client.Unfavourite(context.Background(), "123")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
status, err := client.Unfavourite(context.Background(), 1234567)
|
||||
status, err := client.Unfavourite(context.Background(), "1234567")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if status.Content != "zzz" {
|
||||
t.Fatalf("want %q but %q", "zzz", status.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBookmark(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/statuses/1234567/bookmark" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"content": "zzz"}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.Bookmark(context.Background(), "123")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
status, err := client.Bookmark(context.Background(), "1234567")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if status.Content != "zzz" {
|
||||
t.Fatalf("want %q but %q", "zzz", status.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnbookmark(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/statuses/1234567/unbookmark" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"content": "zzz"}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.Unbookmark(context.Background(), "123")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
status, err := client.Unbookmark(context.Background(), "1234567")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
|
@ -357,6 +528,28 @@ func TestGetTimelinePublic(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGetTimelineDirect(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, `[{"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" {
|
||||
|
@ -364,7 +557,6 @@ func TestGetTimelineHashtag(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `[{"content": "zzz"},{"content": "yyy"}]`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -393,6 +585,41 @@ 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") == "" {
|
||||
|
@ -400,7 +627,6 @@ func TestGetTimelineMedia(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `[{"content": "zzz"},{"content": "yyy"}]`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -439,7 +665,6 @@ func TestDeleteStatus(t *testing.T) {
|
|||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -449,11 +674,11 @@ func TestDeleteStatus(t *testing.T) {
|
|||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
err := client.DeleteStatus(context.Background(), 123)
|
||||
err := client.DeleteStatus(context.Background(), "123")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
err = client.DeleteStatus(context.Background(), 1234567)
|
||||
err = client.DeleteStatus(context.Background(), "1234567")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
|
@ -461,11 +686,11 @@ func TestDeleteStatus(t *testing.T) {
|
|||
|
||||
func TestSearch(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/search" {
|
||||
if r.URL.Path != "/api/v2/search" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if r.RequestURI != "/api/v1/search?q=q&resolve=false" {
|
||||
if r.RequestURI != "/api/v2/search?q=q&resolve=false" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
@ -473,9 +698,8 @@ func TestSearch(t *testing.T) {
|
|||
fmt.Fprintln(w, `
|
||||
{"accounts":[{"username": "zzz"},{"username": "yyy"}],
|
||||
"statuses":[{"content": "aaa"}],
|
||||
"hashtags":["tag","tag2","tag3"]
|
||||
"hashtags":[{"name": "tag"},{"name": "tag2"},{"name": "tag3"}]
|
||||
}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -504,7 +728,7 @@ func TestSearch(t *testing.T) {
|
|||
if len(ret.Hashtags) != 3 {
|
||||
t.Fatalf("Hashtags have %q entries, but %q", "3", len(ret.Hashtags))
|
||||
}
|
||||
if ret.Hashtags[2] != "tag3" {
|
||||
if ret.Hashtags[2].Name != "tag3" {
|
||||
t.Fatalf("Hashtags[2] should %q , but %q", "tag3", ret.Hashtags[2])
|
||||
}
|
||||
}
|
||||
|
@ -520,7 +744,6 @@ func TestUploadMedia(t *testing.T) {
|
|||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"id": 123}`)
|
||||
return
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -534,7 +757,114 @@ func TestUploadMedia(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if attachment.ID != 123 {
|
||||
t.Fatalf("want %q but %q", 123, attachment.ID)
|
||||
if attachment.ID != "123" {
|
||||
t.Fatalf("want %q but %q", "123", attachment.ID)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
86
streaming.go
86
streaming.go
|
@ -2,52 +2,77 @@ package mastodon
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UpdateEvent is struct for passing status event to app.
|
||||
// UpdateEvent is a struct for passing status event to app.
|
||||
type UpdateEvent struct {
|
||||
Status *Status `json:"status"`
|
||||
}
|
||||
|
||||
func (e *UpdateEvent) event() {}
|
||||
|
||||
// NotificationEvent is struct for passing notification event to app.
|
||||
// UpdateEditEvent is a struct for passing status edit event to app.
|
||||
type UpdateEditEvent struct {
|
||||
Status *Status `json:"status"`
|
||||
}
|
||||
|
||||
func (e *UpdateEditEvent) event() {}
|
||||
|
||||
// NotificationEvent is a struct for passing notification event to app.
|
||||
type NotificationEvent struct {
|
||||
Notification *Notification `json:"notification"`
|
||||
}
|
||||
|
||||
func (e *NotificationEvent) event() {}
|
||||
|
||||
// DeleteEvent is struct for passing deletion event to app.
|
||||
type DeleteEvent struct{ ID int64 }
|
||||
// DeleteEvent is a struct for passing deletion event to app.
|
||||
type DeleteEvent struct{ ID ID }
|
||||
|
||||
func (e *DeleteEvent) event() {}
|
||||
|
||||
// ErrorEvent is struct for passing errors to app.
|
||||
// ErrorEvent is a struct for passing errors to app.
|
||||
type ErrorEvent struct{ err error }
|
||||
|
||||
func (e *ErrorEvent) event() {}
|
||||
func (e *ErrorEvent) Error() string { return e.err.Error() }
|
||||
|
||||
// Event is interface passing events to app.
|
||||
// Event is an interface passing events to app.
|
||||
type Event interface {
|
||||
event()
|
||||
}
|
||||
|
||||
func handleReader(q chan Event, r io.Reader) error {
|
||||
var name string
|
||||
s := bufio.NewScanner(r)
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
token := strings.SplitN(line, ":", 2)
|
||||
var lineBuf bytes.Buffer
|
||||
br := bufio.NewReader(r)
|
||||
for {
|
||||
line, isPrefix, err := br.ReadLine()
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if isPrefix {
|
||||
lineBuf.Write(line)
|
||||
continue
|
||||
}
|
||||
if lineBuf.Len() > 0 {
|
||||
lineBuf.Write(line)
|
||||
line = lineBuf.Bytes()
|
||||
lineBuf.Reset()
|
||||
}
|
||||
|
||||
token := strings.SplitN(string(line), ":", 2)
|
||||
if len(token) != 2 {
|
||||
continue
|
||||
}
|
||||
|
@ -63,6 +88,12 @@ func handleReader(q chan Event, r io.Reader) error {
|
|||
if err == nil {
|
||||
q <- &UpdateEvent{&status}
|
||||
}
|
||||
case "status.update":
|
||||
var status Status
|
||||
err = json.Unmarshal([]byte(token[1]), &status)
|
||||
if err == nil {
|
||||
q <- &UpdateEditEvent{&status}
|
||||
}
|
||||
case "notification":
|
||||
var notification Notification
|
||||
err = json.Unmarshal([]byte(token[1]), ¬ification)
|
||||
|
@ -70,22 +101,17 @@ func handleReader(q chan Event, r io.Reader) error {
|
|||
q <- &NotificationEvent{¬ification}
|
||||
}
|
||||
case "delete":
|
||||
var id int64
|
||||
id, err = strconv.ParseInt(strings.TrimSpace(token[1]), 10, 64)
|
||||
if err == nil {
|
||||
q <- &DeleteEvent{id}
|
||||
}
|
||||
q <- &DeleteEvent{ID: ID(strings.TrimSpace(token[1]))}
|
||||
}
|
||||
if err != nil {
|
||||
q <- &ErrorEvent{err}
|
||||
}
|
||||
}
|
||||
}
|
||||
return s.Err()
|
||||
}
|
||||
|
||||
func (c *Client) streaming(ctx context.Context, p string, params url.Values) (chan Event, error) {
|
||||
u, err := url.Parse(c.config.Server)
|
||||
u, err := url.Parse(c.Config.Server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -97,7 +123,10 @@ func (c *Client) streaming(ctx context.Context, p string, params url.Values) (ch
|
|||
return nil, err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
req.Header.Set("Authorization", "Bearer "+c.config.AccessToken)
|
||||
|
||||
if c.Config.AccessToken != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+c.Config.AccessToken)
|
||||
}
|
||||
|
||||
q := make(chan Event)
|
||||
go func() {
|
||||
|
@ -134,12 +163,12 @@ func (c *Client) doStreaming(req *http.Request, q chan Event) {
|
|||
}
|
||||
}
|
||||
|
||||
// StreamingUser return channel to read events on home.
|
||||
// StreamingUser returns a channel to read events on home.
|
||||
func (c *Client) StreamingUser(ctx context.Context) (chan Event, error) {
|
||||
return c.streaming(ctx, "user", nil)
|
||||
}
|
||||
|
||||
// StreamingPublic return channel to read events on public.
|
||||
// StreamingPublic returns a channel to read events on public.
|
||||
func (c *Client) StreamingPublic(ctx context.Context, isLocal bool) (chan Event, error) {
|
||||
p := "public"
|
||||
if isLocal {
|
||||
|
@ -149,7 +178,7 @@ func (c *Client) StreamingPublic(ctx context.Context, isLocal bool) (chan Event,
|
|||
return c.streaming(ctx, p, nil)
|
||||
}
|
||||
|
||||
// StreamingHashtag return channel to read events on tagged timeline.
|
||||
// StreamingHashtag returns a channel to read events on tagged timeline.
|
||||
func (c *Client) StreamingHashtag(ctx context.Context, tag string, isLocal bool) (chan Event, error) {
|
||||
params := url.Values{}
|
||||
params.Set("tag", tag)
|
||||
|
@ -161,3 +190,16 @@ 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)
|
||||
}
|
||||
|
|
|
@ -1,42 +1,65 @@
|
|||
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(`
|
||||
r := strings.NewReader(fmt.Sprintf(`
|
||||
event: update
|
||||
data: {content: error}
|
||||
event: update
|
||||
data: {"content": "foo"}
|
||||
event: update
|
||||
data: {"content": "%s"}
|
||||
event: notification
|
||||
data: {"type": "mention"}
|
||||
event: delete
|
||||
data: 1234567
|
||||
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.Fatalf("should not be fail: %v", err)
|
||||
t.Errorf("should not be fail: %v", err)
|
||||
}
|
||||
}()
|
||||
var passUpdate, passNotification, passDelete, passError bool
|
||||
var passUpdate, passUpdateLarge, passNotification, passDelete, passError bool
|
||||
for e := range q {
|
||||
switch event := e.(type) {
|
||||
case *UpdateEvent:
|
||||
passUpdate = true
|
||||
if event.Status.Content != "foo" {
|
||||
t.Fatalf("want %q but %q", "foo", event.Status.Content)
|
||||
if event.Status.Content == "foo" {
|
||||
passUpdate = true
|
||||
} else if event.Status.Content == largeContent {
|
||||
passUpdateLarge = true
|
||||
} else {
|
||||
t.Fatalf("bad update content: %q", event.Status.Content)
|
||||
}
|
||||
case *UpdateEditEvent:
|
||||
if event.Status.Content == "foo" {
|
||||
passUpdate = true
|
||||
} else if event.Status.Content == largeContent {
|
||||
passUpdateLarge = true
|
||||
} else {
|
||||
t.Fatalf("bad update content: %q", event.Status.Content)
|
||||
}
|
||||
case *NotificationEvent:
|
||||
passNotification = true
|
||||
|
@ -45,8 +68,8 @@ data: 1234567
|
|||
}
|
||||
case *DeleteEvent:
|
||||
passDelete = true
|
||||
if event.ID != 1234567 {
|
||||
t.Fatalf("want %d but %d", 1234567, event.ID)
|
||||
if event.ID != "1234567" {
|
||||
t.Fatalf("want %q but %q", "1234567", event.ID)
|
||||
}
|
||||
case *ErrorEvent:
|
||||
passError = true
|
||||
|
@ -55,11 +78,12 @@ data: 1234567
|
|||
}
|
||||
}
|
||||
}
|
||||
if !passUpdate || !passNotification || !passDelete || !passError {
|
||||
if !passUpdate || !passUpdateLarge || !passNotification || !passDelete || !passError {
|
||||
t.Fatalf("have not passed through somewhere: "+
|
||||
"update %t, notification %t, delete %t, error %t",
|
||||
passUpdate, passNotification, passDelete, passError)
|
||||
"update: %t, update (large): %t, notification: %t, delete: %t, error: %t",
|
||||
passUpdate, passUpdateLarge, passNotification, passDelete, passError)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestStreaming(t *testing.T) {
|
||||
|
@ -111,6 +135,12 @@ data: {"content": "foo"}
|
|||
if event.Status.Content != "foo" {
|
||||
t.Fatalf("want %q but %q", "foo", event.Status.Content)
|
||||
}
|
||||
case *UpdateEditEvent:
|
||||
cnt++
|
||||
passUpdate = true
|
||||
if event.Status.Content != "foo" {
|
||||
t.Fatalf("want %q but %q", "foo", event.Status.Content)
|
||||
}
|
||||
}
|
||||
}
|
||||
if cnt != 1 {
|
||||
|
@ -139,11 +169,14 @@ func TestDoStreaming(t *testing.T) {
|
|||
req = req.WithContext(ctx)
|
||||
|
||||
q := make(chan Event)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer close(q)
|
||||
c.doStreaming(req, q)
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
t.Errorf("should not be fail: %v", err)
|
||||
}
|
||||
}()
|
||||
var passError bool
|
||||
|
@ -158,6 +191,7 @@ func TestDoStreaming(t *testing.T) {
|
|||
if !passError {
|
||||
t.Fatalf("have not passed through: error %t", passError)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestStreamingUser(t *testing.T) {
|
||||
|
@ -293,3 +327,83 @@ 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,10 @@ package mastodon
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
@ -49,15 +51,20 @@ 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
|
||||
}
|
||||
|
@ -120,6 +127,12 @@ func (c *WSClient) handleWS(ctx context.Context, rawurl string, q chan Event) er
|
|||
if err == nil {
|
||||
q <- &UpdateEvent{Status: &status}
|
||||
}
|
||||
case "status.update":
|
||||
var status Status
|
||||
err = json.Unmarshal([]byte(s.Payload.(string)), &status)
|
||||
if err == nil {
|
||||
q <- &UpdateEditEvent{Status: &status}
|
||||
}
|
||||
case "notification":
|
||||
var notification Notification
|
||||
err = json.Unmarshal([]byte(s.Payload.(string)), ¬ification)
|
||||
|
@ -127,7 +140,11 @@ func (c *WSClient) handleWS(ctx context.Context, rawurl string, q chan Event) er
|
|||
q <- &NotificationEvent{Notification: ¬ification}
|
||||
}
|
||||
case "delete":
|
||||
q <- &DeleteEvent{ID: int64(s.Payload.(float64))}
|
||||
if f, ok := s.Payload.(float64); ok {
|
||||
q <- &DeleteEvent{ID: ID(fmt.Sprint(int64(f)))}
|
||||
} else {
|
||||
q <- &DeleteEvent{ID: ID(strings.TrimSpace(s.Payload.(string)))}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
q <- &ErrorEvent{err}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -79,6 +80,13 @@ func wsMock(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
err = conn.WriteMessage(websocket.TextMessage,
|
||||
[]byte(`{"event":"status.update","payload":"{\"content\":\"bar\"}"}`))
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = conn.WriteMessage(websocket.TextMessage,
|
||||
[]byte(`{"event":"notification","payload":"{\"id\":123}"}`))
|
||||
if err != nil {
|
||||
|
@ -111,20 +119,20 @@ func wsTest(t *testing.T, q chan Event, cancel func()) {
|
|||
for e := range q {
|
||||
events = append(events, e)
|
||||
}
|
||||
if len(events) != 6 {
|
||||
t.Fatalf("result should be four: %d", len(events))
|
||||
if len(events) != 7 {
|
||||
t.Fatalf("result should be seven: %d", len(events))
|
||||
}
|
||||
if events[0].(*UpdateEvent).Status.Content != "foo" {
|
||||
t.Fatalf("want %q but %q", "foo", events[0].(*UpdateEvent).Status.Content)
|
||||
}
|
||||
if events[1].(*NotificationEvent).Notification.ID != 123 {
|
||||
t.Fatalf("want %d but %d", 123, events[1].(*NotificationEvent).Notification.ID)
|
||||
if events[1].(*UpdateEditEvent).Status.Content != "bar" {
|
||||
t.Fatalf("want %q but %q", "bar", events[1].(*UpdateEditEvent).Status.Content)
|
||||
}
|
||||
if events[2].(*DeleteEvent).ID != 1234567 {
|
||||
t.Fatalf("want %d but %d", 1234567, events[2].(*DeleteEvent).ID)
|
||||
if events[2].(*NotificationEvent).Notification.ID != "123" {
|
||||
t.Fatalf("want %q but %q", "123", events[2].(*NotificationEvent).Notification.ID)
|
||||
}
|
||||
if errorEvent, ok := events[3].(*ErrorEvent); !ok {
|
||||
t.Fatalf("should be fail: %v", errorEvent.err)
|
||||
if events[3].(*DeleteEvent).ID != "1234567" {
|
||||
t.Fatalf("want %q but %q", "1234567", events[3].(*DeleteEvent).ID)
|
||||
}
|
||||
if errorEvent, ok := events[4].(*ErrorEvent); !ok {
|
||||
t.Fatalf("should be fail: %v", errorEvent.err)
|
||||
|
@ -132,6 +140,9 @@ func wsTest(t *testing.T, q chan Event, cancel func()) {
|
|||
if errorEvent, ok := events[5].(*ErrorEvent); !ok {
|
||||
t.Fatalf("should be fail: %v", errorEvent.err)
|
||||
}
|
||||
if errorEvent, ok := events[6].(*ErrorEvent); !ok {
|
||||
t.Fatalf("should be fail: %v", errorEvent.err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamingWS(t *testing.T) {
|
||||
|
@ -151,12 +162,16 @@ func TestStreamingWS(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
e := <-q
|
||||
if errorEvent, ok := e.(*ErrorEvent); !ok {
|
||||
t.Fatalf("should be fail: %v", errorEvent.err)
|
||||
t.Errorf("should be fail: %v", errorEvent.err)
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestHandleWS(t *testing.T) {
|
||||
|
@ -183,10 +198,13 @@ func TestHandleWS(t *testing.T) {
|
|||
q := make(chan Event)
|
||||
client := NewClient(&Config{}).NewWSClient()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
e := <-q
|
||||
if errorEvent, ok := e.(*ErrorEvent); !ok {
|
||||
t.Fatalf("should be fail: %v", errorEvent.err)
|
||||
t.Errorf("should be fail: %v", errorEvent.err)
|
||||
}
|
||||
}()
|
||||
err := client.handleWS(context.Background(), ":", q)
|
||||
|
@ -196,10 +214,12 @@ func TestHandleWS(t *testing.T) {
|
|||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
e := <-q
|
||||
if errorEvent, ok := e.(*ErrorEvent); !ok {
|
||||
t.Fatalf("should be fail: %v", errorEvent.err)
|
||||
t.Errorf("should be fail: %v", errorEvent.err)
|
||||
}
|
||||
}()
|
||||
err = client.handleWS(ctx, "ws://"+ts.Listener.Addr().String(), q)
|
||||
|
@ -207,13 +227,17 @@ func TestHandleWS(t *testing.T) {
|
|||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
e := <-q
|
||||
if errorEvent, ok := e.(*ErrorEvent); !ok {
|
||||
t.Fatalf("should be fail: %v", errorEvent.err)
|
||||
t.Errorf("should be fail: %v", errorEvent.err)
|
||||
}
|
||||
}()
|
||||
client.handleWS(context.Background(), "ws://"+ts.Listener.Addr().String(), q)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestDialRedirect(t *testing.T) {
|
||||
|
@ -243,12 +267,12 @@ func TestDial(t *testing.T) {
|
|||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
|
||||
_, rawurl, err := client.dial("ws://" + ts.Listener.Addr().String())
|
||||
_, _, err = client.dial("ws://" + ts.Listener.Addr().String())
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
|
||||
_, rawurl, err = client.dial("ws://" + ts.Listener.Addr().String())
|
||||
_, rawurl, err := client.dial("ws://" + ts.Listener.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package mastodon
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Unixtime time.Time
|
||||
|
||||
func (t *Unixtime) UnmarshalJSON(data []byte) error {
|
||||
if len(data) > 0 && data[0] == '"' && data[len(data)-1] == '"' {
|
||||
data = data[1 : len(data)-1]
|
||||
}
|
||||
ts, err := strconv.ParseInt(string(data), 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*t = Unixtime(time.Unix(ts, 0))
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue