UNPKG

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