1 | /*! firebase-admin v12.0.0 */
|
2 | ;
|
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 | */
|
19 | Object.defineProperty(exports, "__esModule", { value: true });
|
20 | exports.Messaging = void 0;
|
21 | const deep_copy_1 = require("../utils/deep-copy");
|
22 | const error_1 = require("../utils/error");
|
23 | const utils = require("../utils");
|
24 | const validator = require("../utils/validator");
|
25 | const messaging_internal_1 = require("./messaging-internal");
|
26 | const messaging_api_request_internal_1 = require("./messaging-api-request-internal");
|
27 | // FCM endpoints
|
28 | const FCM_SEND_HOST = 'fcm.googleapis.com';
|
29 | const FCM_SEND_PATH = '/fcm/send';
|
30 | const FCM_TOPIC_MANAGEMENT_HOST = 'iid.googleapis.com';
|
31 | const FCM_TOPIC_MANAGEMENT_ADD_PATH = '/iid/v1:batchAdd';
|
32 | const FCM_TOPIC_MANAGEMENT_REMOVE_PATH = '/iid/v1:batchRemove';
|
33 | // Maximum messages that can be included in a batch request.
|
34 | const FCM_MAX_BATCH_SIZE = 500;
|
35 | // Key renames for the messaging notification payload object.
|
36 | const 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.
|
44 | const 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.
|
53 | const MESSAGING_DEVICE_RESULT_KEYS_MAP = {
|
54 | message_id: 'messageId',
|
55 | registration_id: 'canonicalRegistrationToken',
|
56 | };
|
57 | // Key renames for the MessagingDevicesResponse object.
|
58 | const 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.
|
65 | const 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.
|
71 | const MESSAGING_TOPIC_RESPONSE_KEYS_MAP = {
|
72 | message_id: 'messageId',
|
73 | };
|
74 | // Key renames for the MessagingConditionResponse object.
|
75 | const 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 | */
|
85 | function 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 | */
|
107 | function 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 | */
|
122 | function 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 | */
|
150 | class 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 | }
|
927 | exports.Messaging = Messaging;
|