Skip to content

Commit 04c0d47

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

11 files changed

Lines changed: 224 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: 48 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 });
@@ -425,11 +415,13 @@ class PushClass {
425415
// This is a general function to validate that the data added to notifications
426416
// is in the correct format. If not this function will throw errors
427417
private _validateDocument(notification: PendingPushNotification): void {
418+
const voipOptionalString = notification.voip ? Match.Optional(String) : String;
419+
428420
// Check the general notification
429421
check(notification, {
430-
from: String,
431-
title: String,
432-
text: String,
422+
from: voipOptionalString,
423+
title: voipOptionalString,
424+
text: voipOptionalString,
433425
sent: Match.Optional(Boolean),
434426
sending: Match.Optional(Match.Integer),
435427
badge: Match.Optional(Match.Integer),
@@ -463,17 +455,31 @@ class PushClass {
463455
return Match.test(options.gcm, Object);
464456
}
465457

466-
public async send(options: IPushNotificationConfig) {
467-
const notification: PendingPushNotification = {
458+
private getPendingNotification(options: IPushNotificationConfig): PendingPushNotification {
459+
const required = {
468460
createdAt: new Date(),
469461
// createdBy is no longer used, but the gateway still expects it
470462
createdBy: '<SERVER>',
471463
sent: false,
472464
sending: 0,
465+
...pick(options, 'userId', 'payload', 'notId', 'priority'),
466+
} as const;
467+
468+
if (options.voip) {
469+
return {
470+
...required,
471+
voip: true,
472+
};
473+
}
474+
475+
return {
476+
voip: false,
477+
...required,
478+
473479
title: truncateString(options.title, PUSH_TITLE_LIMIT),
474480
text: truncateString(options.text, PUSH_MESSAGE_BODY_LIMIT),
475481

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

478484
...(this.hasApnOptions(options)
479485
? {
@@ -490,6 +496,10 @@ class PushClass {
490496
}
491497
: {}),
492498
};
499+
}
500+
501+
public async send(options: IPushNotificationConfig) {
502+
const notification = this.getPendingNotification(options);
493503

494504
// Validate the notification
495505
this._validateDocument(notification);

0 commit comments

Comments
 (0)