-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathclient.go
More file actions
158 lines (132 loc) · 4.25 KB
/
client.go
File metadata and controls
158 lines (132 loc) · 4.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package fcm
import (
"context"
"fmt"
"github.com/valyala/fasthttp"
"golang.org/x/oauth2"
)
// SimpleClient abstracts the interaction between the application server and the
// FCM server via HTTP protocol.
// It uses Service Account Private Key for authentication.
//
// If the `HTTP` field is nil, a zeroed http.SimpleClient will be allocated and used
// to send messages.
type Client interface {
// Send sends a message to the FCM server without retrying in case of service
// unavailability. A non-nil error is returned if a non-recoverable error
// occurs (i.e. if the sendResponse status code is not between 200 and 299).
Send(ctx context.Context, msg *Message) error
}
var _ Client = (*SimpleClient)(nil)
type SimpleClient struct {
client FastHTTPDoer
url urlConfig
tokenSource oauth2.TokenSource
sendPath []byte
}
// NewClient creates new Firebase Cloud Messaging SimpleClient based on API key and
// with default endpoint and http client.
func NewClient(serviceAccountJSONData []byte, opts ...Option) *SimpleClient {
defaultOpts := []Option{
WithEndpoint(DefaultEndpoint),
WithCredentialsData(serviceAccountJSONData),
WithHTTPClient(DefaultHTTPAdapter),
}
opts = append(defaultOpts, opts...)
return newClient(opts...)
}
func newClient(opts ...Option) *SimpleClient {
c := SimpleClient{
tokenSource: &NoopTokenSource{},
}
if err := applyOptions(&c, opts...); err != nil {
panic(err)
}
return &c
}
// Send implementation of Client interface.
// Docs for the reference: https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages/send
func (c *SimpleClient) Send(ctx context.Context, msg *Message) error {
if err := msg.Validate(); err != nil {
return fmt.Errorf("invalid message: %w", err)
}
sendReq := sendRequest{
Message: msg,
}
body, err := sendReq.MarshalJSON()
if err != nil {
return fmt.Errorf("failed to marshal request body: %w", err)
}
authHeaderValue, err := c.authHeaderValue()
if err != nil {
return err
}
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
req.Header.SetMethod(fasthttp.MethodPost)
uri := req.URI()
uri.SetSchemeBytes(c.url.Scheme)
uri.SetHostBytes(c.url.Host)
uri.SetPathBytes(c.sendPath)
req.Header.SetBytesKV(contentTypeHeader, contentTypeHeaderV)
req.Header.SetBytesKV(authorizationHeader, authHeaderValue)
req.SetBody(body)
if err := c.client.Do(ctx, req, resp); err != nil {
return fmt.Errorf("failed to perform request: %w", err)
}
return handleResponse(resp.StatusCode(), resp.Body())
}
func (c *SimpleClient) authHeaderValue() ([]byte, error) {
// TODO: consider to regenerate this value only when token is expired
// e.g. cache and reuse if not expired
token, err := c.tokenSource.Token()
if err != nil {
return nil, fmt.Errorf("failed to grab oauth2 token: %w", err)
}
headerValue := token.Type() + " " + token.AccessToken
return []byte(headerValue), nil
}
// Handle sendResponse
func handleResponse(statusCode int, respBody []byte) error {
// Success sendResponse
if statusCode >= 200 && statusCode <= 299 {
return nil
}
var resp sendResponse
if err := resp.UnmarshalJSON(respBody); err != nil {
return fmt.Errorf("unmarshal sendResponse with status code %d: %w", statusCode, err)
}
// In case if error sendResponse is not like we expect.
if resp.Error == nil {
return fmt.Errorf("empty error in sendResponse for status code %d: %s", statusCode, string(respBody))
}
// Extract errorCode of google.firebase.fcm type.
// Details could be different types of structs
// and some of the errorDetails elements could not have ErrorCode.
var errCode errorCode
var tokenFieldValidationError bool
if len(resp.Error.Details) > 0 {
for _, detail := range resp.Error.Details {
if detail.ErrorCode != "" {
errCode = detail.ErrorCode
}
for _, field := range detail.FieldViolations {
if field.Field == errorFieldNameToken {
tokenFieldValidationError = true
}
}
}
}
switch errCode {
case errorCodeUnregistered:
return ErrUnregistered
case errorCodeInvalidArgument:
if tokenFieldValidationError {
return ErrInvalidToken
}
default:
}
return fmt.Errorf("unsuccessful sendResponse with status code: %d: %s", statusCode, string(respBody))
}