1
+ "use strict" ;
2
+
3
+ import Parse from 'parse' ;
4
+ import log from 'npmlog' ;
5
+ import { initializeApp , cert , getApps , getApp } from 'firebase-admin/app' ;
6
+ import { getMessaging } from 'firebase-admin/messaging' ;
7
+ import { randomString } from './PushAdapterUtils' ;
8
+
9
+ const LOG_PREFIX = 'parse-server-push-adapter FCM' ;
10
+ const FCMRegistrationTokensMax = 500 ;
11
+ const FCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60 ; // FCM allows a max of 4 weeks
12
+
13
+ export default function FCM ( args ) {
14
+ if ( typeof args !== 'object' || ! args . firebaseServiceAccount ) {
15
+ throw new Parse . Error ( Parse . Error . PUSH_MISCONFIGURED ,
16
+ 'FCM Configuration is invalid' ) ;
17
+ }
18
+
19
+ let app ;
20
+ if ( getApps ( ) . length === 0 ) {
21
+ app = initializeApp ( { credential : cert ( args . firebaseServiceAccount ) } ) ;
22
+ }
23
+ else {
24
+ app = getApp ( ) ;
25
+ }
26
+ this . sender = getMessaging ( app ) ;
27
+ }
28
+
29
+ FCM . FCMRegistrationTokensMax = FCMRegistrationTokensMax ;
30
+
31
+ /**
32
+ * Send fcm request.
33
+ * @param {Object } data The data we need to send, the format is the same with api request body
34
+ * @param {Array } devices A array of devices
35
+ * @returns {Object } Array of resolved promises
36
+ */
37
+
38
+ FCM . prototype . send = function ( data , devices ) {
39
+ if ( ! data || ! devices || ! Array . isArray ( devices ) ) {
40
+ log . warn ( LOG_PREFIX , 'invalid push payload' ) ;
41
+ return ;
42
+ }
43
+
44
+ // We can only have 500 recepients per send, so we need to slice devices to
45
+ // chunk if necessary
46
+ const slices = sliceDevices ( devices , FCM . FCMRegistrationTokensMax ) ;
47
+
48
+ const sendToDeviceSlice = ( deviceSlice ) => {
49
+ const pushId = randomString ( 10 ) ;
50
+ const timestamp = Date . now ( ) ;
51
+
52
+ // Build a device map
53
+ const devicesMap = deviceSlice . reduce ( ( memo , device ) => {
54
+ memo [ device . deviceToken ] = device ;
55
+ return memo ;
56
+ } , { } ) ;
57
+
58
+ const deviceTokens = Object . keys ( devicesMap ) ;
59
+ const fcmPayload = generateFCMPayload ( data , pushId , timestamp , deviceTokens ) ;
60
+ const length = deviceTokens . length ;
61
+ log . info ( LOG_PREFIX , `sending push to ${ length } devices` ) ;
62
+
63
+ return this . sender . sendEachForMulticast ( fcmPayload . data )
64
+ . then ( ( response ) => {
65
+ const promises = [ ] ;
66
+ const failedTokens = [ ] ;
67
+ const successfulTokens = [ ] ;
68
+
69
+ response . responses . forEach ( ( resp , idx ) => {
70
+ if ( resp . success ) {
71
+ successfulTokens . push ( deviceTokens [ idx ] ) ;
72
+ promises . push ( createSuccessfulPromise ( deviceTokens [ idx ] , devicesMap [ deviceTokens [ idx ] ] . deviceType ) ) ;
73
+ } else {
74
+ failedTokens . push ( deviceTokens [ idx ] ) ;
75
+ promises . push ( createErrorPromise ( deviceTokens [ idx ] , devicesMap [ deviceTokens [ idx ] ] . deviceType , resp . error ) ) ;
76
+ log . error ( LOG_PREFIX , `failed to send to ${ deviceTokens [ idx ] } with error: ${ JSON . stringify ( resp . error ) } ` ) ;
77
+ }
78
+ } ) ;
79
+
80
+ if ( failedTokens . length ) {
81
+ log . error ( LOG_PREFIX , `tokens with failed pushes: ${ JSON . stringify ( failedTokens ) } ` ) ;
82
+ }
83
+
84
+ if ( successfulTokens . length ) {
85
+ log . verbose ( LOG_PREFIX , `tokens with successful pushes: ${ JSON . stringify ( successfulTokens ) } ` ) ;
86
+ }
87
+
88
+ return Promise . all ( promises ) ;
89
+ } ) ;
90
+ } ;
91
+
92
+ const allPromises = Promise . all ( slices . map ( sendToDeviceSlice ) )
93
+ . catch ( ( err ) => {
94
+ log . error ( LOG_PREFIX , `error sending push: ${ err } ` ) ;
95
+ } ) ;
96
+
97
+ return allPromises ;
98
+ }
99
+
100
+ /**
101
+ * Generate the fcm payload from the data we get from api request.
102
+ * @param {Object } requestData The request body
103
+ * @param {String } pushId A random string
104
+ * @param {Number } timeStamp A number in milliseconds since the Unix Epoch
105
+ * @returns {Object } A payload for FCM
106
+ */
107
+ function generateFCMPayload ( requestData , pushId , timeStamp , deviceTokens ) {
108
+ delete requestData [ 'where' ] ;
109
+
110
+ const payloadToUse = {
111
+ data : { } ,
112
+ push_id : pushId ,
113
+ time : new Date ( timeStamp ) . toISOString ( )
114
+ } ;
115
+
116
+ // Use rawPayload instead of the GCM implementation if it exists
117
+ if ( requestData . hasOwnProperty ( 'rawPayload' ) ) {
118
+ payloadToUse . data = {
119
+ ...requestData . rawPayload ,
120
+ tokens : deviceTokens
121
+ } ;
122
+ } else {
123
+ // Android payload according to GCM implementation
124
+ const androidPayload = {
125
+ android : {
126
+ priority : 'high'
127
+ } ,
128
+ tokens : deviceTokens
129
+ } ;
130
+
131
+ if ( requestData . hasOwnProperty ( 'notification' ) ) {
132
+ androidPayload . notification = requestData . notification ;
133
+ }
134
+
135
+ if ( requestData . hasOwnProperty ( 'data' ) ) {
136
+ androidPayload . data = requestData . data ;
137
+ }
138
+
139
+ if ( requestData [ 'expiration_time' ] ) {
140
+ const expirationTime = requestData [ 'expiration_time' ] ;
141
+ // Convert to seconds
142
+ let timeToLive = Math . floor ( ( expirationTime - timeStamp ) / 1000 ) ;
143
+ if ( timeToLive < 0 ) {
144
+ timeToLive = 0 ;
145
+ }
146
+ if ( timeToLive >= FCMTimeToLiveMax ) {
147
+ timeToLive = FCMTimeToLiveMax ;
148
+ }
149
+
150
+ androidPayload . android . ttl = timeToLive ;
151
+ }
152
+
153
+ payloadToUse . data = androidPayload ;
154
+ }
155
+
156
+ return payloadToUse ;
157
+ }
158
+
159
+ /**
160
+ * Slice a list of devices to several list of devices with fixed chunk size.
161
+ * @param {Array } devices An array of devices
162
+ * @param {Number } chunkSize The size of the a chunk
163
+ * @returns {Array } An array which contains several arrays of devices with fixed chunk size
164
+ */
165
+ function sliceDevices ( devices , chunkSize ) {
166
+ const chunkDevices = [ ] ;
167
+ while ( devices . length > 0 ) {
168
+ chunkDevices . push ( devices . splice ( 0 , chunkSize ) ) ;
169
+ }
170
+ return chunkDevices ;
171
+ }
172
+
173
+ /**
174
+ * Creates an errorPromise for return.
175
+ *
176
+ * @param {String } token Device-Token
177
+ * @param {String } deviceType Device-Type
178
+ * @param {String } errorMessage ErrrorMessage as string
179
+ */
180
+ function createErrorPromise ( token , deviceType , errorMessage ) {
181
+ return Promise . resolve ( {
182
+ transmitted : false ,
183
+ device : {
184
+ deviceToken : token ,
185
+ deviceType : deviceType
186
+ } ,
187
+ response : { error : errorMessage }
188
+ } ) ;
189
+ }
190
+
191
+ /**
192
+ * Creates an successfulPromise for return.
193
+ *
194
+ * @param {String } token Device-Token
195
+ * @param {String } deviceType Device-Type
196
+ */
197
+ function createSuccessfulPromise ( token , deviceType ) {
198
+ return Promise . resolve ( {
199
+ transmitted : true ,
200
+ device : {
201
+ deviceToken : token ,
202
+ deviceType : deviceType
203
+ }
204
+ } ) ;
205
+ }
206
+
207
+
208
+ FCM . generateFCMPayload = generateFCMPayload ;
209
+
210
+ /* istanbul ignore else */
211
+ if ( process . env . TESTING ) {
212
+ FCM . sliceDevices = sliceDevices ;
213
+ }
0 commit comments