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  }