diff --git a/README.md b/README.md index a60f93f..d883b91 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,11 @@ func main() { * [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 diff --git a/filters.go b/filters.go new file mode 100644 index 0000000..56f1229 --- /dev/null +++ b/filters.go @@ -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) +} diff --git a/filters_test.go b/filters_test.go new file mode 100644 index 0000000..71e440d --- /dev/null +++ b/filters_test.go @@ -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) + } +}