-
Notifications
You must be signed in to change notification settings - Fork 387
/
Copy pathmessaging.ts
1188 lines (1093 loc) · 45.6 KB
/
messaging.ts
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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*!
* @license
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { App } from '../app';
import { deepCopy, deepExtend } from '../utils/deep-copy';
import { SubRequest } from './batch-request-internal';
import { ErrorInfo, MessagingClientErrorCode, FirebaseMessagingError } from '../utils/error';
import * as utils from '../utils';
import * as validator from '../utils/validator';
import { validateMessage, BLACKLISTED_DATA_PAYLOAD_KEYS, BLACKLISTED_OPTIONS_KEYS } from './messaging-internal';
import { FirebaseMessagingRequestHandler } from './messaging-api-request-internal';
import {
BatchResponse,
Message,
MessagingTopicManagementResponse,
MulticastMessage,
// Legacy API types
MessagingDevicesResponse,
MessagingDeviceGroupResponse,
MessagingPayload,
MessagingOptions,
MessagingTopicResponse,
MessagingConditionResponse,
DataMessagePayload,
NotificationMessagePayload,
SendResponse,
} from './messaging-api';
// FCM endpoints
const FCM_SEND_HOST = 'fcm.googleapis.com';
const FCM_SEND_PATH = '/fcm/send';
const FCM_TOPIC_MANAGEMENT_HOST = 'iid.googleapis.com';
const FCM_TOPIC_MANAGEMENT_ADD_PATH = '/iid/v1:batchAdd';
const FCM_TOPIC_MANAGEMENT_REMOVE_PATH = '/iid/v1:batchRemove';
// Maximum messages that can be included in a batch request.
const FCM_MAX_BATCH_SIZE = 500;
// Key renames for the messaging notification payload object.
const CAMELCASED_NOTIFICATION_PAYLOAD_KEYS_MAP = {
bodyLocArgs: 'body_loc_args',
bodyLocKey: 'body_loc_key',
clickAction: 'click_action',
titleLocArgs: 'title_loc_args',
titleLocKey: 'title_loc_key',
};
// Key renames for the messaging options object.
const CAMELCASE_OPTIONS_KEYS_MAP = {
dryRun: 'dry_run',
timeToLive: 'time_to_live',
collapseKey: 'collapse_key',
mutableContent: 'mutable_content',
contentAvailable: 'content_available',
restrictedPackageName: 'restricted_package_name',
};
// Key renames for the MessagingDeviceResult object.
const MESSAGING_DEVICE_RESULT_KEYS_MAP = {
message_id: 'messageId',
registration_id: 'canonicalRegistrationToken',
};
// Key renames for the MessagingDevicesResponse object.
const MESSAGING_DEVICES_RESPONSE_KEYS_MAP = {
canonical_ids: 'canonicalRegistrationTokenCount',
failure: 'failureCount',
success: 'successCount',
multicast_id: 'multicastId',
};
// Key renames for the MessagingDeviceGroupResponse object.
const MESSAGING_DEVICE_GROUP_RESPONSE_KEYS_MAP = {
success: 'successCount',
failure: 'failureCount',
failed_registration_ids: 'failedRegistrationTokens',
};
// Key renames for the MessagingTopicResponse object.
const MESSAGING_TOPIC_RESPONSE_KEYS_MAP = {
message_id: 'messageId',
};
// Key renames for the MessagingConditionResponse object.
const MESSAGING_CONDITION_RESPONSE_KEYS_MAP = {
message_id: 'messageId',
};
/**
* Maps a raw FCM server response to a MessagingDevicesResponse object.
*
* @param response - The raw FCM server response to map.
*
* @returns The mapped MessagingDevicesResponse object.
*/
function mapRawResponseToDevicesResponse(response: object): MessagingDevicesResponse {
// Rename properties on the server response
utils.renameProperties(response, MESSAGING_DEVICES_RESPONSE_KEYS_MAP);
if ('results' in response) {
(response as any).results.forEach((messagingDeviceResult: any) => {
utils.renameProperties(messagingDeviceResult, MESSAGING_DEVICE_RESULT_KEYS_MAP);
// Map the FCM server's error strings to actual error objects.
if ('error' in messagingDeviceResult) {
const newError = FirebaseMessagingError.fromServerError(
messagingDeviceResult.error, /* message */ undefined, messagingDeviceResult.error,
);
messagingDeviceResult.error = newError;
}
});
}
return response as MessagingDevicesResponse;
}
/**
* Maps a raw FCM server response to a MessagingDeviceGroupResponse object.
*
* @param response - The raw FCM server response to map.
*
* @returns The mapped MessagingDeviceGroupResponse object.
*/
function mapRawResponseToDeviceGroupResponse(response: object): MessagingDeviceGroupResponse {
// Rename properties on the server response
utils.renameProperties(response, MESSAGING_DEVICE_GROUP_RESPONSE_KEYS_MAP);
// Add the 'failedRegistrationTokens' property if it does not exist on the response, which
// it won't when the 'failureCount' property has a value of 0)
(response as any).failedRegistrationTokens = (response as any).failedRegistrationTokens || [];
return response as MessagingDeviceGroupResponse;
}
/**
* Maps a raw FCM server response to a MessagingTopicManagementResponse object.
*
* @param {object} response The raw FCM server response to map.
*
* @returns {MessagingTopicManagementResponse} The mapped MessagingTopicManagementResponse object.
*/
function mapRawResponseToTopicManagementResponse(response: object): MessagingTopicManagementResponse {
// Add the success and failure counts.
const result: MessagingTopicManagementResponse = {
successCount: 0,
failureCount: 0,
errors: [],
};
if ('results' in response) {
(response as any).results.forEach((tokenManagementResult: any, index: number) => {
// Map the FCM server's error strings to actual error objects.
if ('error' in tokenManagementResult) {
result.failureCount += 1;
const newError = FirebaseMessagingError.fromTopicManagementServerError(
tokenManagementResult.error, /* message */ undefined, tokenManagementResult.error,
);
result.errors.push({
index,
error: newError,
});
} else {
result.successCount += 1;
}
});
}
return result;
}
/**
* Messaging service bound to the provided app.
*/
export class Messaging {
private urlPath: string;
private readonly appInternal: App;
private readonly messagingRequestHandler: FirebaseMessagingRequestHandler;
/**
* @internal
*/
constructor(app: App) {
if (!validator.isNonNullObject(app) || !('options' in app)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT,
'First argument passed to admin.messaging() must be a valid Firebase app instance.',
);
}
this.appInternal = app;
this.messagingRequestHandler = new FirebaseMessagingRequestHandler(app);
}
/**
* The {@link firebase-admin.app#App} associated with the current `Messaging` service
* instance.
*
* @example
* ```javascript
* var app = messaging.app;
* ```
*/
get app(): App {
return this.appInternal;
}
/**
* Sends the given message via FCM.
*
* @param message - The message payload.
* @param dryRun - Whether to send the message in the dry-run
* (validation only) mode.
* @returns A promise fulfilled with a unique message ID
* string after the message has been successfully handed off to the FCM
* service for delivery.
*/
public send(message: Message, dryRun?: boolean): Promise<string> {
const copy: Message = deepCopy(message);
validateMessage(copy);
if (typeof dryRun !== 'undefined' && !validator.isBoolean(dryRun)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean');
}
return this.getUrlPath()
.then((urlPath) => {
const request: { message: Message; validate_only?: boolean } = { message: copy };
if (dryRun) {
request.validate_only = true;
}
return this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, urlPath, request);
})
.then((response) => {
return (response as any).name;
});
}
/**
* Sends each message in the given array via Firebase Cloud Messaging.
*
* Unlike {@link Messaging.sendAll}, this method makes a single RPC call for each message
* in the given array.
*
* The responses list obtained from the return value corresponds to the order of `messages`.
* An error from this method or a `BatchResponse` with all failures indicates a total failure,
* meaning that none of the messages in the list could be sent. Partial failures or no
* failures are only indicated by a `BatchResponse` return value.
*
* @param messages - A non-empty array
* containing up to 500 messages.
* @param dryRun - Whether to send the messages in the dry-run
* (validation only) mode.
* @returns A Promise fulfilled with an object representing the result of the
* send operation.
*/
public sendEach(messages: Message[], dryRun?: boolean): Promise<BatchResponse> {
if (validator.isArray(messages) && messages.constructor !== Array) {
// In more recent JS specs, an array-like object might have a constructor that is not of
// Array type. Our deepCopy() method doesn't handle them properly. Convert such objects to
// a regular array here before calling deepCopy(). See issue #566 for details.
messages = Array.from(messages);
}
const copy: Message[] = deepCopy(messages);
if (!validator.isNonEmptyArray(copy)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'messages must be a non-empty array');
}
if (copy.length > FCM_MAX_BATCH_SIZE) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT,
`messages list must not contain more than ${FCM_MAX_BATCH_SIZE} items`);
}
if (typeof dryRun !== 'undefined' && !validator.isBoolean(dryRun)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean');
}
return this.getUrlPath()
.then((urlPath) => {
const requests: Promise<SendResponse>[] = copy.map(async (message) => {
validateMessage(message);
const request: { message: Message; validate_only?: boolean } = { message };
if (dryRun) {
request.validate_only = true;
}
return this.messagingRequestHandler.invokeRequestHandlerForSendResponse(FCM_SEND_HOST, urlPath, request);
});
return Promise.allSettled(requests);
}).then((results) => {
const responses: SendResponse[] = [];
results.forEach(result => {
if (result.status === 'fulfilled') {
responses.push(result.value);
} else { // rejected
responses.push({ success: false, error: result.reason })
}
})
const successCount: number = responses.filter((resp) => resp.success).length;
return {
responses,
successCount,
failureCount: responses.length - successCount,
};
});
}
/**
* Sends the given multicast message to all the FCM registration tokens
* specified in it.
*
* This method uses the {@link Messaging.sendEach} API under the hood to send the given
* message to all the target recipients. The responses list obtained from the
* return value corresponds to the order of tokens in the `MulticastMessage`.
* An error from this method or a `BatchResponse` with all failures indicates a total
* failure, meaning that the messages in the list could be sent. Partial failures or
* failures are only indicated by a `BatchResponse` return value.
*
* @param message - A multicast message
* containing up to 500 tokens.
* @param dryRun - Whether to send the message in the dry-run
* (validation only) mode.
* @returns A Promise fulfilled with an object representing the result of the
* send operation.
*/
public sendEachForMulticast(message: MulticastMessage, dryRun?: boolean): Promise<BatchResponse> {
const copy: MulticastMessage = deepCopy(message);
if (!validator.isNonNullObject(copy)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'MulticastMessage must be a non-null object');
}
if (!validator.isNonEmptyArray(copy.tokens)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'tokens must be a non-empty array');
}
if (copy.tokens.length > FCM_MAX_BATCH_SIZE) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT,
`tokens list must not contain more than ${FCM_MAX_BATCH_SIZE} items`);
}
const messages: Message[] = copy.tokens.map((token) => {
return {
token,
android: copy.android,
apns: copy.apns,
data: copy.data,
notification: copy.notification,
webpush: copy.webpush,
fcmOptions: copy.fcmOptions,
};
});
return this.sendEach(messages, dryRun);
}
/**
* Sends all the messages in the given array via Firebase Cloud Messaging.
* Employs batching to send the entire list as a single RPC call. Compared
* to the `send()` method, this method is a significantly more efficient way
* to send multiple messages.
*
* The responses list obtained from the return value
* corresponds to the order of tokens in the `MulticastMessage`. An error
* from this method indicates a total failure, meaning that none of the messages
* in the list could be sent. Partial failures are indicated by a `BatchResponse`
* return value.
*
* @param messages - A non-empty array
* containing up to 500 messages.
* @param dryRun - Whether to send the messages in the dry-run
* (validation only) mode.
* @returns A Promise fulfilled with an object representing the result of the
* send operation.
*
* @deprecated Use {@link Messaging.sendEach} instead.
*/
public sendAll(messages: Message[], dryRun?: boolean): Promise<BatchResponse> {
if (validator.isArray(messages) && messages.constructor !== Array) {
// In more recent JS specs, an array-like object might have a constructor that is not of
// Array type. Our deepCopy() method doesn't handle them properly. Convert such objects to
// a regular array here before calling deepCopy(). See issue #566 for details.
messages = Array.from(messages);
}
const copy: Message[] = deepCopy(messages);
if (!validator.isNonEmptyArray(copy)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'messages must be a non-empty array');
}
if (copy.length > FCM_MAX_BATCH_SIZE) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT,
`messages list must not contain more than ${FCM_MAX_BATCH_SIZE} items`);
}
if (typeof dryRun !== 'undefined' && !validator.isBoolean(dryRun)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'dryRun must be a boolean');
}
return this.getUrlPath()
.then((urlPath) => {
const requests: SubRequest[] = copy.map((message) => {
validateMessage(message);
const request: { message: Message; validate_only?: boolean } = { message };
if (dryRun) {
request.validate_only = true;
}
return {
url: `https://${FCM_SEND_HOST}${urlPath}`,
body: request,
};
});
return this.messagingRequestHandler.sendBatchRequest(requests);
});
}
/**
* Sends the given multicast message to all the FCM registration tokens
* specified in it.
*
* This method uses the `sendAll()` API under the hood to send the given
* message to all the target recipients. The responses list obtained from the
* return value corresponds to the order of tokens in the `MulticastMessage`.
* An error from this method indicates a total failure, meaning that the message
* was not sent to any of the tokens in the list. Partial failures are indicated
* by a `BatchResponse` return value.
*
* @param message - A multicast message
* containing up to 500 tokens.
* @param dryRun - Whether to send the message in the dry-run
* (validation only) mode.
* @returns A Promise fulfilled with an object representing the result of the
* send operation.
*
* @deprecated Use {@link Messaging.sendEachForMulticast} instead.
*/
public sendMulticast(message: MulticastMessage, dryRun?: boolean): Promise<BatchResponse> {
const copy: MulticastMessage = deepCopy(message);
if (!validator.isNonNullObject(copy)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'MulticastMessage must be a non-null object');
}
if (!validator.isNonEmptyArray(copy.tokens)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT, 'tokens must be a non-empty array');
}
if (copy.tokens.length > FCM_MAX_BATCH_SIZE) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT,
`tokens list must not contain more than ${FCM_MAX_BATCH_SIZE} items`);
}
const messages: Message[] = copy.tokens.map((token) => {
return {
token,
android: copy.android,
apns: copy.apns,
data: copy.data,
notification: copy.notification,
webpush: copy.webpush,
fcmOptions: copy.fcmOptions,
};
});
return this.sendAll(messages, dryRun);
}
/**
* Sends an FCM message to a single device corresponding to the provided
* registration token.
*
* See {@link https://firebase.google.com/docs/cloud-messaging/admin/legacy-fcm#send_to_individual_devices |
* Send to individual devices}
* for code samples and detailed documentation. Takes either a
* `registrationToken` to send to a single device or a
* `registrationTokens` parameter containing an array of tokens to send
* to multiple devices.
*
* @param registrationToken - A device registration token or an array of
* device registration tokens to which the message should be sent.
* @param payload - The message payload.
* @param options - Optional options to
* alter the message.
*
* @returns A promise fulfilled with the server's response after the message
* has been sent.
*
* @deprecated Use {@link Messaging.send} instead.
*/
public sendToDevice(
registrationTokenOrTokens: string | string[],
payload: MessagingPayload,
options: MessagingOptions = {},
): Promise<MessagingDevicesResponse> {
// Validate the input argument types. Since these are common developer errors when getting
// started, throw an error instead of returning a rejected promise.
this.validateRegistrationTokensType(
registrationTokenOrTokens, 'sendToDevice', MessagingClientErrorCode.INVALID_RECIPIENT,
);
this.validateMessagingPayloadAndOptionsTypes(payload, options);
return Promise.resolve()
.then(() => {
// Validate the contents of the input arguments. Because we are now in a promise, any thrown
// error will cause this method to return a rejected promise.
this.validateRegistrationTokens(
registrationTokenOrTokens, 'sendToDevice', MessagingClientErrorCode.INVALID_RECIPIENT,
);
const payloadCopy = this.validateMessagingPayload(payload);
const optionsCopy = this.validateMessagingOptions(options);
const request: any = deepCopy(payloadCopy);
deepExtend(request, optionsCopy);
if (validator.isString(registrationTokenOrTokens)) {
request.to = registrationTokenOrTokens;
} else {
request.registration_ids = registrationTokenOrTokens;
}
return this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, FCM_SEND_PATH, request);
})
.then((response) => {
// The sendToDevice() and sendToDeviceGroup() methods both set the `to` query parameter in
// the underlying FCM request. If the provided registration token argument is actually a
// valid notification key, the response from the FCM server will be a device group response.
// If that is the case, we map the response to a MessagingDeviceGroupResponse.
// See b/35394951 for more context.
if ('multicast_id' in response) {
return mapRawResponseToDevicesResponse(response);
} else {
const groupResponse = mapRawResponseToDeviceGroupResponse(response);
return {
...groupResponse,
canonicalRegistrationTokenCount: -1,
multicastId: -1,
results: [],
}
}
});
}
/**
* Sends an FCM message to a device group corresponding to the provided
* notification key.
*
* See {@link https://firebase.google.com/docs/cloud-messaging/admin/legacy-fcm#send_to_a_device_group |
* Send to a device group} for code samples and detailed documentation.
*
* @param notificationKey - The notification key for the device group to
* which to send the message.
* @param payload - The message payload.
* @param options - Optional options to
* alter the message.
*
* @returns A promise fulfilled with the server's response after the message
* has been sent.
*
* @deprecated Use {@link Messaging.send} instead.
*/
public sendToDeviceGroup(
notificationKey: string,
payload: MessagingPayload,
options: MessagingOptions = {},
): Promise<MessagingDeviceGroupResponse> {
if (!validator.isNonEmptyString(notificationKey)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_RECIPIENT,
'Notification key provided to sendToDeviceGroup() must be a non-empty string.',
);
} else if (notificationKey.indexOf(':') !== -1) {
// It is possible the developer provides a registration token instead of a notification key
// to this method. We can detect some of those cases by checking to see if the string contains
// a colon. Not all registration tokens will contain a colon (only newer ones will), but no
// notification keys will contain a colon, so we can use it as a rough heuristic.
// See b/35394951 for more context.
return Promise.reject(new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_RECIPIENT,
'Notification key provided to sendToDeviceGroup() has the format of a registration token. ' +
'You should use sendToDevice() instead.',
));
}
// Validate the types of the payload and options arguments. Since these are common developer
// errors, throw an error instead of returning a rejected promise.
this.validateMessagingPayloadAndOptionsTypes(payload, options);
return Promise.resolve()
.then(() => {
// Validate the contents of the payload and options objects. Because we are now in a
// promise, any thrown error will cause this method to return a rejected promise.
const payloadCopy = this.validateMessagingPayload(payload);
const optionsCopy = this.validateMessagingOptions(options);
const request: any = deepCopy(payloadCopy);
deepExtend(request, optionsCopy);
request.to = notificationKey;
return this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, FCM_SEND_PATH, request);
})
.then((response) => {
// The sendToDevice() and sendToDeviceGroup() methods both set the `to` query parameter in
// the underlying FCM request. If the provided notification key argument has an invalid
// format (that is, it is either a registration token or some random string), the response
// from the FCM server will default to a devices response (which we detect by looking for
// the `multicast_id` property). If that is the case, we either throw an error saying the
// provided notification key is invalid (if the message failed to send) or map the response
// to a MessagingDevicesResponse (if the message succeeded).
// See b/35394951 for more context.
if ('multicast_id' in response) {
if ((response as any).success === 0) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_RECIPIENT,
'Notification key provided to sendToDeviceGroup() is invalid.',
);
} else {
const devicesResponse = mapRawResponseToDevicesResponse(response);
return {
...devicesResponse,
failedRegistrationTokens: [],
}
}
}
return mapRawResponseToDeviceGroupResponse(response);
});
}
/**
* Sends an FCM message to a topic.
*
* See {@link https://firebase.google.com/docs/cloud-messaging/admin/legacy-fcm#send_to_a_topic |
* Send to a topic} for code samples and detailed documentation.
*
* @param topic - The topic to which to send the message.
* @param payload - The message payload.
* @param options - Optional options to
* alter the message.
*
* @returns A promise fulfilled with the server's response after the message
* has been sent.
*/
public sendToTopic(
topic: string,
payload: MessagingPayload,
options: MessagingOptions = {},
): Promise<MessagingTopicResponse> {
// Validate the input argument types. Since these are common developer errors when getting
// started, throw an error instead of returning a rejected promise.
this.validateTopicType(topic, 'sendToTopic', MessagingClientErrorCode.INVALID_RECIPIENT);
this.validateMessagingPayloadAndOptionsTypes(payload, options);
// Prepend the topic with /topics/ if necessary.
topic = this.normalizeTopic(topic);
return Promise.resolve()
.then(() => {
// Validate the contents of the payload and options objects. Because we are now in a
// promise, any thrown error will cause this method to return a rejected promise.
const payloadCopy = this.validateMessagingPayload(payload);
const optionsCopy = this.validateMessagingOptions(options);
this.validateTopic(topic, 'sendToTopic', MessagingClientErrorCode.INVALID_RECIPIENT);
const request: any = deepCopy(payloadCopy);
deepExtend(request, optionsCopy);
request.to = topic;
return this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, FCM_SEND_PATH, request);
})
.then((response) => {
// Rename properties on the server response
utils.renameProperties(response, MESSAGING_TOPIC_RESPONSE_KEYS_MAP);
return response as MessagingTopicResponse;
});
}
/**
* Sends an FCM message to a condition.
*
* See {@link https://firebase.google.com/docs/cloud-messaging/admin/legacy-fcm#send_to_a_condition |
* Send to a condition}
* for code samples and detailed documentation.
*
* @param condition - The condition determining to which topics to send
* the message.
* @param payload - The message payload.
* @param options - Optional options to
* alter the message.
*
* @returns A promise fulfilled with the server's response after the message
* has been sent.
*/
public sendToCondition(
condition: string,
payload: MessagingPayload,
options: MessagingOptions = {},
): Promise<MessagingConditionResponse> {
if (!validator.isNonEmptyString(condition)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_RECIPIENT,
'Condition provided to sendToCondition() must be a non-empty string.',
);
}
// Validate the types of the payload and options arguments. Since these are common developer
// errors, throw an error instead of returning a rejected promise.
this.validateMessagingPayloadAndOptionsTypes(payload, options);
// The FCM server rejects conditions which are surrounded in single quotes. When the condition
// is stringified over the wire, double quotes in it get converted to \" which the FCM server
// does not properly handle. We can get around this by replacing internal double quotes with
// single quotes.
condition = condition.replace(/"/g, '\'');
return Promise.resolve()
.then(() => {
// Validate the contents of the payload and options objects. Because we are now in a
// promise, any thrown error will cause this method to return a rejected promise.
const payloadCopy = this.validateMessagingPayload(payload);
const optionsCopy = this.validateMessagingOptions(options);
const request: any = deepCopy(payloadCopy);
deepExtend(request, optionsCopy);
request.condition = condition;
return this.messagingRequestHandler.invokeRequestHandler(FCM_SEND_HOST, FCM_SEND_PATH, request);
})
.then((response) => {
// Rename properties on the server response
utils.renameProperties(response, MESSAGING_CONDITION_RESPONSE_KEYS_MAP);
return response as MessagingConditionResponse;
});
}
/**
* Subscribes a device to an FCM topic.
*
* See {@link https://firebase.google.com/docs/cloud-messaging/manage-topics#suscribe_and_unsubscribe_using_the |
* Subscribe to a topic}
* for code samples and detailed documentation. Optionally, you can provide an
* array of tokens to subscribe multiple devices.
*
* @param registrationTokens - A token or array of registration tokens
* for the devices to subscribe to the topic.
* @param topic - The topic to which to subscribe.
*
* @returns A promise fulfilled with the server's response after the device has been
* subscribed to the topic.
*/
public subscribeToTopic(
registrationTokenOrTokens: string | string[],
topic: string,
): Promise<MessagingTopicManagementResponse> {
return this.sendTopicManagementRequest(
registrationTokenOrTokens,
topic,
'subscribeToTopic',
FCM_TOPIC_MANAGEMENT_ADD_PATH,
);
}
/**
* Unsubscribes a device from an FCM topic.
*
* See {@link https://firebase.google.com/docs/cloud-messaging/admin/manage-topic-subscriptions#unsubscribe_from_a_topic |
* Unsubscribe from a topic}
* for code samples and detailed documentation. Optionally, you can provide an
* array of tokens to unsubscribe multiple devices.
*
* @param registrationTokens - A device registration token or an array of
* device registration tokens to unsubscribe from the topic.
* @param topic - The topic from which to unsubscribe.
*
* @returns A promise fulfilled with the server's response after the device has been
* unsubscribed from the topic.
*/
public unsubscribeFromTopic(
registrationTokenOrTokens: string | string[],
topic: string,
): Promise<MessagingTopicManagementResponse> {
return this.sendTopicManagementRequest(
registrationTokenOrTokens,
topic,
'unsubscribeFromTopic',
FCM_TOPIC_MANAGEMENT_REMOVE_PATH,
);
}
private getUrlPath(): Promise<string> {
if (this.urlPath) {
return Promise.resolve(this.urlPath);
}
return utils.findProjectId(this.app)
.then((projectId) => {
if (!validator.isNonEmptyString(projectId)) {
// Assert for an explicit project ID (either via AppOptions or the cert itself).
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_ARGUMENT,
'Failed to determine project ID for Messaging. Initialize the '
+ 'SDK with service account credentials or set project ID as an app option. '
+ 'Alternatively set the GOOGLE_CLOUD_PROJECT environment variable.',
);
}
this.urlPath = `/v1/projects/${projectId}/messages:send`;
return this.urlPath;
});
}
/**
* Helper method which sends and handles topic subscription management requests.
*
* @param registrationTokenOrTokens - The registration token or an array of
* registration tokens to unsubscribe from the topic.
* @param topic - The topic to which to subscribe.
* @param methodName - The name of the original method called.
* @param path - The endpoint path to use for the request.
*
* @returns A Promise fulfilled with the parsed server
* response.
*/
private sendTopicManagementRequest(
registrationTokenOrTokens: string | string[],
topic: string,
methodName: string,
path: string,
): Promise<MessagingTopicManagementResponse> {
this.validateRegistrationTokensType(registrationTokenOrTokens, methodName);
this.validateTopicType(topic, methodName);
// Prepend the topic with /topics/ if necessary.
topic = this.normalizeTopic(topic);
return Promise.resolve()
.then(() => {
// Validate the contents of the input arguments. Because we are now in a promise, any thrown
// error will cause this method to return a rejected promise.
this.validateRegistrationTokens(registrationTokenOrTokens, methodName);
this.validateTopic(topic, methodName);
// Ensure the registration token(s) input argument is an array.
let registrationTokensArray: string[] = registrationTokenOrTokens as string[];
if (validator.isString(registrationTokenOrTokens)) {
registrationTokensArray = [registrationTokenOrTokens as string];
}
const request = {
to: topic,
registration_tokens: registrationTokensArray,
};
return this.messagingRequestHandler.invokeRequestHandler(
FCM_TOPIC_MANAGEMENT_HOST, path, request,
);
})
.then((response) => {
return mapRawResponseToTopicManagementResponse(response);
});
}
/**
* Validates the types of the messaging payload and options. If invalid, an error will be thrown.
*
* @param payload - The messaging payload to validate.
* @param options - The messaging options to validate.
*/
private validateMessagingPayloadAndOptionsTypes(
payload: MessagingPayload,
options: MessagingOptions,
): void {
// Validate the payload is an object
if (!validator.isNonNullObject(payload)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_PAYLOAD,
'Messaging payload must be an object with at least one of the "data" or "notification" properties.',
);
}
// Validate the options argument is an object
if (!validator.isNonNullObject(options)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_OPTIONS,
'Messaging options must be an object.',
);
}
}
/**
* Validates the messaging payload. If invalid, an error will be thrown.
*
* @param payload - The messaging payload to validate.
*
* @returns A copy of the provided payload with whitelisted properties switched
* from camelCase to underscore_case.
*/
private validateMessagingPayload(payload: MessagingPayload): MessagingPayload {
const payloadCopy: MessagingPayload = deepCopy(payload);
const payloadKeys = Object.keys(payloadCopy);
const validPayloadKeys = ['data', 'notification'];
let containsDataOrNotificationKey = false;
payloadKeys.forEach((payloadKey) => {
// Validate the payload does not contain any invalid keys
if (validPayloadKeys.indexOf(payloadKey) === -1) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_PAYLOAD,
`Messaging payload contains an invalid "${payloadKey}" property. Valid properties are ` +
'"data" and "notification".',
);
} else {
containsDataOrNotificationKey = true;
}
});
// Validate the payload contains at least one of the "data" and "notification" keys
if (!containsDataOrNotificationKey) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_PAYLOAD,
'Messaging payload must contain at least one of the "data" or "notification" properties.',
);
}
const validatePayload = (payloadKey: string, value: DataMessagePayload | NotificationMessagePayload): void => {
// Validate each top-level key in the payload is an object
if (!validator.isNonNullObject(value)) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_PAYLOAD,
`Messaging payload contains an invalid value for the "${payloadKey}" property. ` +
'Value must be an object.',
);
}
Object.keys(value).forEach((subKey) => {
if (!validator.isString(value[subKey])) {
// Validate all sub-keys have a string value
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_PAYLOAD,
`Messaging payload contains an invalid value for the "${payloadKey}.${subKey}" ` +
'property. Values must be strings.',
);
} else if (payloadKey === 'data' && /^google\./.test(subKey)) {
// Validate the data payload does not contain keys which start with 'google.'.
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_PAYLOAD,
`Messaging payload contains the blacklisted "data.${subKey}" property.`,
);
}
});
};
if (payloadCopy.data !== undefined) {
validatePayload('data', payloadCopy.data);
}
if (payloadCopy.notification !== undefined) {
validatePayload('notification', payloadCopy.notification);
}
// Validate the data payload object does not contain blacklisted properties
if ('data' in payloadCopy) {
BLACKLISTED_DATA_PAYLOAD_KEYS.forEach((blacklistedKey) => {
if (blacklistedKey in payloadCopy.data!) {
throw new FirebaseMessagingError(
MessagingClientErrorCode.INVALID_PAYLOAD,
`Messaging payload contains the blacklisted "data.${blacklistedKey}" property.`,
);
}
});
}
// Convert whitelisted camelCase keys to underscore_case
if (payloadCopy.notification) {
utils.renameProperties(payloadCopy.notification, CAMELCASED_NOTIFICATION_PAYLOAD_KEYS_MAP);
}
return payloadCopy;
}
/**
* Validates the messaging options. If invalid, an error will be thrown.
*
* @param options - The messaging options to validate.
*