mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-16 06:07:21 +01:00
Update to new method for fcm (#15238)
* begin moving to new fcm library * Add error handling * Add opening notification to correct screen * Fix tests and make async * lint fix * Rename pushNotificationstest..js to pushNotifications.test.js * fix(potions): remove Fungi Potion time banner * 5.24.3 * update(content): add 2024-06 content prebuild (#15231) * update sprites * add 2024-06 content * add 2024-06 enchanted armoire items * update sprites * update sprites * fix errors found in testing * Fix liveliness probes being rate limited (#15236) * Do not rate limit any liveliness probes * update example config * Translated using Weblate (German) Currently translated at 96.2% (181 of 188 strings) Translated using Weblate (Japanese) Currently translated at 99.4% (769 of 773 strings) Translated using Weblate (German) Currently translated at 93.6% (176 of 188 strings) Translated using Weblate (Japanese) Currently translated at 96.2% (2972 of 3089 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (232 of 232 strings) Translated using Weblate (Japanese) Currently translated at 96.8% (841 of 868 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (94 of 94 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (113 of 113 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (131 of 131 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (German) Currently translated at 86.7% (163 of 188 strings) Translated using Weblate (German) Currently translated at 85.1% (160 of 188 strings) Translated using Weblate (German) Currently translated at 84.0% (158 of 188 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (94 of 94 strings) Translated using Weblate (German) Currently translated at 83.5% (157 of 188 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (113 of 113 strings) Translated using Weblate (German) Currently translated at 82.9% (156 of 188 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (113 of 113 strings) Translated using Weblate (German) Currently translated at 81.9% (154 of 188 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (113 of 113 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (8 of 8 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (8 of 8 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (188 of 188 strings) Translated using Weblate (German) Currently translated at 79.2% (149 of 188 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (189 of 189 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (8 of 8 strings) Translated using Weblate (German) Currently translated at 90.6% (2799 of 3089 strings) Translated using Weblate (German) Currently translated at 77.6% (146 of 188 strings) Translated using Weblate (German) Currently translated at 90.5% (2797 of 3089 strings) Translated using Weblate (German) Currently translated at 90.4% (2794 of 3089 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (131 of 131 strings) Translated using Weblate (German) Currently translated at 90.1% (2786 of 3089 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (188 of 188 strings) Translated using Weblate (German) Currently translated at 77.1% (145 of 188 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 98.7% (763 of 773 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (868 of 868 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (131 of 131 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (German) Currently translated at 90.0% (2782 of 3089 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (773 of 773 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (378 of 378 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (868 of 868 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (167 of 167 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (259 of 259 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (259 of 259 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (131 of 131 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (239 of 239 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (188 of 188 strings) Translated using Weblate (French) Currently translated at 100.0% (188 of 188 strings) Translated using Weblate (German) Currently translated at 75.0% (141 of 188 strings) Translated using Weblate (Spanish) Currently translated at 99.0% (766 of 773 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (189 of 189 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (868 of 868 strings) Translated using Weblate (Japanese) Currently translated at 98.8% (764 of 773 strings) Translated using Weblate (Japanese) Currently translated at 99.6% (258 of 259 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (378 of 378 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (188 of 188 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (140 of 140 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (131 of 131 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (Ukrainian) Currently translated at 62.5% (1931 of 3089 strings) Translated using Weblate (German) Currently translated at 89.8% (2777 of 3089 strings) Translated using Weblate (French) Currently translated at 100.0% (188 of 188 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 98.5% (762 of 773 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (868 of 868 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (French) Currently translated at 82.9% (156 of 188 strings) Translated using Weblate (German) Currently translated at 93.0% (241 of 259 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (8 of 8 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (427 of 427 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 98.5% (762 of 773 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (868 of 868 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (167 of 167 strings) Translated using Weblate (Japanese) Currently translated at 99.2% (257 of 259 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 98.5% (762 of 773 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (868 of 868 strings) Translated using Weblate (German) Currently translated at 92.2% (239 of 259 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (286 of 286 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (239 of 239 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (188 of 188 strings) Translated using Weblate (German) Currently translated at 91.8% (238 of 259 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (131 of 131 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 98.5% (762 of 773 strings) Translated using Weblate (German) Currently translated at 90.3% (234 of 259 strings) Co-authored-by: Finrod <963505255@qq.com> Co-authored-by: Jaime Martí <jaumemarti77@icloud.com> Co-authored-by: Kem Kembo <medamamef@gmail.com> Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com> Co-authored-by: TOMA Mitsuru <toma0001@gmail.com> Co-authored-by: Tetiana <merekka13@gmail.com> Co-authored-by: Toro Mor <thomas.bizer@gmx.de> Co-authored-by: Weblate <noreply@weblate.org> Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/ Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/ Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/content/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/de/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/inventory/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/ Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/overview/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/pets/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/quests/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/es/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/de/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/tasks/zh_Hans/ Translation: Habitica/Achievements Translation: Habitica/Backgrounds Translation: Habitica/Character Translation: Habitica/Content Translation: Habitica/Faq Translation: Habitica/Gear Translation: Habitica/Generic Translation: Habitica/Groups Translation: Habitica/Inventory Translation: Habitica/Limited Translation: Habitica/Npc Translation: Habitica/Overview Translation: Habitica/Pets Translation: Habitica/Quests Translation: Habitica/Questscontent Translation: Habitica/Settings Translation: Habitica/Subscriber Translation: Habitica/Tasks * 5.25.0 * Fix dockerfile (#15241) * Fix issue with l4p not resetting properly (#15240) * actually clear out seeking field on user. Even when creating a party * Add tests to ensure party.seeking is cleared * fix(lint): don't assign unused const --------- Co-authored-by: Sabe Jones <sabe@habitica.com> --------- Co-authored-by: Sabe Jones <sabe@habitica.com> Co-authored-by: Natalie <78037386+CuriousMagpie@users.noreply.github.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: Finrod <963505255@qq.com> Co-authored-by: Jaime Martí <jaumemarti77@icloud.com> Co-authored-by: Kem Kembo <medamamef@gmail.com> Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com> Co-authored-by: TOMA Mitsuru <toma0001@gmail.com> Co-authored-by: Tetiana <merekka13@gmail.com> Co-authored-by: Toro Mor <thomas.bizer@gmx.de> Co-authored-by: Rafał Jagielski <jagielski.rafal.uwm@gmail.com>
This commit is contained in:
1123
package-lock.json
generated
1123
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -30,6 +30,7 @@
|
|||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"express-basic-auth": "^1.2.1",
|
"express-basic-auth": "^1.2.1",
|
||||||
"express-validator": "^5.2.0",
|
"express-validator": "^5.2.0",
|
||||||
|
"firebase-admin": "^12.1.1",
|
||||||
"glob": "^8.1.0",
|
"glob": "^8.1.0",
|
||||||
"got": "^11.8.6",
|
"got": "^11.8.6",
|
||||||
"gulp": "^4.0.0",
|
"gulp": "^4.0.0",
|
||||||
|
|||||||
@@ -1,184 +0,0 @@
|
|||||||
import apn from '@parse/node-apn/mock';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import nconf from 'nconf';
|
|
||||||
import gcmLib from 'node-gcm'; // works with FCM notifications too
|
|
||||||
import { model as User } from '../../../../website/server/models/user';
|
|
||||||
import {
|
|
||||||
sendNotification as sendPushNotification,
|
|
||||||
MAX_MESSAGE_LENGTH,
|
|
||||||
} from '../../../../website/server/libs/pushNotifications';
|
|
||||||
|
|
||||||
describe('pushNotifications', () => {
|
|
||||||
let user;
|
|
||||||
let fcmSendSpy;
|
|
||||||
let apnSendSpy;
|
|
||||||
|
|
||||||
const identifier = 'identifier';
|
|
||||||
const title = 'title';
|
|
||||||
const message = 'message';
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
user = new User();
|
|
||||||
fcmSendSpy = sinon.spy();
|
|
||||||
apnSendSpy = sinon.spy();
|
|
||||||
|
|
||||||
sandbox.stub(nconf, 'get').returns('true-key');
|
|
||||||
|
|
||||||
sandbox.stub(gcmLib.Sender.prototype, 'send').callsFake(fcmSendSpy);
|
|
||||||
|
|
||||||
sandbox.stub(apn.Provider.prototype, 'send').returns({
|
|
||||||
on: () => null,
|
|
||||||
send: apnSendSpy,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
sandbox.restore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws if user is not supplied', () => {
|
|
||||||
expect(sendPushNotification).to.throw;
|
|
||||||
expect(fcmSendSpy).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(fcmSendSpy).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(fcmSendSpy).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(fcmSendSpy).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(fcmSendSpy).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(fcmSendSpy).to.not.have.been.called;
|
|
||||||
expect(apnSendSpy).to.not.have.been.called;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('cuts the message to 300 chars', () => {
|
|
||||||
const longMessage = `12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
|
|
||||||
|
|
||||||
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
|
|
||||||
|
|
||||||
const details = {
|
|
||||||
identifier,
|
|
||||||
title,
|
|
||||||
message: longMessage,
|
|
||||||
payload: {
|
|
||||||
message: longMessage,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
sendPushNotification(user, details);
|
|
||||||
|
|
||||||
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
|
||||||
expect(details.payload.message)
|
|
||||||
.to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
|
||||||
|
|
||||||
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
|
||||||
expect(details.payload.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('cuts the message to 300 chars (no payload)', () => {
|
|
||||||
const longMessage = `12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
|
||||||
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
|
|
||||||
|
|
||||||
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
|
|
||||||
|
|
||||||
const details = {
|
|
||||||
identifier,
|
|
||||||
title,
|
|
||||||
message: longMessage,
|
|
||||||
};
|
|
||||||
|
|
||||||
sendPushNotification(user, details);
|
|
||||||
|
|
||||||
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
|
||||||
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO disabled because APN relies on a Promise
|
|
||||||
xit('uses APN for iOS devices', () => {
|
|
||||||
user.pushDevices.push({
|
|
||||||
type: 'ios',
|
|
||||||
regId: '123',
|
|
||||||
});
|
|
||||||
|
|
||||||
const details = {
|
|
||||||
identifier,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
category: 'fun',
|
|
||||||
payload: {
|
|
||||||
a: true,
|
|
||||||
b: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const expectedNotification = new apn.Notification({
|
|
||||||
alert: message,
|
|
||||||
sound: 'default',
|
|
||||||
category: 'fun',
|
|
||||||
payload: {
|
|
||||||
identifier,
|
|
||||||
a: true,
|
|
||||||
b: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
sendPushNotification(user, details);
|
|
||||||
expect(apnSendSpy).to.have.been.calledOnce;
|
|
||||||
expect(apnSendSpy).to.have.been.calledWithMatch(expectedNotification, '123');
|
|
||||||
expect(fcmSendSpy).to.not.have.been.called;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
354
test/api/unit/libs/pushNotifications.test.js
Normal file
354
test/api/unit/libs/pushNotifications.test.js
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
import apn from '@parse/node-apn';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import nconf from 'nconf';
|
||||||
|
import admin from 'firebase-admin';
|
||||||
|
import { model as User } from '../../../../website/server/models/user';
|
||||||
|
import {
|
||||||
|
MAX_MESSAGE_LENGTH,
|
||||||
|
} from '../../../../website/server/libs/pushNotifications';
|
||||||
|
|
||||||
|
let sendPushNotification;
|
||||||
|
|
||||||
|
describe('pushNotifications', () => {
|
||||||
|
let user;
|
||||||
|
let fcmSendSpy;
|
||||||
|
let apnSendSpy;
|
||||||
|
let updateStub;
|
||||||
|
let classStubbedInstance;
|
||||||
|
|
||||||
|
const identifier = 'identifier';
|
||||||
|
const title = 'title';
|
||||||
|
const message = 'message';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
user = new User();
|
||||||
|
fcmSendSpy = sinon.stub().returns(Promise.resolve('success'));
|
||||||
|
apnSendSpy = sinon.stub().returns(Promise.resolve());
|
||||||
|
|
||||||
|
nconf.set('PUSH_CONFIGS_APN_ENABLED', 'true');
|
||||||
|
|
||||||
|
classStubbedInstance = sandbox.createStubInstance(apn.Provider, {
|
||||||
|
send: apnSendSpy,
|
||||||
|
});
|
||||||
|
sandbox.stub(apn, 'Provider').returns(classStubbedInstance);
|
||||||
|
|
||||||
|
delete require.cache[require.resolve('../../../../website/server/libs/pushNotifications')];
|
||||||
|
// eslint-disable-next-line global-require
|
||||||
|
sendPushNotification = require('../../../../website/server/libs/pushNotifications').sendNotification;
|
||||||
|
|
||||||
|
updateStub = sandbox.stub(User, 'updateOne').resolves();
|
||||||
|
sandbox.stub(admin, 'messaging').get(() => () => ({ send: fcmSendSpy }));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('validates supplied data', () => {
|
||||||
|
it('throws if user is not supplied', () => {
|
||||||
|
expect(sendPushNotification).to.throw;
|
||||||
|
expect(fcmSendSpy).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(fcmSendSpy).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(fcmSendSpy).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(fcmSendSpy).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(fcmSendSpy).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(fcmSendSpy).to.not.have.been.called;
|
||||||
|
expect(apnSendSpy).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cuts the message to 300 chars', () => {
|
||||||
|
const longMessage = `12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
|
||||||
|
|
||||||
|
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
|
||||||
|
|
||||||
|
const details = {
|
||||||
|
identifier,
|
||||||
|
title,
|
||||||
|
message: longMessage,
|
||||||
|
payload: {
|
||||||
|
message: longMessage,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
sendPushNotification(user, details);
|
||||||
|
|
||||||
|
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
||||||
|
expect(details.payload.message)
|
||||||
|
.to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
||||||
|
|
||||||
|
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
||||||
|
expect(details.payload.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cuts the message to 300 chars (no payload)', () => {
|
||||||
|
const longMessage = `12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345
|
||||||
|
12345 12345 12345 12345 12345 12345 12345 12345 12345 12345`;
|
||||||
|
|
||||||
|
expect(longMessage.length > MAX_MESSAGE_LENGTH).to.equal(true);
|
||||||
|
|
||||||
|
const details = {
|
||||||
|
identifier,
|
||||||
|
title,
|
||||||
|
message: longMessage,
|
||||||
|
};
|
||||||
|
|
||||||
|
sendPushNotification(user, details);
|
||||||
|
|
||||||
|
expect(details.message).to.equal(_.truncate(longMessage, { length: MAX_MESSAGE_LENGTH }));
|
||||||
|
expect(details.message.length).to.equal(MAX_MESSAGE_LENGTH);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sends notifications', () => {
|
||||||
|
let details;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
details = {
|
||||||
|
identifier,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
category: 'fun',
|
||||||
|
payload: {
|
||||||
|
a: true,
|
||||||
|
b: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses APN for iOS devices', async () => {
|
||||||
|
user.pushDevices.push({
|
||||||
|
type: 'ios',
|
||||||
|
regId: '123',
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedNotification = new apn.Notification({
|
||||||
|
alert: {
|
||||||
|
title,
|
||||||
|
body: message,
|
||||||
|
},
|
||||||
|
sound: 'default',
|
||||||
|
category: 'fun',
|
||||||
|
payload: {
|
||||||
|
identifier,
|
||||||
|
a: true,
|
||||||
|
b: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendPushNotification(user, details);
|
||||||
|
expect(apnSendSpy).to.have.been.calledOnce;
|
||||||
|
expect(apnSendSpy).to.have.been.calledWithMatch(expectedNotification, '123');
|
||||||
|
expect(fcmSendSpy).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses FCM for Android devices', async () => {
|
||||||
|
user.pushDevices.push({
|
||||||
|
type: 'android',
|
||||||
|
regId: '123',
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedMessage = {
|
||||||
|
notification: {
|
||||||
|
title,
|
||||||
|
body: message,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
identifier,
|
||||||
|
notificationIdentifier: identifier,
|
||||||
|
},
|
||||||
|
token: '123',
|
||||||
|
};
|
||||||
|
|
||||||
|
await sendPushNotification(user, details);
|
||||||
|
expect(fcmSendSpy).to.have.been.calledOnce;
|
||||||
|
expect(fcmSendSpy).to.have.been.calledWithMatch(expectedMessage);
|
||||||
|
expect(apnSendSpy).to.not.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles multiple devices', async () => {
|
||||||
|
user.pushDevices.push({
|
||||||
|
type: 'android',
|
||||||
|
regId: '123',
|
||||||
|
});
|
||||||
|
user.pushDevices.push({
|
||||||
|
type: 'ios',
|
||||||
|
regId: '456',
|
||||||
|
});
|
||||||
|
user.pushDevices.push({
|
||||||
|
type: 'android',
|
||||||
|
regId: '789',
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendPushNotification(user, details);
|
||||||
|
expect(fcmSendSpy).to.have.been.calledTwice;
|
||||||
|
expect(apnSendSpy).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handles sending errors', () => {
|
||||||
|
let clock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
clock = sinon.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
clock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes unregistered fcm devices', async () => {
|
||||||
|
user.pushDevices.push({
|
||||||
|
type: 'android',
|
||||||
|
regId: '123',
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = new Error();
|
||||||
|
error.code = 'messaging/registration-token-not-registered';
|
||||||
|
fcmSendSpy.rejects(error);
|
||||||
|
|
||||||
|
await sendPushNotification(user, {
|
||||||
|
identifier,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fcmSendSpy).to.have.been.calledOnce;
|
||||||
|
expect(apnSendSpy).to.not.have.been.called;
|
||||||
|
await clock.tick(10);
|
||||||
|
expect(updateStub).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes invalid fcm devices', async () => {
|
||||||
|
user.pushDevices.push({
|
||||||
|
type: 'android',
|
||||||
|
regId: '123',
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = new Error();
|
||||||
|
error.code = 'messaging/registration-token-not-registered';
|
||||||
|
fcmSendSpy.rejects(error);
|
||||||
|
|
||||||
|
await sendPushNotification(user, {
|
||||||
|
identifier,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fcmSendSpy).to.have.been.calledOnce;
|
||||||
|
expect(apnSendSpy).to.not.have.been.called;
|
||||||
|
expect(updateStub).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes unregistered apn devices', async () => {
|
||||||
|
user.pushDevices.push({
|
||||||
|
type: 'ios',
|
||||||
|
regId: '123',
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = {
|
||||||
|
failed: [
|
||||||
|
{
|
||||||
|
device: '123',
|
||||||
|
response: { reason: 'Unregistered' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
apnSendSpy.resolves(error);
|
||||||
|
|
||||||
|
await sendPushNotification(user, {
|
||||||
|
identifier,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fcmSendSpy).to.not.have.been.called;
|
||||||
|
expect(apnSendSpy).to.have.been.calledOnce;
|
||||||
|
expect(updateStub).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes invalid apn devices', async () => {
|
||||||
|
user.pushDevices.push({
|
||||||
|
type: 'ios',
|
||||||
|
regId: '123',
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = {
|
||||||
|
failed: [
|
||||||
|
{
|
||||||
|
device: '123',
|
||||||
|
response: { reason: 'BadDeviceToken' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
apnSendSpy.resolves(error);
|
||||||
|
|
||||||
|
await sendPushNotification(user, {
|
||||||
|
identifier,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(fcmSendSpy).to.not.have.been.called;
|
||||||
|
expect(apnSendSpy).to.have.been.calledOnce;
|
||||||
|
expect(updateStub).to.have.been.calledOnce;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1362,8 +1362,8 @@ describe('Group Model', () => {
|
|||||||
sandbox.spy(User, 'updateMany');
|
sandbox.spy(User, 'updateMany');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats message', () => {
|
it('formats message', async () => {
|
||||||
const chatMessage = party.sendChat({
|
const chatMessage = await party.sendChat({
|
||||||
message: 'a _new_ message with *markdown*',
|
message: 'a _new_ message with *markdown*',
|
||||||
user: {
|
user: {
|
||||||
_id: 'user-id',
|
_id: 'user-id',
|
||||||
@@ -1396,8 +1396,8 @@ describe('Group Model', () => {
|
|||||||
expect(chat.user).to.eql('user name');
|
expect(chat.user).to.eql('user name');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('formats message as system if no user is passed in', () => {
|
it('formats message as system if no user is passed in', async () => {
|
||||||
const chat = party.sendChat({ message: 'a system message' });
|
const chat = await party.sendChat({ message: 'a system message' });
|
||||||
|
|
||||||
expect(chat.text).to.eql('a system message');
|
expect(chat.text).to.eql('a system message');
|
||||||
expect(validator.isUUID(chat.id)).to.eql(true);
|
expect(validator.isUUID(chat.id)).to.eql(true);
|
||||||
@@ -1411,8 +1411,8 @@ describe('Group Model', () => {
|
|||||||
expect(chat.user).to.not.exist;
|
expect(chat.user).to.not.exist;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates users about new messages in party', () => {
|
it('updates users about new messages in party', async () => {
|
||||||
party.sendChat({ message: 'message' });
|
await party.sendChat({ message: 'message' });
|
||||||
|
|
||||||
expect(User.updateMany).to.be.calledOnce;
|
expect(User.updateMany).to.be.calledOnce;
|
||||||
expect(User.updateMany).to.be.calledWithMatch({
|
expect(User.updateMany).to.be.calledWithMatch({
|
||||||
@@ -1421,12 +1421,12 @@ describe('Group Model', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates users about new messages in group', () => {
|
it('updates users about new messages in group', async () => {
|
||||||
const group = new Group({
|
const group = new Group({
|
||||||
type: 'guild',
|
type: 'guild',
|
||||||
});
|
});
|
||||||
|
|
||||||
group.sendChat({ message: 'message' });
|
await group.sendChat({ message: 'message' });
|
||||||
|
|
||||||
expect(User.updateMany).to.be.calledOnce;
|
expect(User.updateMany).to.be.calledOnce;
|
||||||
expect(User.updateMany).to.be.calledWithMatch({
|
expect(User.updateMany).to.be.calledWithMatch({
|
||||||
@@ -1435,8 +1435,8 @@ describe('Group Model', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not send update to user that sent the message', () => {
|
it('does not send update to user that sent the message', async () => {
|
||||||
party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } });
|
await party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } });
|
||||||
|
|
||||||
expect(User.updateMany).to.be.calledOnce;
|
expect(User.updateMany).to.be.calledOnce;
|
||||||
expect(User.updateMany).to.be.calledWithMatch({
|
expect(User.updateMany).to.be.calledWithMatch({
|
||||||
@@ -1445,18 +1445,18 @@ describe('Group Model', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('skips sending new message notification for guilds with > 5000 members', () => {
|
it('skips sending new message notification for guilds with > 5000 members', async () => {
|
||||||
party.memberCount = 5001;
|
party.memberCount = 5001;
|
||||||
|
|
||||||
party.sendChat({ message: 'message' });
|
await party.sendChat({ message: 'message' });
|
||||||
|
|
||||||
expect(User.updateMany).to.not.be.called;
|
expect(User.updateMany).to.not.be.called;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('skips sending messages to the tavern', () => {
|
it('skips sending messages to the tavern', async () => {
|
||||||
party._id = TAVERN_ID;
|
party._id = TAVERN_ID;
|
||||||
|
|
||||||
party.sendChat({ message: 'message' });
|
await party.sendChat({ message: 'message' });
|
||||||
|
|
||||||
expect(User.updateMany).to.not.be.called;
|
expect(User.updateMany).to.not.be.called;
|
||||||
});
|
});
|
||||||
@@ -2326,7 +2326,7 @@ describe('Group Model', () => {
|
|||||||
|
|
||||||
await guild.save();
|
await guild.save();
|
||||||
|
|
||||||
const groupMessage = guild.sendChat({ message: 'Test message.' });
|
const groupMessage = await guild.sendChat({ message: 'Test message.' });
|
||||||
await groupMessage.save();
|
await groupMessage.save();
|
||||||
|
|
||||||
await sleep();
|
await sleep();
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ api.postChat = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newChatMessage = group.sendChat({
|
const newChatMessage = await group.sendChat({
|
||||||
message,
|
message,
|
||||||
user,
|
user,
|
||||||
flagCount,
|
flagCount,
|
||||||
|
|||||||
@@ -756,7 +756,7 @@ api.transferGems = {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if (receiver.preferences.pushNotifications.giftedGems !== false) {
|
if (receiver.preferences.pushNotifications.giftedGems !== false) {
|
||||||
sendPushNotification(
|
await sendPushNotification(
|
||||||
receiver,
|
receiver,
|
||||||
{
|
{
|
||||||
title: res.t('giftedGems', receiverLang),
|
title: res.t('giftedGems', receiverLang),
|
||||||
|
|||||||
@@ -120,10 +120,10 @@ api.inviteToQuest = {
|
|||||||
|
|
||||||
// send out invites
|
// send out invites
|
||||||
const inviterVars = getUserInfo(user, ['name', 'email']);
|
const inviterVars = getUserInfo(user, ['name', 'email']);
|
||||||
const membersToEmail = members.filter(member => {
|
const membersToEmail = members.filter(async member => {
|
||||||
// send push notifications while filtering members before sending emails
|
// send push notifications while filtering members before sending emails
|
||||||
if (member.preferences.pushNotifications.invitedQuest !== false) {
|
if (member.preferences.pushNotifications.invitedQuest !== false) {
|
||||||
sendPushNotification(
|
await sendPushNotification(
|
||||||
member,
|
member,
|
||||||
{
|
{
|
||||||
title: quest.text(member.preferences.language),
|
title: quest.text(member.preferences.language),
|
||||||
@@ -394,7 +394,7 @@ api.cancelQuest = {
|
|||||||
if (group.quest.active) throw new NotAuthorized(res.t('cantCancelActiveQuest'));
|
if (group.quest.active) throw new NotAuthorized(res.t('cantCancelActiveQuest'));
|
||||||
|
|
||||||
const questName = questScrolls[group.quest.key].text('en');
|
const questName = questScrolls[group.quest.key].text('en');
|
||||||
const newChatMessage = group.sendChat({
|
const newChatMessage = await group.sendChat({
|
||||||
message: `\`${user.profile.name} cancelled the party quest ${questName}.\``,
|
message: `\`${user.profile.name} cancelled the party quest ${questName}.\``,
|
||||||
info: {
|
info: {
|
||||||
type: 'quest_cancel',
|
type: 'quest_cancel',
|
||||||
@@ -456,7 +456,7 @@ api.abortQuest = {
|
|||||||
if (user._id !== group.leader && user._id !== group.quest.leader) throw new NotAuthorized(res.t('onlyLeaderAbortQuest'));
|
if (user._id !== group.leader && user._id !== group.quest.leader) throw new NotAuthorized(res.t('onlyLeaderAbortQuest'));
|
||||||
|
|
||||||
const questName = questScrolls[group.quest.key].text('en');
|
const questName = questScrolls[group.quest.key].text('en');
|
||||||
const newChatMessage = group.sendChat({
|
const newChatMessage = await group.sendChat({
|
||||||
message: `\`${common.i18n.t('chatQuestAborted', { username: user.profile.name, questName }, 'en')}\``,
|
message: `\`${common.i18n.t('chatQuestAborted', { username: user.profile.name, questName }, 'en')}\``,
|
||||||
info: {
|
info: {
|
||||||
type: 'quest_abort',
|
type: 'quest_abort',
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export async function sendChatPushNotifications (user, group, message, mentions,
|
|||||||
.select('preferences.pushNotifications preferences.language profile.name pushDevices auth.local.username')
|
.select('preferences.pushNotifications preferences.language profile.name pushDevices auth.local.username')
|
||||||
.exec();
|
.exec();
|
||||||
|
|
||||||
members.forEach(member => {
|
members.forEach(async member => {
|
||||||
if (member.preferences.pushNotifications.partyActivity !== false) {
|
if (member.preferences.pushNotifications.partyActivity !== false) {
|
||||||
if (mentions && mentions.includes(`@${member.auth.local.username}`) && member.preferences.pushNotifications.mentionParty !== false) {
|
if (mentions && mentions.includes(`@${member.auth.local.username}`) && member.preferences.pushNotifications.mentionParty !== false) {
|
||||||
return;
|
return;
|
||||||
@@ -33,7 +33,7 @@ export async function sendChatPushNotifications (user, group, message, mentions,
|
|||||||
|
|
||||||
if (!message.unformattedText) return;
|
if (!message.unformattedText) return;
|
||||||
|
|
||||||
sendPushNotification(
|
await sendPushNotification(
|
||||||
member,
|
member,
|
||||||
{
|
{
|
||||||
title: translate('groupActivityNotificationTitle', { user: message.user, group: group.name }, member.preferences.language),
|
title: translate('groupActivityNotificationTitle', { user: message.user, group: group.name }, member.preferences.language),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export async function sentMessage (sender, receiver, message, translate) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (receiver.preferences.pushNotifications.newPM !== false && messageSent.unformattedText) {
|
if (receiver.preferences.pushNotifications.newPM !== false && messageSent.unformattedText) {
|
||||||
sendPushNotification(
|
await sendPushNotification(
|
||||||
receiver,
|
receiver,
|
||||||
{
|
{
|
||||||
title: translate(
|
title: translate(
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ import {
|
|||||||
model as Group,
|
model as Group,
|
||||||
} from '../../models/group';
|
} from '../../models/group';
|
||||||
|
|
||||||
function sendInvitePushNotification (userToInvite, groupLabel, group, publicGuild, res) {
|
async function sendInvitePushNotification (userToInvite, groupLabel, group, publicGuild, res) {
|
||||||
if (userToInvite.preferences.pushNotifications[`invited${groupLabel}`] === false) return;
|
if (userToInvite.preferences.pushNotifications[`invited${groupLabel}`] === false) return;
|
||||||
|
|
||||||
const identifier = group.type === 'guild' ? 'invitedGuild' : 'invitedParty';
|
const identifier = group.type === 'guild' ? 'invitedGuild' : 'invitedParty';
|
||||||
|
|
||||||
sendPushNotification(
|
await sendPushNotification(
|
||||||
userToInvite,
|
userToInvite,
|
||||||
{
|
{
|
||||||
title: group.name,
|
title: group.name,
|
||||||
@@ -110,7 +110,7 @@ async function addInvitationToUser (userToInvite, group, inviter, res) {
|
|||||||
|
|
||||||
const groupLabel = group.type === 'guild' ? 'Guild' : 'Party';
|
const groupLabel = group.type === 'guild' ? 'Guild' : 'Party';
|
||||||
sendInviteEmail(userToInvite, groupLabel, group, inviter);
|
sendInviteEmail(userToInvite, groupLabel, group, inviter);
|
||||||
sendInvitePushNotification(userToInvite, groupLabel, group, publicGuild, res);
|
await sendInvitePushNotification(userToInvite, groupLabel, group, publicGuild, res);
|
||||||
|
|
||||||
const userInvited = await userToInvite.save();
|
const userInvited = await userToInvite.save();
|
||||||
if (group.type === 'guild') {
|
if (group.type === 'guild') {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ async function buyGemGift (data) {
|
|||||||
data.gift.member._id !== data.user._id
|
data.gift.member._id !== data.user._id
|
||||||
&& data.gift.member.preferences.pushNotifications.giftedGems !== false
|
&& data.gift.member.preferences.pushNotifications.giftedGems !== false
|
||||||
) {
|
) {
|
||||||
sendPushNotification(
|
await sendPushNotification(
|
||||||
data.gift.member,
|
data.gift.member,
|
||||||
{
|
{
|
||||||
title: shared.i18n.t('giftedGems', languages[1]),
|
title: shared.i18n.t('giftedGems', languages[1]),
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ async function createSubscription (data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.gift.member.preferences.pushNotifications.giftedSubscription !== false) {
|
if (data.gift.member.preferences.pushNotifications.giftedSubscription !== false) {
|
||||||
sendPushNotification(
|
await sendPushNotification(
|
||||||
data.gift.member,
|
data.gift.member,
|
||||||
{
|
{
|
||||||
title: shared.i18n.t('giftedSubscription', languages[1]),
|
title: shared.i18n.t('giftedSubscription', languages[1]),
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import nconf from 'nconf';
|
import nconf from 'nconf';
|
||||||
import apn from '@parse/node-apn';
|
import apn from '@parse/node-apn';
|
||||||
import gcmLib from 'node-gcm'; // works with FCM notifications too
|
import admin from 'firebase-admin';
|
||||||
import logger from './logger';
|
import logger from './logger';
|
||||||
import { // eslint-disable-line import/no-cycle
|
import { // eslint-disable-line import/no-cycle
|
||||||
model as User,
|
model as User,
|
||||||
} from '../models/user';
|
} from '../models/user';
|
||||||
|
|
||||||
const FCM_API_KEY = nconf.get('PUSH_CONFIGS_FCM_SERVER_API_KEY');
|
|
||||||
const fcmSender = FCM_API_KEY ? new gcmLib.Sender(FCM_API_KEY) : undefined;
|
|
||||||
|
|
||||||
const APN_ENABLED = nconf.get('PUSH_CONFIGS_APN_ENABLED') === 'true';
|
const APN_ENABLED = nconf.get('PUSH_CONFIGS_APN_ENABLED') === 'true';
|
||||||
const apnProvider = APN_ENABLED ? new apn.Provider({
|
const apnProvider = APN_ENABLED ? new apn.Provider({
|
||||||
token: {
|
token: {
|
||||||
@@ -30,7 +27,91 @@ function removePushDevice (user, pushDevice) {
|
|||||||
|
|
||||||
export const MAX_MESSAGE_LENGTH = 300;
|
export const MAX_MESSAGE_LENGTH = 300;
|
||||||
|
|
||||||
export function sendNotification (user, details = {}) {
|
async function sendFCMNotification (user, pushDevice, payload) {
|
||||||
|
const messaging = admin.messaging();
|
||||||
|
if (messaging === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const message = {
|
||||||
|
notification: {
|
||||||
|
title: payload.title,
|
||||||
|
body: payload.body,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
identifier: payload.identifier,
|
||||||
|
notificationIdentifier: payload.identifier,
|
||||||
|
},
|
||||||
|
token: pushDevice.regId,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await messaging.send(message);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'messaging/registration-token-not-registered') {
|
||||||
|
removePushDevice(user, pushDevice);
|
||||||
|
logger.error(new Error('FCM error, unregistered pushDevice'), {
|
||||||
|
regId: pushDevice.regId, userId: user._id,
|
||||||
|
});
|
||||||
|
} else if (error.code === 'messaging/invalid-registration-token') {
|
||||||
|
removePushDevice(user, pushDevice);
|
||||||
|
logger.error(new Error('FCM error, invalid pushDevice'), {
|
||||||
|
regId: pushDevice.regId, userId: user._id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.error(error, 'Unhandled FCM error.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendAPNNotification (user, pushDevice, details, payload) {
|
||||||
|
if (apnProvider) {
|
||||||
|
const notification = new apn.Notification({
|
||||||
|
alert: {
|
||||||
|
title: details.title,
|
||||||
|
body: details.message,
|
||||||
|
},
|
||||||
|
sound: 'default',
|
||||||
|
category: details.category,
|
||||||
|
topic: 'com.habitrpg.ios.Habitica',
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const response = await apnProvider.send(notification, pushDevice.regId);
|
||||||
|
// Handle failed push notifications deliveries
|
||||||
|
response.failed.forEach(failure => {
|
||||||
|
if (failure.error) { // generic error
|
||||||
|
logger.error(new Error('Unhandled APN error'), {
|
||||||
|
response, regId: pushDevice.regId, userId: user._id,
|
||||||
|
});
|
||||||
|
} else { // rejected
|
||||||
|
// see https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW17
|
||||||
|
// for a list of rejection reasons
|
||||||
|
const { reason } = failure.response;
|
||||||
|
if (reason === 'Unregistered') {
|
||||||
|
removePushDevice(user, pushDevice);
|
||||||
|
logger.error(new Error('APN error, unregistered pushDevice'), {
|
||||||
|
regId: pushDevice.regId, userId: user._id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (reason === 'BadDeviceToken') {
|
||||||
|
// An invalid token was registered by mistake
|
||||||
|
// Remove it but log the error differently so that it can be distinguished
|
||||||
|
// from when reason === Unregistered
|
||||||
|
removePushDevice(user, pushDevice);
|
||||||
|
}
|
||||||
|
logger.error(new Error('APN error'), {
|
||||||
|
response, regId: pushDevice.regId, userId: user._id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err, 'Unhandled APN error.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendNotification (user, details = {}) {
|
||||||
if (!user) throw new Error('User is required.');
|
if (!user) throw new Error('User is required.');
|
||||||
if (user.preferences.pushNotifications.unsubscribeFromAll === true) return;
|
if (user.preferences.pushNotifications.unsubscribeFromAll === true) return;
|
||||||
const pushDevices = user.pushDevices.toObject ? user.pushDevices.toObject() : user.pushDevices;
|
const pushDevices = user.pushDevices.toObject ? user.pushDevices.toObject() : user.pushDevices;
|
||||||
@@ -51,110 +132,16 @@ export function sendNotification (user, details = {}) {
|
|||||||
payload.message = _.truncate(payload.message, { length: MAX_MESSAGE_LENGTH });
|
payload.message = _.truncate(payload.message, { length: MAX_MESSAGE_LENGTH });
|
||||||
}
|
}
|
||||||
|
|
||||||
_.each(pushDevices, pushDevice => {
|
await _.each(pushDevices, async pushDevice => {
|
||||||
switch (pushDevice.type) { // eslint-disable-line default-case
|
switch (pushDevice.type) { // eslint-disable-line default-case
|
||||||
case 'android':
|
case 'android':
|
||||||
// Required for fcm to be received in background
|
// Required for fcm to be received in background
|
||||||
payload.title = details.title;
|
payload.title = details.title;
|
||||||
payload.body = details.message;
|
payload.body = details.message;
|
||||||
|
await sendFCMNotification(user, pushDevice, payload);
|
||||||
if (fcmSender) {
|
|
||||||
const message = new gcmLib.Message({
|
|
||||||
data: payload,
|
|
||||||
});
|
|
||||||
|
|
||||||
fcmSender.send(message, {
|
|
||||||
registrationTokens: [pushDevice.regId],
|
|
||||||
}, 5, (err, response) => {
|
|
||||||
if (err) {
|
|
||||||
logger.error(err, 'Unhandled FCM error.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle failed push notifications deliveries
|
|
||||||
// Note that we're always sending to one device, not multiple
|
|
||||||
const failed = response
|
|
||||||
&& response.results && response.results[0] && response.results[0].error;
|
|
||||||
|
|
||||||
if (failed) {
|
|
||||||
// See https://firebase.google.com/docs/cloud-messaging/http-server-ref#table9
|
|
||||||
// for the list of errors
|
|
||||||
|
|
||||||
// The regId is not valid anymore, remove it
|
|
||||||
if (failed === 'NotRegistered') {
|
|
||||||
removePushDevice(user, pushDevice);
|
|
||||||
logger.error(new Error('FCM error, unregistered pushDevice'), {
|
|
||||||
regId: pushDevice.regId, userId: user._id,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// An invalid token was registered by mistake
|
|
||||||
// Remove it but log the error differently so that it can be distinguished
|
|
||||||
// from when failed === NotRegistered
|
|
||||||
// Blacklisted can happen in some rare cases,
|
|
||||||
// see https://stackoverflow.com/questions/42136122/why-does-firebase-push-token-return-blacklisted
|
|
||||||
// MismatchSenderId could be due to old tokens,
|
|
||||||
// see https://stackoverflow.com/questions/11313342/why-do-i-get-mismatchsenderid-from-gcm-server-side
|
|
||||||
if (
|
|
||||||
failed === 'InvalidRegistration'
|
|
||||||
|| failed === 'MismatchSenderId'
|
|
||||||
|| pushDevice.regId === 'BLACKLISTED'
|
|
||||||
) {
|
|
||||||
removePushDevice(user, pushDevice);
|
|
||||||
}
|
|
||||||
logger.error(new Error('FCM error'), {
|
|
||||||
response, regId: pushDevice.regId, userId: user._id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'ios':
|
case 'ios':
|
||||||
if (apnProvider) {
|
sendAPNNotification(user, pushDevice, details, payload);
|
||||||
const notification = new apn.Notification({
|
|
||||||
alert: {
|
|
||||||
title: details.title,
|
|
||||||
body: details.message,
|
|
||||||
},
|
|
||||||
sound: 'default',
|
|
||||||
category: details.category,
|
|
||||||
topic: 'com.habitrpg.ios.Habitica',
|
|
||||||
payload,
|
|
||||||
});
|
|
||||||
apnProvider.send(notification, pushDevice.regId)
|
|
||||||
.then(response => {
|
|
||||||
// Handle failed push notifications deliveries
|
|
||||||
response.failed.forEach(failure => {
|
|
||||||
if (failure.error) { // generic error
|
|
||||||
logger.error(new Error('Unhandled APN error'), {
|
|
||||||
response, regId: pushDevice.regId, userId: user._id,
|
|
||||||
});
|
|
||||||
} else { // rejected
|
|
||||||
// see https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW17
|
|
||||||
// for a list of rejection reasons
|
|
||||||
const { reason } = failure.response;
|
|
||||||
if (reason === 'Unregistered') {
|
|
||||||
removePushDevice(user, pushDevice);
|
|
||||||
logger.error(new Error('APN error, unregistered pushDevice'), {
|
|
||||||
regId: pushDevice.regId, userId: user._id,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (reason === 'BadDeviceToken') {
|
|
||||||
// An invalid token was registered by mistake
|
|
||||||
// Remove it but log the error differently so that it can be distinguished
|
|
||||||
// from when reason === Unregistered
|
|
||||||
removePushDevice(user, pushDevice);
|
|
||||||
}
|
|
||||||
logger.error(new Error('APN error'), {
|
|
||||||
response, regId: pushDevice.regId, userId: user._id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(err => logger.error(err, 'Unhandled APN error.'));
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
14
website/server/libs/setupFirebase.js
Normal file
14
website/server/libs/setupFirebase.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import admin from 'firebase-admin';
|
||||||
|
import nconf from 'nconf';
|
||||||
|
|
||||||
|
if (nconf.get('FIREBASE_PROJECT_ID') !== undefined && nconf.get('FIREBASE_PROJECT_ID') !== '') {
|
||||||
|
if (!global.firebaseApp) {
|
||||||
|
global.firebaseApp = admin.initializeApp({
|
||||||
|
credential: admin.credential.cert({
|
||||||
|
projectId: nconf.get('FIREBASE_PROJECT_ID'),
|
||||||
|
clientEmail: nconf.get('FIREBASE_CLIENT_EMAIL'),
|
||||||
|
privateKey: nconf.get('FIREBASE_PRIVATE_KEY').replace(/\\n/g, '\n'),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -254,7 +254,7 @@ async function castSpell (req, res, { isV3 = false }) {
|
|||||||
if (lastMessage && lastMessage.info.spell === spellId
|
if (lastMessage && lastMessage.info.spell === spellId
|
||||||
&& lastMessage.info.user === user.profile.name
|
&& lastMessage.info.user === user.profile.name
|
||||||
&& lastMessage.info.target === partyMembers.profile.name) {
|
&& lastMessage.info.target === partyMembers.profile.name) {
|
||||||
const newChatMessage = party.sendChat({
|
const newChatMessage = await party.sendChat({
|
||||||
message: `\`${common.i18n.t('chatCastSpellUserTimes', {
|
message: `\`${common.i18n.t('chatCastSpellUserTimes', {
|
||||||
username: user.profile.name,
|
username: user.profile.name,
|
||||||
spell: spell.text(),
|
spell: spell.text(),
|
||||||
@@ -273,7 +273,7 @@ async function castSpell (req, res, { isV3 = false }) {
|
|||||||
await newChatMessage.save();
|
await newChatMessage.save();
|
||||||
await lastMessage.deleteOne();
|
await lastMessage.deleteOne();
|
||||||
} else { // Single target spell, not repeated
|
} else { // Single target spell, not repeated
|
||||||
const newChatMessage = party.sendChat({
|
const newChatMessage = await party.sendChat({
|
||||||
message: `\`${common.i18n.t('chatCastSpellUser', { username: user.profile.name, spell: spell.text(), target: partyMembers.profile.name }, 'en')}\``,
|
message: `\`${common.i18n.t('chatCastSpellUser', { username: user.profile.name, spell: spell.text(), target: partyMembers.profile.name }, 'en')}\``,
|
||||||
info: {
|
info: {
|
||||||
type: 'spell_cast_user',
|
type: 'spell_cast_user',
|
||||||
@@ -288,7 +288,7 @@ async function castSpell (req, res, { isV3 = false }) {
|
|||||||
}
|
}
|
||||||
} else if (lastMessage && lastMessage.info.spell === spellId // Party spell, check for repeat
|
} else if (lastMessage && lastMessage.info.spell === spellId // Party spell, check for repeat
|
||||||
&& lastMessage.info.user === user.profile.name) {
|
&& lastMessage.info.user === user.profile.name) {
|
||||||
const newChatMessage = party.sendChat({
|
const newChatMessage = await party.sendChat({
|
||||||
message: `\`${common.i18n.t('chatCastSpellPartyTimes', {
|
message: `\`${common.i18n.t('chatCastSpellPartyTimes', {
|
||||||
username: user.profile.name,
|
username: user.profile.name,
|
||||||
spell: spell.text(),
|
spell: spell.text(),
|
||||||
@@ -305,7 +305,7 @@ async function castSpell (req, res, { isV3 = false }) {
|
|||||||
await newChatMessage.save();
|
await newChatMessage.save();
|
||||||
await lastMessage.deleteOne();
|
await lastMessage.deleteOne();
|
||||||
} else {
|
} else {
|
||||||
const newChatMessage = party.sendChat({ // Non-repetitive partywide spell
|
const newChatMessage = await party.sendChat({ // Non-repetitive partywide spell
|
||||||
message: `\`${common.i18n.t('chatCastSpellParty', { username: user.profile.name, spell: spell.text() }, 'en')}\``,
|
message: `\`${common.i18n.t('chatCastSpellParty', { username: user.profile.name, spell: spell.text() }, 'en')}\``,
|
||||||
info: {
|
info: {
|
||||||
type: 'spell_cast_party',
|
type: 'spell_cast_party',
|
||||||
|
|||||||
@@ -403,7 +403,7 @@ schema.methods.closeChal = async function closeChal (broken = {}) {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if (savedWinner.preferences.pushNotifications.wonChallenge !== false) {
|
if (savedWinner.preferences.pushNotifications.wonChallenge !== false) {
|
||||||
sendPushNotification(
|
await sendPushNotification(
|
||||||
savedWinner,
|
savedWinner,
|
||||||
{
|
{
|
||||||
title: challenge.name,
|
title: challenge.name,
|
||||||
|
|||||||
@@ -526,7 +526,7 @@ schema.methods.getMemberCount = async function getMemberCount () {
|
|||||||
return User.countDocuments(query).exec();
|
return User.countDocuments(query).exec();
|
||||||
};
|
};
|
||||||
|
|
||||||
schema.methods.sendChat = function sendChat (options = {}) {
|
schema.methods.sendChat = async function sendChat (options = {}) {
|
||||||
const {
|
const {
|
||||||
message, user, metaData,
|
message, user, metaData,
|
||||||
client, flagCount = 0, info = {},
|
client, flagCount = 0, info = {},
|
||||||
@@ -596,7 +596,7 @@ schema.methods.sendChat = function sendChat (options = {}) {
|
|||||||
sendChatPushNotifications(user, this, newChatMessage, mentions, translate);
|
sendChatPushNotifications(user, this, newChatMessage, mentions, translate);
|
||||||
}
|
}
|
||||||
if (mentionedMembers) {
|
if (mentionedMembers) {
|
||||||
mentionedMembers.forEach(member => {
|
await mentionedMembers.forEach(async member => {
|
||||||
if (member._id === user._id) return;
|
if (member._id === user._id) return;
|
||||||
const pushNotifPrefs = member.preferences.pushNotifications;
|
const pushNotifPrefs = member.preferences.pushNotifications;
|
||||||
if (this.type === 'party') {
|
if (this.type === 'party') {
|
||||||
@@ -617,7 +617,7 @@ schema.methods.sendChat = function sendChat (options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newChatMessage.unformattedText) {
|
if (newChatMessage.unformattedText) {
|
||||||
sendPushNotification(member, {
|
await sendPushNotification(member, {
|
||||||
identifier: 'chatMention',
|
identifier: 'chatMention',
|
||||||
title: `${user.profile.name} mentioned you in ${this.name}`,
|
title: `${user.profile.name} mentioned you in ${this.name}`,
|
||||||
message: newChatMessage.unformattedText,
|
message: newChatMessage.unformattedText,
|
||||||
@@ -751,7 +751,7 @@ schema.methods.startQuest = async function startQuest (user) {
|
|||||||
_id: { $in: nonMembers },
|
_id: { $in: nonMembers },
|
||||||
}, _cleanQuestParty()).exec();
|
}, _cleanQuestParty()).exec();
|
||||||
|
|
||||||
const newMessage = this.sendChat({
|
const newMessage = await this.sendChat({
|
||||||
message: `\`${shared.i18n.t('chatQuestStarted', { questName: quest.text('en') }, 'en')}\``,
|
message: `\`${shared.i18n.t('chatQuestStarted', { questName: quest.text('en') }, 'en')}\``,
|
||||||
metaData: {
|
metaData: {
|
||||||
participatingMembers: this.getParticipatingQuestMembers().join(', '),
|
participatingMembers: this.getParticipatingQuestMembers().join(', '),
|
||||||
@@ -766,7 +766,7 @@ schema.methods.startQuest = async function startQuest (user) {
|
|||||||
const membersToEmail = [];
|
const membersToEmail = [];
|
||||||
|
|
||||||
// send notifications and webhooks in the background without blocking
|
// send notifications and webhooks in the background without blocking
|
||||||
members.forEach(member => {
|
await members.forEach(async member => {
|
||||||
if (member._id !== user._id) {
|
if (member._id !== user._id) {
|
||||||
// send push notifications and filter users that disabled emails
|
// send push notifications and filter users that disabled emails
|
||||||
if (member.preferences.emailNotifications.questStarted !== false) {
|
if (member.preferences.emailNotifications.questStarted !== false) {
|
||||||
@@ -776,7 +776,7 @@ schema.methods.startQuest = async function startQuest (user) {
|
|||||||
// send push notifications and filter users that disabled emails
|
// send push notifications and filter users that disabled emails
|
||||||
if (member.preferences.pushNotifications.questStarted !== false) {
|
if (member.preferences.pushNotifications.questStarted !== false) {
|
||||||
const memberLang = member.preferences.language;
|
const memberLang = member.preferences.language;
|
||||||
sendPushNotification(member, {
|
await sendPushNotification(member, {
|
||||||
title: quest.text(memberLang),
|
title: quest.text(memberLang),
|
||||||
message: shared.i18n.t('questStarted', memberLang),
|
message: shared.i18n.t('questStarted', memberLang),
|
||||||
identifier: 'questStarted',
|
identifier: 'questStarted',
|
||||||
@@ -1021,7 +1021,7 @@ schema.methods._processBossQuest = async function processBossQuest (options) {
|
|||||||
|
|
||||||
group.quest.progress.hp -= progress.up;
|
group.quest.progress.hp -= progress.up;
|
||||||
if (CRON_SAFE_MODE || CRON_SEMI_SAFE_MODE) {
|
if (CRON_SAFE_MODE || CRON_SEMI_SAFE_MODE) {
|
||||||
const groupMessage = group.sendChat({
|
const groupMessage = await group.sendChat({
|
||||||
message: `\`${shared.i18n.t('chatBossDontAttack', { bossName: quest.boss.name('en') }, 'en')}\``,
|
message: `\`${shared.i18n.t('chatBossDontAttack', { bossName: quest.boss.name('en') }, 'en')}\``,
|
||||||
info: {
|
info: {
|
||||||
type: 'boss_dont_attack',
|
type: 'boss_dont_attack',
|
||||||
@@ -1032,7 +1032,7 @@ schema.methods._processBossQuest = async function processBossQuest (options) {
|
|||||||
});
|
});
|
||||||
promises.push(groupMessage.save());
|
promises.push(groupMessage.save());
|
||||||
} else {
|
} else {
|
||||||
const groupMessage = group.sendChat({
|
const groupMessage = await group.sendChat({
|
||||||
message: `\`${shared.i18n.t('chatBossDamage', {
|
message: `\`${shared.i18n.t('chatBossDamage', {
|
||||||
username: user.profile.name, bossName: quest.boss.name('en'), userDamage: progress.up.toFixed(1), bossDamage: Math.abs(down).toFixed(1),
|
username: user.profile.name, bossName: quest.boss.name('en'), userDamage: progress.up.toFixed(1), bossDamage: Math.abs(down).toFixed(1),
|
||||||
}, user.preferences.language)}\``,
|
}, user.preferences.language)}\``,
|
||||||
@@ -1051,7 +1051,7 @@ schema.methods._processBossQuest = async function processBossQuest (options) {
|
|||||||
if (quest.boss.rage) {
|
if (quest.boss.rage) {
|
||||||
group.quest.progress.rage += Math.abs(down);
|
group.quest.progress.rage += Math.abs(down);
|
||||||
if (group.quest.progress.rage >= quest.boss.rage.value) {
|
if (group.quest.progress.rage >= quest.boss.rage.value) {
|
||||||
const rageMessage = group.sendChat({
|
const rageMessage = await group.sendChat({
|
||||||
message: quest.boss.rage.effect('en'),
|
message: quest.boss.rage.effect('en'),
|
||||||
info: {
|
info: {
|
||||||
type: 'boss_rage',
|
type: 'boss_rage',
|
||||||
@@ -1094,7 +1094,7 @@ schema.methods._processBossQuest = async function processBossQuest (options) {
|
|||||||
|
|
||||||
// Boss slain, finish quest
|
// Boss slain, finish quest
|
||||||
if (group.quest.progress.hp <= 0) {
|
if (group.quest.progress.hp <= 0) {
|
||||||
const questFinishChat = group.sendChat({
|
const questFinishChat = await group.sendChat({
|
||||||
message: `\`${shared.i18n.t('chatBossDefeated', { bossName: quest.boss.name('en') }, 'en')}\``,
|
message: `\`${shared.i18n.t('chatBossDefeated', { bossName: quest.boss.name('en') }, 'en')}\``,
|
||||||
info: {
|
info: {
|
||||||
type: 'boss_defeated',
|
type: 'boss_defeated',
|
||||||
@@ -1148,7 +1148,7 @@ schema.methods._processCollectionQuest = async function processCollectionQuest (
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
foundText = foundText.join(', ');
|
foundText = foundText.join(', ');
|
||||||
const foundChat = group.sendChat({
|
const foundChat = await group.sendChat({
|
||||||
message: `\`${shared.i18n.t('chatFindItems', { username: user.profile.name, items: foundText }, 'en')}\``,
|
message: `\`${shared.i18n.t('chatFindItems', { username: user.profile.name, items: foundText }, 'en')}\``,
|
||||||
info: {
|
info: {
|
||||||
type: 'user_found_items',
|
type: 'user_found_items',
|
||||||
@@ -1164,7 +1164,7 @@ schema.methods._processCollectionQuest = async function processCollectionQuest (
|
|||||||
const questFinished = collectedItems.length === remainingItems.length;
|
const questFinished = collectedItems.length === remainingItems.length;
|
||||||
if (questFinished) {
|
if (questFinished) {
|
||||||
await group.finishQuest(quest);
|
await group.finishQuest(quest);
|
||||||
const allItemsFoundChat = group.sendChat({
|
const allItemsFoundChat = await group.sendChat({
|
||||||
message: `\`${shared.i18n.t('chatItemQuestFinish', 'en')}\``,
|
message: `\`${shared.i18n.t('chatItemQuestFinish', 'en')}\``,
|
||||||
info: {
|
info: {
|
||||||
type: 'all_items_found',
|
type: 'all_items_found',
|
||||||
@@ -1236,7 +1236,7 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) {
|
|||||||
const chatPromises = [];
|
const chatPromises = [];
|
||||||
|
|
||||||
if (tavern.quest.progress.hp <= 0) {
|
if (tavern.quest.progress.hp <= 0) {
|
||||||
const completeChat = tavern.sendChat({
|
const completeChat = await tavern.sendChat({
|
||||||
message: quest.completionChat('en'),
|
message: quest.completionChat('en'),
|
||||||
info: {
|
info: {
|
||||||
type: 'tavern_quest_completed',
|
type: 'tavern_quest_completed',
|
||||||
@@ -1273,7 +1273,7 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!scene) {
|
if (!scene) {
|
||||||
const tiredChat = tavern.sendChat({
|
const tiredChat = await tavern.sendChat({
|
||||||
message: `\`${shared.i18n.t('tavernBossTired', { rageName: quest.boss.rage.title('en'), bossName: quest.boss.name('en') }, 'en')}\``,
|
message: `\`${shared.i18n.t('tavernBossTired', { rageName: quest.boss.rage.title('en'), bossName: quest.boss.name('en') }, 'en')}\``,
|
||||||
info: {
|
info: {
|
||||||
type: 'tavern_boss_rage_tired',
|
type: 'tavern_boss_rage_tired',
|
||||||
@@ -1283,7 +1283,7 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) {
|
|||||||
chatPromises.push(tiredChat.save());
|
chatPromises.push(tiredChat.save());
|
||||||
tavern.quest.progress.rage = 0; // quest.boss.rage.value;
|
tavern.quest.progress.rage = 0; // quest.boss.rage.value;
|
||||||
} else {
|
} else {
|
||||||
const rageChat = tavern.sendChat({
|
const rageChat = await tavern.sendChat({
|
||||||
message: quest.boss.rage[scene]('en'),
|
message: quest.boss.rage[scene]('en'),
|
||||||
info: {
|
info: {
|
||||||
type: 'tavern_boss_rage',
|
type: 'tavern_boss_rage',
|
||||||
@@ -1306,7 +1306,7 @@ schema.statics.tavernBoss = async function tavernBoss (user, progress) {
|
|||||||
&& tavern.quest.progress.hp < quest.boss.desperation.threshold
|
&& tavern.quest.progress.hp < quest.boss.desperation.threshold
|
||||||
&& !tavern.quest.extra.desperate
|
&& !tavern.quest.extra.desperate
|
||||||
) {
|
) {
|
||||||
const progressChat = tavern.sendChat({
|
const progressChat = await tavern.sendChat({
|
||||||
message: quest.boss.desperation.text('en'),
|
message: quest.boss.desperation.text('en'),
|
||||||
info: {
|
info: {
|
||||||
type: 'tavern_boss_desperation',
|
type: 'tavern_boss_desperation',
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import attachMiddlewares from './middlewares/index';
|
|||||||
// Load config files
|
// Load config files
|
||||||
import './libs/setupMongoose';
|
import './libs/setupMongoose';
|
||||||
import './libs/setupPassport';
|
import './libs/setupPassport';
|
||||||
|
import './libs/setupFirebase';
|
||||||
|
|
||||||
// Load some schemas & models
|
// Load some schemas & models
|
||||||
import './models/challenge';
|
import './models/challenge';
|
||||||
|
|||||||
Reference in New Issue
Block a user