github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/notification/huawei/client.go (about) 1 // Package huawei can be used to send notifications via the Huawei Push Kit APIs. 2 // https://developer.huawei.com/consumer/en/doc/development/HMSCore-References/https-send-api-0000001050986197 3 package huawei 4 5 import ( 6 "bytes" 7 "context" 8 "encoding/json" 9 "fmt" 10 "net/http" 11 "net/url" 12 "sync" 13 "time" 14 15 "github.com/cozy/cozy-stack/pkg/config/config" 16 "github.com/cozy/cozy-stack/pkg/logger" 17 "github.com/labstack/echo/v4" 18 ) 19 20 const invalidTokenCode = "80300007" 21 22 // Client can be used to send notifications via the Huawei Push Kit APIs. 23 type Client struct { 24 getTokenURL string 25 sendMessagesURL string 26 27 // Access token fields 28 token struct { 29 mu sync.Mutex 30 expire time.Time 31 value string 32 } 33 } 34 35 // NewClient create a client for sending notifications. 36 func NewClient(conf config.Notifications) (*Client, error) { 37 _, err := url.Parse(conf.HuaweiSendMessagesURL) 38 if err != nil { 39 return nil, fmt.Errorf("cannot parse huawei_send_message: %s", err) 40 } 41 _, err = url.Parse(conf.HuaweiGetTokenURL) 42 if err != nil { 43 return nil, fmt.Errorf("cannot parse huawei_get_token: %s", err) 44 } 45 client := Client{ 46 getTokenURL: conf.HuaweiGetTokenURL, 47 sendMessagesURL: conf.HuaweiSendMessagesURL, 48 } 49 return &client, nil 50 } 51 52 // Notification is the payload to send to Push Kit for sending a notification. 53 // Cf https://developer.huawei.com/consumer/en/doc/development/HMSCore-References/https-send-api-0000001050986197#section13271045101216 54 type Notification struct { 55 Message NotificationMessage `json:"message"` 56 } 57 58 type NotificationMessage struct { 59 Android AndroidStructure `json:"android"` 60 Token []string `json:"token"` 61 } 62 63 type AndroidStructure struct { 64 Data string `json:"data"` 65 Notification NotificationStructure `json:"notification"` 66 } 67 68 type NotificationStructure struct { 69 Title string `json:"title"` 70 Body string `json:"body"` 71 ClickAction ClickStructure `json:"click_action"` 72 } 73 74 type ClickStructure struct { 75 Type int `json:"type"` 76 } 77 78 func NewNotification(title, body, token string, data map[string]interface{}) *Notification { 79 notif := &Notification{ 80 Message: NotificationMessage{ 81 Android: AndroidStructure{ 82 Notification: NotificationStructure{ 83 Title: title, 84 Body: body, 85 ClickAction: ClickStructure{Type: 3}, 86 }, 87 }, 88 Token: []string{token}, 89 }, 90 } 91 if serializedData, err := json.Marshal(data); err == nil { 92 notif.Message.Android.Data = string(serializedData) 93 } 94 return notif 95 } 96 97 // PushWithContext send the notification to Push Kit. It returns a bool that 98 // indicates true if the client is no longer registered (app has been 99 // uninstalled), and an error. 100 func (c *Client) PushWithContext(ctx context.Context, notification *Notification) (bool, error) { 101 token, err := c.fetchAccessToken() 102 if err != nil { 103 return false, err 104 } 105 106 payload, err := json.Marshal(notification) 107 if err != nil { 108 return false, fmt.Errorf("cannot marshal notification: %s", err) 109 } 110 body := bytes.NewBuffer(payload) 111 112 req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.sendMessagesURL, body) 113 if err != nil { 114 return false, fmt.Errorf("cannot make request: %s", err) 115 } 116 req.Header.Add(echo.HeaderAuthorization, "Bearer "+token) 117 res, err := http.DefaultClient.Do(req) 118 if err != nil { 119 return false, fmt.Errorf("cannot send notification: %s", err) 120 } 121 defer res.Body.Close() 122 123 if res.StatusCode != http.StatusOK { 124 var data map[string]interface{} 125 if err := json.NewDecoder(res.Body).Decode(&data); err == nil { 126 logger.WithNamespace("huawei"). 127 Infof("Failed to send notification (%d): %#v", res.StatusCode, data) 128 } 129 unregistered := data["code"] == invalidTokenCode 130 err = fmt.Errorf("cannot send notification: bad code %d", res.StatusCode) 131 return unregistered, err 132 } 133 return false, nil 134 } 135 136 type accessTokenResponse struct { 137 Value string `json:"accessToken"` 138 } 139 140 func (c *Client) fetchAccessToken() (string, error) { 141 c.token.mu.Lock() 142 defer c.token.mu.Unlock() 143 144 now := time.Now() 145 if c.token.expire.After(now) { 146 return c.token.value, nil 147 } 148 149 res, err := http.Get(c.getTokenURL) 150 if err != nil { 151 return "", fmt.Errorf("cannot fetch access token: %s", err) 152 } 153 defer res.Body.Close() 154 if res.StatusCode != http.StatusOK { 155 return "", fmt.Errorf("cannot fetch access token: bad code %d", res.StatusCode) 156 } 157 158 var token accessTokenResponse 159 if err := json.NewDecoder(res.Body).Decode(&token); err != nil { 160 return "", fmt.Errorf("cannot parse access token response: %s", err) 161 } 162 163 c.token.expire = now.Add(55 * time.Minute) 164 c.token.value = token.Value 165 return token.Value, nil 166 }