Merge 2b33d0ed4b
into 9faaa4f0dc
This commit is contained in:
commit
4fd6f6cc4c
18 changed files with 658 additions and 50 deletions
12
README.md
12
README.md
|
@ -102,6 +102,7 @@ func main() {
|
|||
* [x] GET /api/v1/follow_requests
|
||||
* [x] POST /api/v1/follow_requests/:id/authorize
|
||||
* [x] POST /api/v1/follow_requests/:id/reject
|
||||
* [x] GET /api/v1/followed_tags
|
||||
* [x] POST /api/v1/follows
|
||||
* [x] GET /api/v1/instance
|
||||
* [x] GET /api/v1/instance/activity
|
||||
|
@ -143,16 +144,19 @@ func main() {
|
|||
* [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
|
||||
* [x] GET /api/v1/tags/:hashtag
|
||||
* [x] POST /api/v1/tags/:hashtag/follow
|
||||
* [x] POST /api/v1/tags/:hashtag/unfollow
|
||||
* [x] GET /api/v1/timelines/home
|
||||
* [x] GET /api/v1/timelines/public
|
||||
* [x] GET /api/v1/timelines/tag/:hashtag
|
||||
* [x] GET /api/v1/timelines/list/:id
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
14
accounts.go
14
accounts.go
|
@ -284,6 +284,20 @@ func (c *Client) AccountsSearch(ctx context.Context, q string, limit int64) ([]*
|
|||
return accounts, nil
|
||||
}
|
||||
|
||||
func (c *Client) AccountsSearchResolve(ctx context.Context, q string, limit int64, resolve bool) ([]*Account, error) {
|
||||
params := url.Values{}
|
||||
params.Set("q", q)
|
||||
params.Set("limit", fmt.Sprint(limit))
|
||||
params.Set("resolve", fmt.Sprint(resolve))
|
||||
|
||||
var accounts []*Account
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/accounts/search", params, &accounts, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
// FollowRemoteUser sends follow-request.
|
||||
func (c *Client) FollowRemoteUser(ctx context.Context, uri string) (*Account, error) {
|
||||
params := url.Values{}
|
||||
|
|
|
@ -546,6 +546,44 @@ func TestAccountsSearch(t *testing.T) {
|
|||
t.Fatalf("want %q but %q", "barfoo", res[1].Username)
|
||||
}
|
||||
}
|
||||
func TestAccountsSearchResolve(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Query()["q"][0] != "foo" {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if r.FormValue("resolve") != "true" {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `[{"username": "foobar"}, {"username": "barfoo"}]`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.AccountsSearchResolve(context.Background(), "zzz", 2, false)
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
res, err := client.AccountsSearchResolve(context.Background(), "foo", 2, true)
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if len(res) != 2 {
|
||||
t.Fatalf("result should be two: %d", len(res))
|
||||
}
|
||||
if res[0].Username != "foobar" {
|
||||
t.Fatalf("want %q but %q", "foobar", res[0].Username)
|
||||
}
|
||||
if res[1].Username != "barfoo" {
|
||||
t.Fatalf("want %q but %q", "barfoo", res[1].Username)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFollowRemoteUser(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
12
filters.go
12
filters.go
|
@ -19,6 +19,18 @@ type Filter struct {
|
|||
Irreversible bool `json:"irreversible"`
|
||||
}
|
||||
|
||||
type FilterResult struct {
|
||||
Filter struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Context []string `json:"context"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
FilterAction string `json:"filter_action"`
|
||||
} `json:"filter"`
|
||||
KeywordMatches []string `json:"keyword_matches"`
|
||||
StatusMatches []string `json:"status_matches"`
|
||||
}
|
||||
|
||||
// GetFilters returns all the filters on the current account.
|
||||
func (c *Client) GetFilters(ctx context.Context) ([]*Filter, error) {
|
||||
var filters []*Filter
|
||||
|
|
4
lists.go
4
lists.go
|
@ -90,7 +90,7 @@ func (c *Client) DeleteList(ctx context.Context, id ID) error {
|
|||
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))
|
||||
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)
|
||||
|
@ -100,7 +100,7 @@ func (c *Client) AddToList(ctx context.Context, list ID, accounts ...ID) error {
|
|||
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))
|
||||
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)
|
||||
|
|
|
@ -235,7 +235,7 @@ func TestAddToList(t *testing.T) {
|
|||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if r.PostFormValue("account_ids") != "1" {
|
||||
if r.PostFormValue("account_ids[]") != "1" {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -251,13 +251,6 @@ type Mention struct {
|
|||
ID ID `json:"id"`
|
||||
}
|
||||
|
||||
// Tag hold information for tag.
|
||||
type Tag struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
History []History `json:"history"`
|
||||
}
|
||||
|
||||
// History hold information for history.
|
||||
type History struct {
|
||||
Day string `json:"day"`
|
||||
|
|
|
@ -594,6 +594,7 @@ func TestForTheCoverages(t *testing.T) {
|
|||
(*UpdateEvent)(nil).event()
|
||||
(*UpdateEditEvent)(nil).event()
|
||||
(*NotificationEvent)(nil).event()
|
||||
(*ConversationEvent)(nil).event()
|
||||
(*DeleteEvent)(nil).event()
|
||||
(*ErrorEvent)(nil).event()
|
||||
_ = (&ErrorEvent{io.EOF}).Error()
|
||||
|
|
|
@ -37,8 +37,19 @@ type PushAlerts struct {
|
|||
|
||||
// GetNotifications returns notifications.
|
||||
func (c *Client) GetNotifications(ctx context.Context, pg *Pagination) ([]*Notification, error) {
|
||||
return c.GetNotificationsExclude(ctx, nil, pg)
|
||||
}
|
||||
|
||||
// GetNotificationsExclude returns notifications with excluded notifications
|
||||
func (c *Client) GetNotificationsExclude(ctx context.Context, exclude *[]string, pg *Pagination) ([]*Notification, error) {
|
||||
var notifications []*Notification
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/notifications", nil, ¬ifications, pg)
|
||||
params := url.Values{}
|
||||
if exclude != nil {
|
||||
for _, ex := range *exclude {
|
||||
params.Add("exclude_types[]", ex)
|
||||
}
|
||||
}
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/notifications", params, ¬ifications, pg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -15,7 +15,11 @@ func TestGetNotifications(t *testing.T) {
|
|||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/notifications":
|
||||
fmt.Fprintln(w, `[{"id": 122, "action_taken": false}, {"id": 123, "action_taken": true}]`)
|
||||
if r.URL.Query().Get("exclude_types[]") == "follow" {
|
||||
fmt.Fprintln(w, `[{"id": 321, "action_taken": true}]`)
|
||||
} else {
|
||||
fmt.Fprintln(w, `[{"id": 122, "action_taken": false}, {"id": 123, "action_taken": true}]`)
|
||||
}
|
||||
return
|
||||
case "/api/v1/notifications/123":
|
||||
fmt.Fprintln(w, `{"id": 123, "action_taken": true}`)
|
||||
|
@ -50,6 +54,16 @@ func TestGetNotifications(t *testing.T) {
|
|||
if ns[1].ID != "123" {
|
||||
t.Fatalf("want %v but %v", "123", ns[1].ID)
|
||||
}
|
||||
nse, err := client.GetNotificationsExclude(context.Background(), &[]string{"follow"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if len(nse) != 1 {
|
||||
t.Fatalf("result should be one: %d", len(nse))
|
||||
}
|
||||
if nse[0].ID != "321" {
|
||||
t.Fatalf("want %v but %v", "321", nse[0].ID)
|
||||
}
|
||||
n, err := client.GetNotification(context.Background(), "123")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
|
|
91
status.go
91
status.go
|
@ -15,35 +15,36 @@ import (
|
|||
|
||||
// Status is struct to hold status.
|
||||
type Status struct {
|
||||
ID ID `json:"id"`
|
||||
URI string `json:"uri"`
|
||||
URL string `json:"url"`
|
||||
Account Account `json:"account"`
|
||||
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"`
|
||||
MediaAttachments []Attachment `json:"media_attachments"`
|
||||
Mentions []Mention `json:"mentions"`
|
||||
Tags []Tag `json:"tags"`
|
||||
Card *Card `json:"card"`
|
||||
Poll *Poll `json:"poll"`
|
||||
Application Application `json:"application"`
|
||||
Language string `json:"language"`
|
||||
Pinned interface{} `json:"pinned"`
|
||||
ID ID `json:"id"`
|
||||
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"`
|
||||
MediaAttachments []Attachment `json:"media_attachments"`
|
||||
Mentions []Mention `json:"mentions"`
|
||||
Tags []Tag `json:"tags"`
|
||||
Card *Card `json:"card"`
|
||||
Poll *Poll `json:"poll"`
|
||||
Application Application `json:"application"`
|
||||
Language string `json:"language"`
|
||||
Pinned interface{} `json:"pinned"`
|
||||
Filtered []FilterResult `json:"filtered"`
|
||||
}
|
||||
|
||||
// StatusHistory is a struct to hold status history data.
|
||||
|
@ -102,6 +103,12 @@ type Media struct {
|
|||
Focus string
|
||||
}
|
||||
|
||||
type TagData struct {
|
||||
Any []string
|
||||
All []string
|
||||
None []string
|
||||
}
|
||||
|
||||
func (m *Media) bodyAndContentType() (io.Reader, string, error) {
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
|
@ -349,6 +356,32 @@ func (c *Client) GetTimelineHashtag(ctx context.Context, tag string, isLocal boo
|
|||
return statuses, nil
|
||||
}
|
||||
|
||||
// GetTimelineHashtag return statuses from tagged timeline.
|
||||
func (c *Client) GetTimelineHashtagMultiple(ctx context.Context, tag string, isLocal bool, td *TagData, pg *Pagination) ([]*Status, error) {
|
||||
params := url.Values{}
|
||||
if isLocal {
|
||||
params.Set("local", "t")
|
||||
}
|
||||
if td != nil {
|
||||
for _, v := range td.Any {
|
||||
params.Add("any[]", v)
|
||||
}
|
||||
for _, v := range td.All {
|
||||
params.Add("all[]", v)
|
||||
}
|
||||
for _, v := range td.None {
|
||||
params.Add("none[]", v)
|
||||
}
|
||||
}
|
||||
|
||||
var statuses []*Status
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/timelines/tag/%s", url.PathEscape(tag)), params, &statuses, pg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
|
|
|
@ -70,7 +70,7 @@ func TestGetStatus(t *testing.T) {
|
|||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `{"content": "zzz", "emojis":[{"shortcode":"💩", "url":"http://example.com", "static_url": "http://example.com/static"}]}`)
|
||||
fmt.Fprintln(w, `{"content": "zzz", "emojis":[{"shortcode":"💩", "url":"http://example.com", "static_url": "http://example.com/static"}], "filtered": [{"filter": {"id": "3", "title": "Hide completely", "context": ["home"], "expires_at": "2022-09-20T17:27:39.296Z", "filter_action": "hide"}, "keyword_matches": ["bad word"], "status_matches": ["109031743575371913"]}]}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
|
@ -103,6 +103,36 @@ func TestGetStatus(t *testing.T) {
|
|||
if status.Emojis[0].StaticURL != "http://example.com/static" {
|
||||
t.Fatalf("want %q but %q", "https://example.com/static", status.Emojis[0].StaticURL)
|
||||
}
|
||||
if len(status.Filtered) != 1 {
|
||||
t.Fatal("should have filtered")
|
||||
}
|
||||
if status.Filtered[0].Filter.ID != "3" {
|
||||
t.Fatalf("want %q but %q", "3", status.Filtered[0].Filter.ID)
|
||||
}
|
||||
if status.Filtered[0].Filter.Title != "Hide completely" {
|
||||
t.Fatalf("want %q but %q", "Hide completely", status.Filtered[0].Filter.Title)
|
||||
}
|
||||
if len(status.Filtered[0].Filter.Context) != 1 {
|
||||
t.Fatal("should have one context")
|
||||
}
|
||||
if status.Filtered[0].Filter.Context[0] != "home" {
|
||||
t.Fatalf("want %q but %q", "home", status.Filtered[0].Filter.Context[0])
|
||||
}
|
||||
if status.Filtered[0].Filter.FilterAction != "hide" {
|
||||
t.Fatalf("want %q but %q", "hide", status.Filtered[0].Filter.FilterAction)
|
||||
}
|
||||
if len(status.Filtered[0].KeywordMatches) != 1 {
|
||||
t.Fatal("should have one matching keyword")
|
||||
}
|
||||
if status.Filtered[0].KeywordMatches[0] != "bad word" {
|
||||
t.Fatalf("want %q but %q", "bad word", status.Filtered[0].KeywordMatches[0])
|
||||
}
|
||||
if len(status.Filtered[0].StatusMatches) != 1 {
|
||||
t.Fatal("should have one matching status")
|
||||
}
|
||||
if status.Filtered[0].StatusMatches[0] != "109031743575371913" {
|
||||
t.Fatalf("want %q but %q", "109031743575371913", status.Filtered[0].StatusMatches[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStatusCard(t *testing.T) {
|
||||
|
@ -551,6 +581,53 @@ func TestGetTimelineDirect(t *testing.T) {
|
|||
}
|
||||
|
||||
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" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if r.FormValue("any[]") != "aaa" {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if r.FormValue("all[]") != "bbb" {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if r.FormValue("none[]") != "ccc" {
|
||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
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.GetTimelineHashtagMultiple(context.Background(), "notfound", false, &TagData{}, nil)
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
tags, err := client.GetTimelineHashtagMultiple(context.Background(), "zzz", true, &TagData{Any: []string{"aaa"}, All: []string{"bbb"}, None: []string{"ccc"}}, 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 TestGetTimelineHashtagMultiple(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/timelines/tag/zzz" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
|
|
13
streaming.go
13
streaming.go
|
@ -39,6 +39,13 @@ type DeleteEvent struct{ ID ID }
|
|||
|
||||
func (e *DeleteEvent) event() {}
|
||||
|
||||
// ConversationEvent is a struct for passing conversationevent to app.
|
||||
type ConversationEvent struct {
|
||||
Conversation *Conversation `json:"conversation"`
|
||||
}
|
||||
|
||||
func (e *ConversationEvent) event() {}
|
||||
|
||||
// ErrorEvent is a struct for passing errors to app.
|
||||
type ErrorEvent struct{ err error }
|
||||
|
||||
|
@ -100,6 +107,12 @@ func handleReader(q chan Event, r io.Reader) error {
|
|||
if err == nil {
|
||||
q <- &NotificationEvent{¬ification}
|
||||
}
|
||||
case "conversation":
|
||||
var conversation Conversation
|
||||
err = json.Unmarshal([]byte(token[1]), &conversation)
|
||||
if err == nil {
|
||||
q <- &ConversationEvent{&conversation}
|
||||
}
|
||||
case "delete":
|
||||
q <- &DeleteEvent{ID: ID(strings.TrimSpace(token[1]))}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@ event: delete
|
|||
data: 1234567
|
||||
event: status.update
|
||||
data: {"content": "foo"}
|
||||
event: conversation
|
||||
data: {"id":"819516","unread":true,"accounts":[{"id":"108892712797543112","username":"a","acct":"a@pl.nulled.red","display_name":"a","locked":false,"bot":true,"discoverable":false,"group":false,"created_at":"2022-08-27T00:00:00.000Z","note":"a (pleroma edition)","url":"https://pl.nulled.red/users/a","avatar":"https://files.mastodon.social/cache/accounts/avatars/108/892/712/797/543/112/original/975674b2caa61034.png","avatar_static":"https://files.mastodon.social/cache/accounts/avatars/108/892/712/797/543/112/original/975674b2caa61034.png","header":"https://files.mastodon.social/cache/accounts/headers/108/892/712/797/543/112/original/f61d0382356caa0e.png","header_static":"https://files.mastodon.social/cache/accounts/headers/108/892/712/797/543/112/original/f61d0382356caa0e.png","followers_count":0,"following_count":0,"statuses_count":362,"last_status_at":"2022-11-13","emojis":[],"fields":[]}],"last_status":{"id":"109346889330629417","created_at":"2022-11-15T08:31:57.476Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"direct","language":null,"uri":"https://pl.nulled.red/objects/c869c5be-c184-4706-a45d-3459d9aa711c","url":"https://pl.nulled.red/objects/c869c5be-c184-4706-a45d-3459d9aa711c","replies_count":0,"reblogs_count":0,"favourites_count":0,"edited_at":null,"favourited":false,"reblogged":false,"muted":false,"bookmarked":false,"content":"test <span class=\"h-card\"><a class=\"u-url mention\" href=\"https://mastodon.social/@trwnh\" rel=\"nofollow noopener noreferrer\" target=\"_blank\">@<span>trwnh</span></a></span>","filtered":[],"reblog":null,"account":{"id":"108892712797543112","username":"a","acct":"a@pl.nulled.red","display_name":"a","locked":false,"bot":true,"discoverable":false,"group":false,"created_at":"2022-08-27T00:00:00.000Z","note":"a (pleroma edition)","url":"https://pl.nulled.red/users/a","avatar":"https://files.mastodon.social/cache/accounts/avatars/108/892/712/797/543/112/original/975674b2caa61034.png","avatar_static":"https://files.mastodon.social/cache/accounts/avatars/108/892/712/797/543/112/original/975674b2caa61034.png","header":"https://files.mastodon.social/cache/accounts/headers/108/892/712/797/543/112/original/f61d0382356caa0e.png","header_static":"https://files.mastodon.social/cache/accounts/headers/108/892/712/797/543/112/original/f61d0382356caa0e.png","followers_count":0,"following_count":0,"statuses_count":362,"last_status_at":"2022-11-13","emojis":[],"fields":[]},"media_attachments":[],"mentions":[{"id":"14715","username":"trwnh","url":"https://mastodon.social/@trwnh","acct":"trwnh"}],"tags":[],"emojis":[],"card":null,"poll":null}}
|
||||
:thump
|
||||
`, largeContent))
|
||||
var wg sync.WaitGroup
|
||||
|
@ -61,6 +63,11 @@ data: {"content": "foo"}
|
|||
} else {
|
||||
t.Fatalf("bad update content: %q", event.Status.Content)
|
||||
}
|
||||
case *ConversationEvent:
|
||||
passNotification = true
|
||||
if event.Conversation.ID != "819516" {
|
||||
t.Fatalf("want %q but %q", "819516", event.Conversation.ID)
|
||||
}
|
||||
case *NotificationEvent:
|
||||
passNotification = true
|
||||
if event.Notification.Type != "mention" {
|
||||
|
|
|
@ -56,6 +56,11 @@ func (c *WSClient) StreamingWSList(ctx context.Context, id ID) (chan Event, erro
|
|||
return c.streamingWS(ctx, "list", string(id))
|
||||
}
|
||||
|
||||
// StreamingWSDirect return channel to read events on a direct messages using WebSocket.
|
||||
func (c *WSClient) StreamingWSDirect(ctx context.Context) (chan Event, error) {
|
||||
return c.streamingWS(ctx, "direct", "")
|
||||
}
|
||||
|
||||
func (c *WSClient) streamingWS(ctx context.Context, stream, tag string) (chan Event, error) {
|
||||
params := url.Values{}
|
||||
params.Set("access_token", c.client.Config.AccessToken)
|
||||
|
@ -139,6 +144,12 @@ func (c *WSClient) handleWS(ctx context.Context, rawurl string, q chan Event) er
|
|||
if err == nil {
|
||||
q <- &NotificationEvent{Notification: ¬ification}
|
||||
}
|
||||
case "conversation":
|
||||
var conversation Conversation
|
||||
err = json.Unmarshal([]byte(s.Payload.(string)), &conversation)
|
||||
if err == nil {
|
||||
q <- &ConversationEvent{Conversation: &conversation}
|
||||
}
|
||||
case "delete":
|
||||
if f, ok := s.Payload.(float64); ok {
|
||||
q <- &DeleteEvent{ID: ID(fmt.Sprint(int64(f)))}
|
||||
|
|
|
@ -59,6 +59,32 @@ func TestStreamingWSHashtag(t *testing.T) {
|
|||
wsTest(t, q, cancel)
|
||||
}
|
||||
|
||||
func TestStreamingWSList(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(wsMock))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{Server: ts.URL}).NewWSClient()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
q, err := client.StreamingWSList(ctx, "123")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
wsTest(t, q, cancel)
|
||||
}
|
||||
|
||||
func TestStreamingWSDirect(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(wsMock))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{Server: ts.URL}).NewWSClient()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
q, err := client.StreamingWSDirect(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
wsTest(t, q, cancel)
|
||||
}
|
||||
|
||||
func wsMock(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/streaming" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
|
@ -101,6 +127,13 @@ func wsMock(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
err = conn.WriteMessage(websocket.TextMessage,
|
||||
[]byte(`{"event":"conversation","payload":"{\"id\":819516}"}`))
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = conn.WriteMessage(websocket.TextMessage,
|
||||
[]byte(`{"event":"update","payload":"<html></html>"}`))
|
||||
if err != nil {
|
||||
|
@ -119,8 +152,8 @@ func wsTest(t *testing.T, q chan Event, cancel func()) {
|
|||
for e := range q {
|
||||
events = append(events, e)
|
||||
}
|
||||
if len(events) != 7 {
|
||||
t.Fatalf("result should be seven: %d", len(events))
|
||||
if len(events) != 8 {
|
||||
t.Fatalf("result should be 8: %d", len(events))
|
||||
}
|
||||
if events[0].(*UpdateEvent).Status.Content != "foo" {
|
||||
t.Fatalf("want %q but %q", "foo", events[0].(*UpdateEvent).Status.Content)
|
||||
|
@ -134,8 +167,8 @@ func wsTest(t *testing.T, q chan Event, cancel func()) {
|
|||
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)
|
||||
if events[4].(*ConversationEvent).Conversation.ID != "819516" {
|
||||
t.Fatalf("want %q but %q", "819516", events[4].(*ConversationEvent).Conversation.ID)
|
||||
}
|
||||
if errorEvent, ok := events[5].(*ErrorEvent); !ok {
|
||||
t.Fatalf("should be fail: %v", errorEvent.err)
|
||||
|
@ -143,6 +176,9 @@ func wsTest(t *testing.T, q chan Event, cancel func()) {
|
|||
if errorEvent, ok := events[6].(*ErrorEvent); !ok {
|
||||
t.Fatalf("should be fail: %v", errorEvent.err)
|
||||
}
|
||||
if errorEvent, ok := events[7].(*ErrorEvent); !ok {
|
||||
t.Fatalf("should be fail: %v", errorEvent.err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamingWS(t *testing.T) {
|
||||
|
|
55
tags.go
Normal file
55
tags.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package mastodon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Tag hold information for tag.
|
||||
type Tag struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
History []History `json:"history"`
|
||||
Following interface{} `json:"following"`
|
||||
}
|
||||
|
||||
// TagInfo gets statistics and information about a tag
|
||||
func (c *Client) TagInfo(ctx context.Context, tag string) (*Tag, error) {
|
||||
var hashtag Tag
|
||||
err := c.doAPI(ctx, http.MethodGet, fmt.Sprintf("/api/v1/tags/%s", url.PathEscape(string(tag))), nil, &hashtag, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &hashtag, nil
|
||||
}
|
||||
|
||||
// TagFollow lets you follow a hashtag
|
||||
func (c *Client) TagFollow(ctx context.Context, tag string) (*Tag, error) {
|
||||
var hashtag Tag
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/tags/%s/follow", url.PathEscape(string(tag))), nil, &hashtag, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &hashtag, nil
|
||||
}
|
||||
|
||||
// TagUnfollow lets you unfollow a hashtag
|
||||
func (c *Client) TagUnfollow(ctx context.Context, tag string) (*Tag, error) {
|
||||
var hashtag Tag
|
||||
err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/tags/%s/unfollow", url.PathEscape(string(tag))), nil, &hashtag, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &hashtag, nil
|
||||
}
|
||||
|
||||
func (c *Client) TagsFollowed(ctx context.Context, pg *Pagination) ([]*Tag, error) {
|
||||
var hashtags []*Tag
|
||||
err := c.doAPI(ctx, http.MethodGet, "/api/v1/followed_tags", nil, &hashtags, pg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hashtags, nil
|
||||
}
|
289
tags_test.go
Normal file
289
tags_test.go
Normal file
|
@ -0,0 +1,289 @@
|
|||
package mastodon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTagInfo(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/tags/test" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `
|
||||
{
|
||||
"name": "test",
|
||||
"url": "http://mastodon.example/tags/test",
|
||||
"history": [
|
||||
{
|
||||
"day": "1668124800",
|
||||
"accounts": "1",
|
||||
"uses": "2"
|
||||
},
|
||||
{
|
||||
"day": "1668038400",
|
||||
"accounts": "0",
|
||||
"uses": "0"
|
||||
}
|
||||
]
|
||||
}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.TagInfo(context.Background(), "foo")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
tag, err := client.TagInfo(context.Background(), "test")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if tag.Name != "test" {
|
||||
t.Fatalf("want %q but %q", "test", tag.Name)
|
||||
}
|
||||
if tag.URL != "http://mastodon.example/tags/test" {
|
||||
t.Fatalf("want %q but %q", "http://mastodon.example/tags/test", tag.URL)
|
||||
}
|
||||
if len(tag.History) != 2 {
|
||||
t.Fatalf("result should be two: %d", len(tag.History))
|
||||
}
|
||||
if tag.History[0].Day != "1668124800" {
|
||||
t.Fatalf("want %q but %q", "1668124800", tag.History[0].Day)
|
||||
}
|
||||
if tag.History[0].Accounts != "1" {
|
||||
t.Fatalf("want %q but %q", "1", tag.History[0].Accounts)
|
||||
}
|
||||
if tag.History[0].Uses != "2" {
|
||||
t.Fatalf("want %q but %q", "2", tag.History[0].Uses)
|
||||
}
|
||||
if tag.Following != nil {
|
||||
t.Fatalf("want %v but %q", nil, tag.Following)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagFollow(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/tags/test/follow" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `
|
||||
{
|
||||
"name": "test",
|
||||
"url": "http://mastodon.example/tags/test",
|
||||
"history": [
|
||||
{
|
||||
"day": "1668124800",
|
||||
"accounts": "1",
|
||||
"uses": "2"
|
||||
},
|
||||
{
|
||||
"day": "1668038400",
|
||||
"accounts": "0",
|
||||
"uses": "0"
|
||||
}
|
||||
],
|
||||
"following": true
|
||||
}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.TagFollow(context.Background(), "foo")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
tag, err := client.TagFollow(context.Background(), "test")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if tag.Name != "test" {
|
||||
t.Fatalf("want %q but %q", "test", tag.Name)
|
||||
}
|
||||
if tag.URL != "http://mastodon.example/tags/test" {
|
||||
t.Fatalf("want %q but %q", "http://mastodon.example/tags/test", tag.URL)
|
||||
}
|
||||
if len(tag.History) != 2 {
|
||||
t.Fatalf("result should be two: %d", len(tag.History))
|
||||
}
|
||||
if tag.History[0].Day != "1668124800" {
|
||||
t.Fatalf("want %q but %q", "1668124800", tag.History[0].Day)
|
||||
}
|
||||
if tag.History[0].Accounts != "1" {
|
||||
t.Fatalf("want %q but %q", "1", tag.History[0].Accounts)
|
||||
}
|
||||
if tag.History[0].Uses != "2" {
|
||||
t.Fatalf("want %q but %q", "2", tag.History[0].Uses)
|
||||
}
|
||||
if tag.Following != true {
|
||||
t.Fatalf("want %v but %q", true, tag.Following)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagUnfollow(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/tags/test/unfollow" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
fmt.Fprintln(w, `
|
||||
{
|
||||
"name": "test",
|
||||
"url": "http://mastodon.example/tags/test",
|
||||
"history": [
|
||||
{
|
||||
"day": "1668124800",
|
||||
"accounts": "1",
|
||||
"uses": "2"
|
||||
},
|
||||
{
|
||||
"day": "1668038400",
|
||||
"accounts": "0",
|
||||
"uses": "0"
|
||||
}
|
||||
],
|
||||
"following": false
|
||||
}`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.TagUnfollow(context.Background(), "foo")
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
tag, err := client.TagUnfollow(context.Background(), "test")
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if tag.Name != "test" {
|
||||
t.Fatalf("want %q but %q", "test", tag.Name)
|
||||
}
|
||||
if tag.URL != "http://mastodon.example/tags/test" {
|
||||
t.Fatalf("want %q but %q", "http://mastodon.example/tags/test", tag.URL)
|
||||
}
|
||||
if len(tag.History) != 2 {
|
||||
t.Fatalf("result should be two: %d", len(tag.History))
|
||||
}
|
||||
if tag.History[0].Day != "1668124800" {
|
||||
t.Fatalf("want %q but %q", "1668124800", tag.History[0].Day)
|
||||
}
|
||||
if tag.History[0].Accounts != "1" {
|
||||
t.Fatalf("want %q but %q", "1", tag.History[0].Accounts)
|
||||
}
|
||||
if tag.History[0].Uses != "2" {
|
||||
t.Fatalf("want %q but %q", "2", tag.History[0].Uses)
|
||||
}
|
||||
if tag.Following != false {
|
||||
t.Fatalf("want %v but %q", false, tag.Following)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagsFollowed(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/api/v1/followed_tags" {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if r.FormValue("limit") == "1" {
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, `
|
||||
[{
|
||||
"name": "test",
|
||||
"url": "http://mastodon.example/tags/test",
|
||||
"history": [
|
||||
{
|
||||
"day": "1668124800",
|
||||
"accounts": "1",
|
||||
"uses": "2"
|
||||
},
|
||||
{
|
||||
"day": "1668038400",
|
||||
"accounts": "0",
|
||||
"uses": "0"
|
||||
}
|
||||
],
|
||||
"following": true
|
||||
},
|
||||
{
|
||||
"name": "foo",
|
||||
"url": "http://mastodon.example/tags/foo",
|
||||
"history": [
|
||||
{
|
||||
"day": "1668124800",
|
||||
"accounts": "1",
|
||||
"uses": "2"
|
||||
},
|
||||
{
|
||||
"day": "1668038400",
|
||||
"accounts": "0",
|
||||
"uses": "0"
|
||||
}
|
||||
],
|
||||
"following": true
|
||||
}]`)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := NewClient(&Config{
|
||||
Server: ts.URL,
|
||||
ClientID: "foo",
|
||||
ClientSecret: "bar",
|
||||
AccessToken: "zoo",
|
||||
})
|
||||
_, err := client.TagsFollowed(context.Background(), &Pagination{Limit: 1})
|
||||
if err == nil {
|
||||
t.Fatalf("should be fail: %v", err)
|
||||
}
|
||||
tags, err := client.TagsFollowed(context.Background(), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("should not be fail: %v", err)
|
||||
}
|
||||
if len(tags) != 2 {
|
||||
t.Fatalf("want %q but %q", 2, len(tags))
|
||||
}
|
||||
if tags[0].Name != "test" {
|
||||
t.Fatalf("want %q but %q", "test", tags[0].Name)
|
||||
}
|
||||
if tags[0].URL != "http://mastodon.example/tags/test" {
|
||||
t.Fatalf("want %q but %q", "http://mastodon.example/tags/test", tags[0].URL)
|
||||
}
|
||||
if len(tags[0].History) != 2 {
|
||||
t.Fatalf("result should be two: %d", len(tags[0].History))
|
||||
}
|
||||
if tags[0].History[0].Day != "1668124800" {
|
||||
t.Fatalf("want %q but %q", "1668124800", tags[0].History[0].Day)
|
||||
}
|
||||
if tags[0].History[0].Accounts != "1" {
|
||||
t.Fatalf("want %q but %q", "1", tags[0].History[0].Accounts)
|
||||
}
|
||||
if tags[0].History[0].Uses != "2" {
|
||||
t.Fatalf("want %q but %q", "2", tags[0].History[0].Uses)
|
||||
}
|
||||
if tags[0].Following != true {
|
||||
t.Fatalf("want %v but %q", nil, tags[0].Following)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue