mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 06:07:21 +01:00
Push notifications (#7682)
* Fix Social Push notifications * Fix code formatting issues * Fix commented issues * Fix Syntax errors * update push notify dependency * specify push-notify version * change how apn key is loaded * feat(push-notifications): improve logging * feat(push-notifications): disable v2 push notifications * test(push-notifications): add unit tests and improve integration ones * fix(push-notifications): throw when required params are missing * fix(tests): correct descriptions and remove wrong comment * fix(push-notifications): trim APN key * fix(apn): log feedback only if it has data * fix(apn): load cert and key differently * fix(tests): correctly load apn during tests * download creds from S3 and create AWS lib * convert s3 buffer to a string * fix(apn): remove console.log and do not use cert twice * invert key and cert, disable failing test * invert key and cert
This commit is contained in:
@@ -108,10 +108,12 @@
|
|||||||
"emailNotifications": "Email Notifications",
|
"emailNotifications": "Email Notifications",
|
||||||
"wonChallenge": "You won a Challenge!",
|
"wonChallenge": "You won a Challenge!",
|
||||||
"newPM": "Received Private Message",
|
"newPM": "Received Private Message",
|
||||||
|
"newPMInfo": "New Message from <%= name %>: <%= message %>",
|
||||||
"sentGems": "Sent gems!",
|
"sentGems": "Sent gems!",
|
||||||
"giftedGems": "Gifted Gems",
|
"giftedGems": "Gifted Gems",
|
||||||
"giftedGemsInfo": "<%= amount %> Gems - by <%= name %>",
|
"giftedGemsInfo": "<%= name %> gifted you <%= amount %> Gems",
|
||||||
"giftedSubscription": "Gifted Subscription",
|
"giftedSubscription": "Gifted Subscription",
|
||||||
|
"giftedSubscriptionInfo": "<%= name %> gifted you a <%= months %> Subscription",
|
||||||
"invitedParty": "Invited To Party",
|
"invitedParty": "Invited To Party",
|
||||||
"invitedGuild": "Invited To Guild",
|
"invitedGuild": "Invited To Guild",
|
||||||
"importantAnnouncements": "Your account is inactive",
|
"importantAnnouncements": "Your account is inactive",
|
||||||
@@ -126,6 +128,7 @@
|
|||||||
"unsubscribedTextOthers": "You won't receive any other email from Habitica.",
|
"unsubscribedTextOthers": "You won't receive any other email from Habitica.",
|
||||||
"unsubscribeAllEmails": "Check to Unsubscribe from Emails",
|
"unsubscribeAllEmails": "Check to Unsubscribe from Emails",
|
||||||
"unsubscribeAllEmailsText": "By checking this box, I certify that I understand that by unsubscribing from all emails, Habitica will never be able to notify me via email about important changes to the site or my account.",
|
"unsubscribeAllEmailsText": "By checking this box, I certify that I understand that by unsubscribing from all emails, Habitica will never be able to notify me via email about important changes to the site or my account.",
|
||||||
|
"unsubscribeAllPush": "Check to Unsubscribe from all Push Notifications",
|
||||||
"correctlyUnsubscribedEmailType": "Correctly unsubscribed from \"<%= emailType %>\" emails.",
|
"correctlyUnsubscribedEmailType": "Correctly unsubscribed from \"<%= emailType %>\" emails.",
|
||||||
"subscriptionRateText": "Recurring $<%= price %> USD every <%= months %> months",
|
"subscriptionRateText": "Recurring $<%= price %> USD every <%= months %> months",
|
||||||
"recurringText": "recurring",
|
"recurringText": "recurring",
|
||||||
@@ -138,8 +141,8 @@
|
|||||||
"promoCode": "Promo Code",
|
"promoCode": "Promo Code",
|
||||||
"promoCodeApplied": "Promo Code Applied! Check your inventory",
|
"promoCodeApplied": "Promo Code Applied! Check your inventory",
|
||||||
"promoPlaceholder": "Enter Promotion Code",
|
"promoPlaceholder": "Enter Promotion Code",
|
||||||
"couponText": "We sometimes have events and give out coupon codes for special gear. (eg, those who stop by our Wondercon booth)",
|
|
||||||
"displayInviteToPartyWhenPartyIs1": "Display Invite To Party button when party has 1 member.",
|
"displayInviteToPartyWhenPartyIs1": "Display Invite To Party button when party has 1 member.",
|
||||||
|
"couponText": "We sometimes have events and give out coupon codes for special gear. (eg, those who stop by our Wondercon booth)",
|
||||||
"saveCustomDayStart": "Save Custom Day Start",
|
"saveCustomDayStart": "Save Custom Day Start",
|
||||||
"registration": "Registration",
|
"registration": "Registration",
|
||||||
"addLocalAuth": "Add local authentication:",
|
"addLocalAuth": "Add local authentication:",
|
||||||
@@ -152,8 +155,11 @@
|
|||||||
"invalidUrl": "invalid url",
|
"invalidUrl": "invalid url",
|
||||||
"invalidEnabled": "the \"enabled\" parameter should be a boolean",
|
"invalidEnabled": "the \"enabled\" parameter should be a boolean",
|
||||||
"regIdRequired": "RegId is required",
|
"regIdRequired": "RegId is required",
|
||||||
|
"invalidPushClient": "Invalid client. Only Official Habitica clients can receive push notifications.",
|
||||||
"pushDeviceAdded": "Push device added successfully",
|
"pushDeviceAdded": "Push device added successfully",
|
||||||
"pushDeviceAlreadyAdded": "The user already has the push device",
|
"pushDeviceAlreadyAdded": "The user already has the push device",
|
||||||
|
"pushDeviceNotFound": "The user has no push device with this id.",
|
||||||
|
"pushDeviceRemoved": "Push device removed successfully.",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"buyGemsGoldCap": "Cap raised to <%= amount %>",
|
"buyGemsGoldCap": "Cap raised to <%= amount %>",
|
||||||
"mysticHourglass": "<%= amount %> Mystic Hourglass",
|
"mysticHourglass": "<%= amount %> Mystic Hourglass",
|
||||||
@@ -168,5 +174,6 @@
|
|||||||
"amazonPayments": "Amazon Payments",
|
"amazonPayments": "Amazon Payments",
|
||||||
"timezone": "Time Zone",
|
"timezone": "Time Zone",
|
||||||
"timezoneUTC": "Habitica uses the time zone set on your PC, which is: <strong><%= utc %></strong>",
|
"timezoneUTC": "Habitica uses the time zone set on your PC, which is: <strong><%= utc %></strong>",
|
||||||
"timezoneInfo": "If that time zone is wrong, first reload this page using your browser's reload or refresh button to ensure that Habitica has the most recent information. If it is still wrong, adjust the time zone on your PC and then reload this page again.<br><br> <strong>If you use Habitica on other PCs or mobile devices, the time zone must be the same on them all.</strong> If your Dailies have been resetting at the wrong time, repeat this check on all other PCs and on a browser on your mobile devices."
|
"timezoneInfo": "If that time zone is wrong, first reload this page using your browser's reload or refresh button to ensure that Habitica has the most recent information. If it is still wrong, adjust the time zone on your PC and then reload this page again.<br><br> <strong>If you use Habitica on other PCs or mobile devices, the time zone must be the same on them all.</strong> If your Dailies have been resetting at the wrong time, repeat this check on all other PCs and on a browser on your mobile devices.",
|
||||||
|
"push": "Push"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ import blockUser from './ops/blockUser';
|
|||||||
import clearPMs from './ops/clearPMs';
|
import clearPMs from './ops/clearPMs';
|
||||||
import deletePM from './ops/deletePM';
|
import deletePM from './ops/deletePM';
|
||||||
import reroll from './ops/reroll';
|
import reroll from './ops/reroll';
|
||||||
import addPushDevice from './ops/addPushDevice';
|
|
||||||
import reset from './ops/reset';
|
import reset from './ops/reset';
|
||||||
import markPmsRead from './ops/markPMSRead';
|
import markPmsRead from './ops/markPMSRead';
|
||||||
|
|
||||||
@@ -191,7 +190,6 @@ api.ops = {
|
|||||||
clearPMs,
|
clearPMs,
|
||||||
deletePM,
|
deletePM,
|
||||||
reroll,
|
reroll,
|
||||||
addPushDevice,
|
|
||||||
reset,
|
reset,
|
||||||
markPmsRead,
|
markPmsRead,
|
||||||
};
|
};
|
||||||
@@ -272,7 +270,6 @@ api.wrap = function wrapUser (user, main = true) {
|
|||||||
addWebhook: _.partial(importedOps.addWebhook, user),
|
addWebhook: _.partial(importedOps.addWebhook, user),
|
||||||
updateWebhook: _.partial(importedOps.updateWebhook, user),
|
updateWebhook: _.partial(importedOps.updateWebhook, user),
|
||||||
deleteWebhook: _.partial(importedOps.deleteWebhook, user),
|
deleteWebhook: _.partial(importedOps.deleteWebhook, user),
|
||||||
addPushDevice: _.partial(importedOps.addPushDevice, user),
|
|
||||||
clearPMs: _.partial(importedOps.clearPMs, user),
|
clearPMs: _.partial(importedOps.clearPMs, user),
|
||||||
deletePM: _.partial(importedOps.deletePM, user),
|
deletePM: _.partial(importedOps.deletePM, user),
|
||||||
blockUser: _.partial(importedOps.blockUser, user),
|
blockUser: _.partial(importedOps.blockUser, user),
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import i18n from '../i18n';
|
|
||||||
import {
|
|
||||||
BadRequest,
|
|
||||||
NotAuthorized,
|
|
||||||
} from '../libs/errors';
|
|
||||||
|
|
||||||
// TODO move to server code
|
|
||||||
module.exports = function addPushDevice (user, req = {}) {
|
|
||||||
let regId = _.get(req, 'body.regId');
|
|
||||||
if (!regId) throw new BadRequest(i18n.t('regIdRequired', req.language));
|
|
||||||
|
|
||||||
let type = _.get(req, 'body.type');
|
|
||||||
if (!type) throw new BadRequest(i18n.t('typeRequired', req.language));
|
|
||||||
|
|
||||||
if (!user.pushDevices) {
|
|
||||||
user.pushDevices = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
let pushDevices = user.pushDevices;
|
|
||||||
|
|
||||||
let item = {
|
|
||||||
regId,
|
|
||||||
type,
|
|
||||||
};
|
|
||||||
|
|
||||||
let indexOfPushDevice = _.findIndex(pushDevices, {
|
|
||||||
regId: item.regId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (indexOfPushDevice !== -1) {
|
|
||||||
throw new NotAuthorized(i18n.t('pushDeviceAlreadyAdded', req.language));
|
|
||||||
}
|
|
||||||
|
|
||||||
pushDevices.push(item);
|
|
||||||
|
|
||||||
return [
|
|
||||||
user.pushDevices,
|
|
||||||
i18n.t('pushDeviceAdded', req.language),
|
|
||||||
];
|
|
||||||
};
|
|
||||||
@@ -18,7 +18,6 @@ import deleteTag from './deleteTag';
|
|||||||
import addWebhook from './addWebhook';
|
import addWebhook from './addWebhook';
|
||||||
import updateWebhook from './updateWebhook';
|
import updateWebhook from './updateWebhook';
|
||||||
import deleteWebhook from './deleteWebhook';
|
import deleteWebhook from './deleteWebhook';
|
||||||
import addPushDevice from './addPushDevice';
|
|
||||||
import clearPMs from './clearPMs';
|
import clearPMs from './clearPMs';
|
||||||
import deletePM from './deletePM';
|
import deletePM from './deletePM';
|
||||||
import blockUser from './blockUser';
|
import blockUser from './blockUser';
|
||||||
@@ -68,7 +67,6 @@ module.exports = {
|
|||||||
addWebhook,
|
addWebhook,
|
||||||
updateWebhook,
|
updateWebhook,
|
||||||
deleteWebhook,
|
deleteWebhook,
|
||||||
addPushDevice,
|
|
||||||
clearPMs,
|
clearPMs,
|
||||||
deletePM,
|
deletePM,
|
||||||
blockUser,
|
blockUser,
|
||||||
|
|||||||
@@ -65,10 +65,7 @@
|
|||||||
"LOGGLY_ACCOUNT": "account",
|
"LOGGLY_ACCOUNT": "account",
|
||||||
"PUSH_CONFIGS": {
|
"PUSH_CONFIGS": {
|
||||||
"GCM_SERVER_API_KEY": "",
|
"GCM_SERVER_API_KEY": "",
|
||||||
"APN_PEM_FILES": {
|
"APN_ENABLED": "true"
|
||||||
"KEY": "key.pem",
|
|
||||||
"CERT": "cert.pem"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"FIREBASE": {
|
"FIREBASE": {
|
||||||
"APP": "app-name",
|
"APP": "app-name",
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
"paypal-rest-sdk": "^1.2.1",
|
"paypal-rest-sdk": "^1.2.1",
|
||||||
"pretty-data": "^0.40.0",
|
"pretty-data": "^0.40.0",
|
||||||
"ps-tree": "^1.0.0",
|
"ps-tree": "^1.0.0",
|
||||||
"push-notify": "^1.1.1",
|
"push-notify": "habitrpg/push-notify#v1.2.0",
|
||||||
"request": "~2.72.0",
|
"request": "~2.72.0",
|
||||||
"rimraf": "^2.4.3",
|
"rimraf": "^2.4.3",
|
||||||
"run-sequence": "^1.1.4",
|
"run-sequence": "^1.1.4",
|
||||||
|
|||||||
32
test/api/v3/integration/user/DELETE-user_push_device.test.js
Normal file
32
test/api/v3/integration/user/DELETE-user_push_device.test.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../../helpers/api-integration/v3';
|
||||||
|
|
||||||
|
describe('DELETE /user/push-devices', () => {
|
||||||
|
let user;
|
||||||
|
let regId = '10';
|
||||||
|
let type = 'ios';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if user does not have the push device', async () => {
|
||||||
|
await expect(user.del(`/user/push-devices/${regId}`))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('pushDeviceNotFound'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes a push device from the user', async () => {
|
||||||
|
await user.post('/user/push-devices', {type, regId});
|
||||||
|
let response = await user.del(`/user/push-devices/${regId}`);
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
expect(response.message).to.equal(t('pushDeviceRemoved'));
|
||||||
|
expect(user.pushDevices.length).to.equal(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import {
|
|
||||||
generateUser,
|
|
||||||
translate as t,
|
|
||||||
} from '../../../../helpers/api-integration/v3';
|
|
||||||
|
|
||||||
describe('POST /user/addPushDevice', () => {
|
|
||||||
let user;
|
|
||||||
let regId = '10';
|
|
||||||
let type = 'someRandomType';
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
user = await generateUser();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns an error if user already has the push device', async () => {
|
|
||||||
await user.post('/user/addPushDevice', {type, regId});
|
|
||||||
await expect(user.post('/user/addPushDevice', {type, regId}))
|
|
||||||
.to.eventually.be.rejected.and.eql({
|
|
||||||
code: 401,
|
|
||||||
error: 'NotAuthorized',
|
|
||||||
message: t('pushDeviceAlreadyAdded'),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// More tests in common code unit tests
|
|
||||||
|
|
||||||
it('adds a push device to the user', async () => {
|
|
||||||
let response = await user.post('/user/addPushDevice', {type, regId});
|
|
||||||
await user.sync();
|
|
||||||
|
|
||||||
expect(response.message).to.equal(t('pushDeviceAdded'));
|
|
||||||
expect(user.pushDevices[0].type).to.equal(type);
|
|
||||||
expect(user.pushDevices[0].regId).to.equal(regId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
60
test/api/v3/integration/user/POST-user_push_device.test.js
Normal file
60
test/api/v3/integration/user/POST-user_push_device.test.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../../helpers/api-integration/v3';
|
||||||
|
|
||||||
|
describe('POST /user/push-devices', () => {
|
||||||
|
let user;
|
||||||
|
let regId = '10';
|
||||||
|
let type = 'ios';
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when regId is not provided', async () => {
|
||||||
|
await expect(user.post('/user/push-devices'), {type})
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'Invalid request parameters.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when type is not provided', async () => {
|
||||||
|
await expect(user.post('/user/push-devices', {regId}))
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'Invalid request parameters.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when type is not valid', async () => {
|
||||||
|
await expect(user.post('/user/push-devices', {regId, type: 'invalid'}))
|
||||||
|
.to.eventually.be.rejected.and.to.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: 'Invalid request parameters.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if user already has the push device', async () => {
|
||||||
|
await user.post('/user/push-devices', {type, regId});
|
||||||
|
await expect(user.post('/user/push-devices', {type, regId}))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('pushDeviceAlreadyAdded'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds a push device to the user', async () => {
|
||||||
|
let response = await user.post('/user/push-devices', {type, regId});
|
||||||
|
await user.sync();
|
||||||
|
|
||||||
|
expect(response.message).to.equal(t('pushDeviceAdded'));
|
||||||
|
expect(user.pushDevices[0].type).to.equal(type);
|
||||||
|
expect(user.pushDevices[0].regId).to.equal(regId);
|
||||||
|
});
|
||||||
|
});
|
||||||
185
test/api/v3/unit/libs/pushNotifications.js
Normal file
185
test/api/v3/unit/libs/pushNotifications.js
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import { model as User } from '../../../../../website/server/models/user';
|
||||||
|
import requireAgain from 'require-again';
|
||||||
|
import pushNotify from 'push-notify';
|
||||||
|
import nconf from 'nconf';
|
||||||
|
|
||||||
|
describe('pushNotifications', () => {
|
||||||
|
let user;
|
||||||
|
let sendPushNotification;
|
||||||
|
let pathToPushNotifications = '../../../../../website/server/libs/api-v3/pushNotifications';
|
||||||
|
let gcmSendSpy;
|
||||||
|
let apnSendSpy;
|
||||||
|
|
||||||
|
let identifier = 'identifier';
|
||||||
|
let title = 'title';
|
||||||
|
let message = 'message';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user = new User();
|
||||||
|
gcmSendSpy = sinon.spy();
|
||||||
|
apnSendSpy = sinon.spy();
|
||||||
|
|
||||||
|
sandbox.stub(nconf, 'get').returns('true');
|
||||||
|
|
||||||
|
sandbox.stub(pushNotify, 'gcm').returns({
|
||||||
|
on: () => null,
|
||||||
|
send: gcmSendSpy,
|
||||||
|
});
|
||||||
|
|
||||||
|
sandbox.stub(pushNotify, 'apn').returns({
|
||||||
|
on: () => null,
|
||||||
|
send: apnSendSpy,
|
||||||
|
});
|
||||||
|
|
||||||
|
sendPushNotification = requireAgain(pathToPushNotifications);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if user is not supplied', () => {
|
||||||
|
expect(sendPushNotification).to.throw;
|
||||||
|
expect(gcmSendSpy).to.not.have.been.called;
|
||||||
|
expect(apnSendSpy).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if user.preferences.pushNotifications.unsubscribeFromAll is true', () => {
|
||||||
|
user.preferences.pushNotifications.unsubscribeFromAll = true;
|
||||||
|
expect(() => sendPushNotification(user)).to.throw;
|
||||||
|
expect(gcmSendSpy).to.not.have.been.called;
|
||||||
|
expect(apnSendSpy).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if details.identifier is not supplied', () => {
|
||||||
|
expect(() => sendPushNotification(user, {
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
})).to.throw;
|
||||||
|
expect(gcmSendSpy).to.not.have.been.called;
|
||||||
|
expect(apnSendSpy).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if details.title is not supplied', () => {
|
||||||
|
expect(() => sendPushNotification(user, {
|
||||||
|
identifier,
|
||||||
|
message,
|
||||||
|
})).to.throw;
|
||||||
|
expect(gcmSendSpy).to.not.have.been.called;
|
||||||
|
expect(apnSendSpy).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if details.message is not supplied', () => {
|
||||||
|
expect(() => sendPushNotification(user, {
|
||||||
|
identifier,
|
||||||
|
title,
|
||||||
|
})).to.throw;
|
||||||
|
expect(gcmSendSpy).to.not.have.been.called;
|
||||||
|
expect(apnSendSpy).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns if no device is registered', () => {
|
||||||
|
sendPushNotification(user, {
|
||||||
|
identifier,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
expect(gcmSendSpy).to.not.have.been.called;
|
||||||
|
expect(apnSendSpy).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses GCM for Android devices', () => {
|
||||||
|
user.pushDevices.push({
|
||||||
|
type: 'android',
|
||||||
|
regId: '123',
|
||||||
|
});
|
||||||
|
|
||||||
|
let details = {
|
||||||
|
identifier,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
payload: {
|
||||||
|
a: true,
|
||||||
|
b: true,
|
||||||
|
},
|
||||||
|
timeToLive: 23,
|
||||||
|
};
|
||||||
|
|
||||||
|
sendPushNotification(user, details);
|
||||||
|
expect(gcmSendSpy).to.have.been.calledOnce;
|
||||||
|
expect(gcmSendSpy).to.have.been.calledWithMatch({
|
||||||
|
registrationId: '123',
|
||||||
|
delayWhileIdle: true,
|
||||||
|
timeToLive: 23,
|
||||||
|
data: {
|
||||||
|
identifier,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
a: true,
|
||||||
|
b: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(apnSendSpy).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults timeToLive to 15', () => {
|
||||||
|
user.pushDevices.push({
|
||||||
|
type: 'android',
|
||||||
|
regId: '123',
|
||||||
|
});
|
||||||
|
|
||||||
|
let details = {
|
||||||
|
identifier,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
|
||||||
|
sendPushNotification(user, details);
|
||||||
|
expect(gcmSendSpy).to.have.been.calledOnce;
|
||||||
|
expect(gcmSendSpy).to.have.been.calledWithMatch({
|
||||||
|
registrationId: '123',
|
||||||
|
delayWhileIdle: true,
|
||||||
|
timeToLive: 15,
|
||||||
|
data: {
|
||||||
|
identifier,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(apnSendSpy).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO disabled because APN relies on a Promise
|
||||||
|
xit('uses APN for iOS devices', () => {
|
||||||
|
user.pushDevices.push({
|
||||||
|
type: 'ios',
|
||||||
|
regId: '123',
|
||||||
|
});
|
||||||
|
|
||||||
|
let details = {
|
||||||
|
identifier,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
category: 'fun',
|
||||||
|
payload: {
|
||||||
|
a: true,
|
||||||
|
b: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
sendPushNotification(user, details);
|
||||||
|
expect(apnSendSpy).to.have.been.calledOnce;
|
||||||
|
expect(apnSendSpy).to.have.been.calledWithMatch({
|
||||||
|
token: '123',
|
||||||
|
alert: message,
|
||||||
|
sound: 'default',
|
||||||
|
category: 'fun',
|
||||||
|
payload: {
|
||||||
|
identifier,
|
||||||
|
a: true,
|
||||||
|
b: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(gcmSendSpy).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import addPushDevice from '../../../common/script/ops/addPushDevice';
|
|
||||||
import i18n from '../../../common/script/i18n';
|
|
||||||
import {
|
|
||||||
generateUser,
|
|
||||||
} from '../../helpers/common.helper';
|
|
||||||
import {
|
|
||||||
NotAuthorized,
|
|
||||||
BadRequest,
|
|
||||||
} from '../../../common/script/libs/errors';
|
|
||||||
|
|
||||||
describe('shared.ops.addPushDevice', () => {
|
|
||||||
let user;
|
|
||||||
let regId = '10';
|
|
||||||
let type = 'someRandomType';
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
user = generateUser();
|
|
||||||
user.stats.hp = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns an error when regId is not provided', (done) => {
|
|
||||||
try {
|
|
||||||
addPushDevice(user);
|
|
||||||
} catch (err) {
|
|
||||||
expect(err).to.be.an.instanceof(BadRequest);
|
|
||||||
expect(err.message).to.equal(i18n.t('regIdRequired'));
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns an error when type is not provided', (done) => {
|
|
||||||
try {
|
|
||||||
addPushDevice(user, {body: {regId}});
|
|
||||||
} catch (err) {
|
|
||||||
expect(err).to.be.an.instanceof(BadRequest);
|
|
||||||
expect(err.message).to.equal(i18n.t('typeRequired'));
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds a push device', () => {
|
|
||||||
let [, message] = addPushDevice(user, {body: {regId, type}});
|
|
||||||
|
|
||||||
expect(message).to.equal(i18n.t('pushDeviceAdded'));
|
|
||||||
expect(user.pushDevices[0].type).to.equal(type);
|
|
||||||
expect(user.pushDevices[0].regId).to.equal(regId);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not a push device twice', (done) => {
|
|
||||||
try {
|
|
||||||
addPushDevice(user, {body: {regId, type}});
|
|
||||||
addPushDevice(user, {body: {regId, type}});
|
|
||||||
} catch (err) {
|
|
||||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
|
||||||
expect(err.message).to.equal(i18n.t('pushDeviceAlreadyAdded'));
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,58 +1,6 @@
|
|||||||
// TODO move to /api-v2
|
// TODO move to /api-v2
|
||||||
var api = module.exports;
|
var api = module.exports;
|
||||||
var _ = require('lodash');
|
|
||||||
var nconf = require('nconf');
|
|
||||||
|
|
||||||
var pushNotify = require('push-notify');
|
|
||||||
|
|
||||||
var gcmApiKey = nconf.get("PUSH_CONFIGS:GCM_SERVER_API_KEY");
|
|
||||||
|
|
||||||
var gcm = gcmApiKey ? pushNotify.gcm({
|
|
||||||
apiKey: gcmApiKey,
|
|
||||||
retries: 3
|
|
||||||
}) : undefined;
|
|
||||||
|
|
||||||
if(gcm){
|
|
||||||
gcm.on('transmitted', function (result, message, registrationId) {
|
|
||||||
//console.info("transmitted", result, message, registrationId);
|
|
||||||
});
|
|
||||||
|
|
||||||
gcm.on('transmissionError', function (error, message, registrationId) {
|
|
||||||
//console.info("transmissionError", error, message, registrationId);
|
|
||||||
});
|
|
||||||
gcm.on('updated', function (result, registrationId) {
|
|
||||||
//console.info("updated", result, registrationId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api.sendNotify = function(user, title, msg, timeToLive){
|
api.sendNotify = function(user, title, msg, timeToLive){
|
||||||
timeToLive = timeToLive || 15;
|
// Disabled, see libs/api-v3/pushNotifications
|
||||||
|
|
||||||
// need investigation:
|
|
||||||
// https://github.com/HabitRPG/habitrpg/issues/5252
|
|
||||||
if(!user)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_.forEach(user.pushDevices, function(pushDevice){
|
|
||||||
switch(pushDevice.type){
|
|
||||||
case "android":
|
|
||||||
if(gcm){
|
|
||||||
gcm.send({
|
|
||||||
registrationId: pushDevice.regId,
|
|
||||||
//collapseKey: 'COLLAPSE_KEY',
|
|
||||||
delayWhileIdle: true,
|
|
||||||
timeToLive: timeToLive,
|
|
||||||
data: {
|
|
||||||
title: title,
|
|
||||||
message: msg
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "ios":
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import { removeFromArray } from '../../libs/api-v3/collectionManipulators';
|
|||||||
import * as firebase from '../../libs/api-v3/firebase';
|
import * as firebase from '../../libs/api-v3/firebase';
|
||||||
import { sendTxn as sendTxnEmail } from '../../libs/api-v3/email';
|
import { sendTxn as sendTxnEmail } from '../../libs/api-v3/email';
|
||||||
import { encrypt } from '../../libs/api-v3/encryption';
|
import { encrypt } from '../../libs/api-v3/encryption';
|
||||||
import common from '../../../../common';
|
|
||||||
import sendPushNotification from '../../libs/api-v3/pushNotifications';
|
import sendPushNotification from '../../libs/api-v3/pushNotifications';
|
||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
@@ -537,11 +536,18 @@ async function _inviteByUUID (uuid, group, inviter, req, res) {
|
|||||||
sendTxnEmail(userToInvite, `invited-${groupTemplate}`, emailVars);
|
sendTxnEmail(userToInvite, `invited-${groupTemplate}`, emailVars);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendPushNotification(
|
if (userToInvite.preferences.pushNotifications[`invited${groupLabel}`] !== false) {
|
||||||
userToInvite,
|
let identifier = group.type === 'guild' ? 'invitedGuild' : 'invitedParty';
|
||||||
common.i18n.t(group.type === 'guild' ? 'invitedGuild' : 'invitedParty'),
|
sendPushNotification(
|
||||||
group.name
|
userToInvite,
|
||||||
);
|
{
|
||||||
|
title: group.name,
|
||||||
|
message: res.t(identifier),
|
||||||
|
identifier,
|
||||||
|
payload: {groupID: group._id},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let userInvited = await userToInvite.save();
|
let userInvited = await userToInvite.save();
|
||||||
if (group.type === 'guild') {
|
if (group.type === 'guild') {
|
||||||
|
|||||||
@@ -302,6 +302,18 @@ api.sendPrivateMessage = {
|
|||||||
{name: 'PMS_INBOX_URL', content: '/#/options/groups/inbox'},
|
{name: 'PMS_INBOX_URL', content: '/#/options/groups/inbox'},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
if (receiver.preferences.pushNotifications.newPM !== false) {
|
||||||
|
sendPushNotification(
|
||||||
|
receiver,
|
||||||
|
{
|
||||||
|
title: res.t('newPM'),
|
||||||
|
message: res.t('newPMInfo', {name: getUserInfo(sender, ['name']).name, message}),
|
||||||
|
identifier: 'newPM',
|
||||||
|
category: 'newPM',
|
||||||
|
payload: {replyTo: sender._id},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
res.respond(200, {});
|
res.respond(200, {});
|
||||||
},
|
},
|
||||||
@@ -372,8 +384,15 @@ api.transferGems = {
|
|||||||
{name: 'X_GEMS_GIFTED', content: gemAmount},
|
{name: 'X_GEMS_GIFTED', content: gemAmount},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
if (receiver.preferences.pushNotifications.giftedGems !== false) {
|
||||||
sendPushNotification(sender, res.t('giftedGems'), res.t('giftedGemsInfo', { amount: gemAmount, name: byUsername }));
|
sendPushNotification(receiver,
|
||||||
|
{
|
||||||
|
title: res.t('giftedGems'),
|
||||||
|
message: res.t('giftedGemsInfo', {amount: gemAmount, name: byUsername}),
|
||||||
|
identifier: 'giftedGems',
|
||||||
|
payload: {replyTo: sender._id},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
res.respond(200, {});
|
res.respond(200, {});
|
||||||
},
|
},
|
||||||
|
|||||||
95
website/server/controllers/api-v3/pushNotifications.js
Normal file
95
website/server/controllers/api-v3/pushNotifications.js
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { authWithHeaders } from '../../middlewares/api-v3/auth';
|
||||||
|
import {
|
||||||
|
NotAuthorized,
|
||||||
|
NotFound,
|
||||||
|
} from '../../libs/api-v3/errors';
|
||||||
|
|
||||||
|
let api = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiIgnore
|
||||||
|
* @api {post} /api/v3/user/push-devices Add a push device to a user
|
||||||
|
* @apiVersion 3.0.0
|
||||||
|
* @apiName UserAddPushDevice
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiParam {string} regId The id of the push device
|
||||||
|
* @apiParam {string} type The type of push device
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} data List of push devices
|
||||||
|
* @apiSuccess {string} message Success message
|
||||||
|
*/
|
||||||
|
api.addPushDevice = {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/user/push-devices',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
async handler (req, res) {
|
||||||
|
let user = res.locals.user;
|
||||||
|
|
||||||
|
req.checkBody('regId', res.t('regIdRequired')).notEmpty();
|
||||||
|
req.checkBody('type', res.t('typeRequired')).notEmpty().isIn(['ios', 'android']);
|
||||||
|
|
||||||
|
let validationErrors = req.validationErrors();
|
||||||
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
|
let pushDevices = user.pushDevices;
|
||||||
|
|
||||||
|
let item = {
|
||||||
|
regId: req.body.regId,
|
||||||
|
type: req.body.type,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pushDevices.find(device => device.regId === item.regId)) {
|
||||||
|
throw new NotAuthorized(res.t('pushDeviceAlreadyAdded'));
|
||||||
|
}
|
||||||
|
|
||||||
|
pushDevices.push(item);
|
||||||
|
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.respond(200, user.pushDevices, res.t('pushDeviceAdded'));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @apiIgnore
|
||||||
|
* @api {delete} /api/v3/user/push-devices remove a push device from a user
|
||||||
|
* @apiVersion 3.0.0
|
||||||
|
* @apiName UserRemovePushDevice
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiParam {string} regId The id of the push device
|
||||||
|
*
|
||||||
|
* @apiSuccess {Object} data List of push devices
|
||||||
|
* @apiSuccess {string} message Success message
|
||||||
|
*/
|
||||||
|
api.removePushDevice = {
|
||||||
|
method: 'DELETE',
|
||||||
|
url: '/user/push-devices/:regId',
|
||||||
|
middlewares: [authWithHeaders()],
|
||||||
|
async handler (req, res) {
|
||||||
|
let user = res.locals.user;
|
||||||
|
|
||||||
|
req.checkParams('regId', res.t('regIdRequired')).notEmpty();
|
||||||
|
let validationErrors = req.validationErrors();
|
||||||
|
if (validationErrors) throw validationErrors;
|
||||||
|
let regId = req.params.regId;
|
||||||
|
|
||||||
|
let pushDevices = user.pushDevices;
|
||||||
|
|
||||||
|
let indexOfPushDevice = pushDevices.findIndex((element) => {
|
||||||
|
return element.regId === regId;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (indexOfPushDevice === -1) {
|
||||||
|
throw new NotFound(res.t('pushDeviceNotFound'));
|
||||||
|
}
|
||||||
|
|
||||||
|
pushDevices.splice(indexOfPushDevice, 1);
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.respond(200, user.pushDevices, res.t('pushDeviceRemoved'));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = api;
|
||||||
@@ -106,11 +106,18 @@ api.inviteToQuest = {
|
|||||||
let inviterVars = getUserInfo(user, ['name', 'email']);
|
let inviterVars = getUserInfo(user, ['name', 'email']);
|
||||||
let membersToEmail = members.filter(member => {
|
let membersToEmail = members.filter(member => {
|
||||||
// send push notifications while filtering members before sending emails
|
// send push notifications while filtering members before sending emails
|
||||||
sendPushNotification(
|
if (member.preferences.pushNotifications.invitedQuest !== false) {
|
||||||
member,
|
sendPushNotification(
|
||||||
common.i18n.t('questInvitationTitle'),
|
member,
|
||||||
common.i18n.t('questInvitationInfo', { quest: quest.text() })
|
{
|
||||||
);
|
title: res.t('questInvitationTitle'),
|
||||||
|
message: res.t('questInvitationInfo', {quest: quest.text(req.language)}),
|
||||||
|
identifier: 'questInvitation',
|
||||||
|
category: 'questInvitation',
|
||||||
|
}
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return member.preferences.emailNotifications.invitedQuest !== false;
|
return member.preferences.emailNotifications.invitedQuest !== false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1266,32 +1266,6 @@ api.userReroll = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {post} /api/v3/user/addPushDevice Add a push device to a user
|
|
||||||
* @apiVersion 3.0.0
|
|
||||||
* @apiName UserAddPushDevice
|
|
||||||
* @apiGroup User
|
|
||||||
*
|
|
||||||
* @apiParam {string} regId The id of the push device
|
|
||||||
* @apiParam {string} uuid The type of push device
|
|
||||||
*
|
|
||||||
* @apiSuccess {Object} data List of push devices
|
|
||||||
* @apiSuccess {string} message Success message
|
|
||||||
*/
|
|
||||||
api.userAddPushDevice = {
|
|
||||||
method: 'POST',
|
|
||||||
middlewares: [authWithHeaders()],
|
|
||||||
url: '/user/addPushDevice',
|
|
||||||
async handler (req, res) {
|
|
||||||
let user = res.locals.user;
|
|
||||||
|
|
||||||
let addPushDeviceRes = common.ops.addPushDevice(user, req);
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
res.respond(200, ...addPushDeviceRes);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} /api/v3/user/reset Reset user
|
* @api {post} /api/v3/user/reset Reset user
|
||||||
* @apiVersion 3.0.0
|
* @apiVersion 3.0.0
|
||||||
|
|||||||
@@ -9,16 +9,14 @@ import csvStringify from '../../libs/api-v3/csvStringify';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import js2xml from 'js2xmlparser';
|
import js2xml from 'js2xmlparser';
|
||||||
import Pageres from 'pageres';
|
import Pageres from 'pageres';
|
||||||
import AWS from 'aws-sdk';
|
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import got from 'got';
|
import got from 'got';
|
||||||
import Bluebird from 'bluebird';
|
import Bluebird from 'bluebird';
|
||||||
import locals from '../../middlewares/api-v3/locals';
|
import locals from '../../middlewares/api-v3/locals';
|
||||||
|
import {
|
||||||
|
S3,
|
||||||
|
} from '../../libs/api-v3/aws';
|
||||||
|
|
||||||
let S3 = new AWS.S3({
|
|
||||||
accessKeyId: nconf.get('S3:accessKeyId'),
|
|
||||||
secretAccessKey: nconf.get('S3:secretAccessKey'),
|
|
||||||
});
|
|
||||||
const S3_BUCKET = nconf.get('S3:bucket');
|
const S3_BUCKET = nconf.get('S3:bucket');
|
||||||
|
|
||||||
const BASE_URL = nconf.get('BASE_URL');
|
const BASE_URL = nconf.get('BASE_URL');
|
||||||
|
|||||||
7
website/server/libs/api-v3/aws.js
Normal file
7
website/server/libs/api-v3/aws.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import AWS from 'aws-sdk';
|
||||||
|
import nconf from 'nconf';
|
||||||
|
|
||||||
|
export const S3 = new AWS.S3({
|
||||||
|
accessKeyId: nconf.get('S3:accessKeyId'),
|
||||||
|
secretAccessKey: nconf.get('S3:secretAccessKey'),
|
||||||
|
});
|
||||||
@@ -105,7 +105,16 @@ api.createSubscription = async function createSubscription (data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.gift.member._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself
|
if (data.gift.member._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself
|
||||||
sendPushNotification(data.gift.member, shared.i18n.t('giftedSubscription'), `${months} months - by ${byUserName}`);
|
if (data.gift.member.preferences.pushNotifications.giftedSubscription !== false) {
|
||||||
|
sendPushNotification(data.gift.member,
|
||||||
|
{
|
||||||
|
title: shared.i18n.t('giftedSubscription'),
|
||||||
|
message: shared.i18n.t('giftedSubscriptionInfo', {months, name: byUserName}),
|
||||||
|
identifier: 'giftedSubscription',
|
||||||
|
payload: {replyTo: data.user._id},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +187,16 @@ api.buyGems = async function buyGems (data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.gift.member._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself
|
if (data.gift.member._id !== data.user._id) { // Only send push notifications if sending to a user other than yourself
|
||||||
sendPushNotification(data.gift.member, shared.i18n.t('giftedGems'), `${gemAmount} Gems - by ${byUsername}`);
|
if (data.gift.member.preferences.pushNotifications.giftedGems !== false) {
|
||||||
|
sendPushNotification(
|
||||||
|
data.gift.member,
|
||||||
|
{
|
||||||
|
title: shared.i18n.t('giftedGems'),
|
||||||
|
message: shared.i18n.t('giftedGemsInfo', {amount: gemAmount, name: byUsername}),
|
||||||
|
identifier: 'giftedGems',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await data.gift.member.save();
|
await data.gift.member.save();
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
/* eslint-disable */
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import pushNotify from 'push-notify';
|
import pushNotify from 'push-notify';
|
||||||
|
import apnLib from 'apn';
|
||||||
|
import logger from './logger';
|
||||||
|
import Bluebird from 'bluebird';
|
||||||
|
import {
|
||||||
|
S3,
|
||||||
|
} from './aws';
|
||||||
|
|
||||||
const GCM_API_KEY = nconf.get('PUSH_CONFIGS:GCM_SERVER_API_KEY');
|
const GCM_API_KEY = nconf.get('PUSH_CONFIGS:GCM_SERVER_API_KEY');
|
||||||
|
|
||||||
@@ -11,47 +15,95 @@ let gcm = GCM_API_KEY ? pushNotify.gcm({
|
|||||||
retries: 3,
|
retries: 3,
|
||||||
}) : undefined;
|
}) : undefined;
|
||||||
|
|
||||||
// TODO review and test this file when push notifications are added back
|
|
||||||
|
|
||||||
if (gcm) {
|
if (gcm) {
|
||||||
gcm.on('transmitted', (/* result, message, registrationId */) => {
|
gcm.on('transmissionError', (err, message, registrationId) => {
|
||||||
// console.info("transmitted", result, message, registrationId);
|
logger.error('GCM Error', err, message, registrationId);
|
||||||
});
|
|
||||||
|
|
||||||
gcm.on('transmissionError', (/* error, message, registrationId */) => {
|
|
||||||
// console.info("transmissionError", error, message, registrationId);
|
|
||||||
});
|
|
||||||
|
|
||||||
gcm.on('updated', (/* result, registrationId */) => {
|
|
||||||
// console.info("updated", result, registrationId);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function sendNotification (user, title, message, timeToLive = 15) {
|
let apn;
|
||||||
return; // TODO push notifications are not currently enabled
|
|
||||||
|
|
||||||
if (!user) return;
|
// Load APN certificate and key from S3
|
||||||
|
const APN_ENABLED = nconf.get('PUSH_CONFIGS:APN_ENABLED') === 'true';
|
||||||
|
const S3_BUCKET = nconf.get('S3:bucket');
|
||||||
|
|
||||||
|
if (APN_ENABLED) {
|
||||||
|
Bluebird.all([
|
||||||
|
S3.getObject({
|
||||||
|
Bucket: S3_BUCKET,
|
||||||
|
Key: 'apple_apn/cert.pem',
|
||||||
|
}).promise(),
|
||||||
|
S3.getObject({
|
||||||
|
Bucket: S3_BUCKET,
|
||||||
|
Key: 'apple_apn/key.pem',
|
||||||
|
}).promise(),
|
||||||
|
])
|
||||||
|
.then(([certObj, keyObj]) => {
|
||||||
|
let cert = certObj.Body.toString();
|
||||||
|
let key = keyObj.Body.toString();
|
||||||
|
|
||||||
|
apn = pushNotify.apn({
|
||||||
|
key,
|
||||||
|
cert,
|
||||||
|
});
|
||||||
|
|
||||||
|
apn.on('error', err => logger.error('APN error', err));
|
||||||
|
apn.on('transmissionError', (errorCode, notification, device) => {
|
||||||
|
logger.error('APN transmissionError', errorCode, notification, device);
|
||||||
|
});
|
||||||
|
|
||||||
|
let feedback = new apnLib.Feedback({
|
||||||
|
key,
|
||||||
|
cert,
|
||||||
|
batchFeedback: true,
|
||||||
|
interval: 3600, // Check for feedback once an hour
|
||||||
|
});
|
||||||
|
|
||||||
|
feedback.on('feedback', (devices) => {
|
||||||
|
if (devices && devices.length > 0) {
|
||||||
|
logger.info('Delivery of push notifications failed for some Apple devices.', devices);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function sendNotification (user, details = {}) {
|
||||||
|
if (!user) throw new Error('User is required.');
|
||||||
|
if (user.preferences.pushNotifications.unsubscribeFromAll === true) return;
|
||||||
let pushDevices = user.pushDevices.toObject ? user.pushDevices.toObject() : user.pushDevices;
|
let pushDevices = user.pushDevices.toObject ? user.pushDevices.toObject() : user.pushDevices;
|
||||||
|
|
||||||
|
if (!details.identifier) throw new Error('details.identifier is required.');
|
||||||
|
if (!details.title) throw new Error('details.title is required.');
|
||||||
|
if (!details.message) throw new Error('details.message is required.');
|
||||||
|
|
||||||
|
let payload = details.payload ? details.payload : {};
|
||||||
|
payload.identifier = details.identifier;
|
||||||
|
|
||||||
_.each(pushDevices, pushDevice => {
|
_.each(pushDevices, pushDevice => {
|
||||||
switch (pushDevice.type) {
|
switch (pushDevice.type) {
|
||||||
case 'android':
|
case 'android':
|
||||||
if (gcm) {
|
if (gcm) {
|
||||||
|
payload.title = details.title;
|
||||||
|
payload.message = details.message;
|
||||||
gcm.send({
|
gcm.send({
|
||||||
registrationId: pushDevice.regId,
|
registrationId: pushDevice.regId,
|
||||||
// collapseKey: 'COLLAPSE_KEY',
|
|
||||||
delayWhileIdle: true,
|
delayWhileIdle: true,
|
||||||
timeToLive,
|
timeToLive: details.timeToLive ? details.timeToLive : 15,
|
||||||
data: {
|
data: payload,
|
||||||
title,
|
|
||||||
message,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ios':
|
case 'ios':
|
||||||
|
if (apn) {
|
||||||
|
apn.send({
|
||||||
|
token: pushDevice.regId,
|
||||||
|
alert: details.message,
|
||||||
|
sound: 'default',
|
||||||
|
category: details.category,
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -296,8 +296,14 @@ schema.methods.closeChal = async function closeChal (broken = {}) {
|
|||||||
{name: 'CHALLENGE_NAME', content: challenge.name},
|
{name: 'CHALLENGE_NAME', content: challenge.name},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
if (savedWinner.preferences.pushNotifications.wonChallenge !== false) {
|
||||||
sendPushNotification(savedWinner, shared.i18n.t('wonChallenge'), challenge.name);
|
sendPushNotification(savedWinner,
|
||||||
|
{
|
||||||
|
title: challenge.name,
|
||||||
|
message: shared.i18n.t('wonChallenge'),
|
||||||
|
identifier: 'wonChallenge',
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run some operations in the background withouth blocking the thread
|
// Run some operations in the background withouth blocking the thread
|
||||||
|
|||||||
@@ -405,18 +405,29 @@ schema.methods.startQuest = async function startQuest (user) {
|
|||||||
// send notifications in the background without blocking
|
// send notifications in the background without blocking
|
||||||
User.find(
|
User.find(
|
||||||
{ _id: { $in: nonUserQuestMembers } },
|
{ _id: { $in: nonUserQuestMembers } },
|
||||||
'party.quest items.quests auth.facebook auth.local preferences.emailNotifications pushDevices profile.name'
|
'party.quest items.quests auth.facebook auth.local preferences.emailNotifications preferences.pushNotifications pushDevices profile.name'
|
||||||
).exec().then((membersToNotify) => {
|
).exec().then((membersToNotify) => {
|
||||||
let membersToEmail = _.filter(membersToNotify, (member) => {
|
let membersToEmail = _.filter(membersToNotify, (member) => {
|
||||||
// send push notifications and filter users that disabled emails
|
// send push notifications and filter users that disabled emails
|
||||||
sendPushNotification(member, 'HabitRPG', `${shared.i18n.t('questStarted')}: ${quest.text()}`);
|
|
||||||
|
|
||||||
return member.preferences.emailNotifications.questStarted !== false &&
|
return member.preferences.emailNotifications.questStarted !== false &&
|
||||||
member._id !== user._id;
|
member._id !== user._id;
|
||||||
});
|
});
|
||||||
sendTxnEmail(membersToEmail, 'quest-started', [
|
sendTxnEmail(membersToEmail, 'quest-started', [
|
||||||
{ name: 'PARTY_URL', content: '/#/options/groups/party' },
|
{ name: 'PARTY_URL', content: '/#/options/groups/party' },
|
||||||
]);
|
]);
|
||||||
|
let membersToPush = _.filter(membersToNotify, (member) => {
|
||||||
|
// send push notifications and filter users that disabled emails
|
||||||
|
return member.preferences.pushNotifications.questStarted !== false &&
|
||||||
|
member._id !== user._id;
|
||||||
|
});
|
||||||
|
_.each(membersToPush, (member) => {
|
||||||
|
sendPushNotification(member,
|
||||||
|
{
|
||||||
|
title: quest.text(),
|
||||||
|
message: `${shared.i18n.t('questStarted')}: ${quest.text()}`,
|
||||||
|
identifier: 'questStarted',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
21
website/server/models/pushDevice.js
Normal file
21
website/server/models/pushDevice.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import baseModel from '../libs/api-v3/baseModel';
|
||||||
|
|
||||||
|
const Schema = mongoose.Schema;
|
||||||
|
|
||||||
|
export let schema = new Schema({
|
||||||
|
regId: {type: String, required: true},
|
||||||
|
type: {type: String, required: true, enum: ['ios', 'android']},
|
||||||
|
}, {
|
||||||
|
strict: true,
|
||||||
|
minimize: false, // So empty objects are returned
|
||||||
|
_id: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
schema.plugin(baseModel, {
|
||||||
|
noSet: ['_id', 'regId'],
|
||||||
|
timestamps: true,
|
||||||
|
_id: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export let model = mongoose.model('PushDevice', schema);
|
||||||
@@ -3,6 +3,7 @@ import shared from '../../../../common';
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import { schema as TagSchema } from '../tag';
|
import { schema as TagSchema } from '../tag';
|
||||||
|
import { schema as PushDeviceSchema } from '../pushDevice';
|
||||||
import {
|
import {
|
||||||
schema as UserNotificationSchema,
|
schema as UserNotificationSchema,
|
||||||
} from '../userNotification';
|
} from '../userNotification';
|
||||||
@@ -431,6 +432,17 @@ let schema = new Schema({
|
|||||||
importantAnnouncements: {type: Boolean, default: true},
|
importantAnnouncements: {type: Boolean, default: true},
|
||||||
weeklyRecaps: {type: Boolean, default: true},
|
weeklyRecaps: {type: Boolean, default: true},
|
||||||
},
|
},
|
||||||
|
pushNotifications: {
|
||||||
|
unsubscribeFromAll: {type: Boolean, default: false},
|
||||||
|
newPM: {type: Boolean, default: true},
|
||||||
|
wonChallenge: {type: Boolean, default: true},
|
||||||
|
giftedGems: {type: Boolean, default: true},
|
||||||
|
giftedSubscription: {type: Boolean, default: true},
|
||||||
|
invitedParty: {type: Boolean, default: true},
|
||||||
|
invitedGuild: {type: Boolean, default: true},
|
||||||
|
questStarted: {type: Boolean, default: true},
|
||||||
|
invitedQuest: {type: Boolean, default: true},
|
||||||
|
},
|
||||||
suppressModals: {
|
suppressModals: {
|
||||||
levelUp: {type: Boolean, default: false},
|
levelUp: {type: Boolean, default: false},
|
||||||
hatchPet: {type: Boolean, default: false},
|
hatchPet: {type: Boolean, default: false},
|
||||||
@@ -505,10 +517,7 @@ let schema = new Schema({
|
|||||||
extra: {type: Schema.Types.Mixed, default: () => {
|
extra: {type: Schema.Types.Mixed, default: () => {
|
||||||
return {};
|
return {};
|
||||||
}},
|
}},
|
||||||
pushDevices: [{
|
pushDevices: [PushDeviceSchema],
|
||||||
regId: {type: String},
|
|
||||||
type: {type: String},
|
|
||||||
}],
|
|
||||||
}, {
|
}, {
|
||||||
strict: true,
|
strict: true,
|
||||||
minimize: false, // So empty objects are returned
|
minimize: false, // So empty objects are returned
|
||||||
|
|||||||
@@ -321,25 +321,46 @@ script(id='partials/options.settings.notifications.html', type="text/ng-template
|
|||||||
.personal-options.col-md-6
|
.personal-options.col-md-6
|
||||||
.panel.panel-default
|
.panel.panel-default
|
||||||
.panel-heading
|
.panel-heading
|
||||||
=env.t('emailNotifications')
|
=env.t('notifications')
|
||||||
.panel-body
|
.panel-body
|
||||||
|
table.table
|
||||||
each notification in ['newPM', 'wonChallenge', 'giftedGems', 'giftedSubscription', 'invitedParty', 'invitedGuild', 'kickedGroup', 'questStarted', 'invitedQuest', 'importantAnnouncements', 'weeklyRecaps']
|
tr
|
||||||
-var preference = 'user.preferences.emailNotifications.' + notification
|
td
|
||||||
-var unsubscribeFromAll = 'user.preferences.emailNotifications.unsubscribeFromAll'
|
th
|
||||||
|
span=env.t("email")
|
||||||
.checkbox: label
|
th
|
||||||
input(type='checkbox', ng-model='#{preference}',
|
span=env.t("push")
|
||||||
ng-disabled='#{unsubscribeFromAll} === true',
|
-var unsubscribeFromAllEmails = 'user.preferences.emailNotifications.unsubscribeFromAll'
|
||||||
ng-checked='#{unsubscribeFromAll} === false && #{preference} === true',
|
-var unsubscribeFromAllPush = 'user.preferences.pushNotifications.unsubscribeFromAll'
|
||||||
ng-change='set({"preferences.emailNotifications.#{notification}": #{preference} ? true: false})')
|
each notification in ['newPM', 'wonChallenge', 'giftedGems', 'giftedSubscription', 'invitedParty', 'invitedGuild', 'kickedGroup', 'questStarted', 'invitedQuest', 'importantAnnouncements', 'weeklyRecaps']
|
||||||
span=env.t(notification)
|
tr
|
||||||
|
td
|
||||||
|
span=env.t(notification)
|
||||||
|
td
|
||||||
|
-var preference = 'user.preferences.emailNotifications.' + notification
|
||||||
|
input(type='checkbox', ng-model='#{preference}',
|
||||||
|
ng-disabled='#{unsubscribeFromAllEmails} === true || #{preference} === undefined',
|
||||||
|
ng-checked='#{unsubscribeFromAllEmails} === false && #{preference} === true',
|
||||||
|
ng-change='set({"preferences.emailNotifications.#{notification}": #{preference} ? true: false})')
|
||||||
|
td
|
||||||
|
-var preference = 'user.preferences.pushNotifications.' + notification
|
||||||
|
input(type='checkbox', ng-model='#{preference}',
|
||||||
|
ng-disabled='#{unsubscribeFromAllPush} === true || #{preference} === undefined',
|
||||||
|
ng-checked='#{unsubscribeFromAllPush} === false && #{preference} === true',
|
||||||
|
ng-change='set({"preferences.pushNotifications.#{notification}": #{preference} ? true: false})')
|
||||||
|
|
||||||
hr
|
hr
|
||||||
|
|
||||||
.checkbox
|
.checkbox
|
||||||
label
|
label
|
||||||
input(type='checkbox', ng-model='user.preferences.emailNotifications.unsubscribeFromAll', ng-change='set({"preferences.emailNotifications.unsubscribeFromAll": user.preferences.emailNotifications.unsubscribeFromAll ? true: false})')
|
input(type='checkbox', ng-model='user.preferences.pushNotifications.unsubscribeFromAll',
|
||||||
|
ng-change='set({"preferences.pushNotifications.unsubscribeFromAll": user.preferences.pushNotifications.unsubscribeFromAll ? true: false})')
|
||||||
|
span=env.t('unsubscribeAllPush')
|
||||||
|
|
||||||
|
.checkbox
|
||||||
|
label
|
||||||
|
input(type='checkbox', ng-model='user.preferences.emailNotifications.unsubscribeFromAll',
|
||||||
|
ng-change='set({"preferences.emailNotifications.unsubscribeFromAll": user.preferences.emailNotifications.unsubscribeFromAll ? true: false})')
|
||||||
span=env.t('unsubscribeAllEmails')
|
span=env.t('unsubscribeAllEmails')
|
||||||
|
|
||||||
small=env.t('unsubscribeAllEmailsText')
|
small=env.t('unsubscribeAllEmailsText')
|
||||||
|
|||||||
Reference in New Issue
Block a user