Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve: Better Push Notification code #17338

Merged
merged 17 commits into from
Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 0 additions & 2 deletions .meteor/packages
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ nooitaf:colors
ostrio:cookies
pauli:accounts-linkedin
raix:handlebar-helpers
rocketchat:push
raix:ui-dropped-event

rocketchat:tap-i18n
Expand All @@ -87,7 +86,6 @@ matb33:collection-hooks
meteorhacks:inject-initial
[email protected]
[email protected]
raix:eventemitter
[email protected]
[email protected]
templating
Expand Down
2 changes: 0 additions & 2 deletions .meteor/versions
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ pauli:[email protected]
pauli:[email protected]
[email protected]
raix:[email protected]
raix:[email protected]
raix:[email protected]
raix:[email protected]
[email protected]
Expand All @@ -127,7 +126,6 @@ rocketchat:[email protected]
rocketchat:[email protected]
rocketchat:[email protected]
rocketchat:[email protected]
rocketchat:[email protected]
rocketchat:[email protected]
rocketchat:[email protected]
rocketchat:[email protected]
Expand Down
4 changes: 2 additions & 2 deletions app/api/server/v1/push.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { Push } from 'meteor/rocketchat:push';

import { appTokensCollection } from '../../../push/server';
import { API } from '../api';

API.v1.addRoute('push.token', { authRequired: true }, {
Expand Down Expand Up @@ -47,7 +47,7 @@ API.v1.addRoute('push.token', { authRequired: true }, {
throw new Meteor.Error('error-token-param-not-valid', 'The required "token" body param is missing or invalid.');
}

const affectedRecords = Push.appCollection.remove({
const affectedRecords = appTokensCollection.remove({
$or: [{
'token.apn': token,
}, {
Expand Down
3 changes: 1 addition & 2 deletions app/push-notifications/server/lib/PushNotification.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Push } from 'meteor/rocketchat:push';

import { Push } from '../../../push/server';
import { settings } from '../../../settings';
import { metrics } from '../../../metrics';
import { RocketChatAssets } from '../../../assets';
Expand Down
115 changes: 115 additions & 0 deletions app/push/server/apn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import apn from 'apn';

import { logger } from './logger';

let apnConnection;

export const sendAPN = ({ userToken, notification, _removeToken }) => {
if (typeof notification.apn === 'object') {
notification = Object.assign({}, notification, notification.apn);
}

const priority = notification.priority || notification.priority === 0 ? notification.priority : 10;

const note = new apn.Notification();

note.expiry = Math.floor(Date.now() / 1000) + 3600; // Expires 1 hour from now.
note.badge = notification.badge;
note.sound = notification.sound;

if (notification.contentAvailable != null) {
note.setContentAvailable(notification.contentAvailable);
}

// 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.category;

note.body = notification.text;
note.title = notification.title;

if (notification.notId != null) {
note.threadId = String(notification.notId);
}

// Allow the user to set payload data
note.payload = notification.payload || {};

note.payload.messageFrom = notification.from;
note.priority = priority;

// Store the token on the note so we can reference it if there was an error
note.token = userToken;
note.topic = notification.topic;
note.mutableContent = 1;

apnConnection.send(note, userToken).then((response) => {
response.failed.forEach((failure) => {
logger.debug(`Got error code ${ failure.status } for token ${ userToken }`);

if (['400', '410'].includes(failure.status)) {
logger.debug(`Removing token ${ userToken }`);
_removeToken({
apn: userToken,
});
}
});
});
};

export const initAPN = ({ options, absoluteUrl }) => {
logger.debug('APN configured');

// Allow production to be a general option for push notifications
if (options.production === Boolean(options.production)) {
options.apn.production = options.production;
}

// Give the user warnings about development settings
if (options.apn.development) {
// This flag is normally set by the configuration file
console.warn('WARNING: Push APN is using development key and certificate');
} else if (options.apn.gateway) {
// We check the apn gateway i the options, we could risk shipping
// server into production while using the production configuration.
// On the other hand we could be in development but using the production
// configuration. And finally we could have configured an unknown apn
// gateway (this could change in the future - but a warning about typos
// can save hours of debugging)
//
// Warn about gateway configurations - it's more a guide

if (options.apn.gateway === 'gateway.sandbox.push.apple.com') {
// Using the development sandbox
console.warn('WARNING: Push APN is in development mode');
} else if (options.apn.gateway === 'gateway.push.apple.com') {
// In production - but warn if we are running on localhost
if (/http:\/\/localhost/.test(absoluteUrl)) {
console.warn('WARNING: Push APN is configured to production mode - but server is running from localhost');
}
} else {
// Warn about gateways we dont know about
console.warn(`WARNING: Push APN unknown gateway "${ options.apn.gateway }"`);
}
} else if (options.apn.production) {
if (/http:\/\/localhost/.test(absoluteUrl)) {
console.warn('WARNING: Push APN is configured to production mode - but server is running from localhost');
}
} else {
console.warn('WARNING: Push APN is in development mode');
}

// Check certificate data
if (!options.apn.cert || !options.apn.cert.length) {
console.error('ERROR: Push server could not find cert');
}

// Check key data
if (!options.apn.key || !options.apn.key.length) {
console.error('ERROR: Push server could not find key');
}

// Rig apn connection
apnConnection = new apn.Provider(options.apn);
};
121 changes: 121 additions & 0 deletions app/push/server/gcm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import gcm from 'node-gcm';

import { logger } from './logger';

export const sendGCM = function({ userTokens, notification, _replaceToken, _removeToken, options }) {
if (typeof notification.gcm === 'object') {
notification = Object.assign({}, notification, notification.gcm);
}

// Make sure userTokens are an array of strings
if (typeof userTokens === 'string') {
userTokens = [userTokens];
}

// Check if any tokens in there to send
if (!userTokens.length) {
logger.debug('sendGCM no push tokens found');
return;
}

logger.debug('sendGCM', userTokens, notification);

// Allow user to set payload
const data = notification.payload || {};

data.title = notification.title;
data.message = notification.text;

// Set image
if (notification.image != null) {
data.image = notification.image;
}

if (notification.android_channel_id != null) {
data.android_channel_id = notification.android_channel_id;
} else {
logger.debug('For devices running Android 8.0 or later you are required to provide an android_channel_id. See https://github.com/raix/push/issues/341 for more info');
}

// Set extra details
if (notification.badge != null) {
data.msgcnt = notification.badge;
}
if (notification.sound != null) {
data.soundname = notification.sound;
}
if (notification.notId != null) {
data.notId = notification.notId;
}
if (notification.style != null) {
data.style = notification.style;
}
if (notification.summaryText != null) {
data.summaryText = notification.summaryText;
}
if (notification.picture != null) {
data.picture = notification.picture;
}

// Action Buttons
if (notification.actions != null) {
data.actions = notification.actions;
}

// Force Start
if (notification.forceStart != null) {
data['force-start'] = notification.forceStart;
}

if (notification.contentAvailable != null) {
data['content-available'] = notification.contentAvailable;
}

const message = new gcm.Message({
collapseKey: notification.from,
// delayWhileIdle: true,
// timeToLive: 4,
// restricted_package_name: 'dk.gi2.app'
data,
});

logger.debug(`Create GCM Sender using "${ options.gcm.apiKey }"`);
const sender = new gcm.Sender(options.gcm.apiKey);

userTokens.forEach((value) => logger.debug(`A:Send message to: ${ value }`));

const userToken = userTokens.length === 1 ? userTokens[0] : null;

sender.send(message, userTokens, 5, function(err, result) {
if (err) {
logger.debug(`ANDROID ERROR: result of sender: ${ result }`);
return;
}

if (result === null) {
logger.debug('ANDROID: Result of sender is null');
return;
}

logger.debug(`ANDROID: Result of sender: ${ JSON.stringify(result) }`);

if (result.canonical_ids === 1 && userToken) {
// This is an old device, token is replaced
try {
_replaceToken({ gcm: userToken }, { gcm: result.results[0].registration_id });
} catch (err) {
logger.error('Error replacing token', err);
}
}
// We cant send to that token - might not be registered
// ask the user to remove the token from the list
if (result.failure !== 0 && userToken) {
// This is an old device, token is replaced
try {
_removeToken({ gcm: userToken });
} catch (err) {
logger.error('Error removing token', err);
}
}
});
};
3 changes: 3 additions & 0 deletions app/push/server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import './methods';

export { Push, appTokensCollection, notificationsCollection } from './push';
4 changes: 4 additions & 0 deletions app/push/server/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Logger, LoggerManager } from '../../logger/server';

export const logger = new Logger('Push');
export { LoggerManager };
Loading