package notify
import (
"context"
"encoding/json"
"fmt"
"net/url"
)
// PushResult is how one triggered push went, as notify recorded it on
// the push message: Status is notify's word (sent, error, skipped) and
// Error its text, which names each device that refused and why. Found
// is false until the push step has run.
type PushResult struct {
Found bool
Status string
Error string
// Devices is each device's outcome (extras.devices, notify 0.1.9);
// empty when notify recorded none.
Devices []PushDevice
}
// PushDevice is one device's outcome on a push: the push service's host,
// delivered, failed, retired or gone, and the status and reason when it
// refused. notify never echoes the endpoint, and neither does this.
type PushDevice struct {
// EndpointSHA256 is the lowercase hex SHA-256 of the endpoint as
// registered (notify 0.1.11): the one thing a client holding the
// endpoint can match an outcome to its own record by.
EndpointSHA256 string `json:"endpointSha256"`
Host string `json:"host"`
Outcome string `json:"outcome"`
Status int `json:"status,omitempty"`
Reason string `json:"reason,omitempty"`
}
// PushOutcome reads the push message notify wrote for one trigger,
// found by its transaction id: the notification first, then its
// messages. notify's domain types carry no json tags, so the keys are
// the Go field names (ID, Channel, Status, ErrorText).
func (c *Client) PushOutcome(ctx context.Context, subscriber, txn string) (PushResult, error) {
if c == nil || c.base == "" || txn == "" {
return PushResult{}, nil
}
q := url.Values{"transactionId": {txn}, "limit": {"1"}}
if subscriber != "" {
q.Set("subscriberId", subscriber)
}
status, body, err := c.do(ctx, "GET", "/v1/notifications?"+q.Encode(), nil)
if err != nil {
return PushResult{}, fmt.Errorf("notify: push outcome: %w", err)
}
if status >= 300 {
return PushResult{}, fmt.Errorf("notify: push outcome: status %d", status)
}
var list struct {
Items []struct {
ID string `json:"ID"`
} `json:"items"`
}
if err := json.Unmarshal(body, &list); err != nil {
return PushResult{}, fmt.Errorf("notify: push outcome: %w", err)
}
if len(list.Items) == 0 || list.Items[0].ID == "" {
return PushResult{}, nil
}
status, body, err = c.do(ctx, "GET", "/v1/notifications/"+url.PathEscape(list.Items[0].ID)+"/messages", nil)
if err != nil {
return PushResult{}, fmt.Errorf("notify: push outcome: %w", err)
}
if status >= 300 {
return PushResult{}, fmt.Errorf("notify: push outcome: status %d", status)
}
var msgs []struct {
Channel string `json:"Channel"`
Status string `json:"Status"`
ErrorText string `json:"ErrorText"`
Extras struct {
Devices []PushDevice `json:"devices"`
} `json:"extras"`
}
if err := json.Unmarshal(body, &msgs); err != nil {
return PushResult{}, fmt.Errorf("notify: push outcome: %w", err)
}
for _, m := range msgs {
if m.Channel == "push" {
return PushResult{Found: true, Status: m.Status, Error: m.ErrorText, Devices: m.Extras.Devices}, nil
}
}
return PushResult{}, nil
}