From 2abdb8e37c000f18c689f404f42ee3db6eda3143 Mon Sep 17 00:00:00 2001 From: buckket Date: Fri, 17 May 2019 23:47:32 +0200 Subject: [PATCH] Add support for /api/v1/push/subscription --- README.md | 4 +++ compat.go | 24 +++++++++++++ notification.go | 83 ++++++++++++++++++++++++++++++++++++++++++++ notification_test.go | 79 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+) diff --git a/README.md b/README.md index bd7bcb9..b6639a8 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,10 @@ func main() { * [x] GET /api/v1/notifications/:id * [x] POST /api/v1/notifications/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 diff --git a/compat.go b/compat.go index 0031ae4..789906d 100644 --- a/compat.go +++ b/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 +} diff --git a/notification.go b/notification.go index c16f3be..66f8e30 100644 --- a/notification.go +++ b/notification.go @@ -2,9 +2,13 @@ package mastodon import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "encoding/base64" "fmt" "net/http" "net/url" + "strconv" "time" ) @@ -17,6 +21,20 @@ type Notification struct { Status *Status `json:"status"` } +type PushSubscription struct { + ID ID `json:"id"` + Endpoint string `json:"endpoint"` + ServerKey string `json:"server_key"` + Alerts *PushAlerts `json:"alerts"` +} + +type PushAlerts struct { + Follow *Sbool `json:"follow"` + Favourite *Sbool `json:"favourite"` + Reblog *Sbool `json:"reblog"` + Mention *Sbool `json:"mention"` +} + // GetNotifications return notifications. func (c *Client) GetNotifications(ctx context.Context, pg *Pagination) ([]*Notification, error) { var notifications []*Notification @@ -52,3 +70,68 @@ func (c *Client) DismissNotification(ctx context.Context, id ID) error { 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 +} diff --git a/notification_test.go b/notification_test.go index 024e1b4..2154702 100644 --- a/notification_test.go +++ b/notification_test.go @@ -2,6 +2,9 @@ package mastodon import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" "fmt" "net/http" "net/http/httptest" @@ -64,3 +67,79 @@ func TestGetNotifications(t *testing.T) { 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) + return + })) + 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) + } +}