From aa9c1859b5daaecbabe1e36366293574592225a2 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Fri, 30 Dec 2022 10:45:58 +0100 Subject: [PATCH 01/10] bug fix for conversation/direct --- streaming.go | 13 +++++++++++++ streaming_test.go | 7 +++++++ streaming_ws.go | 11 +++++++++++ streaming_ws_test.go | 18 ++++++++++++++---- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/streaming.go b/streaming.go index b109f6c..9ebf997 100644 --- a/streaming.go +++ b/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]))} } diff --git a/streaming_test.go b/streaming_test.go index a9e90d9..0a65cec 100644 --- a/streaming_test.go +++ b/streaming_test.go @@ -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 @trwnh","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" { diff --git a/streaming_ws.go b/streaming_ws.go index 5658bbf..2dff42e 100644 --- a/streaming_ws.go +++ b/streaming_ws.go @@ -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)))} diff --git a/streaming_ws_test.go b/streaming_ws_test.go index 7e78890..e6b6661 100644 --- a/streaming_ws_test.go +++ b/streaming_ws_test.go @@ -101,6 +101,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":""}`)) if err != nil { @@ -119,8 +126,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 +141,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 +150,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) { From e86f463667b26690642d6ab25947a21ff807dfa3 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Fri, 30 Dec 2022 11:03:08 +0100 Subject: [PATCH 02/10] add filtered to Status --- filters.go | 12 ++++++++++ status.go | 59 +++++++++++++++++++++++++------------------------- status_test.go | 32 ++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 30 deletions(-) diff --git a/filters.go b/filters.go index 56f1229..c2be553 100644 --- a/filters.go +++ b/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 diff --git a/status.go b/status.go index f12e4eb..ba164d6 100644 --- a/status.go +++ b/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. diff --git a/status_test.go b/status_test.go index 2cac4b8..a2e6528 100644 --- a/status_test.go +++ b/status_test.go @@ -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) { From ad3aa348dda09c5fdf4a4052f32a88c4ff5c709b Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Fri, 30 Dec 2022 11:14:06 +0100 Subject: [PATCH 03/10] add GetTimelineHashtagMultiple --- status.go | 32 ++++++++++++++++++++++++++++++++ status_test.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/status.go b/status.go index ba164d6..ae961cf 100644 --- a/status.go +++ b/status.go @@ -103,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) @@ -350,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 diff --git a/status_test.go b/status_test.go index a2e6528..3133507 100644 --- a/status_test.go +++ b/status_test.go @@ -581,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) From fe547bf14f3762547948b7a7e90984e22f8d97e1 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Fri, 30 Dec 2022 11:18:51 +0100 Subject: [PATCH 04/10] add GetNotificationsExclude --- notification.go | 13 ++++++++++++- notification_test.go | 16 +++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/notification.go b/notification.go index fa0cab9..4e83487 100644 --- a/notification.go +++ b/notification.go @@ -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 } diff --git a/notification_test.go b/notification_test.go index 78cdbdd..efdb6f6 100644 --- a/notification_test.go +++ b/notification_test.go @@ -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) From f54995fdf914fee0091f74db4dbcbedf3e0dfb13 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Fri, 30 Dec 2022 11:22:00 +0100 Subject: [PATCH 05/10] add ConversationEvent in TestForTheCoverages --- mastodon_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/mastodon_test.go b/mastodon_test.go index 5d14a44..3667e09 100644 --- a/mastodon_test.go +++ b/mastodon_test.go @@ -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() From 2aa0406a44fd6bcbffeeefa6e3febf540d72db21 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Fri, 30 Dec 2022 11:44:05 +0100 Subject: [PATCH 06/10] bug fix for AddToList and RemoveFromList --- lists.go | 4 ++-- lists_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lists.go b/lists.go index 59610cc..9256f5d 100644 --- a/lists.go +++ b/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) diff --git a/lists_test.go b/lists_test.go index b1c9e3b..b26bc4a 100644 --- a/lists_test.go +++ b/lists_test.go @@ -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 } From f59d7e630f3949e80e59fe07264822a342e9575e Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Fri, 30 Dec 2022 11:51:49 +0100 Subject: [PATCH 07/10] add AccountsSearchResolve --- accounts.go | 14 ++++++++++++++ accounts_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/accounts.go b/accounts.go index 0b43e4c..a0aab3c 100644 --- a/accounts.go +++ b/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{} diff --git a/accounts_test.go b/accounts_test.go index 47e0310..e90f84a 100644 --- a/accounts_test.go +++ b/accounts_test.go @@ -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) { From 481c724c6d6ea2ffa4b46fbe4284a517eaa0093d Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Fri, 30 Dec 2022 11:55:06 +0100 Subject: [PATCH 08/10] add TagInfo, TagFollow, TagUnfollow and TagsFollowed --- mastodon.go | 7 -- tags.go | 55 ++++++++++ tags_test.go | 276 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 331 insertions(+), 7 deletions(-) create mode 100644 tags.go create mode 100644 tags_test.go diff --git a/mastodon.go b/mastodon.go index c704e10..942e1d0 100644 --- a/mastodon.go +++ b/mastodon.go @@ -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"` diff --git a/tags.go b/tags.go new file mode 100644 index 0000000..97ba820 --- /dev/null +++ b/tags.go @@ -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 +} diff --git a/tags_test.go b/tags_test.go new file mode 100644 index 0000000..dc1e9e9 --- /dev/null +++ b/tags_test.go @@ -0,0 +1,276 @@ +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/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": false + }`) + })) + 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 != 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) { + 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", + }) + 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) + } +} From 2a17549744d53213b29d676af52831d75065976d Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Fri, 30 Dec 2022 11:58:33 +0100 Subject: [PATCH 09/10] update readme with api calls --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8440744..a33e181 100644 --- a/README.md +++ b/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 From 2b33d0ed4be085a8958ac80868fa9c158df08201 Mon Sep 17 00:00:00 2001 From: Rasmus Lindroth Date: Fri, 30 Dec 2022 12:38:22 +0100 Subject: [PATCH 10/10] fix more tests --- streaming_ws_test.go | 26 ++++++++++++++++++++++++++ tags_test.go | 19 ++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/streaming_ws_test.go b/streaming_ws_test.go index e6b6661..a0d8536 100644 --- a/streaming_ws_test.go +++ b/streaming_ws_test.go @@ -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) diff --git a/tags_test.go b/tags_test.go index dc1e9e9..cddd1d8 100644 --- a/tags_test.go +++ b/tags_test.go @@ -137,7 +137,7 @@ func TestTagFollow(t *testing.T) { 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/follow" { + if r.URL.Path != "/api/v1/tags/test/unfollow" { http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) return } @@ -168,11 +168,11 @@ func TestTagUnfollow(t *testing.T) { ClientSecret: "bar", AccessToken: "zoo", }) - _, err := client.TagFollow(context.Background(), "foo") + _, err := client.TagUnfollow(context.Background(), "foo") if err == nil { t.Fatalf("should be fail: %v", err) } - tag, err := client.TagFollow(context.Background(), "test") + tag, err := client.TagUnfollow(context.Background(), "test") if err != nil { t.Fatalf("should not be fail: %v", err) } @@ -201,6 +201,15 @@ func TestTagUnfollow(t *testing.T) { 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", @@ -245,6 +254,10 @@ func TestTagsFollowed(t *testing.T) { 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)