Skip to content

Commit b58f756

Browse files
authored
Merge pull request #6 from mayank-SFIN571/gcm
feat: fcm implementation
2 parents daa277c + 77e1302 commit b58f756

File tree

8 files changed

+1434
-53
lines changed

8 files changed

+1434
-53
lines changed

package-lock.json

Lines changed: 1105 additions & 48 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@loopback/context": "^3.8.1",
4646
"@loopback/core": "^2.7.0",
4747
"@loopback/rest": "^5.0.1",
48+
"firebase-admin": "^9.2.0",
4849
"socket.io-client": "^2.3.0",
4950
"tslib": "^1.10.0"
5051
},
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import {inject, Provider} from '@loopback/core';
2+
import {HttpErrors} from '@loopback/rest';
3+
import * as admin from 'firebase-admin';
4+
import {FcmBindings} from './keys';
5+
import {
6+
FcmConfig,
7+
FcmMessage,
8+
FcmNotification,
9+
FcmSubscriberType,
10+
} from './types';
11+
12+
export class FcmProvider implements Provider<FcmNotification> {
13+
constructor(
14+
@inject(FcmBindings.Config, {
15+
optional: true,
16+
})
17+
private readonly fcmConfig?: FcmConfig,
18+
) {
19+
if (this.fcmConfig) {
20+
this.fcmService = admin.initializeApp({
21+
credential: admin.credential.cert(this.fcmConfig.serviceAccountPath),
22+
databaseURL: this.fcmConfig.dbUrl,
23+
});
24+
} else {
25+
throw new HttpErrors.PreconditionFailed('Firebase Config missing !');
26+
}
27+
}
28+
29+
fcmService: admin.app.App;
30+
31+
initialValidations(message: FcmMessage) {
32+
if (
33+
message.receiver.to.length === 0 &&
34+
!message.options.topic &&
35+
!message.options.condition
36+
) {
37+
throw new HttpErrors.BadRequest(
38+
'Message receiver, topic or condition not found in request !',
39+
);
40+
}
41+
42+
if (message.receiver.to.length > 500) {
43+
throw new HttpErrors.BadRequest(
44+
'Message receiver count cannot exceed 500 !',
45+
);
46+
}
47+
48+
if (!message.subject) {
49+
throw new HttpErrors.BadRequest('Message title not found !');
50+
}
51+
}
52+
53+
sendingPushToReceiverTokens(
54+
message: FcmMessage,
55+
generalMessageObj: {
56+
notification: admin.messaging.Notification;
57+
android?: admin.messaging.AndroidConfig;
58+
webpush?: admin.messaging.WebpushConfig;
59+
apns?: admin.messaging.ApnsConfig;
60+
fcmOptions?: admin.messaging.FcmOptions;
61+
},
62+
) {
63+
const promises: Promise<string | admin.messaging.BatchResponse>[] = [];
64+
/**Partial<admin.messaging.MulticastMessage>
65+
* These are the registration tokens for all devices which this message
66+
* is intended for.
67+
*
68+
* If receiver does not hold information for type, then it is considered
69+
* as devce token.
70+
*/
71+
const receiverTokens = message.receiver.to.filter(
72+
item => item.type === FcmSubscriberType.RegistrationToken || !item.type,
73+
);
74+
75+
/**
76+
* if the receivers are of type
77+
* */
78+
if (receiverTokens.length >= 1) {
79+
const tokens = receiverTokens.map(item => item.id);
80+
let msgToTransfer = {
81+
tokens: tokens,
82+
...generalMessageObj,
83+
data: {...message.options.data},
84+
};
85+
promises.push(
86+
this.fcmService
87+
.messaging()
88+
.sendMulticast(msgToTransfer, (message.options.dryRun = false)),
89+
);
90+
}
91+
return promises;
92+
}
93+
94+
sendingPushToTopics(
95+
message: FcmMessage,
96+
generalMessageObj: {
97+
notification: admin.messaging.Notification;
98+
android?: admin.messaging.AndroidConfig;
99+
webpush?: admin.messaging.WebpushConfig;
100+
apns?: admin.messaging.ApnsConfig;
101+
fcmOptions?: admin.messaging.FcmOptions;
102+
},
103+
) {
104+
const promises: Promise<string | admin.messaging.BatchResponse>[] = [];
105+
const topics = message.receiver.to.filter(
106+
item => item.type === FcmSubscriberType.FCMTopic,
107+
);
108+
109+
if (topics.length > 0) {
110+
// Messages to multiple Topics is not allowed in single transaction.
111+
112+
topics.forEach(topic => {
113+
let msgToTransfer = {
114+
topic: topic.id,
115+
...generalMessageObj,
116+
data: {...message.options.data},
117+
};
118+
119+
promises.push(
120+
this.fcmService
121+
.messaging()
122+
.send(msgToTransfer, (message.options.dryRun = false)),
123+
);
124+
});
125+
}
126+
127+
return promises;
128+
}
129+
130+
sendingPushToConditions(
131+
message: FcmMessage,
132+
generalMessageObj: {
133+
notification: admin.messaging.Notification;
134+
android?: admin.messaging.AndroidConfig;
135+
webpush?: admin.messaging.WebpushConfig;
136+
apns?: admin.messaging.ApnsConfig;
137+
fcmOptions?: admin.messaging.FcmOptions;
138+
},
139+
) {
140+
const promises: Promise<string | admin.messaging.BatchResponse>[] = [];
141+
const conditions = message.receiver.to.filter(
142+
item => item.type === FcmSubscriberType.FCMCondition,
143+
);
144+
145+
if (conditions.length > 0) {
146+
// Condition message
147+
148+
conditions.forEach(condition => {
149+
let msgToTransfer = {
150+
condition: condition.id,
151+
...generalMessageObj,
152+
data: {...message.options.data},
153+
};
154+
promises.push(
155+
this.fcmService
156+
.messaging()
157+
.send(msgToTransfer, (message.options.dryRun = false)),
158+
);
159+
});
160+
}
161+
162+
return promises;
163+
}
164+
165+
value() {
166+
return {
167+
publish: async (message: FcmMessage) => {
168+
/**
169+
* validating the initial request
170+
*/
171+
this.initialValidations(message);
172+
173+
/**
174+
* This method is responsible to send all the required data to mobile application
175+
* The mobile device will recieve push notification.
176+
* Push will be sent to the devices with registration token sent in receiver
177+
* Notification object holds title, body and imageUrl
178+
* FCM message must contain 2 attributes, i.e title and body
179+
*
180+
*/
181+
182+
const promises: Promise<string | admin.messaging.BatchResponse>[] = [];
183+
184+
const standardNotifForFCM: admin.messaging.Notification = {
185+
body: message.body,
186+
title: message.subject,
187+
imageUrl: message.options.imageUrl,
188+
};
189+
190+
/**
191+
* Message attributes for all kinds of messages
192+
*
193+
* If android configurations are sent in options, it will take the
194+
* precedence over normal notification
195+
*
196+
*/
197+
let generalMessageObj = {
198+
notification: standardNotifForFCM,
199+
android: message.options.android,
200+
webpush: message.options.webpush,
201+
apns: message.options.apns,
202+
fcmOptions: message.options.fcmOptions,
203+
};
204+
205+
/**
206+
* Sending messages for all the tokens in the request
207+
*/
208+
promises.push(
209+
...this.sendingPushToReceiverTokens(message, generalMessageObj),
210+
);
211+
212+
/**
213+
* Sending messages for all the topics in the request
214+
*/
215+
promises.push(...this.sendingPushToTopics(message, generalMessageObj));
216+
217+
/**
218+
* Sending messages for all the conditions in the request
219+
*/
220+
promises.push(
221+
...this.sendingPushToConditions(message, generalMessageObj),
222+
);
223+
224+
await Promise.all(promises);
225+
},
226+
};
227+
}
228+
}

src/providers/push/fcm/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './fcm.provider';
2+
export * from './keys';
3+
export * from './types';

src/providers/push/fcm/keys.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {BindingKey} from '@loopback/core';
2+
import {FcmConfig} from './types';
3+
4+
export namespace FcmBindings {
5+
export const Config = BindingKey.create<FcmConfig | null>(
6+
'sf.notification.config.fcm',
7+
);
8+
}

src/providers/push/fcm/types.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import * as admin from 'firebase-admin';
2+
import {
3+
PushMessage,
4+
PushNotification,
5+
PushReceiver,
6+
PushSubscriber,
7+
} from '../types';
8+
9+
export interface FcmNotification extends PushNotification {
10+
publish(message: FcmMessage): Promise<void>;
11+
}
12+
13+
export interface FcmMessage extends PushMessage {
14+
/**
15+
* If the requirement is to send push on topic or condition,
16+
* send receiver as empty array
17+
*/
18+
receiver: FcmReceiver;
19+
options: {
20+
/**
21+
* URL of an image to be displayed in the notification.
22+
*/
23+
imageUrl?: string;
24+
/**
25+
* @param dryRun Whether to send the message in the dry-run
26+
* (validation only) mode.
27+
*
28+
* Whether or not the message should actually be sent. When set to `true`,
29+
* allows developers to test a request without actually sending a message. When
30+
* set to `false`, the message will be sent.
31+
*
32+
* **Default value:** `false`
33+
*/
34+
dryRun?: boolean;
35+
android?: admin.messaging.AndroidConfig;
36+
webpush?: admin.messaging.WebpushConfig;
37+
apns?: admin.messaging.ApnsConfig;
38+
fcmOptions?: admin.messaging.FcmOptions;
39+
// sonarignore:start
40+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
41+
[key: string]: any;
42+
// sonarignore:end
43+
};
44+
}
45+
46+
export interface FcmReceiver extends PushReceiver {
47+
to: FcmSubscriber[];
48+
}
49+
50+
export interface FcmSubscriber extends PushSubscriber {
51+
type: FcmSubscriberType;
52+
id: string;
53+
}
54+
55+
/**
56+
* The topic name can be optionally prefixed with "/topics/".
57+
*
58+
* the following condition will send messages to devices that are subscribed
59+
* to TopicA and either TopicB or TopicC
60+
*
61+
* "'TopicA' in topics && ('TopicB' in topics || 'TopicC' in topics)"
62+
*
63+
*
64+
* topic?: string;
65+
*
66+
* FCM first evaluates any conditions in parentheses, and then evaluates the
67+
* expression from left to right. In the above expression, a user subscribed
68+
* to any single topic does not receive the message. Likewise, a user who does
69+
* not subscribe to TopicA does not receive the message.
70+
*
71+
* You can include up to five topics in your conditional expression.
72+
*
73+
* example"
74+
* "'stock-GOOG' in topics || 'industry-tech' in topics"
75+
*
76+
* condition?: string;
77+
*/
78+
79+
export const enum FcmSubscriberType {
80+
RegistrationToken,
81+
FCMTopic,
82+
FCMCondition,
83+
}
84+
85+
export interface FcmConfig {
86+
dbUrl: string;
87+
serviceAccountPath: string;
88+
}

src/providers/push/socketio/socketio.provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export class SocketIOProvider implements Provider<SocketNotification> {
3838
);
3939
}
4040
this.socketService.emit(
41-
message.path || this.socketConfig.defaultPath,
41+
message.options?.path || this.socketConfig.defaultPath,
4242
JSON.stringify(message),
4343
);
4444
} else {

src/providers/push/socketio/types.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ export interface SocketNotification extends PushNotification {
1111

1212
export interface SocketMessage extends PushMessage {
1313
receiver: SocketReceiver;
14-
/**
15-
* Path represents the socket server endpoint at which we have handling for this kind of messages
16-
*/
17-
path: string;
1814
}
1915

2016
export interface SocketReceiver extends PushReceiver {

0 commit comments

Comments
 (0)