Skip to content

Commit 1a94148

Browse files
feat: send push notifications for voice calls
1 parent 453508e commit 1a94148

11 files changed

Lines changed: 235 additions & 67 deletions

File tree

apps/meteor/app/push/server/apn.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import apn from '@parse/node-apn';
2-
import type { IPushToken, RequiredField } from '@rocket.chat/core-typings';
2+
import type { RequiredField } from '@rocket.chat/core-typings';
33
import EJSON from 'ejson';
44

55
import type { PushOptions, PendingPushNotification } from './definition';
@@ -24,7 +24,7 @@ export const sendAPN = ({
2424
}: {
2525
userToken: string;
2626
notification: PendingPushNotification & { topic: string };
27-
_removeToken: (token: IPushToken['token']) => void;
27+
_removeToken: (token: string) => void;
2828
}) => {
2929
if (!apnConnection) {
3030
throw new Error('Apn Connection not initialized.');
@@ -34,7 +34,13 @@ export const sendAPN = ({
3434

3535
const note = new apn.Notification();
3636

37-
note.expiry = Math.floor(Date.now() / 1000) + 3600; // Expires 1 hour from now.
37+
if (notification.voip) {
38+
note.expiry = Math.floor(Date.now() / 1000) + 60; // Expires in 60 seconds
39+
note.pushType = 'voip';
40+
} else {
41+
note.expiry = Math.floor(Date.now() / 1000) + 3600; // Expires 1 hour from now.
42+
}
43+
3844
if (notification.badge !== undefined) {
3945
note.badge = notification.badge;
4046
}
@@ -50,10 +56,16 @@ export const sendAPN = ({
5056
// adds category support for iOS8 custom actions as described here:
5157
// https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/
5258
// RemoteNotificationsPG/Chapters/IPhoneOSClientImp.html#//apple_ref/doc/uid/TP40008194-CH103-SW36
53-
note.category = notification.apn?.category;
59+
if (notification.apn?.category) {
60+
note.category = notification.apn.category;
61+
}
5462

55-
note.body = notification.text;
56-
note.title = notification.title;
63+
if (notification.text) {
64+
note.body = notification.text;
65+
}
66+
if (notification.title) {
67+
note.title = notification.title;
68+
}
5769

5870
if (notification.notId != null) {
5971
note.threadId = String(notification.notId);
@@ -62,7 +74,9 @@ export const sendAPN = ({
6274
// Allow the user to set payload data
6375
note.payload = notification.payload ? { ejson: EJSON.stringify(notification.payload) } : {};
6476

65-
note.payload.messageFrom = notification.from;
77+
if (notification.from) {
78+
note.payload.messageFrom = notification.from;
79+
}
6680
note.priority = priority;
6781

6882
note.topic = notification.topic;
@@ -81,9 +95,7 @@ export const sendAPN = ({
8195
msg: 'Removing APN token',
8296
token: userToken,
8397
});
84-
_removeToken({
85-
apn: userToken,
86-
});
98+
_removeToken(userToken);
8799
}
88100
});
89101
});

apps/meteor/app/push/server/definition.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { type RequiredField } from '@rocket.chat/core-typings';
2+
13
export type PushOptions = {
24
sendTimeout?: number;
35
production?: boolean;
@@ -17,10 +19,10 @@ export type PushOptions = {
1719
getAuthorization?: () => Promise<string>;
1820
};
1921

20-
export type PendingPushNotification = {
21-
from: string;
22-
title: string;
23-
text: string;
22+
export type BasePendingPushNotification = {
23+
from?: string;
24+
title?: string;
25+
text?: string;
2426
badge?: number;
2527
sound?: string;
2628
notId?: number;
@@ -42,4 +44,11 @@ export type PendingPushNotification = {
4244
priority?: number;
4345

4446
contentAvailable?: 1 | 0;
47+
voip?: boolean;
4548
};
49+
50+
export type PendingMessagePushNotification = RequiredField<BasePendingPushNotification, 'from' | 'title' | 'text'> & { voip?: false };
51+
52+
export type PendingVoipPushNotification = BasePendingPushNotification & { voip: true };
53+
54+
export type PendingPushNotification = PendingMessagePushNotification | PendingVoipPushNotification;

apps/meteor/app/push/server/fcm.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,15 +138,9 @@ function getFCMMessagesFromPushData(userTokens: string[], notification: PendingP
138138
data['content-available'] = notification.contentAvailable.toString();
139139
}
140140

141-
// then we will create the notification field
142-
const notificationField: FCMNotificationField = {
143-
title: notification.title,
144-
body: notification.text,
145-
};
146-
147141
// then we will create the message
148142
const message: FCMMessage = {
149-
notification: notificationField,
143+
...(!notification.voip && { notification: { title: notification.title, body: notification.text } }),
150144
data,
151145
android: {
152146
priority: 'HIGH',
@@ -185,7 +179,7 @@ export const sendFCM = function ({ userTokens, notification, _removeToken, optio
185179

186180
const removeToken = () => {
187181
const { token } = fcmRequest.message;
188-
token && _removeToken({ gcm: token });
182+
token && _removeToken(token);
189183
};
190184

191185
const response = fetchWithRetry(url, removeToken, {

apps/meteor/app/push/server/push.ts

Lines changed: 59 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@ export const isFCMCredentials = ajv.compile<FCMCredentials>(FCMCredentialsValida
7878
// This type must match the type defined in the push gateway
7979
type GatewayNotification = {
8080
uniqueId: string;
81-
from: string;
82-
title: string;
83-
text: string;
81+
from?: string;
82+
title?: string;
83+
text?: string;
8484
badge?: number;
8585
sound?: string;
8686
notId?: number;
@@ -123,8 +123,7 @@ type GatewayNotification = {
123123
export type NativeNotificationParameters = {
124124
userTokens: string | string[];
125125
notification: PendingPushNotification;
126-
_replaceToken: (currentToken: IPushToken['token'], newToken: IPushToken['token']) => void;
127-
_removeToken: (token: IPushToken['token']) => void;
126+
_removeToken: (token: string) => void;
128127
options: RequiredField<PushOptions, 'gcm'>;
129128
};
130129

@@ -167,12 +166,10 @@ class PushClass {
167166
}
168167
}
169168

170-
private replaceToken(currentToken: IPushToken['token'], newToken: IPushToken['token']): void {
171-
void PushToken.updateMany({ token: currentToken }, { $set: { token: newToken } });
172-
}
173-
174-
private removeToken(token: IPushToken['token']): void {
175-
void PushToken.deleteOne({ token });
169+
private removeToken(token: string): void {
170+
void PushToken.removeAllByTokenString(token).catch((err) => {
171+
logger.error({ msg: 'Failed to remove push token', err });
172+
});
176173
}
177174

178175
private shouldUseGateway(): boolean {
@@ -188,10 +185,13 @@ class PushClass {
188185
logger.debug({ msg: 'send to token', token: app.token });
189186

190187
if ('apn' in app.token && app.token.apn) {
188+
const userToken = notification.voip ? app.voipToken : app.token.apn;
189+
const topic = notification.voip ? `${app.appName}.voip` : app.appName;
190+
191191
countApn.push(app._id);
192192
// Send to APN
193-
if (this.options.apn) {
194-
sendAPN({ userToken: app.token.apn, notification: { topic: app.appName, ...notification }, _removeToken: this.removeToken });
193+
if (this.options.apn && userToken) {
194+
sendAPN({ userToken, notification: { topic, ...notification }, _removeToken: this.removeToken });
195195
}
196196
} else if ('gcm' in app.token && app.token.gcm) {
197197
countGcm.push(app._id);
@@ -210,7 +210,6 @@ class PushClass {
210210
sendFCM({
211211
userTokens: app.token.gcm,
212212
notification,
213-
_replaceToken: this.replaceToken,
214213
_removeToken: this.removeToken,
215214
options: sendGCMOptions as RequiredField<PushOptions, 'gcm'>,
216215
});
@@ -275,16 +274,7 @@ class PushClass {
275274

276275
if (result.status === 406) {
277276
logger.info({ msg: 'removing push token', token });
278-
await PushToken.deleteMany({
279-
$or: [
280-
{
281-
'token.apn': token,
282-
},
283-
{
284-
'token.gcm': token,
285-
},
286-
],
287-
});
277+
this.removeToken(token);
288278
return;
289279
}
290280

@@ -340,8 +330,13 @@ class PushClass {
340330
logger.debug({ msg: 'send to token', token: app.token });
341331

342332
if ('apn' in app.token && app.token.apn) {
343-
countApn.push(app._id);
344-
return this.sendGatewayPush(gateway, 'apn', app.token.apn, { topic: app.appName, ...gatewayNotification });
333+
const token = notification.voip ? app.voipToken : app.token.apn;
334+
const topic = notification.voip ? `${app.appName}.voip` : app.appName;
335+
336+
if (token) {
337+
countApn.push(app._id);
338+
return this.sendGatewayPush(gateway, 'apn', token, { topic, ...gatewayNotification });
339+
}
345340
}
346341

347342
if ('gcm' in app.token && app.token.gcm) {
@@ -373,12 +368,7 @@ class PushClass {
373368
userId: notification.userId,
374369
});
375370

376-
const query = {
377-
userId: notification.userId,
378-
$or: [{ 'token.apn': { $exists: true } }, { 'token.gcm': { $exists: true } }],
379-
};
380-
381-
const appTokens = PushToken.find(query);
371+
const appTokens = PushToken.findAllTokensByUserId(notification.userId);
382372

383373
for await (const app of appTokens) {
384374
logger.debug({ msg: 'send to token', token: app.token });
@@ -427,9 +417,9 @@ class PushClass {
427417
private _validateDocument(notification: PendingPushNotification): void {
428418
// Check the general notification
429419
check(notification, {
430-
from: String,
431-
title: String,
432-
text: String,
420+
from: Match.Optional(String),
421+
title: Match.Optional(String),
422+
text: Match.Optional(String),
433423
sent: Match.Optional(Boolean),
434424
sending: Match.Optional(Match.Integer),
435425
badge: Match.Optional(Match.Integer),
@@ -448,11 +438,24 @@ class PushClass {
448438
createdAt: Date,
449439
createdBy: Match.OneOf(String, null),
450440
priority: Match.Optional(Match.Integer),
441+
voip: Match.Optional(Boolean),
451442
});
452443

453444
if (!notification.userId) {
454445
throw new Error('No userId found');
455446
}
447+
448+
if (!notification.voip) {
449+
const { from, title, text } = notification;
450+
check(
451+
{ from, title, text },
452+
{
453+
from: String,
454+
title: String,
455+
text: String,
456+
},
457+
);
458+
}
456459
}
457460

458461
private hasApnOptions(options: IPushNotificationConfig): options is RequiredField<IPushNotificationConfig, 'apn'> {
@@ -463,17 +466,31 @@ class PushClass {
463466
return Match.test(options.gcm, Object);
464467
}
465468

466-
public async send(options: IPushNotificationConfig) {
467-
const notification: PendingPushNotification = {
469+
private getPendingNotification(options: IPushNotificationConfig): PendingPushNotification {
470+
const required = {
468471
createdAt: new Date(),
469472
// createdBy is no longer used, but the gateway still expects it
470473
createdBy: '<SERVER>',
471474
sent: false,
472475
sending: 0,
476+
...pick(options, 'userId', 'payload', 'notId', 'priority'),
477+
} as const;
478+
479+
if (options.voip) {
480+
return {
481+
...required,
482+
voip: true,
483+
};
484+
}
485+
486+
return {
487+
voip: false,
488+
...required,
489+
473490
title: truncateString(options.title, PUSH_TITLE_LIMIT),
474491
text: truncateString(options.text, PUSH_MESSAGE_BODY_LIMIT),
475492

476-
...pick(options, 'from', 'userId', 'payload', 'badge', 'sound', 'notId', 'priority'),
493+
...pick(options, 'from', 'badge', 'sound'),
477494

478495
...(this.hasApnOptions(options)
479496
? {
@@ -490,6 +507,10 @@ class PushClass {
490507
}
491508
: {}),
492509
};
510+
}
511+
512+
public async send(options: IPushNotificationConfig) {
513+
const notification = this.getPendingNotification(options);
493514

494515
// Validate the notification
495516
this._validateDocument(notification);

0 commit comments

Comments
 (0)