Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/meteor/app/api/server/v1/push.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Push } from '@rocket.chat/core-services';
import { pushTokenTypes } from '@rocket.chat/core-typings';
import type { IPushToken, IPushTokenTypes } from '@rocket.chat/core-typings';
import { Messages, PushToken, Users, Rooms, Settings } from '@rocket.chat/models';
import {
Expand Down Expand Up @@ -38,7 +39,7 @@ const PushTokenPOSTSchema: JSONSchemaType<PushTokenPOST> = {
},
type: {
type: 'string',
enum: ['apn', 'gcm'],
enum: pushTokenTypes,
},
value: {
type: 'string',
Expand Down
32 changes: 22 additions & 10 deletions apps/meteor/app/push/server/apn.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import apn from '@parse/node-apn';
import type { IPushToken, RequiredField } from '@rocket.chat/core-typings';
import type { RequiredField } from '@rocket.chat/core-typings';
import EJSON from 'ejson';

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

const note = new apn.Notification();

note.expiry = Math.floor(Date.now() / 1000) + 3600; // Expires 1 hour from now.
if (notification.useVoipToken) {
note.expiry = Math.floor(Date.now() / 1000) + 60; // Expires in 60 seconds
note.pushType = 'voip';
} else {
note.expiry = Math.floor(Date.now() / 1000) + 3600; // Expires 1 hour from now.
}

if (notification.badge !== undefined) {
note.badge = notification.badge;
}
Expand All @@ -50,10 +56,16 @@ export const sendAPN = ({
// adds category support for iOS8 custom actions as described here:
// https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/
// RemoteNotificationsPG/Chapters/IPhoneOSClientImp.html#//apple_ref/doc/uid/TP40008194-CH103-SW36
note.category = notification.apn?.category;
if (notification.apn?.category) {
note.category = notification.apn.category;
}

note.body = notification.text;
note.title = notification.title;
if (notification.text) {
note.body = notification.text;
}
if (notification.title) {
note.title = notification.title;
}

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

note.payload.messageFrom = notification.from;
if (notification.from) {
note.payload.messageFrom = notification.from;
}
note.priority = priority;

note.topic = notification.topic;
Expand All @@ -81,9 +95,7 @@ export const sendAPN = ({
msg: 'Removing APN token',
token: userToken,
});
_removeToken({
apn: userToken,
});
_removeToken(userToken);
}
});
});
Expand Down
7 changes: 4 additions & 3 deletions apps/meteor/app/push/server/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ export type PushOptions = {
};

export type PendingPushNotification = {
from: string;
title: string;
text: string;
from?: string;
title?: string;
text?: string;
badge?: number;
sound?: string;
notId?: number;
Expand All @@ -42,4 +42,5 @@ export type PendingPushNotification = {
priority?: number;

contentAvailable?: 1 | 0;
useVoipToken?: boolean;
};
12 changes: 6 additions & 6 deletions apps/meteor/app/push/server/fcm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import type { NativeNotificationParameters } from './push';
type FCMDataField = Record<string, any>;

type FCMNotificationField = {
title: string;
body: string;
title?: string;
body?: string;
image?: string;
};

Expand Down Expand Up @@ -140,13 +140,13 @@ function getFCMMessagesFromPushData(userTokens: string[], notification: PendingP

// then we will create the notification field
const notificationField: FCMNotificationField = {
title: notification.title,
body: notification.text,
...(notification.title && { title: notification.title }),
...(notification.text && { body: notification.text }),
};

// then we will create the message
const message: FCMMessage = {
notification: notificationField,
...(Object.keys(notificationField).length && { notification: notificationField }),
data,
android: {
priority: 'HIGH',
Expand Down Expand Up @@ -185,7 +185,7 @@ export const sendFCM = function ({ userTokens, notification, _removeToken, optio

const removeToken = () => {
const { token } = fcmRequest.message;
token && _removeToken({ gcm: token });
token && _removeToken(token);
};

const response = fetchWithRetry(url, removeToken, {
Expand Down
103 changes: 53 additions & 50 deletions apps/meteor/app/push/server/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const _matchToken = Match.OneOf({ apn: String }, { gcm: String });

const PUSH_TITLE_LIMIT = 65;
const PUSH_MESSAGE_BODY_LIMIT = 240;
const PUSH_GATEWAY_MAX_ATTEMPTS = 5;

type FCMCredentials = {
type: string;
Expand Down Expand Up @@ -78,9 +79,9 @@ export const isFCMCredentials = ajv.compile<FCMCredentials>(FCMCredentialsValida
// This type must match the type defined in the push gateway
type GatewayNotification = {
uniqueId: string;
from: string;
title: string;
text: string;
from?: string;
title?: string;
text?: string;
badge?: number;
sound?: string;
notId?: number;
Expand Down Expand Up @@ -123,8 +124,7 @@ type GatewayNotification = {
export type NativeNotificationParameters = {
userTokens: string | string[];
notification: PendingPushNotification;
_replaceToken: (currentToken: IPushToken['token'], newToken: IPushToken['token']) => void;
_removeToken: (token: IPushToken['token']) => void;
_removeToken: (token: string) => void;
options: RequiredField<PushOptions, 'gcm'>;
};

Expand Down Expand Up @@ -167,12 +167,10 @@ class PushClass {
}
}

private replaceToken(currentToken: IPushToken['token'], newToken: IPushToken['token']): void {
void PushToken.updateMany({ token: currentToken }, { $set: { token: newToken } });
}

private removeToken(token: IPushToken['token']): void {
void PushToken.deleteOne({ token });
private removeToken(token: string): void {
void PushToken.removeOrUnsetByTokenString(token).catch((err) => {
logger.error({ msg: 'Failed to remove push token', err });
});
}

private shouldUseGateway(): boolean {
Expand All @@ -188,10 +186,13 @@ class PushClass {
logger.debug({ msg: 'send to token', token: app.token });

if ('apn' in app.token && app.token.apn) {
countApn.push(app._id);
const userToken = notification.useVoipToken ? app.voipToken : app.token.apn;
const topic = notification.useVoipToken ? `${app.appName}.voip` : app.appName;

// Send to APN
if (this.options.apn) {
sendAPN({ userToken: app.token.apn, notification: { topic: app.appName, ...notification }, _removeToken: this.removeToken });
if (this.options.apn && userToken) {
countApn.push(app._id);
sendAPN({ userToken, notification: { topic, ...notification }, _removeToken: this.removeToken });
}
} else if ('gcm' in app.token && app.token.gcm) {
countGcm.push(app._id);
Expand All @@ -210,7 +211,6 @@ class PushClass {
sendFCM({
userTokens: app.token.gcm,
notification,
_replaceToken: this.replaceToken,
_removeToken: this.removeToken,
options: sendGCMOptions as RequiredField<PushOptions, 'gcm'>,
});
Expand Down Expand Up @@ -255,7 +255,7 @@ class PushClass {
service: 'apn' | 'gcm',
token: string,
notification: Optional<GatewayNotification, 'uniqueId'>,
tries = 0,
retryOptions: { tries: number; maxTries: number } = { tries: 0, maxTries: PUSH_GATEWAY_MAX_ATTEMPTS },
): Promise<void> {
notification.uniqueId = this.options.uniqueId;

Expand All @@ -275,16 +275,7 @@ class PushClass {

if (result.status === 406) {
logger.info({ msg: 'removing push token', token });
await PushToken.deleteMany({
$or: [
{
'token.apn': token,
},
{
'token.gcm': token,
},
],
});
this.removeToken(token);
return;
}

Expand All @@ -302,22 +293,24 @@ class PushClass {
return;
}

const { tries, maxTries } = retryOptions;

logger.error({ msg: 'Error sending push to gateway', tries, err: response });

if (tries <= 4) {
if (tries < maxTries) {
// [1, 2, 4, 8, 16] minutes (total 31)
const ms = 60000 * Math.pow(2, tries);

logger.log({ msg: 'Retrying push to gateway', tries: tries + 1, in: ms });

setTimeout(() => this.sendGatewayPush(gateway, service, token, notification, tries + 1), ms);
setTimeout(() => this.sendGatewayPush(gateway, service, token, notification, { tries: tries + 1, maxTries }), ms);
}
}

private getGatewayNotificationData(notification: PendingPushNotification): Omit<GatewayNotification, 'uniqueId'> {
// Gateway currently accepts every attribute from the PendingPushNotification type, except for the priority
// Gateway currently accepts every attribute from the PendingPushNotification type, except for the priority and useVoipToken
// If new attributes are added to the PendingPushNotification type, they'll need to be removed here as well.
const { priority: _priority, ...notifData } = notification;
const { priority: _priority, useVoipToken: _useVoipToken, ...notifData } = notification;

return {
...notifData,
Expand All @@ -335,35 +328,47 @@ class PushClass {
}

const gatewayNotification = this.getGatewayNotificationData(notification);
const retryOptions = {
tries: 0,
maxTries: notification.useVoipToken ? 1 : PUSH_GATEWAY_MAX_ATTEMPTS,
};

for (const gateway of this.options.gateways) {
logger.debug({ msg: 'send to token', token: app.token });

if ('apn' in app.token && app.token.apn) {
countApn.push(app._id);
return this.sendGatewayPush(gateway, 'apn', app.token.apn, { topic: app.appName, ...gatewayNotification });
const token = notification.useVoipToken ? app.voipToken : app.token.apn;
const topic = notification.useVoipToken ? `${app.appName}.voip` : app.appName;

if (token) {
countApn.push(app._id);
return this.sendGatewayPush(gateway, 'apn', token, { topic, ...gatewayNotification }, retryOptions);
}
}

if ('gcm' in app.token && app.token.gcm) {
countGcm.push(app._id);
return this.sendGatewayPush(gateway, 'gcm', app.token.gcm, gatewayNotification);
return this.sendGatewayPush(gateway, 'gcm', app.token.gcm, gatewayNotification, retryOptions);
}
}
}

private async sendNotification(notification: PendingPushNotification): Promise<{ apn: string[]; gcm: string[] }> {
private async sendNotification(
notification: PendingPushNotification,
options: { skipTokenId?: IPushToken['_id'] } = {},
): Promise<{ apn: string[]; gcm: string[] }> {
logger.debug({ msg: 'Sending notification', notification });

const countApn: string[] = [];
const countGcm: string[] = [];

if (notification.from !== String(notification.from)) {
if (notification.from && notification.from !== String(notification.from)) {
throw new Error('Push.send: option "from" not a string');
}
if (notification.title !== String(notification.title)) {
if (notification.title && notification.title !== String(notification.title)) {
throw new Error('Push.send: option "title" not a string');
}
if (notification.text !== String(notification.text)) {
if (notification.text && notification.text !== String(notification.text)) {
throw new Error('Push.send: option "text" not a string');
}

Expand All @@ -373,12 +378,9 @@ class PushClass {
userId: notification.userId,
});

const query = {
userId: notification.userId,
$or: [{ 'token.apn': { $exists: true } }, { 'token.gcm': { $exists: true } }],
};

const appTokens = PushToken.find(query);
const appTokens = options.skipTokenId
? PushToken.findTokensByUserIdExceptId(notification.userId, options.skipTokenId)
: PushToken.findAllTokensByUserId(notification.userId);

for await (const app of appTokens) {
logger.debug({ msg: 'send to token', token: app.token });
Expand Down Expand Up @@ -427,9 +429,9 @@ class PushClass {
private _validateDocument(notification: PendingPushNotification): void {
// Check the general notification
check(notification, {
from: String,
title: String,
text: String,
from: Match.Optional(String),
title: Match.Optional(String),
text: Match.Optional(String),
sent: Match.Optional(Boolean),
sending: Match.Optional(Match.Integer),
badge: Match.Optional(Match.Integer),
Expand All @@ -448,6 +450,7 @@ class PushClass {
createdAt: Date,
createdBy: Match.OneOf(String, null),
priority: Match.Optional(Match.Integer),
useVoipToken: Match.Optional(Boolean),
});

if (!notification.userId) {
Expand All @@ -470,10 +473,10 @@ class PushClass {
createdBy: '<SERVER>',
sent: false,
sending: 0,
title: truncateString(options.title, PUSH_TITLE_LIMIT),
text: truncateString(options.text, PUSH_MESSAGE_BODY_LIMIT),
...(options.title && { title: truncateString(options.title, PUSH_TITLE_LIMIT) }),
...(options.text && { text: truncateString(options.text, PUSH_MESSAGE_BODY_LIMIT) }),

...pick(options, 'from', 'userId', 'payload', 'badge', 'sound', 'notId', 'priority'),
...pick(options, 'from', 'userId', 'payload', 'badge', 'sound', 'notId', 'priority', 'useVoipToken'),

...(this.hasApnOptions(options)
? {
Expand All @@ -495,7 +498,7 @@ class PushClass {
this._validateDocument(notification);

try {
await this.sendNotification(notification);
await this.sendNotification(notification, pick(options, 'skipTokenId'));
} catch (error: any) {
logger.debug({
msg: 'Could not send notification to user',
Expand Down
3 changes: 3 additions & 0 deletions apps/meteor/server/services/media-call/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Logger } from '@rocket.chat/logger';

export const logger = new Logger('media-call service');
Loading
Loading