Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Unreleased

- [added] Implemented `messaging.MulticastMessage` type and the
`messaging.SendMulticast()` function for sending the same
message to multiple recipients.
- [added] Implemented `messaging.SendAllDryRun()` and
`messaging.SendMulticastDryRun()` functions for sending messages
in the validate only mode.
- [added] Implemented `messaging.SendAll()` function for sending
up to 100 FCM messages at a time.

Expand Down
50 changes: 48 additions & 2 deletions integration/messaging/messaging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func TestSendAll(t *testing.T) {
},
}

br, err := client.SendAll(context.Background(), messages)
br, err := client.SendAllDryRun(context.Background(), messages)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -173,7 +173,7 @@ func TestSendHundred(t *testing.T) {
messages = append(messages, m)
}

br, err := client.SendAll(context.Background(), messages)
br, err := client.SendAllDryRun(context.Background(), messages)
if err != nil {
t.Fatal(err)
}
Expand All @@ -196,6 +196,38 @@ func TestSendHundred(t *testing.T) {
}
}

func TestSendMulticast(t *testing.T) {
message := &messaging.MulticastMessage{
Notification: &messaging.Notification{
Title: "title",
Body: "body",
},
Tokens: []string{"INVALID_TOKEN", "ANOTHER_INVALID_TOKEN"},
}

br, err := client.SendMulticastDryRun(context.Background(), message)
if err != nil {
t.Fatal(err)
}

if len(br.Responses) != 2 {
t.Errorf("len(Responses) = %d; want = 2", len(br.Responses))
}
if br.SuccessCount != 0 {
t.Errorf("SuccessCount = %d; want = 0", br.SuccessCount)
}
if br.FailureCount != 2 {
t.Errorf("FailureCount = %d; want = 2", br.FailureCount)
}

for i := 0; i < 2; i++ {
sr := br.Responses[i]
if err := checkErrorSendResponse(sr); err != nil {
t.Errorf("Responses[%d]: %v", i, err)
}
}
}

func TestSubscribe(t *testing.T) {
tmr, err := client.SubscribeToTopic(context.Background(), []string{testRegistrationToken}, "mock-topic")
if err != nil {
Expand Down Expand Up @@ -229,3 +261,17 @@ func checkSuccessfulSendResponse(sr *messaging.SendResponse) error {

return nil
}

func checkErrorSendResponse(sr *messaging.SendResponse) error {
if sr.Success {
return fmt.Errorf("Success = true; want = false")
}
if sr.MessageID != "" {
return fmt.Errorf("MessageID = %q; want = %q", sr.MessageID, "")
}
if sr.Error == nil || !messaging.IsInvalidArgument(sr.Error) {
return fmt.Errorf("Error = %v; want = InvalidArgumentError", sr.Error)
}

return nil
}
109 changes: 104 additions & 5 deletions messaging/messaging_batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,47 @@ import (
"firebase.google.com/go/internal"
)

const maxMessages = 100
const multipartBoundary = "__END_OF_PART__"

// MulticastMessage represents a message that can be sent to multiple devices via Firebase Cloud
// Messaging (FCM).
//
// It contains payload information as well as the list of device registration tokens to which the
// message should be sent. A single MulticastMessage may contain up to 100 registration tokens.
type MulticastMessage struct {
Tokens []string
Data map[string]string
Notification *Notification
Android *AndroidConfig
Webpush *WebpushConfig
APNS *APNSConfig
}

func (mm *MulticastMessage) toMessages() ([]*Message, error) {
if len(mm.Tokens) == 0 {
return nil, errors.New("tokens must not be nil or empty")
}
if len(mm.Tokens) > maxMessages {
return nil, fmt.Errorf("tokens must not contain more than %d elements", maxMessages)
}

var messages []*Message
for _, token := range mm.Tokens {
temp := &Message{
Token: token,
Data: mm.Data,
Notification: mm.Notification,
Android: mm.Android,
Webpush: mm.Webpush,
APNS: mm.APNS,
}
messages = append(messages, temp)
}

return messages, nil
}

// SendResponse represents the status of an individual message that was sent as part of a batch
// request.
type SendResponse struct {
Expand All @@ -48,27 +87,87 @@ type BatchResponse struct {
Responses []*SendResponse
}

// SendAll sends all the messages in the given array via Firebase Cloud Messaging.
// SendAll sends the messages in the given array via Firebase Cloud Messaging.
//
// The messages array may contain up to 100 messages. SendAll employs batching to send the entire
// array of mssages as a single RPC call. Compared to the `Send()` function,
// this is a significantly more efficient way to send multiple messages. The responses
// list obtained from the return value corresponds to the order of input messages. An error from
// this is a significantly more efficient way to send multiple messages. The responses list
// obtained from the return value corresponds to the order of the input messages. An error from
// SendAll indicates a total failure -- i.e. none of the messages in the array could be sent.
// Partial failures are indicated by a `BatchResponse` return value.
func (c *Client) SendAll(ctx context.Context, messages []*Message) (*BatchResponse, error) {
return c.sendBatch(ctx, messages, false)
}

// SendAllDryRun sends the messages in the given array via Firebase Cloud Messaging in the
// dry run (validation only) mode.
//
// This function does not actually deliver any messages to target devices. Instead, it performs all
// the SDK-level and backend validations on the messages, and emulates the send operation.
//
// The messages array may contain up to 100 messages. SendAllDryRun employs batching to send the
// entire array of mssages as a single RPC call. Compared to the `SendDryRun()` function, this
// is a significantly more efficient way to validate sending multiple messages. The responses list
// obtained from the return value corresponds to the order of the input messages. An error from
// SendAllDryRun indicates a total failure -- i.e. none of the messages in the array could be sent
// for validation. Partial failures are indicated by a `BatchResponse` return value.
func (c *Client) SendAllDryRun(ctx context.Context, messages []*Message) (*BatchResponse, error) {
return c.sendBatch(ctx, messages, true)
}

// SendMulticast sends the given multicast message to all the FCM registration tokens specified.
//
// The tokens array in MulticastMessage may contain up to 100 tokens. SendMulticast uses the
// `SendAll()` function to send the given message to all the target recipients. The
// responses list obtained from the return value corresponds to the order of the input tokens. An
// error from SendMulticast indicates a total failure -- i.e. the message could not be sent to any
// of the recipients. Partial failures are indicated by a `BatchResponse` return value.
func (c *Client) SendMulticast(ctx context.Context, message *MulticastMessage) (*BatchResponse, error) {
messages, err := toMessages(message)
if err != nil {
return nil, err
}

return c.SendAll(ctx, messages)
}

// SendMulticastDryRun sends the given multicast message to all the specified FCM registration
// tokens in the dry run (validation only) mode.
//
// This function does not actually deliver any messages to target devices. Instead, it performs all
// the SDK-level and backend validations on the messages, and emulates the send operation.
//
// The tokens array in MulticastMessage may contain up to 100 tokens. SendMulticastDryRun uses the
// `SendAllDryRun()` function to send the given message. The responses list obtained from
// the return value corresponds to the order of the input tokens. An error from SendMulticastDryRun
// indicates a total failure -- i.e. none of the messages were sent to FCM for validation. Partial
// failures are indicated by a `BatchResponse` return value.
func (c *Client) SendMulticastDryRun(ctx context.Context, message *MulticastMessage) (*BatchResponse, error) {
messages, err := toMessages(message)
if err != nil {
return nil, err
}

return c.SendAllDryRun(ctx, messages)
}

func toMessages(message *MulticastMessage) ([]*Message, error) {
if message == nil {
return nil, errors.New("message must not be nil")
}

return message.toMessages()
}

func (c *Client) sendBatch(
ctx context.Context, messages []*Message, dryRun bool) (*BatchResponse, error) {

if len(messages) == 0 {
return nil, errors.New("messages must not be nil or empty")
}

if len(messages) > 100 {
return nil, errors.New("messages must not contain more than 100 elements")
if len(messages) > maxMessages {
return nil, fmt.Errorf("messages must not contain more than %d elements", maxMessages)
}

request, err := c.newBatchRequest(messages, dryRun)
Expand Down
Loading