messaging.js 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927
  1. /*! firebase-admin v12.1.1 */
  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. */
  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(async (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;